}|array{ok: false, error: string} */ public static function generate_pairing_preview(string $api_base_url, string $pairing_code): array { $destination = TheDotDevAgenticBridge_Connector::normalize_destination($api_base_url); if (is_wp_error($destination)) { return ['ok' => false, 'error' => 'invalid_destination']; } $pairing_code = sanitize_text_field($pairing_code); if ($pairing_code === '') { return ['ok' => false, 'error' => 'missing_pairing_code']; } $payload = TheDotDevAgenticBridge_Connector::build_pairing_payload($pairing_code); $payload_bytes = TheDotDevAgenticBridge_Connector::serialize_pairing_payload($payload); if ($payload_bytes === false) { return ['ok' => false, 'error' => 'encode_failed']; } if (strlen($payload_bytes) > self::MAX_PAYLOAD_BYTES) { return ['ok' => false, 'error' => 'oversize']; } $endpoint = TheDotDevAgenticBridge_Connector::pairing_endpoint($destination); $payload_hash = hash('sha256', $payload_bytes); $preview = self::build_preview_record( self::OP_PAIRING, $destination, $endpoint, $payload_hash, $payload_bytes, null, $payload, null, ); if ($preview === null) { return ['ok' => false, 'error' => 'malformed']; } self::persist_preview(self::OP_PAIRING, $preview); return ['ok' => true, 'preview' => self::public_preview_view($preview)]; } /** @return array{ok: true, preview: array}|array{ok: false, error: string} */ public static function generate_snapshot_preview(): array { if (!TheDotDevAgenticBridge_Storage::is_connected()) { return ['ok' => false, 'error' => 'not_connected']; } $settings = TheDotDevAgenticBridge_Storage::get(); $api_base_url = (string) ($settings['api_base_url'] ?? ''); $site_id = (string) ($settings['site_id'] ?? ''); $destination = TheDotDevAgenticBridge_Connector::normalize_destination($api_base_url); if (is_wp_error($destination)) { return ['ok' => false, 'error' => 'invalid_destination']; } if ($site_id === '') { return ['ok' => false, 'error' => 'not_connected']; } $snapshot = TheDotDevAgenticBridge_Snapshot::build(); $payload_bytes = TheDotDevAgenticBridge_Connector::serialize_snapshot_payload($snapshot); if ($payload_bytes === false) { return ['ok' => false, 'error' => 'encode_failed']; } if (strlen($payload_bytes) > self::MAX_PAYLOAD_BYTES) { return ['ok' => false, 'error' => 'oversize']; } $endpoint = TheDotDevAgenticBridge_Connector::snapshot_endpoint($destination, $site_id); $path = self::snapshot_signing_path($site_id); $payload_hash = hash('sha256', $payload_bytes); $preview = self::build_preview_record( self::OP_SNAPSHOT, $destination, $endpoint, $payload_hash, $payload_bytes, $snapshot, null, $site_id, ); if ($preview === null) { return ['ok' => false, 'error' => 'malformed']; } self::persist_preview(self::OP_SNAPSHOT, $preview); return ['ok' => true, 'preview' => self::public_preview_view($preview)]; } /** @return array|null */ public static function get_active_preview(string $operation): ?array { if (!self::is_valid_operation($operation)) { return null; } $preview = self::load_preview_from_slot($operation); if ($preview === null) { return null; } return self::public_preview_view($preview); } public static function discard_preview(string $operation): void { if (!self::is_valid_operation($operation)) { return; } self::clear_preview($operation); } /** * @return array{ * ok: true, * payload_bytes: string, * destination: array{origin: string, base_url: string}, * endpoint: string, * path: string * }|array{ok: false, error: string} */ public static function consume_pairing_consent(string $preview_id, string $fingerprint, string $pairing_code): array { $preview = self::load_preview_for_consent(self::OP_PAIRING, $preview_id, $fingerprint); if ($preview === null) { return ['ok' => false, 'error' => 'consent_invalid']; } if (!empty($preview['consumed']) || self::is_expired($preview)) { return ['ok' => false, 'error' => 'consent_invalid']; } if (!self::current_configured_destination_matches_preview($preview)) { return ['ok' => false, 'error' => 'destination_changed']; } if (self::is_preview_source_stale($preview)) { return ['ok' => false, 'error' => 'consent_invalid']; } $pairing_code = sanitize_text_field($pairing_code); if ($pairing_code === '') { return ['ok' => false, 'error' => 'missing_pairing_code']; } $payload_bytes = TheDotDevAgenticBridge_Connector::serialize_pairing_payload(TheDotDevAgenticBridge_Connector::build_pairing_payload($pairing_code)); if ($payload_bytes === false) { return ['ok' => false, 'error' => 'encode_failed']; } $payload_hash = hash('sha256', $payload_bytes); if (!hash_equals((string) ($preview['payloadHash'] ?? ''), $payload_hash)) { return ['ok' => false, 'error' => 'pairing_mismatch']; } self::mark_consumed(self::OP_PAIRING); return [ 'ok' => true, 'payload_bytes' => $payload_bytes, 'destination' => self::bound_destination_from_preview($preview), 'endpoint' => (string) ($preview['destinationEndpoint'] ?? ''), 'path' => (string) ($preview['destinationPath'] ?? ''), ]; } /** * @return array{ * ok: true, * payload_bytes: string, * bound: array{ * destination: array{origin: string, base_url: string}, * site_id: string, * endpoint: string, * path: string * } * }|array{ok: false, error: string} */ public static function consume_snapshot_consent(string $preview_id, string $fingerprint): array { $preview = self::load_preview_for_consent(self::OP_SNAPSHOT, $preview_id, $fingerprint); if ($preview === null) { return ['ok' => false, 'error' => 'consent_invalid']; } if (!empty($preview['consumed']) || self::is_expired($preview)) { return ['ok' => false, 'error' => 'consent_invalid']; } if (!self::current_snapshot_binding_matches_preview($preview)) { return ['ok' => false, 'error' => 'destination_changed']; } if (self::is_preview_source_stale($preview)) { return ['ok' => false, 'error' => 'consent_invalid']; } $payload_bytes = (string) ($preview['payloadBytes'] ?? ''); if ($payload_bytes === '' || !hash_equals((string) ($preview['payloadHash'] ?? ''), hash('sha256', $payload_bytes))) { self::clear_preview(self::OP_SNAPSHOT); return ['ok' => false, 'error' => 'malformed']; } self::mark_consumed(self::OP_SNAPSHOT); return [ 'ok' => true, 'payload_bytes' => $payload_bytes, 'bound' => [ 'destination' => self::bound_destination_from_preview($preview), 'site_id' => (string) ($preview['boundSiteId'] ?? ''), 'endpoint' => (string) ($preview['destinationEndpoint'] ?? ''), 'path' => (string) ($preview['destinationPath'] ?? ''), ], ]; } /** @param array $report */ public static function cache_report(array $report): void { $normalized = self::normalize_cached_report($report); if ($normalized === null) { return; } $store = self::store(); $store['cached_report'] = $normalized; self::save_store($store); } /** @return array{fetchedAt: string, latestReport: array|null, findings: list>, score: string}|null */ public static function cached_report(): ?array { $cached = self::store()['cached_report'] ?? null; if (!is_array($cached) || !self::is_cached_report_valid($cached)) { if (is_array($cached)) { self::clear_cached_report(); } return null; } return [ 'fetchedAt' => (string) $cached['fetchedAt'], 'latestReport' => is_array($cached['latestReport'] ?? null) ? $cached['latestReport'] : null, 'findings' => is_array($cached['findings'] ?? null) ? $cached['findings'] : [], 'score' => (string) ($cached['score'] ?? ''), ]; } public static function clear_cached_report(): void { $store = self::store(); $store['cached_report'] = null; self::save_store($store); } /** @return array */ public static function excluded_category_labels(): array { return [ 'drafts' => __('Draft, pending, future, private, and trash post statuses', 'agentic-bridge'), 'password_protected' => __('Password-protected posts and pages', 'agentic-bridge'), 'noindex' => __('Posts and pages marked noindex', 'agentic-bridge'), 'internal_global_blocks' => __('Site-wide non-indexable or internal visibility blocks', 'agentic-bridge'), 'metadata_exclusions' => __('Per-item metadata exclusion mode', 'agentic-bridge'), 'global_content_exclusions' => __('Explicit global content exclusions (IDs are never transmitted)', 'agentic-bridge'), 'users' => __('WordPress users and accounts', 'agentic-bridge'), 'comments' => __('Comments', 'agentic-bridge'), 'orders_products' => __('Orders, products, and commerce records', 'agentic-bridge'), 'form_submissions' => __('Form submissions', 'agentic-bridge'), 'credentials_secrets' => __('Pairing codes, HMAC keys, signatures, and auth headers', 'agentic-bridge'), 'unsupported_post_types' => __('Unsupported post types beyond public posts and pages', 'agentic-bridge'), ]; } /** @return array */ public static function snapshot_top_level_field_descriptions(): array { return [ 'schemaVersion' => __('Snapshot format version sent to the control plane.', 'agentic-bridge'), 'siteId' => __('Connected site identifier assigned by the hosted service.', 'agentic-bridge'), 'meta' => __('Site/plugin state: URL, name, versions, timezone, artifact and kill-switch flags, generatedAt; physicalLlmsTxtPath only when a readable physical file exists.', 'agentic-bridge'), 'content' => __('Artifact-eligible content items (id, type, slug, title, url, modifiedAt, isIndexable, hasNoindex, isPasswordProtected, status).', 'agentic-bridge'), 'claims' => __('Approved outbound claim metadata (id, contentId, claimType, claimText, state).', 'agentic-bridge'), 'publishedArtifacts' => __('Exact last-known-good llmsTxt and agentMap when artifacts are published; omitted when unpublished.', 'agentic-bridge'), ]; } /** @param array $preview */ public static function is_preview_expired(array $preview): bool { return self::is_expired($preview); } /** @param array $preview */ public static function is_preview_source_stale(array $preview): bool { if (!self::is_preview_record_shape_valid($preview, (string) ($preview['operation'] ?? ''))) { return true; } if (!empty($preview['consumed'])) { return true; } $operation = (string) ($preview['operation'] ?? ''); $anchor = $operation === self::OP_SNAPSHOT ? (string) ($preview['previewGeneratedAt'] ?? '') : null; $current = self::compute_fingerprint($operation, $preview, $anchor); return !hash_equals((string) ($preview['fingerprint'] ?? ''), $current); } /** @param array $preview */ public static function compute_fingerprint(string $operation, array $preview, ?string $anchor_generated_at = null): string { if (!self::is_valid_operation($operation)) { return ''; } $state = self::normalized_source_state($operation, $anchor_generated_at); $state['operation'] = $operation; $state['previewPolicyVersion'] = self::POLICY_VERSION; $state['destinationOrigin'] = (string) ($preview['destinationOrigin'] ?? ''); $state['destinationBaseUrl'] = (string) ($preview['destinationBaseUrl'] ?? ''); $state['destinationPath'] = (string) ($preview['destinationPath'] ?? ''); $state['payloadHash'] = (string) ($preview['payloadHash'] ?? ''); $state['blogId'] = get_current_blog_id(); if ($operation === self::OP_SNAPSHOT) { $state['boundSiteId'] = (string) ($preview['boundSiteId'] ?? ''); } $encoded = wp_json_encode($state, JSON_UNESCAPED_SLASHES); return is_string($encoded) ? hash('sha256', $encoded) : ''; } /** @return string|null */ public static function get_stored_snapshot_payload_bytes(): ?string { $preview = self::load_preview_from_slot(self::OP_SNAPSHOT); if ($preview === null) { return null; } $bytes = (string) ($preview['payloadBytes'] ?? ''); return $bytes !== '' ? $bytes : null; } public static function purge_all(): void { delete_option(self::OPTION_KEY); } public static function snapshot_signing_path(string $site_id): string { return '/v1/sites/' . rawurlencode($site_id) . '/snapshots'; } /** @return array */ private static function normalized_source_state(string $operation, ?string $anchor_generated_at = null): array { $excluded_ids = TheDotDevAgenticBridge_Content_Policy::excluded_content_ids(); sort($excluded_ids); $metadata_store = TheDotDevAgenticBridge_Content_Metadata::normalized_store(); $metadata_exclusions = []; foreach ($metadata_store['by_content_id'] ?? [] as $content_id => $config) { if (!is_array($config)) { continue; } if (($config['inclusionMode'] ?? '') === TheDotDevAgenticBridge_Content_Metadata::INCLUSION_EXCLUDE) { $metadata_exclusions[] = (string) $content_id; } } sort($metadata_exclusions); $configured = self::configured_destination_from_storage(); $state = [ 'contentMetadataVersion' => TheDotDevAgenticBridge_Content_Metadata::VERSION, 'excludedContentIds' => $excluded_ids, 'metadataExclusions' => $metadata_exclusions, 'siteUrl' => home_url('/'), 'siteName' => get_bloginfo('name'), 'wordpressVersion' => get_bloginfo('version'), 'pluginVersion' => THEDOTDEV_AGENTIC_BRIDGE_VERSION, 'timezone' => wp_timezone_string(), 'killSwitchEnabled' => TheDotDevAgenticBridge_Storage::kill_switch_enabled(), 'artifactsPublished' => TheDotDevAgenticBridge_Storage::artifacts_published(), 'hasPhysicalLlmsTxt' => TheDotDevAgenticBridge_Snapshot::has_physical_llms_txt(), 'configuredDestinationOrigin' => is_array($configured) ? $configured['origin'] : '', 'configuredDestinationBaseUrl' => is_array($configured) ? $configured['base_url'] : '', ]; if ($operation === self::OP_SNAPSHOT) { $state['connectedSiteId'] = (string) (TheDotDevAgenticBridge_Storage::get()['site_id'] ?? ''); $snapshot = TheDotDevAgenticBridge_Snapshot::build(); if ($anchor_generated_at !== null && $anchor_generated_at !== '') { if (isset($snapshot['meta']) && is_array($snapshot['meta'])) { $snapshot['meta']['generatedAt'] = $anchor_generated_at; } } $state['snapshotSchemaVersion'] = (string) ($snapshot['schemaVersion'] ?? ''); $state['previewGeneratedAt'] = (string) ($snapshot['meta']['generatedAt'] ?? ''); $state['content'] = $snapshot['content'] ?? []; $state['claims'] = $snapshot['claims'] ?? []; $state['publishedArtifacts'] = $snapshot['publishedArtifacts'] ?? null; } return $state; } /** * @param array{origin: string, base_url: string} $destination * @param array|null $snapshot * @param array|null $pairing_payload * @return array|null */ private static function build_preview_record( string $operation, array $destination, string $endpoint, string $payload_hash, string $payload_bytes, ?array $snapshot, ?array $pairing_payload, ?string $bound_site_id, ): ?array { $path = wp_parse_url($endpoint, PHP_URL_PATH); if (!is_string($path) || $path === '') { return null; } if ($operation === self::OP_SNAPSHOT) { if ($bound_site_id === null || $bound_site_id === '') { return null; } $path = self::snapshot_signing_path($bound_site_id); } $preview = [ 'id' => wp_generate_uuid4(), 'operation' => $operation, 'blogId' => get_current_blog_id(), 'createdAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'expiresAt' => gmdate('c', TheDotDevAgenticBridge_Readiness_Clock::now() + self::TTL_SECONDS), 'consumed' => false, 'destinationOrigin' => $destination['origin'], 'destinationBaseUrl' => $destination['base_url'], 'destinationEndpoint' => $endpoint, 'destinationPath' => $path, 'method' => 'POST', 'payloadHash' => $payload_hash, 'previewPolicyVersion' => self::POLICY_VERSION, 'contentMetadataVersion' => TheDotDevAgenticBridge_Content_Metadata::VERSION, 'excludedCategories' => array_keys(self::excluded_category_labels()), ]; if ($operation === self::OP_PAIRING) { if (!is_array($pairing_payload)) { return null; } $preview['redactedFields'] = self::redacted_pairing_fields($pairing_payload); $preview['topLevelFields'] = array_keys($pairing_payload); $preview['fingerprint'] = self::compute_fingerprint($operation, $preview, null); return self::is_preview_record_shape_valid($preview, self::OP_PAIRING) ? $preview : null; } if (!is_array($snapshot) || $bound_site_id === null || $bound_site_id === '') { return null; } $preview['boundSiteId'] = $bound_site_id; $preview['payloadBytes'] = $payload_bytes; $preview['payloadByteSize'] = strlen($payload_bytes); $preview['snapshotSchemaVersion'] = (string) ($snapshot['schemaVersion'] ?? ''); $preview['previewGeneratedAt'] = (string) ($snapshot['meta']['generatedAt'] ?? ''); $preview['contentCount'] = is_array($snapshot['content'] ?? null) ? count($snapshot['content']) : 0; $preview['claimCount'] = is_array($snapshot['claims'] ?? null) ? count($snapshot['claims']) : 0; $preview['topLevelFields'] = array_keys($snapshot); $preview['fingerprint'] = self::compute_fingerprint($operation, $preview, $preview['previewGeneratedAt']); return self::is_preview_record_shape_valid($preview, self::OP_SNAPSHOT) ? $preview : null; } /** @param array $payload @return list */ private static function redacted_pairing_fields(array $payload): array { $rows = []; foreach ($payload as $field => $value) { if (!is_string($field) || $field === '') { continue; } if ($field === 'pairingCode') { $rows[] = [ 'field' => 'pairingCode', 'value' => __('[credential — not stored or displayed]', 'agentic-bridge'), 'sensitive' => true, ]; continue; } if (!is_scalar($value)) { continue; } $rows[] = [ 'field' => $field, 'value' => (string) $value, 'sensitive' => false, ]; } return $rows; } /** @param array $preview */ private static function public_preview_view(array $preview): array { $public = $preview; unset($public['payloadBytes']); $public['expired'] = self::is_expired($preview); $public['stale'] = !$public['expired'] && self::is_preview_source_stale($preview); $public['confirmBlocked'] = $public['expired'] || $public['stale'] || !empty($preview['consumed']); return $public; } /** @param array $preview */ private static function is_expired(array $preview): bool { $expires_at = strtotime((string) ($preview['expiresAt'] ?? '')); if ($expires_at === false) { return true; } return TheDotDevAgenticBridge_Readiness_Clock::now() >= $expires_at; } /** @return array{origin: string, base_url: string} */ private static function bound_destination_from_preview(array $preview): array { return [ 'origin' => (string) ($preview['destinationOrigin'] ?? ''), 'base_url' => (string) ($preview['destinationBaseUrl'] ?? ''), ]; } /** @return array{origin: string, base_url: string}|null */ private static function configured_destination_from_storage(): ?array { $api_base_url = (string) (TheDotDevAgenticBridge_Storage::get()['api_base_url'] ?? ''); if ($api_base_url === '') { return null; } $destination = TheDotDevAgenticBridge_Connector::normalize_destination($api_base_url); if (is_wp_error($destination)) { return null; } return $destination; } /** @param array $preview */ private static function current_configured_destination_matches_preview(array $preview): bool { $configured = self::configured_destination_from_storage(); if ($configured === null) { return false; } return $configured['origin'] === (string) ($preview['destinationOrigin'] ?? '') && $configured['base_url'] === (string) ($preview['destinationBaseUrl'] ?? ''); } /** @param array $preview */ private static function current_snapshot_binding_matches_preview(array $preview): bool { if (!self::current_configured_destination_matches_preview($preview)) { return false; } $settings = TheDotDevAgenticBridge_Storage::get(); $site_id = (string) ($settings['site_id'] ?? ''); return $site_id !== '' && hash_equals((string) ($preview['boundSiteId'] ?? ''), $site_id) && TheDotDevAgenticBridge_Storage::is_connected(); } /** @return array|null */ private static function load_preview_for_consent(string $operation, string $preview_id, string $fingerprint): ?array { $preview = self::load_preview_from_slot($operation); if ($preview === null) { return null; } if (!self::is_valid_uuid(sanitize_text_field($preview_id))) { return null; } if (!self::is_valid_hash(sanitize_text_field($fingerprint))) { return null; } if (!hash_equals((string) ($preview['id'] ?? ''), sanitize_text_field($preview_id))) { return null; } if (!hash_equals((string) ($preview['fingerprint'] ?? ''), sanitize_text_field($fingerprint))) { return null; } if (!empty($preview['consumed']) || self::is_expired($preview)) { return null; } if ((int) ($preview['blogId'] ?? 0) !== get_current_blog_id()) { return null; } return $preview; } /** @return array|null */ private static function load_consumable_preview(string $operation, string $preview_id, string $fingerprint): ?array { $preview = self::load_preview_for_consent($operation, $preview_id, $fingerprint); if ($preview === null) { return null; } if (!empty($preview['consumed']) || self::is_expired($preview) || self::is_preview_source_stale($preview)) { return null; } return $preview; } /** @return array|null */ private static function load_preview_from_slot(string $operation): ?array { if (!self::is_valid_operation($operation)) { return null; } $preview = self::store()[$operation] ?? null; if ($preview === null) { return null; } if (!is_array($preview) || !self::is_preview_record_shape_valid($preview, $operation)) { self::clear_preview($operation); return null; } return $preview; } /** @param array $preview */ private static function persist_preview(string $operation, array $preview): void { $store = self::store(); $store[$operation] = $preview; self::save_store($store); } private static function mark_consumed(string $operation): void { $store = self::store(); if (!is_array($store[$operation] ?? null)) { return; } $store[$operation]['consumed'] = true; self::save_store($store); } private static function clear_preview(string $operation): void { $store = self::store(); $store[$operation] = null; self::save_store($store); } /** @return array{pairing: mixed, snapshot: mixed, cached_report: mixed} */ private static function store(): array { $stored = get_option(self::OPTION_KEY, []); if (!is_array($stored)) { return [ 'pairing' => null, 'snapshot' => null, 'cached_report' => null, ]; } return array_merge([ 'pairing' => null, 'snapshot' => null, 'cached_report' => null, ], $stored); } /** @param array{pairing: mixed, snapshot: mixed, cached_report: mixed} $store */ private static function save_store(array $store): void { update_option(self::OPTION_KEY, $store, false); } private static function is_valid_operation(string $operation): bool { return in_array($operation, [self::OP_PAIRING, self::OP_SNAPSHOT], true); } private static function is_valid_uuid(string $value): bool { return (bool) preg_match( '/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i', $value, ); } private static function is_valid_hash(string $value): bool { return (bool) preg_match('/^[a-f0-9]{' . self::HASH_LENGTH . '}$/', $value); } private static function is_valid_iso8601(string $value): bool { return strtotime($value) !== false; } /** @param array $preview */ private static function is_preview_record_shape_valid(array $preview, string $expected_slot): bool { if (!self::is_valid_operation($expected_slot)) { return false; } if ((string) ($preview['operation'] ?? '') !== $expected_slot) { return false; } if (!self::is_valid_uuid((string) ($preview['id'] ?? ''))) { return false; } if (!is_int($preview['blogId'] ?? null) || (int) $preview['blogId'] <= 0) { return false; } if ((int) $preview['blogId'] !== get_current_blog_id()) { return false; } if (!self::is_valid_iso8601((string) ($preview['createdAt'] ?? ''))) { return false; } if (!self::is_valid_iso8601((string) ($preview['expiresAt'] ?? ''))) { return false; } if (!is_bool($preview['consumed'] ?? null)) { return false; } if ((string) ($preview['method'] ?? '') !== 'POST') { return false; } if ((string) ($preview['previewPolicyVersion'] ?? '') !== self::POLICY_VERSION) { return false; } if (!self::is_valid_hash((string) ($preview['payloadHash'] ?? ''))) { return false; } if (!self::is_valid_hash((string) ($preview['fingerprint'] ?? ''))) { return false; } foreach (['destinationOrigin', 'destinationBaseUrl', 'destinationEndpoint'] as $url_key) { $url = (string) ($preview[$url_key] ?? ''); if ($url === '' || !str_starts_with($url, 'https://')) { return false; } } $path = (string) ($preview['destinationPath'] ?? ''); if ($path === '' || !str_starts_with($path, '/')) { return false; } if ($expected_slot === self::OP_PAIRING) { if (!is_array($preview['redactedFields'] ?? null)) { return false; } foreach ($preview['redactedFields'] as $row) { if (!is_array($row)) { return false; } if (!is_string($row['field'] ?? null) || !is_string($row['value'] ?? null) || !is_bool($row['sensitive'] ?? null)) { return false; } } return is_array($preview['topLevelFields'] ?? null); } $payload_bytes = (string) ($preview['payloadBytes'] ?? ''); if ($payload_bytes === '' || strlen($payload_bytes) > self::MAX_PAYLOAD_BYTES) { return false; } if (!hash_equals((string) ($preview['payloadHash'] ?? ''), hash('sha256', $payload_bytes))) { return false; } $bound_site_id = (string) ($preview['boundSiteId'] ?? ''); if ($bound_site_id === '') { return false; } if (!is_int($preview['payloadByteSize'] ?? null) || (int) $preview['payloadByteSize'] !== strlen($payload_bytes)) { return false; } return is_array($preview['topLevelFields'] ?? null); } /** @param array $report @return array|null */ private static function normalize_cached_report(array $report): ?array { if (!is_array($report['latestReport'] ?? null)) { return [ 'fetchedAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'latestReport' => null, 'findings' => [], 'score' => '', ]; } $latest = $report['latestReport']; $score = isset($latest['score']) && (is_string($latest['score']) || is_int($latest['score']) || is_float($latest['score'])) ? (string) $latest['score'] : ''; $findings = []; if (is_array($latest['findings'] ?? null)) { foreach (array_slice($latest['findings'], 0, self::MAX_CACHED_FINDINGS) as $finding) { if (!is_array($finding)) { continue; } $normalized = [ 'severity' => is_scalar($finding['severity'] ?? null) ? sanitize_text_field((string) $finding['severity']) : '', 'title' => is_scalar($finding['title'] ?? null) ? sanitize_text_field(wp_strip_all_tags((string) $finding['title'])) : '', 'description' => is_scalar($finding['description'] ?? null) ? sanitize_text_field(wp_strip_all_tags((string) $finding['description'])) : '', ]; if ($normalized['title'] === '' && $normalized['description'] === '') { continue; } $findings[] = $normalized; } } return [ 'fetchedAt' => TheDotDevAgenticBridge_Readiness_Clock::iso(), 'latestReport' => [ 'score' => $score, 'findings' => $findings, ], 'findings' => $findings, 'score' => $score, ]; } /** @param array $cached */ private static function is_cached_report_valid(array $cached): bool { if (!isset($cached['fetchedAt']) || !self::is_valid_iso8601((string) $cached['fetchedAt'])) { return false; } if (!is_array($cached['findings'] ?? null)) { return false; } if (count($cached['findings']) > self::MAX_CACHED_FINDINGS) { return false; } foreach ($cached['findings'] as $finding) { if (!is_array($finding)) { return false; } foreach (['severity', 'title', 'description'] as $key) { if (!is_string($finding[$key] ?? null)) { return false; } } } return is_string($cached['score'] ?? null); } }