get('/bird/health', function (): void { global $response; self::requirePermission('modules_bird_health_read'); $client = new bird(); $workspaceId = $this->getConfiguredWorkspaceId($client); $configured = $workspaceId !== '' && trim((string)$client->config->api_key->getVariableValue()) !== '' && trim((string)$client->config->server_url->getVariableValue()) !== ''; $provider = null; $channels = []; if ($configured && $client->config->enabled->isTrue()) { try { $provider = $client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/channels', ['limit' => 10] ); $channels = $this->channelSummaries($provider, $this->getAllowedChannelIds($client)); } catch (Throwable $throwable) { error_log('[bird-health] Read-only provider check failed: ' . get_class($throwable)); $response->error('Bird read-only health check failed', 502); } } $response->success([ 'enabled' => $client->config->enabled->isTrue(), 'configured' => $configured, 'workspaceId' => $workspaceId, 'channelId' => $this->getConfiguredChannelId($client), 'allowedChannelIds' => $this->getAllowedChannelIds($client), 'providerReachable' => $provider !== null, 'healthy' => $provider !== null, 'channels' => $channels, 'check' => 'channels.list', ]); }, [ 'modules_bird_health_read' => 'Run a read-only Bird connection health check', ]); $this->get('/bird/control-plane/v1/bootstrap', function (): void { global $response; header('Cache-Control: no-store, max-age=0'); header('Pragma: no-cache'); header('X-Content-Type-Options: nosniff'); try { $envelope = (new bird_control_plane_auto_activation( db::getPDO() ))->publicEnvelope(); } catch (Throwable) { $response->error('Not found', 404); } $response->success($envelope); }); $this->get('/bird/control-plane/v1/status', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $schema = $this->schemaStatus(); $response->success([ 'enabled' => true, 'birdEnabled' => $client->config->enabled->isTrue(), 'workspaceId' => $workspaceId, 'participantId' => trim((string)$client->config->participantId->getVariableValue()), 'channelId' => $this->getAllowedChannelIds($client)[0] ?? '', 'allowedChannelIds' => $this->getAllowedChannelIds($client), 'schema' => $schema, 'webhookConfigured' => $schema['ready'] && $this->webhookConfigured($client), 'flowEnabled' => $client->config->flow_enabled->isTrue(), 'eventLedgerReady' => $schema['ready'], 'capabilities' => [ 'channels.read', ...($schema['ready'] ? ['events.read'] : []), 'conversations.read', 'messages.read', 'voice.calls.read', 'voice.recordings.read', 'voice.insights.read', 'numbers.read', ], 'writeCapabilities' => [ 'operations.actions' => $schema['ready'] && $client->config->operations_actions_enabled->isTrue(), 'conversations.reply' => $schema['ready'] && $client->config->outbound_messages_enabled->isTrue(), 'conversations.template' => $schema['ready'] && $client->config->outbound_messages_enabled->isTrue(), ], ]); }); $this->get('/bird/control-plane/v1/channels', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $provider = $client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/channels', $this->paginationQuery() ); $response->success([ 'channels' => bird_control_plane_contract::channels( $provider, 100, $this->getAllowedChannelIds($client) ), ]); }); $this->get('/bird/control-plane/v1/webhook-subscriptions', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $response->success($client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/webhook-subscriptions', $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/events', function (): void { global $response; $this->requireControlPlaneClient(); $this->requireControlPlaneSchema(); $cursor = $this->boundedIntegerQuery('cursor', 0, PHP_INT_MAX, 0); $limit = $this->boundedIntegerQuery('limit', 1, 100, 100); $response->success($this->eventStore()->listAfter($cursor, $limit)); }); $this->get('/bird/control-plane/v1/conversations', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $query = $this->paginationQuery(); $query['channelId'] = $this->requiredChannelId($client); $status = $this->queryString('status'); if ($status !== '') { if (!in_array($status, ['active', 'archived'], true)) { $response->error('Invalid conversation status', 400); } $query['status'] = $status; } $response->success($client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/conversations', $query ) ?? []); }); $this->get('/bird/control-plane/v1/messages', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $conversationId = $this->requiredOpaqueIdQuery('conversationId'); $this->requireAllowedConversation($client, $workspaceId, $conversationId); $response->success($client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/conversations/' . rawurlencode($conversationId) . '/messages', $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/calls', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $channelId = $this->queryString('channelId'); if ($channelId === '') { $channelId = $this->getAllowedChannelIds($client)[0] ?? ''; } if (!in_array($channelId, $this->getAllowedChannelIds($client), true)) { $response->error('Bird channel is not allowlisted', 403); } $response->success($client->listVoiceCalls( $workspaceId, $channelId, $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/call-log', function (): void { global $response; $client = $this->requireControlPlaneClient(); $response->success($client->getVoiceCallsLog( $this->requireWorkspaceId($client), $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/recordings', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $channelId = $this->requiredChannelId($client); $callId = $this->requiredOpaqueIdQuery('callId'); $response->success($client->listVoiceCallRecordings( $workspaceId, $channelId, $callId, $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/insights', function (): void { global $response; $client = $this->requireControlPlaneClient(); $response->success($client->getVoiceCallInsights( $this->requireWorkspaceId($client), $this->requiredChannelId($client), $this->requiredOpaqueIdQuery('callId') ) ?? []); }); $this->get('/bird/control-plane/v1/numbers', function (): void { global $response; $client = $this->requireControlPlaneClient(); $response->success($client->listNumbers( $this->requireWorkspaceId($client), $this->paginationQuery() ) ?? []); }); $this->get('/bird/control-plane/v1/operations', function (): void { global $response; $client = $this->requireControlPlaneClient(); $workspaceId = $this->requireWorkspaceId($client); $allowedChannelIds = $this->getAllowedChannelIds($client); $channelId = $allowedChannelIds[0] ?? ''; $calls = []; foreach ($allowedChannelIds as $allowedChannelId) { $providerCalls = $client->listVoiceCalls( $workspaceId, $allowedChannelId, ['limit' => 20] ); foreach (bird_control_plane_contract::collectionItems($providerCalls) as $call) { $call['channelId'] = $allowedChannelId; $call['state'] = $call['state'] ?? $call['status'] ?? null; $calls[] = $call; } } $response->success([ 'workspaceId' => $workspaceId, 'channelId' => $channelId, 'allowedChannelIds' => $allowedChannelIds, 'calls' => $calls, 'callLog' => $client->getVoiceCallsLog($workspaceId, ['limit' => 20]) ?? [], 'numbers' => $client->listNumbers($workspaceId, ['limit' => 20]) ?? [], 'actionsEnabled' => $this->schemaStatus()['ready'] && $client->config->operations_actions_enabled->isTrue(), 'supportedActions' => ['voice.call.hangup'], ]); }); $this->post('/bird/control-plane/v1/operations/actions', function (): void { global $response; $client = $this->requireControlPlaneClient(); if (!$client->config->operations_actions_enabled->isTrue()) { $response->error('Bird operational actions are disabled', 503); } $this->requireControlPlaneSchema(); $payload = $this->jsonBody(); if (($payload['confirmed'] ?? false) !== true) { $response->error('Explicit confirmation is required', 409); } if (($payload['action'] ?? null) !== 'voice.call.hangup') { $response->error('Unsupported Bird operation action', 400); } try { $operation = bird_control_plane_contract::hangup( $payload, $this->getAllowedChannelIds($client) ); } catch (\InvalidArgumentException $exception) { $response->error($exception->getMessage(), 400); } $action = $operation['action']; $resourceId = $operation['resourceId']; $channelId = $operation['channelId']; $cause = $operation['cause']; $workspaceId = $this->requireWorkspaceId($client); $idempotencyKey = $this->idempotencyKey(); $requestHash = bird_control_plane_contract::operationRequestHash($operation); $store = $this->outboundStore(); $reservation = $store->begin( 'operation:' . $idempotencyKey, $resourceId, 'operation', $requestHash ); if (!$reservation['created']) { $this->respondReservation($reservation['record']); } try { $call = $client->getVoiceCall($workspaceId, $channelId, $resourceId); $callArray = is_object($call) ? (array)$call : $call; $status = strtolower(trim((string)($callArray['status'] ?? ''))); if (!in_array($status, ['accepted', 'ongoing'], true)) { $store->complete('operation:' . $idempotencyKey, 'rejected', [ 'reason' => 'call_not_active', 'providerStatus' => $status, ]); $response->error('Bird call is not in an actionable state', 409); } $result = $client->hangupVoiceCall( $workspaceId, $channelId, $resourceId, ['cause' => $cause] ); $result = is_object($result) ? (array)$result : ($result ?? ['status' => 'ok']); $store->complete('operation:' . $idempotencyKey, 'completed', $result); } catch (Throwable $throwable) { $store->complete('operation:' . $idempotencyKey, 'ambiguous', [ 'errorClass' => get_class($throwable), ]); $record = $store->find('operation:' . $idempotencyKey) ?? $reservation['record']; $this->respondReservation($record); } $response->success($store->find('operation:' . $idempotencyKey)); }); $this->get('/bird/control-plane/v1/messages/by-reference', function (): void { global $response; $client = $this->requireControlPlaneClient(); $this->requireControlPlaneSchema(); $reference = $this->validatedReference($this->queryString('reference')); $record = $this->outboundStore()->find($reference); if ($record === null) { $response->error('Bird outbound reference not found', 404); } $record = $this->reconcileProviderMessage( $client, $this->requireWorkspaceId($client), $record ); $response->success($record); }); $this->post('/bird/control-plane/v1/messages', function (): void { $this->sendConversationReply(false, $this->jsonBody()); }); $this->post('/bird/control-plane/v1/messages/template', function (): void { $this->sendConversationReply(true, $this->jsonBody()); }); $this->post('/bird/webhooks/notifications', function (): void { global $response; $client = new bird(); if (!$client->config->enabled->isTrue() || !$this->webhookConfigured($client)) { $response->error('Bird webhook ingestion is not configured', 503); } $headers = function_exists('getallheaders') ? getallheaders() : []; $rawBody = $this->boundedRawBody('Bird webhook'); $timestamp = bird_control_plane_security::header( $_SERVER, is_array($headers) ? $headers : [], 'messagebird-request-timestamp' ); $signature = bird_control_plane_security::header( $_SERVER, is_array($headers) ? $headers : [], 'messagebird-signature' ); $requestId = bird_control_plane_security::header( $_SERVER, is_array($headers) ? $headers : [], 'messagebird-request-id' ); $publicUrl = trim((string)$client->config->webhook_public_url->getVariableValue()); $window = (int)$client->config->webhook_replay_window_seconds->getVariableValue(); if ($requestId === '' || strlen($requestId) > 191) { $response->error('Missing or invalid messagebird-request-id', 400); } if (!bird_control_plane_security::timestampWithinReplayWindow($timestamp, $window)) { $response->error('Bird webhook timestamp is outside the replay window', 401); } if (!bird_control_plane_security::verifyBirdWebhookSignature( (string)$client->config->webhook_signing_key->getVariableValue(), $timestamp, $publicUrl, $rawBody, $signature )) { $response->error('Invalid Bird webhook signature', 401); } $payload = json_decode($rawBody, true); if (!is_array($payload)) { $response->error('Invalid Bird webhook JSON', 400); } try { $scope = bird_control_plane_contract::webhookScope( $payload, $this->getConfiguredWorkspaceId($client), $this->getAllowedChannelIds($client) ); } catch (\InvalidArgumentException $exception) { $response->error($exception->getMessage(), 403); } $this->requireControlPlaneSchema(); $result = $this->eventStore()->append( $requestId, hash('sha256', $timestamp . "\n" . $signature . "\n" . $rawBody), $this->payloadString($payload, ['event', 'type']), $scope['workspaceId'], $scope['channelId'], $rawBody, strlen($timestamp) === 13 ? (int)floor((int)$timestamp / 1000) : (int)$timestamp ); $response->rawJson([ 'accepted' => true, 'duplicate' => !$result['inserted'], 'cursor' => $result['id'], ], $result['inserted'] ? 202 : 200); }); $this->post('/bird/flows/evaluate', function (): void { global $response; $client = new bird(); $headers = function_exists('getallheaders') ? getallheaders() : []; $headers = is_array($headers) ? $headers : []; $timestamp = bird_control_plane_security::header( $_SERVER, $headers, 'x-pleno-flow-timestamp' ); $signature = bird_control_plane_security::header( $_SERVER, $headers, 'x-pleno-flow-signature' ); $rawBody = $this->boundedRawBody('Bird Flow'); if (!bird_control_plane_security::timestampWithinReplayWindow( $timestamp, (int)$client->config->webhook_replay_window_seconds->getVariableValue() ) || !bird_control_plane_security::verifyFlowSignature( (string)$client->config->flow_shared_secret->getVariableValue(), $timestamp, $rawBody, $signature )) { $response->error('Unauthorized Bird Flow request', 401); } if (!$client->config->enabled->isTrue() || !$client->config->flow_enabled->isTrue()) { $response->error('Bird Flow evaluation is disabled', 503); } $payload = json_decode($rawBody, true); if (!is_array($payload) || !is_array($payload['event'] ?? null)) { $response->error('Bird Flow event must be an object', 400); } $response->success(bird_flow_policy_evaluator::evaluate( (string)$client->config->flow_policy_json->getVariableValue(), $payload['event'] )); }); } private function requireControlPlaneClient(): bird { global $response; $client = new bird(); $headers = function_exists('getallheaders') ? getallheaders() : []; $token = bird_control_plane_security::bearerToken( $_SERVER, is_array($headers) ? $headers : [] ); if (!bird_control_plane_security::verifyBearer( (string)$client->config->control_plane_token->getVariableValue(), $token )) { $response->error('Unauthorized Bird Control Plane request', 401); } if (!$client->config->control_plane_enabled->isTrue()) { $response->error('Bird Control Plane gateway is disabled', 503); } if (!$client->config->enabled->isTrue()) { $response->error('Bird module is disabled', 503); } return $client; } private function webhookConfigured(bird $client): bool { $url = trim((string)$client->config->webhook_public_url->getVariableValue()); return trim((string)$client->config->webhook_signing_key->getVariableValue()) !== '' && filter_var($url, FILTER_VALIDATE_URL) !== false && strtolower((string)parse_url($url, PHP_URL_SCHEME)) === 'https'; } private function requireWorkspaceId(bird $client): string { global $response; $workspaceId = $this->getConfiguredWorkspaceId($client); if ($workspaceId === '') { $response->error('Bird workspaceId is not configured', 503); } return $workspaceId; } private function requiredChannelId(bird $client): string { global $response; $channelId = $this->queryString('channelId'); if ($channelId === '') { $channelId = $this->getAllowedChannelIds($client)[0] ?? ''; } if (!in_array($channelId, $this->getAllowedChannelIds($client), true)) { $response->error('Bird channel is not allowlisted', 403); } return $channelId; } private function paginationQuery(): array { $query = ['limit' => $this->boundedIntegerQuery('limit', 1, 100, 100)]; $pageToken = $this->queryString('pageToken'); if ($pageToken !== '') { $query['pageToken'] = $pageToken; } return $query; } private function queryString(string $key): string { $value = $_GET[$key] ?? ''; if (!is_scalar($value)) { return ''; } $value = trim((string)$value); return strlen($value) <= 512 ? $value : ''; } private function requiredOpaqueIdQuery(string $key): string { global $response; $value = $this->queryString($key); if ($value === '' || preg_match('/^[A-Za-z0-9._:-]{1,191}$/', $value) !== 1) { $response->error('Missing or invalid parameter: ' . $key, 400); } return $value; } private function boundedIntegerQuery( string $key, int $minimum, int $maximum, int $default ): int { $value = $_GET[$key] ?? null; if ($value === null || $value === '') { return $default; } if (!is_scalar($value) || filter_var($value, FILTER_VALIDATE_INT) === false) { return $default; } return max($minimum, min($maximum, (int)$value)); } private function eventStore(): bird_webhook_event_store { try { return new bird_webhook_event_store(db::getPDO()); } catch (Throwable $throwable) { throw new RuntimeException('Bird event ledger is unavailable', 0, $throwable); } } private function payloadString(array $payload, array $paths): string { foreach ($paths as $path) { $value = $payload; foreach (explode('.', $path) as $segment) { if (!is_array($value) || !array_key_exists($segment, $value)) { $value = null; break; } $value = $value[$segment]; } if (is_scalar($value) && trim((string)$value) !== '') { return substr(trim((string)$value), 0, 191); } } return ''; } private function sendConversationReply(bool $template, array $payload): void { global $response; $client = $this->requireControlPlaneClient(); if (!$client->config->outbound_messages_enabled->isTrue()) { $response->error('Bird outbound messages are disabled', 503); } $this->requireControlPlaneSchema(); if (($payload['confirmed'] ?? false) !== true) { $response->error('Explicit confirmation is required', 409); } $conversationId = $this->validatedOpaqueId( $payload['conversationId'] ?? null, 'conversationId' ); $reference = $this->validatedReference( $payload['reference'] ?? $this->idempotencyKey() ); $workspaceId = $this->requireWorkspaceId($client); $participantId = $this->validatedOpaqueId( $client->config->participantId->getVariableValue(), 'participantId' ); $conversation = $this->requireAllowedConversation($client, $workspaceId, $conversationId); if (($conversation['status'] ?? null) !== 'active') { $response->error('Bird conversation is not active', 409); } $recipient = $this->recipientFromConversation($conversation); if ($recipient === null) { $response->error('Bird conversation has no provider-derived contact recipient', 409); } $birdPayload = [ 'participantType' => 'accessKey', 'participantId' => $participantId, 'addMissingParticipants' => false, 'recipients' => [$recipient], 'reference' => $reference, ]; if ($template) { $birdPayload['template'] = $this->validatedTemplate( $client, is_array($payload['template'] ?? null) ? $payload['template'] : [] ); } else { $text = trim((string)($payload['text'] ?? '')); if ($text === '' || mb_strlen($text) > 10000) { $response->error('Bird reply text must contain 1 to 10000 characters', 400); } $birdPayload['body'] = [ 'type' => 'text', 'text' => ['text' => $text], ]; } $requestHash = hash('sha256', json_encode( [$conversationId, $birdPayload], JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES )); $store = $this->outboundStore(); $reservation = $store->begin( $reference, $conversationId, $template ? 'template' : 'text', $requestHash ); if (!$reservation['created']) { $record = $this->reconcileProviderMessage( $client, $workspaceId, $reservation['record'] ); $this->respondReservation($record); } try { $result = $client->sendPostRequest( '/workspaces/' . rawurlencode($workspaceId) . '/conversations/' . rawurlencode($conversationId) . '/messages', $birdPayload ); $result = is_object($result) ? (array)$result : ($result ?? []); $store->complete($reference, 'completed', $result); } catch (Throwable $throwable) { $store->complete($reference, 'ambiguous', [ 'errorClass' => get_class($throwable), ]); $record = $store->find($reference) ?? $reservation['record']; $this->respondReservation($record); } $response->success($store->find($reference), 201); } private function validatedTemplate(bird $client, array $template): array { global $response; $projectId = $this->validatedOpaqueId($template['projectId'] ?? null, 'template.projectId'); $version = $this->validatedOpaqueId($template['version'] ?? null, 'template.version'); $locale = trim((string)($template['locale'] ?? '')); $parameters = is_array($template['parameters'] ?? null) ? $template['parameters'] : []; $policy = json_decode((string)$client->config->template_policy_json->getVariableValue(), true); $definitions = is_array($policy) && ($policy['version'] ?? null) === 'v1' && is_array($policy['templates'] ?? null) ? $policy['templates'] : []; $allowed = null; foreach ($definitions as $definition) { if (is_array($definition) && ($definition['enabled'] ?? false) === true && ($definition['projectId'] ?? null) === $projectId && ($definition['version'] ?? null) === $version && ($definition['locale'] ?? null) === $locale) { $allowed = $definition; break; } } if ($allowed === null) { $response->error('Bird template version is not allowlisted', 403); } $parameterKeys = is_array($allowed['parameterKeys'] ?? null) ? array_values($allowed['parameterKeys']) : []; $normalized = []; foreach ($parameters as $parameter) { if (!is_array($parameter) || ($parameter['type'] ?? null) !== 'string' || !is_string($parameter['key'] ?? null) || !is_scalar($parameter['value'] ?? null) || !in_array($parameter['key'], $parameterKeys, true)) { $response->error('Bird template parameters do not match the immutable allowlist', 400); } $normalized[] = [ 'type' => 'string', 'key' => $parameter['key'], 'value' => substr((string)$parameter['value'], 0, 2000), ]; } if (array_values(array_column($normalized, 'key')) !== $parameterKeys) { $response->error('Bird template parameter keys or order do not match the immutable allowlist', 400); } return [ 'projectId' => $projectId, 'version' => $version, 'locale' => $locale, 'parameters' => $normalized, ]; } private function recipientFromConversation(array $conversation): ?array { $participants = is_array($conversation['featuredParticipants'] ?? null) ? $conversation['featuredParticipants'] : []; $lastSender = is_array($conversation['lastMessage']['sender'] ?? null) ? $conversation['lastMessage']['sender'] : []; array_unshift($participants, $lastSender); foreach ($participants as $participant) { if (!is_array($participant)) { continue; } $contact = is_array($participant['contact'] ?? null) ? $participant['contact'] : $participant; $key = trim((string)($contact['identifierKey'] ?? '')); $value = trim((string)($contact['identifierValue'] ?? '')); if ($key !== '' && $value !== '' && ($participant['type'] ?? 'contact') === 'contact') { return [ 'type' => 'to', 'identifierKey' => substr($key, 0, 191), 'identifierValue' => substr($value, 0, 512), ]; } } return null; } private function requireAllowedConversation( bird $client, string $workspaceId, string $conversationId ): array { global $response; $conversation = $client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/conversations/' . rawurlencode($conversationId) ); $conversation = is_object($conversation) ? (array)$conversation : $conversation; if (!is_array($conversation)) { $response->error('Bird conversation was not found', 404); } try { bird_control_plane_contract::assertConversationChannel( $conversation, $this->getAllowedChannelIds($client) ); } catch (\InvalidArgumentException $exception) { $response->error($exception->getMessage(), 403); } return $conversation; } private function jsonBody(): array { global $response; $payload = json_decode($this->boundedRawBody('Bird request'), true); if (!is_array($payload)) { $response->error('Request body must be a JSON object', 400); } return $payload; } private function boundedRawBody(string $label): string { global $response; $maximumBytes = 2 * 1024 * 1024; $rawBody = file_get_contents('php://input', false, null, 0, $maximumBytes + 1); $rawBody = is_string($rawBody) ? $rawBody : ''; if (strlen($rawBody) > $maximumBytes) { $response->error($label . ' payload exceeds 2 MiB', 413); } return $rawBody; } private function validatedOpaqueId(mixed $value, string $field): string { global $response; if (!is_scalar($value)) { $response->error('Missing or invalid parameter: ' . $field, 400); } $value = trim((string)$value); if (preg_match('/^[A-Za-z0-9._:-]{1,191}$/', $value) !== 1) { $response->error('Missing or invalid parameter: ' . $field, 400); } return $value; } private function validatedReference(mixed $value): string { global $response; if (!is_scalar($value)) { $response->error('Missing or invalid outbound reference', 400); } $value = trim((string)$value); if (preg_match('/^[A-Za-z0-9._:-]{8,191}$/', $value) !== 1) { $response->error('Missing or invalid outbound reference', 400); } return $value; } private function idempotencyKey(): string { global $response; $headers = function_exists('getallheaders') ? getallheaders() : []; $key = bird_control_plane_security::header( $_SERVER, is_array($headers) ? $headers : [], 'Idempotency-Key' ); if (preg_match('/^[A-Za-z0-9._:-]{8,160}$/', $key) !== 1) { $response->error('A valid Idempotency-Key header is required', 400); } return $key; } private function outboundStore(): bird_outbound_message_store { return new bird_outbound_message_store(db::getPDO()); } /** * @return array{ready:bool,version:int,expectedVersion:int,missing:array} */ private function schemaStatus(): array { try { return bird_control_plane_schema_bootstrap::check(db::getPDO()); } catch (Throwable $throwable) { error_log('[bird-control-plane] Schema preflight failed: ' . get_class($throwable)); return [ 'ready' => false, 'version' => 0, 'expectedVersion' => bird_control_plane_schema_bootstrap::VERSION, 'missing' => ['database:unavailable'], ]; } } private function requireControlPlaneSchema(): void { global $response; $status = $this->schemaStatus(); if (!$status['ready']) { $response->error([ 'code' => 'bird_schema_not_ready', 'message' => 'Bird Control Plane schema is not ready; run the deployment schema command.', 'schema' => $status, ], 503); } } private function respondReservation(array $record): void { global $response; $outcome = bird_control_plane_contract::reservationOutcome($record); if ($outcome['ambiguous']) { $response->error($outcome['payload'], $outcome['statusCode']); } $response->success($outcome['payload'], $outcome['statusCode']); } private function reconcileProviderMessage( bird $client, string $workspaceId, array $record ): array { $status = trim((string)($record['status'] ?? '')); $kind = trim((string)($record['kind'] ?? '')); $reference = trim((string)($record['reference'] ?? '')); $conversationId = trim((string)($record['conversationId'] ?? '')); if (!in_array($status, ['pending', 'ambiguous'], true) || !in_array($kind, ['text', 'template'], true) || $reference === '' || $conversationId === '') { return $record; } try { $provider = $client->sendGetRequest( '/workspaces/' . rawurlencode($workspaceId) . '/conversations/' . rawurlencode($conversationId) . '/messages', ['reference' => $reference, 'limit' => 100] ); foreach (bird_control_plane_contract::collectionItems($provider) as $message) { if (trim((string)($message['reference'] ?? '')) !== $reference || !is_scalar($message['id'] ?? null) || trim((string)$message['id']) === '') { continue; } $store = $this->outboundStore(); $store->complete($reference, 'completed', $message); return $store->find($reference) ?? $record; } } catch (Throwable $throwable) { error_log('[bird-control-plane] Reference reconciliation failed: ' . get_class($throwable)); } return $record; } /** * @return array */ private function channelSummaries(array|object|null $provider, array $allowedChannelIds): array { return bird_control_plane_contract::channels($provider, 10, $allowedChannelIds); } }