@astrasyncai/verification-gateway 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +45 -21
  2. package/dist/adapter-interface/interface.d.mts +2 -2
  3. package/dist/adapter-interface/interface.d.ts +2 -2
  4. package/dist/adapters/express.d.mts +2 -2
  5. package/dist/adapters/express.d.ts +2 -2
  6. package/dist/adapters/express.js +145 -21
  7. package/dist/adapters/express.js.map +1 -1
  8. package/dist/adapters/express.mjs +145 -21
  9. package/dist/adapters/express.mjs.map +1 -1
  10. package/dist/adapters/mcp.d.mts +38 -30
  11. package/dist/adapters/mcp.d.ts +38 -30
  12. package/dist/adapters/mcp.js +72 -16
  13. package/dist/adapters/mcp.js.map +1 -1
  14. package/dist/adapters/mcp.mjs +72 -16
  15. package/dist/adapters/mcp.mjs.map +1 -1
  16. package/dist/adapters/nextjs.d.mts +2 -2
  17. package/dist/adapters/nextjs.d.ts +2 -2
  18. package/dist/adapters/nextjs.js +141 -19
  19. package/dist/adapters/nextjs.js.map +1 -1
  20. package/dist/adapters/nextjs.mjs +141 -19
  21. package/dist/adapters/nextjs.mjs.map +1 -1
  22. package/dist/adapters/sdk.d.mts +2 -2
  23. package/dist/adapters/sdk.d.ts +2 -2
  24. package/dist/adapters/sdk.js +24 -6
  25. package/dist/adapters/sdk.js.map +1 -1
  26. package/dist/adapters/sdk.mjs +24 -6
  27. package/dist/adapters/sdk.mjs.map +1 -1
  28. package/dist/agent/index.d.mts +2 -2
  29. package/dist/agent/index.d.ts +2 -2
  30. package/dist/agent/index.js +24 -3
  31. package/dist/agent/index.js.map +1 -1
  32. package/dist/agent/index.mjs +24 -3
  33. package/dist/agent/index.mjs.map +1 -1
  34. package/dist/bin/astrasync-claude-hook.js +22 -4
  35. package/dist/bin/astrasync-codex-hook.js +22 -4
  36. package/dist/bin/astrasync-guard.js +26 -8
  37. package/dist/bin/astrasync.js +76 -23
  38. package/dist/browser/background.js +22 -4
  39. package/dist/browser/background.js.map +1 -1
  40. package/dist/browser/background.mjs +22 -4
  41. package/dist/browser/background.mjs.map +1 -1
  42. package/dist/browser/browser-adapter.d.mts +2 -2
  43. package/dist/browser/browser-adapter.d.ts +2 -2
  44. package/dist/claude-code/claude-code-adapter.d.mts +2 -2
  45. package/dist/claude-code/claude-code-adapter.d.ts +2 -2
  46. package/dist/cli/index.d.mts +2 -2
  47. package/dist/cli/index.d.ts +2 -2
  48. package/dist/cli/index.js +23 -2
  49. package/dist/cli/index.js.map +1 -1
  50. package/dist/cli/index.mjs +23 -2
  51. package/dist/cli/index.mjs.map +1 -1
  52. package/dist/codex/index.d.mts +2 -2
  53. package/dist/codex/index.d.ts +2 -2
  54. package/dist/codex/index.js +22 -4
  55. package/dist/codex/index.js.map +1 -1
  56. package/dist/codex/index.mjs +22 -4
  57. package/dist/codex/index.mjs.map +1 -1
  58. package/dist/cursor/cursor-adapter.d.mts +2 -2
  59. package/dist/cursor/cursor-adapter.d.ts +2 -2
  60. package/dist/cursor/extension.d.mts +2 -2
  61. package/dist/cursor/extension.d.ts +2 -2
  62. package/dist/cursor/extension.js +22 -4
  63. package/dist/cursor/extension.js.map +1 -1
  64. package/dist/cursor/extension.mjs +22 -4
  65. package/dist/cursor/extension.mjs.map +1 -1
  66. package/dist/edge-config.d.mts +1 -1
  67. package/dist/edge-config.d.ts +1 -1
  68. package/dist/edge-config.js +24 -1
  69. package/dist/edge-config.js.map +1 -1
  70. package/dist/edge-config.mjs +22 -1
  71. package/dist/edge-config.mjs.map +1 -1
  72. package/dist/edge-core/index.d.mts +1 -1
  73. package/dist/edge-core/index.d.ts +1 -1
  74. package/dist/edge-core/index.js +121 -16
  75. package/dist/edge-core/index.js.map +1 -1
  76. package/dist/edge-core/index.mjs +121 -16
  77. package/dist/edge-core/index.mjs.map +1 -1
  78. package/dist/{express-D_4hTn5Z.d.mts → express-CR6F3njD.d.mts} +1 -1
  79. package/dist/{express-BVd1_3FE.d.ts → express-DaqL11tx.d.ts} +1 -1
  80. package/dist/gateway/gateway.d.mts +2 -2
  81. package/dist/gateway/gateway.d.ts +2 -2
  82. package/dist/gateway/gateway.js +22 -4
  83. package/dist/gateway/gateway.js.map +1 -1
  84. package/dist/gateway/gateway.mjs +22 -4
  85. package/dist/gateway/gateway.mjs.map +1 -1
  86. package/dist/git-trigger/git-hooks.d.mts +2 -2
  87. package/dist/git-trigger/git-hooks.d.ts +2 -2
  88. package/dist/{index-BqHKlLOU.d.ts → index-B5Ts1WtM.d.ts} +1 -1
  89. package/dist/{index-T1aBoUcc.d.ts → index-BfIxCYIZ.d.ts} +1 -1
  90. package/dist/{index-BTeQ9fgV.d.mts → index-Cub7lK-T.d.mts} +1 -1
  91. package/dist/{index-DQb5-_1X.d.mts → index-CwAh7Tln.d.mts} +1 -1
  92. package/dist/index.d.mts +58 -9
  93. package/dist/index.d.ts +58 -9
  94. package/dist/index.js +218 -54
  95. package/dist/index.js.map +1 -1
  96. package/dist/index.mjs +214 -54
  97. package/dist/index.mjs.map +1 -1
  98. package/dist/local-evaluator/evaluator.d.mts +2 -2
  99. package/dist/local-evaluator/evaluator.d.ts +2 -2
  100. package/dist/metadata-capture.d.mts +35 -5
  101. package/dist/metadata-capture.d.ts +35 -5
  102. package/dist/metadata-capture.js +44 -6
  103. package/dist/metadata-capture.js.map +1 -1
  104. package/dist/metadata-capture.mjs +43 -6
  105. package/dist/metadata-capture.mjs.map +1 -1
  106. package/dist/{nextjs-w7RTbP1P.d.ts → nextjs-DL6uE1ye.d.ts} +1 -1
  107. package/dist/{nextjs-WyeVr2Kp.d.mts → nextjs-DlX6xwbU.d.mts} +1 -1
  108. package/dist/platform-signatures.d.mts +18 -1
  109. package/dist/platform-signatures.d.ts +18 -1
  110. package/dist/platform-signatures.js +35 -2
  111. package/dist/platform-signatures.js.map +1 -1
  112. package/dist/platform-signatures.mjs +35 -2
  113. package/dist/platform-signatures.mjs.map +1 -1
  114. package/dist/registration/index.js +25 -4
  115. package/dist/registration/index.js.map +1 -1
  116. package/dist/registration/index.mjs +25 -4
  117. package/dist/registration/index.mjs.map +1 -1
  118. package/dist/{sdk-CbTNIkAa.d.mts → sdk-Bbh9B_dw.d.mts} +1 -1
  119. package/dist/{sdk-QeX0Z8Ho.d.ts → sdk-C6SASmsh.d.ts} +1 -1
  120. package/dist/transport/index.d.mts +2 -2
  121. package/dist/transport/index.d.ts +2 -2
  122. package/dist/transport/index.js +22 -1
  123. package/dist/transport/index.js.map +1 -1
  124. package/dist/transport/index.mjs +22 -1
  125. package/dist/transport/index.mjs.map +1 -1
  126. package/dist/{types-r850cOt0.d.mts → types-C0zfSXMB.d.mts} +22 -0
  127. package/dist/{types-DqfPU5Bl.d.mts → types-C7Rb-w0d.d.mts} +1 -1
  128. package/dist/{types-sMxBT-nM.d.ts → types-DwSfO7Sb.d.ts} +22 -0
  129. package/dist/{types-BLUx92FJ.d.ts → types-nIh3HtuA.d.ts} +1 -1
  130. package/dist/ui/index.d.mts +1 -1
  131. package/dist/ui/index.d.ts +1 -1
  132. package/dist/verify.d.mts +1 -1
  133. package/dist/verify.d.ts +1 -1
  134. package/dist/verify.js +29 -11
  135. package/dist/verify.js.map +1 -1
  136. package/dist/verify.mjs +29 -11
  137. package/dist/verify.mjs.map +1 -1
  138. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * Version of the capture semantics (what is kept, dropped, reduced, or\n * derived — and under which key names).\n *\n * Capture semantics are FROZEN within a package major version: a given\n * `schemaVersion` always means the same field vocabulary and the same\n * keep/drop/reduce rules. The number bumps only on a SEMANTIC change to\n * capture (a renamed key, a changed reduction rule) — never for additive\n * signals, and never merely because the package version moved. To tell\n * builds apart, use the `sdkVersion` already present on the verify body;\n * `schemaVersion` answers the different question \"how do I interpret this\n * stored metadata?\".\n */\nexport const CAPTURE_SCHEMA_VERSION = 1;\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals (IP, ASN, country, TLS version, HTTP version,\n * device class, TLS fingerprint). Edge adapters populate this from the\n * platform's native connection surface — the authoritative source. SDK\n * adapters populate it as a fallback via {@link deriveConnectionFromHeaders}\n * when a CDN in front of the merchant injected the equivalent headers;\n * absent when neither source had anything.\n */\n connection?: Record<string, string>;\n /**\n * Version of the capture semantics this object was produced under — see\n * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with\n * metadata captured before the field existed.\n */\n schemaVersion?: number;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\nconst PLATFORM_HEADER_PREFIXES = ['x-goog-', 'x-stainless-', 'x-anthropic-', 'x-openai-'];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n\n/**\n * Precedence-ordered CDN header sources for each derived `connection` key.\n * First present header wins; the special `x-forwarded-for` source takes the\n * LEFTMOST (client) entry of the comma-separated chain.\n */\nconst CONNECTION_HEADER_SOURCES: ReadonlyArray<readonly [key: string, headers: readonly string[]]> =\n [\n ['ip', ['cf-connecting-ip', 'fastly-client-ip', 'true-client-ip', 'x-real-ip']],\n [\n 'country',\n ['cf-ipcountry', 'x-vercel-ip-country', 'cloudfront-viewer-country', 'fastly-geo-country'],\n ],\n ['region', ['x-vercel-ip-country-region', 'cloudfront-viewer-country-region']],\n ['city', ['x-vercel-ip-city', 'cloudfront-viewer-city']],\n ['asn', ['cloudfront-viewer-asn']],\n ['tlsVersion', ['cloudfront-viewer-tls']],\n ['httpVersion', ['cloudfront-viewer-http-version']],\n // TLS fingerprints can only be computed where TLS terminates. When a CDN\n // terminates TLS in front of the merchant, its fingerprint headers ARE the\n // connection-layer truth — normalising them here is the permanent design.\n ['tlsFingerprint', ['cf-ja3-hash']],\n ['ja4', ['cf-ja4']],\n ];\n\n/**\n * Derive the connection-layer block from CDN-injected request headers.\n *\n * When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,\n * Vercel, CloudFront), the CDN has already seen the connection layer and\n * injected it as headers. This promotes those headers to the same\n * `connection` keys the edge adapters populate natively, so SDK-only\n * deployments still get IP / geo / TLS attribution. Precedence per key:\n *\n * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >\n * `x-real-ip` > leftmost `x-forwarded-for`\n * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >\n * `cloudfront-viewer-country` > `fastly-geo-country`\n * - `region` / `city`: Vercel then CloudFront viewer headers\n * - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`\n * - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`\n *\n * Edge adapters keep their richer native collectors — native platform\n * signals are authoritative; this header derivation is the SDK-level\n * fallback. Header names are matched case-insensitively. Returns undefined\n * when no source header is present, so the `connection` key is simply\n * omitted (matching edge behaviour).\n */\nexport function deriveConnectionFromHeaders(\n headers: Record<string, string> | undefined | null\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const lower: Record<string, string> = {};\n for (const name of Object.keys(headers)) {\n const value = headers[name];\n if (typeof value === 'string' && value.length > 0) lower[name.toLowerCase()] = value;\n }\n\n const connection: Record<string, string> = {};\n for (const [key, sources] of CONNECTION_HEADER_SOURCES) {\n for (const source of sources) {\n const value = lower[source]?.trim();\n if (value) {\n connection[key] = value;\n break;\n }\n }\n }\n // ip fallback: leftmost (client) hop of the x-forwarded-for chain.\n if (!connection.ip) {\n const xff = lower['x-forwarded-for'];\n const client = xff?.split(',')[0]?.trim();\n if (client) connection.ip = client;\n }\n\n return Object.keys(connection).length > 0 ? connection : undefined;\n}\n\n/**\n * The FULL metadata capture for one request at an SDK adapter (express /\n * nextjs / mcp): sanitised headers, the safe credential format prefix, the\n * platform-signal subset, the CDN-derived connection block, and the capture\n * schema version. This is the ONE builder all SDK adapters share, so their\n * verify-access bodies agree byte-for-byte.\n *\n * Connection derivation runs over the RAW header map (pre-cap) — the size\n * caps guard storage growth and must never hide a geo/IP header that\n * happened to arrive late in an oversized map.\n */\nexport function buildSdkObservedMetadata(\n rawHeaders: Record<string, string | string[] | undefined> | undefined | null\n): ObservedMetadata {\n const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);\n const flat: Record<string, string> = {};\n if (rawHeaders) {\n for (const name of Object.keys(rawHeaders)) {\n const value = rawHeaders[name];\n if (value === undefined) continue;\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value);\n }\n }\n const connection = deriveConnectionFromHeaders(flat);\n return {\n headers,\n ...(apiKeyFormat && { apiKeyFormat }),\n ...(platformHeaders && { platformHeaders }),\n ...(connection && { connection }),\n schemaVersion: CAPTURE_SCHEMA_VERSION,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/metadata-capture.ts"],"sourcesContent":["/**\n * Maximal metadata capture — the shared, edge-safe sanitiser.\n *\n * AstraSync's thesis is total metadata capture: every header and connection\n * signal an inbound agent presents is a signal we want to keep. This module is\n * the ONE place that decides what is safe to store verbatim, what is a secret\n * to drop, and what credential headers reduce to a safe *format prefix* (a\n * platform signal, never the secret itself).\n *\n * It runs in three places that must agree byte-for-byte:\n * - the edge adapter (`@astrasyncai/adapter-lambda`) — the richest capture\n * point (full CloudFront header arrays),\n * - the SDK adapters (express / nextjs) — for SDK-only merchants with no edge,\n * - (indirectly) the backend, which stores whatever the above forward.\n *\n * Pure — no Node built-ins, no I/O — so it is safe in Lambda@Edge, the browser\n * build, and Deno. Capture is cheap: at the edge and SDK it is local string\n * work, and the backend stores it on a fire-and-forget event insert, so it adds\n * ZERO verify-access decision latency. Detection (which few signals imply which\n * vendor) is a separate, curated concern — see `platform-signatures.ts`.\n */\n\n/**\n * Version of the capture semantics (what is kept, dropped, reduced, or\n * derived — and under which key names).\n *\n * Capture semantics are FROZEN within a package major version: a given\n * `schemaVersion` always means the same field vocabulary and the same\n * keep/drop/reduce rules. The number bumps only on a SEMANTIC change to\n * capture (a renamed key, a changed reduction rule) — never for additive\n * signals, and never merely because the package version moved. To tell\n * builds apart, use the `sdkVersion` already present on the verify body;\n * `schemaVersion` answers the different question \"how do I interpret this\n * stored metadata?\".\n */\nexport const CAPTURE_SCHEMA_VERSION = 1;\n\n/**\n * The captured, sanitised view of an inbound agent's request metadata. Attached\n * to verify-access / beacon payloads as `callerMetadata.observedMetadata`.\n */\nexport interface ObservedMetadata {\n /** Header name → value, secrets removed, verbatim otherwise. */\n headers: Record<string, string>;\n /**\n * Safe key-format prefix of a credential header (e.g. `sk-ant-api03`,\n * `sk-proj`, `AIza`) — a platform signal, NEVER the secret. Absent if no\n * credential header was present or none matched a known format.\n */\n apiKeyFormat?: string;\n /**\n * The subset of `headers` that are known platform-signal headers\n * (`openai-organization`, `x-goog-user-project`, `anthropic-version`, …),\n * pulled out for convenient detection + display. Values are the same\n * sanitised strings as in `headers`.\n */\n platformHeaders?: Record<string, string>;\n /**\n * Connection-layer signals (IP, ASN, country, TLS version, HTTP version,\n * device class, TLS fingerprint). Edge adapters populate this from the\n * platform's native connection surface — the authoritative source. SDK\n * adapters populate it as a fallback via {@link deriveConnectionFromHeaders}\n * when a CDN in front of the merchant injected the equivalent headers;\n * absent when neither source had anything.\n */\n connection?: Record<string, string>;\n /**\n * Number of cookie pairs the caller sent. The `cookie` header VALUE is a\n * secret and is always dropped; the COUNT is a client-shape signal\n * (browsers carry cookies, most automation carries none) that would\n * otherwise be unobservable downstream. Absent when no cookie header\n * arrived.\n */\n cookieCount?: number;\n /**\n * Query-string parameter NAMES (deduped, lowercased) — values are NEVER\n * captured (they routinely carry OAuth codes, tokens, and PII, and no\n * deny-list is complete). Names alone fingerprint client shape. Populated\n * by the edge path, which sees the raw query string.\n */\n queryKeys?: string[];\n /**\n * Version of the capture semantics this object was produced under — see\n * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with\n * metadata captured before the field existed.\n */\n schemaVersion?: number;\n}\n\n/**\n * Result of sanitising a raw header map. `headers` is always present (possibly\n * empty); `apiKeyFormat` / `platformHeaders` only when a signal was found.\n */\nexport interface SanitizeHeadersResult {\n headers: Record<string, string>;\n apiKeyFormat?: string;\n platformHeaders?: Record<string, string>;\n /** Cookie-pair count; the cookie value itself is always dropped. */\n cookieCount?: number;\n}\n\n/**\n * Header names dropped entirely — genuine secrets or our own credentials that\n * describe the request's transport, not the agent. Matched case-insensitively\n * on the exact header name.\n */\nconst SECRET_HEADER_NAMES = new Set([\n 'cookie',\n 'set-cookie',\n 'x-csrf-token',\n 'proxy-authorization',\n 'x-astrasync-signature',\n 'x-astrasync-secret',\n 'x-hub-signature',\n 'x-hub-signature-256',\n // Short-lived verify dedupe token — must never persist in stored metadata.\n 'x-astra-verified-hop',\n]);\n\n/**\n * Header-name prefixes dropped entirely — our own credential / signing surface\n * (`x-astrasync-*`, `kya-*`) must never round-trip into stored metadata.\n */\nconst SECRET_HEADER_PREFIXES = ['x-astrasync-', 'kya-', 'kya_'];\n\n/**\n * Credential headers whose VALUE is a secret but whose FORMAT is a signal. We\n * keep the leading format prefix (up to the random tail) and drop the rest.\n */\nconst CREDENTIAL_HEADER_NAMES = new Set([\n 'authorization',\n 'x-api-key',\n 'x-goog-api-key',\n 'api-key',\n]);\n\n/**\n * Header names that carry a raw signature value (drop the value; the presence\n * is not worth a bloat risk). Matched as a suffix so `x-*-signature` variants\n * are covered without an exhaustive list.\n */\nfunction isSignatureHeader(name: string): boolean {\n return name === 'signature' || name.endsWith('-signature') || name.endsWith('-signature-256');\n}\n\n/**\n * Known platform-signal headers — pulled into `platformHeaders` for convenient\n * detection + display. Exact names plus a few prefixes (`x-goog-`,\n * `x-stainless-` — the OpenAI/Anthropic SDK client fingerprint headers).\n */\nconst PLATFORM_HEADER_NAMES = new Set([\n 'openai-organization',\n 'openai-project',\n 'openai-version',\n 'openai-processing-ms',\n 'anthropic-version',\n 'anthropic-beta',\n 'x-goog-user-project',\n 'x-goog-api-client',\n 'user-agent',\n 'x-agent-card-url',\n 'x-agent-provider',\n]);\n// `sec-ch-*` client hints are platform signals: Chromium forks (agentic\n// browsers included) often carry their real brand in `sec-ch-ua` even when\n// the UA string mimics stock Chrome.\nconst PLATFORM_HEADER_PREFIXES = [\n 'x-goog-',\n 'x-stainless-',\n 'x-anthropic-',\n 'x-openai-',\n 'sec-ch-',\n];\n\n/**\n * Known API-key format prefixes, longest-first so `sk-ant-api03` wins over\n * `sk-ant` and `sk-`. Each is the SAFE, non-secret leading token of a\n * credential; the random tail is never captured.\n */\nconst KEY_FORMAT_PREFIXES = [\n 'sk-ant-api03',\n 'sk-ant-api',\n 'sk-ant',\n 'sk-proj',\n 'sk-svcacct',\n 'sk-or-v1',\n 'sk-or',\n 'sk-lf',\n 'sk-',\n 'gsk_',\n 'aiza',\n 'ya29',\n 'ghp_',\n 'gho_',\n 'github_pat_',\n 'xai-',\n 'pplx-',\n 'r8_',\n 'hf_',\n 'astra-',\n 'astrae-',\n];\n\n/** Size caps — the only guard against jsonb bloat from a hostile POST. */\nexport const MAX_HEADERS = 64;\nexport const MAX_HEADER_VALUE_BYTES = 1024;\nexport const MAX_TOTAL_BYTES = 16 * 1024;\n\nfunction byteLength(s: string): number {\n // Edge-safe (no Buffer): count UTF-8 bytes without allocating a Buffer.\n let bytes = 0;\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i);\n if (code < 0x80) bytes += 1;\n else if (code < 0x800) bytes += 2;\n else if (code >= 0xd800 && code <= 0xdbff) {\n bytes += 4;\n i++; // surrogate pair\n } else bytes += 3;\n }\n return bytes;\n}\n\n/**\n * Extract the safe key-format prefix from a credential header value. Strips a\n * leading `Bearer ` / `Basic ` scheme, then returns the longest known prefix\n * that the (lowercased) value starts with. Returns undefined for opaque values\n * (e.g. a bare UUID token) so we never guess a format we don't recognise.\n */\nexport function extractApiKeyFormat(rawValue: string): string | undefined {\n if (!rawValue) return undefined;\n let value = rawValue.trim();\n const spaceIdx = value.indexOf(' ');\n if (spaceIdx > 0) {\n const scheme = value.slice(0, spaceIdx).toLowerCase();\n if (scheme === 'bearer' || scheme === 'basic' || scheme === 'token') {\n value = value.slice(spaceIdx + 1).trim();\n }\n }\n const lower = value.toLowerCase();\n for (const prefix of KEY_FORMAT_PREFIXES) {\n if (lower.startsWith(prefix)) {\n // Return the prefix as-declared in the list (canonical casing).\n return prefix;\n }\n }\n return undefined;\n}\n\nfunction isSecretHeader(name: string): boolean {\n if (SECRET_HEADER_NAMES.has(name)) return true;\n if (SECRET_HEADER_PREFIXES.some((p) => name.startsWith(p))) return true;\n if (isSignatureHeader(name)) return true;\n return false;\n}\n\nfunction isPlatformHeader(name: string): boolean {\n if (PLATFORM_HEADER_NAMES.has(name)) return true;\n return PLATFORM_HEADER_PREFIXES.some((p) => name.startsWith(p));\n}\n\n/**\n * Sanitise a raw header map for storage. Deny-lists genuine secrets, reduces\n * credential headers to a safe format prefix, keeps everything else verbatim,\n * and enforces the size caps that bound jsonb growth.\n *\n * Accepts any string→(string|string[]|undefined) map (Node's `req.headers`,\n * CloudFront's flattened headers, a plain object). Multi-value headers are\n * joined with `, ` per RFC 7230.\n */\nexport function sanitizeHeaders(\n raw: Record<string, string | string[] | undefined> | undefined | null\n): SanitizeHeadersResult {\n const headers: Record<string, string> = {};\n const platformHeaders: Record<string, string> = {};\n let apiKeyFormat: string | undefined;\n let cookieCount: number | undefined;\n let totalBytes = 0;\n let count = 0;\n\n if (!raw) return { headers };\n\n for (const rawName of Object.keys(raw)) {\n if (count >= MAX_HEADERS) break;\n const name = rawName.toLowerCase();\n const rawValue = raw[rawName];\n if (rawValue === undefined) continue;\n const joined = Array.isArray(rawValue) ? rawValue.join(', ') : String(rawValue);\n\n // Credential headers: capture the format prefix, drop the secret.\n if (CREDENTIAL_HEADER_NAMES.has(name)) {\n const fmt = extractApiKeyFormat(joined);\n if (fmt && !apiKeyFormat) apiKeyFormat = fmt;\n continue;\n }\n\n // Cookie: the value is a secret (dropped below), but the PAIR COUNT is a\n // client-shape signal worth keeping.\n if (name === 'cookie') {\n cookieCount = joined.split(';').filter((s) => s.trim().length > 0).length;\n }\n\n // Genuine secrets: drop entirely.\n if (isSecretHeader(name)) continue;\n\n // Everything else describing the agent: keep, bounded.\n let value = joined;\n if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {\n value = value.slice(0, MAX_HEADER_VALUE_BYTES);\n }\n const entryBytes = byteLength(name) + byteLength(value);\n if (totalBytes + entryBytes > MAX_TOTAL_BYTES) continue;\n totalBytes += entryBytes;\n count++;\n headers[name] = value;\n if (isPlatformHeader(name)) platformHeaders[name] = value;\n }\n\n const result: SanitizeHeadersResult = { headers };\n if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;\n if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;\n if (cookieCount !== undefined) result.cookieCount = cookieCount;\n return result;\n}\n\n/**\n * Pull the known platform-signal headers out of an ALREADY-sanitised header\n * map. `sanitizeHeaders` already computes this as a by-product; this standalone\n * form is exported for call-sites (edge, tests) that hold a sanitised map and\n * want only the platform subset. Never re-run over raw (unsanitised) headers.\n */\nexport function extractPlatformHeaders(\n sanitized: Record<string, string> | undefined | null\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (!sanitized) return out;\n for (const name of Object.keys(sanitized)) {\n if (isPlatformHeader(name.toLowerCase())) out[name.toLowerCase()] = sanitized[name];\n }\n return out;\n}\n\n/**\n * Precedence-ordered CDN header sources for each derived `connection` key.\n * First present header wins; the special `x-forwarded-for` source takes the\n * LEFTMOST (client) entry of the comma-separated chain.\n */\nconst CONNECTION_HEADER_SOURCES: ReadonlyArray<readonly [key: string, headers: readonly string[]]> =\n [\n ['ip', ['cf-connecting-ip', 'fastly-client-ip', 'true-client-ip', 'x-real-ip']],\n [\n 'country',\n ['cf-ipcountry', 'x-vercel-ip-country', 'cloudfront-viewer-country', 'fastly-geo-country'],\n ],\n ['region', ['x-vercel-ip-country-region', 'cloudfront-viewer-country-region']],\n ['countryRegionName', ['cloudfront-viewer-country-region-name']],\n ['city', ['x-vercel-ip-city', 'cloudfront-viewer-city']],\n ['latitude', ['x-vercel-ip-latitude', 'cloudfront-viewer-latitude']],\n ['longitude', ['x-vercel-ip-longitude', 'cloudfront-viewer-longitude']],\n ['postalCode', ['x-vercel-ip-postal-code', 'cloudfront-viewer-postal-code']],\n ['asn', ['cloudfront-viewer-asn']],\n ['tlsVersion', ['cloudfront-viewer-tls']],\n ['httpVersion', ['cloudfront-viewer-http-version']],\n ['timeZone', ['x-vercel-ip-timezone', 'cloudfront-viewer-time-zone']],\n ['metroCode', ['cloudfront-viewer-metro-code']],\n // TLS fingerprints can only be computed where TLS terminates. When a CDN\n // terminates TLS in front of the merchant, its fingerprint headers ARE the\n // connection-layer truth — normalising them here is the permanent design.\n // CloudFront delivers JA3/JA4 only via an origin request policy and only\n // for HTTPS viewer connections.\n ['tlsFingerprint', ['cf-ja3-hash', 'cloudfront-viewer-ja3-fingerprint']],\n ['ja4', ['cf-ja4', 'cloudfront-viewer-ja4-fingerprint']],\n // Header-structure fingerprint: browser/SDK-distinctive header ordering.\n ['headerOrder', ['cloudfront-viewer-header-order']],\n ['headerCount', ['cloudfront-viewer-header-count']],\n ];\n\n/**\n * Backend bound: observedMetadata.connection values longer than this fail\n * the platform's zod parse outright (boundedStringRecord(32, 256)), so every\n * emitter truncates — headerOrder routinely exceeds it.\n */\nexport const MAX_CONNECTION_VALUE_CHARS = 256;\n\n/**\n * Derive the connection-layer block from CDN-injected request headers.\n *\n * When a merchant's SDK-gated origin sits behind a CDN (Cloudflare, Fastly,\n * Vercel, CloudFront), the CDN has already seen the connection layer and\n * injected it as headers. This promotes those headers to the same\n * `connection` keys the edge adapters populate natively, so SDK-only\n * deployments still get IP / geo / TLS attribution. Precedence per key:\n *\n * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >\n * `x-real-ip` > `cloudfront-viewer-address` (port stripped) > leftmost\n * `x-forwarded-for`\n * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >\n * `cloudfront-viewer-country` > `fastly-geo-country`\n * - `region` / `city` / `timeZone`: Vercel then CloudFront viewer headers\n * - `countryRegionName` / `asn` / `tlsVersion` / `httpVersion` /\n * `metroCode` / `headerOrder` / `headerCount`: `cloudfront-viewer-*`\n * - `tlsFingerprint`: `cf-ja3-hash` > `cloudfront-viewer-ja3-fingerprint`\n * - `ja4`: `cf-ja4` > `cloudfront-viewer-ja4-fingerprint`\n *\n * All keys are additive under `schemaVersion` 1 (the frozen-capture doctrine\n * bumps only for changed semantics, never for additive signals). Values are\n * truncated to MAX_CONNECTION_VALUE_CHARS — the platform's hard bound.\n *\n * Edge adapters keep their richer native collectors — native platform\n * signals are authoritative; this header derivation is the SDK-level\n * fallback. Header names are matched case-insensitively. Returns undefined\n * when no source header is present, so the `connection` key is simply\n * omitted (matching edge behaviour).\n */\nexport function deriveConnectionFromHeaders(\n headers: Record<string, string> | undefined | null\n): Record<string, string> | undefined {\n if (!headers) return undefined;\n const lower: Record<string, string> = {};\n for (const name of Object.keys(headers)) {\n const value = headers[name];\n if (typeof value === 'string' && value.length > 0) lower[name.toLowerCase()] = value;\n }\n\n const connection: Record<string, string> = {};\n for (const [key, sources] of CONNECTION_HEADER_SOURCES) {\n for (const source of sources) {\n const value = lower[source]?.trim();\n if (value) {\n connection[key] = value.slice(0, MAX_CONNECTION_VALUE_CHARS);\n break;\n }\n }\n }\n // ip fallback 1: cloudfront-viewer-address arrives as ip:port — strip the\n // trailing port only (IPv6-safe), unlike the dedicated client-ip headers.\n // The port itself is kept as its own key: ephemeral-port patterns signal\n // NAT/proxy connection reuse.\n {\n const addr = lower['cloudfront-viewer-address']?.trim();\n const portMatch = addr?.match(/:(\\d+)$/);\n if (portMatch) connection.sourcePort = portMatch[1];\n if (!connection.ip && addr) connection.ip = addr.replace(/:\\d+$/, '');\n }\n // ip fallback 2: leftmost (client) hop of the x-forwarded-for chain.\n if (!connection.ip) {\n const xff = lower['x-forwarded-for'];\n const client = xff?.split(',')[0]?.trim();\n if (client) connection.ip = client;\n }\n // tlsCipher: CloudFront folds the cipher into the viewer-tls string\n // (`TLSv1.3:TLS_AES_128_GCM_SHA256:fullHandshake`). `tlsVersion` stays\n // verbatim (reducing it would be a semantic change → schema v2); the\n // cipher is pulled out additively so it is queryable on its own.\n if (!connection.tlsCipher) {\n const cipher = connection.tlsVersion?.split(':')[1];\n if (cipher) connection.tlsCipher = cipher;\n }\n\n return Object.keys(connection).length > 0 ? connection : undefined;\n}\n\n/**\n * The FULL metadata capture for one request at an SDK adapter (express /\n * nextjs / mcp): sanitised headers, the safe credential format prefix, the\n * platform-signal subset, the CDN-derived connection block, and the capture\n * schema version. This is the ONE builder all SDK adapters share, so their\n * verify-access bodies agree byte-for-byte.\n *\n * Connection derivation runs over the RAW header map (pre-cap) — the size\n * caps guard storage growth and must never hide a geo/IP header that\n * happened to arrive late in an oversized map.\n */\nexport function buildSdkObservedMetadata(\n rawHeaders: Record<string, string | string[] | undefined> | undefined | null\n): ObservedMetadata {\n const { headers, apiKeyFormat, platformHeaders, cookieCount } = sanitizeHeaders(rawHeaders);\n const flat: Record<string, string> = {};\n if (rawHeaders) {\n for (const name of Object.keys(rawHeaders)) {\n const value = rawHeaders[name];\n if (value === undefined) continue;\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(', ') : String(value);\n }\n }\n const connection = deriveConnectionFromHeaders(flat);\n return {\n headers,\n ...(apiKeyFormat && { apiKeyFormat }),\n ...(platformHeaders && { platformHeaders }),\n ...(connection && { connection }),\n ...(cookieCount !== undefined && { cookieCount }),\n schemaVersion: CAPTURE_SCHEMA_VERSION,\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import * as next_server from 'next/server';
2
2
  import { NextRequest } from 'next/server';
3
- import { N as NextJsMiddlewareOptions } from './types-sMxBT-nM.js';
3
+ import { N as NextJsMiddlewareOptions } from './types-DwSfO7Sb.js';
4
4
 
5
5
  /**
6
6
  * Create Next.js middleware for agent verification.
@@ -1,6 +1,6 @@
1
1
  import * as next_server from 'next/server';
2
2
  import { NextRequest } from 'next/server';
3
- import { N as NextJsMiddlewareOptions } from './types-r850cOt0.mjs';
3
+ import { N as NextJsMiddlewareOptions } from './types-C0zfSXMB.mjs';
4
4
 
5
5
  /**
6
6
  * Create Next.js middleware for agent verification.
@@ -12,7 +12,7 @@
12
12
  * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for
13
13
  * v2.9 — TLS fingerprinting / behavioural signatures are deferred.
14
14
  */
15
- type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'unknown';
15
+ type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-sdk' | 'unknown';
16
16
  interface PlatformFingerprint {
17
17
  vendor: PlatformAgentVendor;
18
18
  /** A stable identifier used as the orphan-agent dedup key. */
@@ -35,6 +35,19 @@ interface PlatformDetectionInput {
35
35
  * against `headerPatterns` (which test on `name` and `name: value`).
36
36
  */
37
37
  platformHeaders?: Record<string, string>;
38
+ /**
39
+ * The caller's JA4 TLS-client fingerprint (from the connection capture).
40
+ * Matched against `ja4Patterns`. A JA4 identifies the client TLS stack,
41
+ * so signatures should pair it with other evidence when attributing a
42
+ * vendor.
43
+ */
44
+ ja4?: string;
45
+ /**
46
+ * The caller's origin ASN (from the connection capture). Matched against
47
+ * `asnList` by exact membership. An ASN alone attributes an entire cloud
48
+ * provider — registry validation rejects ASN-only signatures.
49
+ */
50
+ asn?: string;
38
51
  }
39
52
  /**
40
53
  * A signature definition, generic over the vendor key. The seed constant
@@ -59,6 +72,10 @@ interface PlatformSignatureDef<V extends string = string> {
59
72
  * `/x-goog-user-project/i` match on presence).
60
73
  */
61
74
  headerPatterns?: RegExp[];
75
+ /** Patterns tested against the observed JA4 TLS-client fingerprint. */
76
+ ja4Patterns?: RegExp[];
77
+ /** Exact ASN strings this signature matches (never regex). */
78
+ asnList?: string[];
62
79
  }
63
80
  /**
64
81
  * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.
@@ -12,7 +12,7 @@
12
12
  * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for
13
13
  * v2.9 — TLS fingerprinting / behavioural signatures are deferred.
14
14
  */
15
- type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'unknown';
15
+ type PlatformAgentVendor = 'claude' | 'chatgpt' | 'gemini' | 'cursor' | 'goose' | 'perplexity' | 'chatgpt-atlas' | 'perplexity-comet' | 'astrasync-sdk' | 'unknown';
16
16
  interface PlatformFingerprint {
17
17
  vendor: PlatformAgentVendor;
18
18
  /** A stable identifier used as the orphan-agent dedup key. */
@@ -35,6 +35,19 @@ interface PlatformDetectionInput {
35
35
  * against `headerPatterns` (which test on `name` and `name: value`).
36
36
  */
37
37
  platformHeaders?: Record<string, string>;
38
+ /**
39
+ * The caller's JA4 TLS-client fingerprint (from the connection capture).
40
+ * Matched against `ja4Patterns`. A JA4 identifies the client TLS stack,
41
+ * so signatures should pair it with other evidence when attributing a
42
+ * vendor.
43
+ */
44
+ ja4?: string;
45
+ /**
46
+ * The caller's origin ASN (from the connection capture). Matched against
47
+ * `asnList` by exact membership. An ASN alone attributes an entire cloud
48
+ * provider — registry validation rejects ASN-only signatures.
49
+ */
50
+ asn?: string;
38
51
  }
39
52
  /**
40
53
  * A signature definition, generic over the vendor key. The seed constant
@@ -59,6 +72,10 @@ interface PlatformSignatureDef<V extends string = string> {
59
72
  * `/x-goog-user-project/i` match on presence).
60
73
  */
61
74
  headerPatterns?: RegExp[];
75
+ /** Patterns tested against the observed JA4 TLS-client fingerprint. */
76
+ ja4Patterns?: RegExp[];
77
+ /** Exact ASN strings this signature matches (never regex). */
78
+ asnList?: string[];
62
79
  }
63
80
  /**
64
81
  * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.
@@ -68,12 +68,43 @@ var PLATFORM_AGENT_SIGNATURES = [
68
68
  uaPatterns: [/perplexity/i],
69
69
  agentCardPatterns: [/perplexity\.ai/i],
70
70
  apiKeyFormatPatterns: [/^pplx-/i]
71
+ },
72
+ // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must
73
+ // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the
74
+ // browser product is a different surface than the assistant service, and
75
+ // the DB registry mirrors one row per vendor. These sit after the primary
76
+ // vendor entries so a UA carrying an explicit vendor token keeps its
77
+ // primary attribution; a browser-shaped UA with the agentic token is
78
+ // caught here BEFORE classify's human tier, which is what makes these
79
+ // visits observable at all. Token drift is real (some builds mimic Chrome
80
+ // outright) — re-verify against captured events when a new browser ships,
81
+ // and check `sec-ch-ua` platformHeaders for the real brand.
82
+ {
83
+ vendor: "chatgpt-atlas",
84
+ displayName: "ChatGPT Atlas browser",
85
+ uaPatterns: [/\batlas\/\d/i],
86
+ headerPatterns: [/^sec-ch-ua: .*atlas/i]
87
+ },
88
+ {
89
+ vendor: "perplexity-comet",
90
+ displayName: "Perplexity Comet browser",
91
+ uaPatterns: [/\bcomet\/\d/i],
92
+ headerPatterns: [/^sec-ch-ua: .*comet/i]
93
+ },
94
+ // Our own SDK self-identifies (sdkFetch stamps this UA) — first-party
95
+ // traffic must never pollute the unknown/anonymous pool it exists to audit.
96
+ {
97
+ vendor: "astrasync-sdk",
98
+ displayName: "AstraSync SDK",
99
+ uaPatterns: [/^astrasync-sdk\//i]
71
100
  }
72
101
  ];
73
102
  function matchPlatformSignature(signatures, input) {
74
103
  const ua = input.userAgent ?? "";
75
104
  const cardUrl = input.agentCardUrl ?? "";
76
105
  const apiKeyFormat = input.apiKeyFormat ?? "";
106
+ const ja4 = input.ja4 ?? "";
107
+ const asn = input.asn ?? "";
77
108
  const headerLines = [];
78
109
  if (input.platformHeaders) {
79
110
  for (const [name, value] of Object.entries(input.platformHeaders)) {
@@ -85,7 +116,9 @@ function matchPlatformSignature(signatures, input) {
85
116
  const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));
86
117
  const keyMatch = apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));
87
118
  const headerMatch = headerLines.length > 0 && sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));
88
- if (uaMatch || cardMatch || keyMatch || headerMatch) {
119
+ const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));
120
+ const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);
121
+ if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {
89
122
  const uaBase = ua.split(/[\s/]/)[0]?.toLowerCase() ?? "";
90
123
  const cardHost = (() => {
91
124
  try {
@@ -94,7 +127,7 @@ function matchPlatformSignature(signatures, input) {
94
127
  return "";
95
128
  }
96
129
  })();
97
- const keyBasis = uaBase || cardHost || apiKeyFormat || "unknown";
130
+ const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || "unknown";
98
131
  return {
99
132
  vendor: sig.vendor,
100
133
  fingerprintKey: `${sig.vendor}:${keyBasis}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/platform-signatures.ts"],"sourcesContent":["/**\n * Platform-agent fingerprint signatures — the shared registry of known\n * platform-agent UA / agent-card patterns (Claude / ChatGPT / Gemini /\n * Cursor / Goose).\n *\n * Single source of truth, lifted verbatim from the backend's\n * `platform-agent-detector.service` (which now imports detection from\n * here): the backend's verify-access anonymous handler and the edge\n * adapter (`@astrasyncai/adapter-lambda`) must classify identically or\n * the three-tier visibility numbers drift between capture points.\n *\n * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for\n * v2.9 — TLS fingerprinting / behavioural signatures are deferred.\n */\n\nexport type PlatformAgentVendor =\n | 'claude'\n | 'chatgpt'\n | 'gemini'\n | 'cursor'\n | 'goose'\n | 'perplexity'\n | 'unknown';\n\nexport interface PlatformFingerprint {\n vendor: PlatformAgentVendor;\n /** A stable identifier used as the orphan-agent dedup key. */\n fingerprintKey: string;\n /** Human-readable name surfaced in the admin panel. */\n displayName: string;\n}\n\nexport interface PlatformDetectionInput {\n userAgent?: string;\n agentCardUrl?: string;\n /**\n * A safe API-key format prefix observed on the caller's\n * credential header (e.g. `sk-ant-api03`, `sk-proj`, `aiza`). A strong\n * platform signal; never the secret. Matched against `apiKeyFormatPatterns`.\n */\n apiKeyFormat?: string;\n /**\n * The sanitised platform-signal headers (lowercased names →\n * values), e.g. `openai-organization`, `x-goog-user-project`. Matched\n * against `headerPatterns` (which test on `name` and `name: value`).\n */\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * A signature definition, generic over the vendor key. The seed constant\n * below uses the closed `PlatformAgentVendor` union; DB-promoted signatures\n * (backend dynamic registry, console round) carry arbitrary string vendors —\n * `matchPlatformSignature` accepts both.\n */\nexport interface PlatformSignatureDef<V extends string = string> {\n vendor: V;\n displayName: string;\n uaPatterns?: RegExp[];\n agentCardPatterns?: RegExp[];\n /**\n * Patterns tested against an observed API-key format prefix\n * (e.g. `/^sk-ant/i` → claude). A credential-header format is a stronger\n * platform signal than a spoofable User-Agent.\n */\n apiKeyFormatPatterns?: RegExp[];\n /**\n * Patterns tested against each observed platform-signal\n * header, as both `name` and `name: value` (so `/openai-organization/i` and\n * `/x-goog-user-project/i` match on presence).\n */\n headerPatterns?: RegExp[];\n}\n\n/**\n * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.\n * Structurally a superset of `PlatformFingerprint`.\n */\nexport interface DynamicPlatformFingerprint {\n vendor: string;\n fingerprintKey: string;\n displayName: string;\n}\n\n/**\n * Registry of known platform-agent signatures. Order matters — first match wins.\n *\n * Patterns are intentionally permissive (case-insensitive substring match) since\n * platform UA strings drift between versions. False positives are tolerable —\n * the orphan record is informational, not enforcement.\n */\nexport const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>> = [\n {\n vendor: 'claude',\n displayName: 'Anthropic Claude',\n uaPatterns: [/claude/i, /anthropic/i],\n agentCardPatterns: [/anthropic\\.com/i, /claude\\.ai/i],\n apiKeyFormatPatterns: [/^sk-ant/i],\n headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],\n },\n {\n vendor: 'chatgpt',\n displayName: 'OpenAI ChatGPT',\n uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\\d/i],\n agentCardPatterns: [/openai\\.com/i, /chatgpt\\.com/i],\n apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],\n headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i],\n },\n {\n vendor: 'gemini',\n displayName: 'Google Gemini',\n uaPatterns: [/gemini/i, /google-gemini/i, /bard/i],\n agentCardPatterns: [/gemini\\.google\\.com/i, /ai\\.google\\.dev/i],\n apiKeyFormatPatterns: [/^aiza/i],\n headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],\n },\n {\n vendor: 'cursor',\n displayName: 'Cursor',\n uaPatterns: [/cursor/i],\n agentCardPatterns: [/cursor\\.sh/i, /cursor\\.com/i],\n },\n {\n vendor: 'goose',\n displayName: 'Block Goose',\n uaPatterns: [/goose/i, /block-goose/i],\n agentCardPatterns: [/block\\.xyz\\/goose/i, /goose\\.tools/i],\n },\n {\n vendor: 'perplexity',\n displayName: 'Perplexity',\n uaPatterns: [/perplexity/i],\n agentCardPatterns: [/perplexity\\.ai/i],\n apiKeyFormatPatterns: [/^pplx-/i],\n },\n];\n\n/**\n * Generic first-match-wins matcher over an arbitrary signature set. The\n * backend's dynamic registry merges the seed constant with DB-promoted\n * signatures and calls this — ONE matching implementation everywhere\n * (fingerprint-key rule included: vendor + lowercased UA basename before\n * the first space/slash, falling back to the agent-card host).\n */\nexport function matchPlatformSignature(\n signatures: ReadonlyArray<PlatformSignatureDef>,\n input: PlatformDetectionInput\n): DynamicPlatformFingerprint | undefined {\n const ua = input.userAgent ?? '';\n const cardUrl = input.agentCardUrl ?? '';\n const apiKeyFormat = input.apiKeyFormat ?? '';\n // Flatten platform headers to `name` and `name: value` lines for matching.\n const headerLines: string[] = [];\n if (input.platformHeaders) {\n for (const [name, value] of Object.entries(input.platformHeaders)) {\n headerLines.push(name, `${name}: ${value}`);\n }\n }\n\n for (const sig of signatures) {\n const uaMatch = sig.uaPatterns?.some((p) => p.test(ua));\n const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));\n const keyMatch =\n apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));\n const headerMatch =\n headerLines.length > 0 &&\n sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));\n if (uaMatch || cardMatch || keyMatch || headerMatch) {\n const uaBase = ua.split(/[\\s/]/)[0]?.toLowerCase() ?? '';\n const cardHost = (() => {\n try {\n return cardUrl ? new URL(cardUrl).host : '';\n } catch {\n return '';\n }\n })();\n // Prefer a UA/card basename for the dedup key; fall back to the credential\n // format (still a stable per-vendor key) when only a key/header matched.\n const keyBasis = uaBase || cardHost || apiKeyFormat || 'unknown';\n return {\n vendor: sig.vendor,\n fingerprintKey: `${sig.vendor}:${keyBasis}`,\n displayName: sig.displayName,\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Inspect caller metadata for a known SEED platform-agent fingerprint.\n * Returns undefined if no signature matches. (Backends with the dynamic\n * registry should call `matchPlatformSignature` over their merged set\n * instead — same semantics, live signature additions included.)\n */\nexport function detectPlatformFingerprint(\n input: PlatformDetectionInput\n): PlatformFingerprint | undefined {\n // Seed vendors are all members of the PlatformAgentVendor union, so the\n // narrowing cast is sound by construction.\n return matchPlatformSignature(PLATFORM_AGENT_SIGNATURES, input) as\n | PlatformFingerprint\n | undefined;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/platform-signatures.ts"],"sourcesContent":["/**\n * Platform-agent fingerprint signatures — the shared registry of known\n * platform-agent UA / agent-card patterns (Claude / ChatGPT / Gemini /\n * Cursor / Goose).\n *\n * Single source of truth, lifted verbatim from the backend's\n * `platform-agent-detector.service` (which now imports detection from\n * here): the backend's verify-access anonymous handler and the edge\n * adapter (`@astrasyncai/adapter-lambda`) must classify identically or\n * the three-tier visibility numbers drift between capture points.\n *\n * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for\n * v2.9 — TLS fingerprinting / behavioural signatures are deferred.\n */\n\nexport type PlatformAgentVendor =\n | 'claude'\n | 'chatgpt'\n | 'gemini'\n | 'cursor'\n | 'goose'\n | 'perplexity'\n | 'chatgpt-atlas'\n | 'perplexity-comet'\n | 'astrasync-sdk'\n | 'unknown';\n\nexport interface PlatformFingerprint {\n vendor: PlatformAgentVendor;\n /** A stable identifier used as the orphan-agent dedup key. */\n fingerprintKey: string;\n /** Human-readable name surfaced in the admin panel. */\n displayName: string;\n}\n\nexport interface PlatformDetectionInput {\n userAgent?: string;\n agentCardUrl?: string;\n /**\n * A safe API-key format prefix observed on the caller's\n * credential header (e.g. `sk-ant-api03`, `sk-proj`, `aiza`). A strong\n * platform signal; never the secret. Matched against `apiKeyFormatPatterns`.\n */\n apiKeyFormat?: string;\n /**\n * The sanitised platform-signal headers (lowercased names →\n * values), e.g. `openai-organization`, `x-goog-user-project`. Matched\n * against `headerPatterns` (which test on `name` and `name: value`).\n */\n platformHeaders?: Record<string, string>;\n /**\n * The caller's JA4 TLS-client fingerprint (from the connection capture).\n * Matched against `ja4Patterns`. A JA4 identifies the client TLS stack,\n * so signatures should pair it with other evidence when attributing a\n * vendor.\n */\n ja4?: string;\n /**\n * The caller's origin ASN (from the connection capture). Matched against\n * `asnList` by exact membership. An ASN alone attributes an entire cloud\n * provider — registry validation rejects ASN-only signatures.\n */\n asn?: string;\n}\n\n/**\n * A signature definition, generic over the vendor key. The seed constant\n * below uses the closed `PlatformAgentVendor` union; DB-promoted signatures\n * (backend dynamic registry, console round) carry arbitrary string vendors —\n * `matchPlatformSignature` accepts both.\n */\nexport interface PlatformSignatureDef<V extends string = string> {\n vendor: V;\n displayName: string;\n uaPatterns?: RegExp[];\n agentCardPatterns?: RegExp[];\n /**\n * Patterns tested against an observed API-key format prefix\n * (e.g. `/^sk-ant/i` → claude). A credential-header format is a stronger\n * platform signal than a spoofable User-Agent.\n */\n apiKeyFormatPatterns?: RegExp[];\n /**\n * Patterns tested against each observed platform-signal\n * header, as both `name` and `name: value` (so `/openai-organization/i` and\n * `/x-goog-user-project/i` match on presence).\n */\n headerPatterns?: RegExp[];\n /** Patterns tested against the observed JA4 TLS-client fingerprint. */\n ja4Patterns?: RegExp[];\n /** Exact ASN strings this signature matches (never regex). */\n asnList?: string[];\n}\n\n/**\n * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.\n * Structurally a superset of `PlatformFingerprint`.\n */\nexport interface DynamicPlatformFingerprint {\n vendor: string;\n fingerprintKey: string;\n displayName: string;\n}\n\n/**\n * Registry of known platform-agent signatures. Order matters — first match wins.\n *\n * Patterns are intentionally permissive (case-insensitive substring match) since\n * platform UA strings drift between versions. False positives are tolerable —\n * the orphan record is informational, not enforcement.\n */\nexport const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>> = [\n {\n vendor: 'claude',\n displayName: 'Anthropic Claude',\n uaPatterns: [/claude/i, /anthropic/i],\n agentCardPatterns: [/anthropic\\.com/i, /claude\\.ai/i],\n apiKeyFormatPatterns: [/^sk-ant/i],\n headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],\n },\n {\n vendor: 'chatgpt',\n displayName: 'OpenAI ChatGPT',\n uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\\d/i],\n agentCardPatterns: [/openai\\.com/i, /chatgpt\\.com/i],\n apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],\n headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i],\n },\n {\n vendor: 'gemini',\n displayName: 'Google Gemini',\n uaPatterns: [/gemini/i, /google-gemini/i, /bard/i],\n agentCardPatterns: [/gemini\\.google\\.com/i, /ai\\.google\\.dev/i],\n apiKeyFormatPatterns: [/^aiza/i],\n headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],\n },\n {\n vendor: 'cursor',\n displayName: 'Cursor',\n uaPatterns: [/cursor/i],\n agentCardPatterns: [/cursor\\.sh/i, /cursor\\.com/i],\n },\n {\n vendor: 'goose',\n displayName: 'Block Goose',\n uaPatterns: [/goose/i, /block-goose/i],\n agentCardPatterns: [/block\\.xyz\\/goose/i, /goose\\.tools/i],\n },\n {\n vendor: 'perplexity',\n displayName: 'Perplexity',\n uaPatterns: [/perplexity/i],\n agentCardPatterns: [/perplexity\\.ai/i],\n apiKeyFormatPatterns: [/^pplx-/i],\n },\n // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must\n // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the\n // browser product is a different surface than the assistant service, and\n // the DB registry mirrors one row per vendor. These sit after the primary\n // vendor entries so a UA carrying an explicit vendor token keeps its\n // primary attribution; a browser-shaped UA with the agentic token is\n // caught here BEFORE classify's human tier, which is what makes these\n // visits observable at all. Token drift is real (some builds mimic Chrome\n // outright) — re-verify against captured events when a new browser ships,\n // and check `sec-ch-ua` platformHeaders for the real brand.\n {\n vendor: 'chatgpt-atlas',\n displayName: 'ChatGPT Atlas browser',\n uaPatterns: [/\\batlas\\/\\d/i],\n headerPatterns: [/^sec-ch-ua: .*atlas/i],\n },\n {\n vendor: 'perplexity-comet',\n displayName: 'Perplexity Comet browser',\n uaPatterns: [/\\bcomet\\/\\d/i],\n headerPatterns: [/^sec-ch-ua: .*comet/i],\n },\n // Our own SDK self-identifies (sdkFetch stamps this UA) — first-party\n // traffic must never pollute the unknown/anonymous pool it exists to audit.\n {\n vendor: 'astrasync-sdk',\n displayName: 'AstraSync SDK',\n uaPatterns: [/^astrasync-sdk\\//i],\n },\n];\n\n/**\n * Generic first-match-wins matcher over an arbitrary signature set. The\n * backend's dynamic registry merges the seed constant with DB-promoted\n * signatures and calls this — ONE matching implementation everywhere\n * (fingerprint-key rule included: vendor + lowercased UA basename before\n * the first space/slash, falling back to the agent-card host).\n */\nexport function matchPlatformSignature(\n signatures: ReadonlyArray<PlatformSignatureDef>,\n input: PlatformDetectionInput\n): DynamicPlatformFingerprint | undefined {\n const ua = input.userAgent ?? '';\n const cardUrl = input.agentCardUrl ?? '';\n const apiKeyFormat = input.apiKeyFormat ?? '';\n const ja4 = input.ja4 ?? '';\n const asn = input.asn ?? '';\n // Flatten platform headers to `name` and `name: value` lines for matching.\n const headerLines: string[] = [];\n if (input.platformHeaders) {\n for (const [name, value] of Object.entries(input.platformHeaders)) {\n headerLines.push(name, `${name}: ${value}`);\n }\n }\n\n for (const sig of signatures) {\n const uaMatch = sig.uaPatterns?.some((p) => p.test(ua));\n const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));\n const keyMatch =\n apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));\n const headerMatch =\n headerLines.length > 0 &&\n sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));\n const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));\n const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);\n if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {\n const uaBase = ua.split(/[\\s/]/)[0]?.toLowerCase() ?? '';\n const cardHost = (() => {\n try {\n return cardUrl ? new URL(cardUrl).host : '';\n } catch {\n return '';\n }\n })();\n // Prefer a UA/card basename for the dedup key; fall back to the\n // credential format, then the connection fingerprint (still stable\n // per-vendor keys) when only a key/header/ja4/asn matched.\n const keyBasis =\n uaBase || cardHost || apiKeyFormat || ja4 || (asn && `asn${asn}`) || 'unknown';\n return {\n vendor: sig.vendor,\n fingerprintKey: `${sig.vendor}:${keyBasis}`,\n displayName: sig.displayName,\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Inspect caller metadata for a known SEED platform-agent fingerprint.\n * Returns undefined if no signature matches. (Backends with the dynamic\n * registry should call `matchPlatformSignature` over their merged set\n * instead — same semantics, live signature additions included.)\n */\nexport function detectPlatformFingerprint(\n input: PlatformDetectionInput\n): PlatformFingerprint | undefined {\n // Seed vendors are all members of the PlatformAgentVendor union, so the\n // narrowing cast is sound by construction.\n return matchPlatformSignature(PLATFORM_AGENT_SIGNATURES, input) as\n | PlatformFingerprint\n | undefined;\n}\n"],"mappings":"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","names":[]}
@@ -42,12 +42,43 @@ var PLATFORM_AGENT_SIGNATURES = [
42
42
  uaPatterns: [/perplexity/i],
43
43
  agentCardPatterns: [/perplexity\.ai/i],
44
44
  apiKeyFormatPatterns: [/^pplx-/i]
45
+ },
46
+ // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must
47
+ // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the
48
+ // browser product is a different surface than the assistant service, and
49
+ // the DB registry mirrors one row per vendor. These sit after the primary
50
+ // vendor entries so a UA carrying an explicit vendor token keeps its
51
+ // primary attribution; a browser-shaped UA with the agentic token is
52
+ // caught here BEFORE classify's human tier, which is what makes these
53
+ // visits observable at all. Token drift is real (some builds mimic Chrome
54
+ // outright) — re-verify against captured events when a new browser ships,
55
+ // and check `sec-ch-ua` platformHeaders for the real brand.
56
+ {
57
+ vendor: "chatgpt-atlas",
58
+ displayName: "ChatGPT Atlas browser",
59
+ uaPatterns: [/\batlas\/\d/i],
60
+ headerPatterns: [/^sec-ch-ua: .*atlas/i]
61
+ },
62
+ {
63
+ vendor: "perplexity-comet",
64
+ displayName: "Perplexity Comet browser",
65
+ uaPatterns: [/\bcomet\/\d/i],
66
+ headerPatterns: [/^sec-ch-ua: .*comet/i]
67
+ },
68
+ // Our own SDK self-identifies (sdkFetch stamps this UA) — first-party
69
+ // traffic must never pollute the unknown/anonymous pool it exists to audit.
70
+ {
71
+ vendor: "astrasync-sdk",
72
+ displayName: "AstraSync SDK",
73
+ uaPatterns: [/^astrasync-sdk\//i]
45
74
  }
46
75
  ];
47
76
  function matchPlatformSignature(signatures, input) {
48
77
  const ua = input.userAgent ?? "";
49
78
  const cardUrl = input.agentCardUrl ?? "";
50
79
  const apiKeyFormat = input.apiKeyFormat ?? "";
80
+ const ja4 = input.ja4 ?? "";
81
+ const asn = input.asn ?? "";
51
82
  const headerLines = [];
52
83
  if (input.platformHeaders) {
53
84
  for (const [name, value] of Object.entries(input.platformHeaders)) {
@@ -59,7 +90,9 @@ function matchPlatformSignature(signatures, input) {
59
90
  const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));
60
91
  const keyMatch = apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));
61
92
  const headerMatch = headerLines.length > 0 && sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));
62
- if (uaMatch || cardMatch || keyMatch || headerMatch) {
93
+ const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));
94
+ const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);
95
+ if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {
63
96
  const uaBase = ua.split(/[\s/]/)[0]?.toLowerCase() ?? "";
64
97
  const cardHost = (() => {
65
98
  try {
@@ -68,7 +101,7 @@ function matchPlatformSignature(signatures, input) {
68
101
  return "";
69
102
  }
70
103
  })();
71
- const keyBasis = uaBase || cardHost || apiKeyFormat || "unknown";
104
+ const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || "unknown";
72
105
  return {
73
106
  vendor: sig.vendor,
74
107
  fingerprintKey: `${sig.vendor}:${keyBasis}`,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/platform-signatures.ts"],"sourcesContent":["/**\n * Platform-agent fingerprint signatures — the shared registry of known\n * platform-agent UA / agent-card patterns (Claude / ChatGPT / Gemini /\n * Cursor / Goose).\n *\n * Single source of truth, lifted verbatim from the backend's\n * `platform-agent-detector.service` (which now imports detection from\n * here): the backend's verify-access anonymous handler and the edge\n * adapter (`@astrasyncai/adapter-lambda`) must classify identically or\n * the three-tier visibility numbers drift between capture points.\n *\n * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for\n * v2.9 — TLS fingerprinting / behavioural signatures are deferred.\n */\n\nexport type PlatformAgentVendor =\n | 'claude'\n | 'chatgpt'\n | 'gemini'\n | 'cursor'\n | 'goose'\n | 'perplexity'\n | 'unknown';\n\nexport interface PlatformFingerprint {\n vendor: PlatformAgentVendor;\n /** A stable identifier used as the orphan-agent dedup key. */\n fingerprintKey: string;\n /** Human-readable name surfaced in the admin panel. */\n displayName: string;\n}\n\nexport interface PlatformDetectionInput {\n userAgent?: string;\n agentCardUrl?: string;\n /**\n * A safe API-key format prefix observed on the caller's\n * credential header (e.g. `sk-ant-api03`, `sk-proj`, `aiza`). A strong\n * platform signal; never the secret. Matched against `apiKeyFormatPatterns`.\n */\n apiKeyFormat?: string;\n /**\n * The sanitised platform-signal headers (lowercased names →\n * values), e.g. `openai-organization`, `x-goog-user-project`. Matched\n * against `headerPatterns` (which test on `name` and `name: value`).\n */\n platformHeaders?: Record<string, string>;\n}\n\n/**\n * A signature definition, generic over the vendor key. The seed constant\n * below uses the closed `PlatformAgentVendor` union; DB-promoted signatures\n * (backend dynamic registry, console round) carry arbitrary string vendors —\n * `matchPlatformSignature` accepts both.\n */\nexport interface PlatformSignatureDef<V extends string = string> {\n vendor: V;\n displayName: string;\n uaPatterns?: RegExp[];\n agentCardPatterns?: RegExp[];\n /**\n * Patterns tested against an observed API-key format prefix\n * (e.g. `/^sk-ant/i` → claude). A credential-header format is a stronger\n * platform signal than a spoofable User-Agent.\n */\n apiKeyFormatPatterns?: RegExp[];\n /**\n * Patterns tested against each observed platform-signal\n * header, as both `name` and `name: value` (so `/openai-organization/i` and\n * `/x-goog-user-project/i` match on presence).\n */\n headerPatterns?: RegExp[];\n}\n\n/**\n * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.\n * Structurally a superset of `PlatformFingerprint`.\n */\nexport interface DynamicPlatformFingerprint {\n vendor: string;\n fingerprintKey: string;\n displayName: string;\n}\n\n/**\n * Registry of known platform-agent signatures. Order matters — first match wins.\n *\n * Patterns are intentionally permissive (case-insensitive substring match) since\n * platform UA strings drift between versions. False positives are tolerable —\n * the orphan record is informational, not enforcement.\n */\nexport const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>> = [\n {\n vendor: 'claude',\n displayName: 'Anthropic Claude',\n uaPatterns: [/claude/i, /anthropic/i],\n agentCardPatterns: [/anthropic\\.com/i, /claude\\.ai/i],\n apiKeyFormatPatterns: [/^sk-ant/i],\n headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],\n },\n {\n vendor: 'chatgpt',\n displayName: 'OpenAI ChatGPT',\n uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\\d/i],\n agentCardPatterns: [/openai\\.com/i, /chatgpt\\.com/i],\n apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],\n headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i],\n },\n {\n vendor: 'gemini',\n displayName: 'Google Gemini',\n uaPatterns: [/gemini/i, /google-gemini/i, /bard/i],\n agentCardPatterns: [/gemini\\.google\\.com/i, /ai\\.google\\.dev/i],\n apiKeyFormatPatterns: [/^aiza/i],\n headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],\n },\n {\n vendor: 'cursor',\n displayName: 'Cursor',\n uaPatterns: [/cursor/i],\n agentCardPatterns: [/cursor\\.sh/i, /cursor\\.com/i],\n },\n {\n vendor: 'goose',\n displayName: 'Block Goose',\n uaPatterns: [/goose/i, /block-goose/i],\n agentCardPatterns: [/block\\.xyz\\/goose/i, /goose\\.tools/i],\n },\n {\n vendor: 'perplexity',\n displayName: 'Perplexity',\n uaPatterns: [/perplexity/i],\n agentCardPatterns: [/perplexity\\.ai/i],\n apiKeyFormatPatterns: [/^pplx-/i],\n },\n];\n\n/**\n * Generic first-match-wins matcher over an arbitrary signature set. The\n * backend's dynamic registry merges the seed constant with DB-promoted\n * signatures and calls this — ONE matching implementation everywhere\n * (fingerprint-key rule included: vendor + lowercased UA basename before\n * the first space/slash, falling back to the agent-card host).\n */\nexport function matchPlatformSignature(\n signatures: ReadonlyArray<PlatformSignatureDef>,\n input: PlatformDetectionInput\n): DynamicPlatformFingerprint | undefined {\n const ua = input.userAgent ?? '';\n const cardUrl = input.agentCardUrl ?? '';\n const apiKeyFormat = input.apiKeyFormat ?? '';\n // Flatten platform headers to `name` and `name: value` lines for matching.\n const headerLines: string[] = [];\n if (input.platformHeaders) {\n for (const [name, value] of Object.entries(input.platformHeaders)) {\n headerLines.push(name, `${name}: ${value}`);\n }\n }\n\n for (const sig of signatures) {\n const uaMatch = sig.uaPatterns?.some((p) => p.test(ua));\n const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));\n const keyMatch =\n apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));\n const headerMatch =\n headerLines.length > 0 &&\n sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));\n if (uaMatch || cardMatch || keyMatch || headerMatch) {\n const uaBase = ua.split(/[\\s/]/)[0]?.toLowerCase() ?? '';\n const cardHost = (() => {\n try {\n return cardUrl ? new URL(cardUrl).host : '';\n } catch {\n return '';\n }\n })();\n // Prefer a UA/card basename for the dedup key; fall back to the credential\n // format (still a stable per-vendor key) when only a key/header matched.\n const keyBasis = uaBase || cardHost || apiKeyFormat || 'unknown';\n return {\n vendor: sig.vendor,\n fingerprintKey: `${sig.vendor}:${keyBasis}`,\n displayName: sig.displayName,\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Inspect caller metadata for a known SEED platform-agent fingerprint.\n * Returns undefined if no signature matches. (Backends with the dynamic\n * registry should call `matchPlatformSignature` over their merged set\n * instead — same semantics, live signature additions included.)\n */\nexport function detectPlatformFingerprint(\n input: PlatformDetectionInput\n): PlatformFingerprint | undefined {\n // Seed vendors are all members of the PlatformAgentVendor union, so the\n // narrowing cast is sound by construction.\n return matchPlatformSignature(PLATFORM_AGENT_SIGNATURES, input) as\n | PlatformFingerprint\n | undefined;\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/platform-signatures.ts"],"sourcesContent":["/**\n * Platform-agent fingerprint signatures — the shared registry of known\n * platform-agent UA / agent-card patterns (Claude / ChatGPT / Gemini /\n * Cursor / Goose).\n *\n * Single source of truth, lifted verbatim from the backend's\n * `platform-agent-detector.service` (which now imports detection from\n * here): the backend's verify-access anonymous handler and the edge\n * adapter (`@astrasyncai/adapter-lambda`) must classify identically or\n * the three-tier visibility numbers drift between capture points.\n *\n * Pure regex — no Node built-ins, edge-safe. Detection is regex-only for\n * v2.9 — TLS fingerprinting / behavioural signatures are deferred.\n */\n\nexport type PlatformAgentVendor =\n | 'claude'\n | 'chatgpt'\n | 'gemini'\n | 'cursor'\n | 'goose'\n | 'perplexity'\n | 'chatgpt-atlas'\n | 'perplexity-comet'\n | 'astrasync-sdk'\n | 'unknown';\n\nexport interface PlatformFingerprint {\n vendor: PlatformAgentVendor;\n /** A stable identifier used as the orphan-agent dedup key. */\n fingerprintKey: string;\n /** Human-readable name surfaced in the admin panel. */\n displayName: string;\n}\n\nexport interface PlatformDetectionInput {\n userAgent?: string;\n agentCardUrl?: string;\n /**\n * A safe API-key format prefix observed on the caller's\n * credential header (e.g. `sk-ant-api03`, `sk-proj`, `aiza`). A strong\n * platform signal; never the secret. Matched against `apiKeyFormatPatterns`.\n */\n apiKeyFormat?: string;\n /**\n * The sanitised platform-signal headers (lowercased names →\n * values), e.g. `openai-organization`, `x-goog-user-project`. Matched\n * against `headerPatterns` (which test on `name` and `name: value`).\n */\n platformHeaders?: Record<string, string>;\n /**\n * The caller's JA4 TLS-client fingerprint (from the connection capture).\n * Matched against `ja4Patterns`. A JA4 identifies the client TLS stack,\n * so signatures should pair it with other evidence when attributing a\n * vendor.\n */\n ja4?: string;\n /**\n * The caller's origin ASN (from the connection capture). Matched against\n * `asnList` by exact membership. An ASN alone attributes an entire cloud\n * provider — registry validation rejects ASN-only signatures.\n */\n asn?: string;\n}\n\n/**\n * A signature definition, generic over the vendor key. The seed constant\n * below uses the closed `PlatformAgentVendor` union; DB-promoted signatures\n * (backend dynamic registry, console round) carry arbitrary string vendors —\n * `matchPlatformSignature` accepts both.\n */\nexport interface PlatformSignatureDef<V extends string = string> {\n vendor: V;\n displayName: string;\n uaPatterns?: RegExp[];\n agentCardPatterns?: RegExp[];\n /**\n * Patterns tested against an observed API-key format prefix\n * (e.g. `/^sk-ant/i` → claude). A credential-header format is a stronger\n * platform signal than a spoofable User-Agent.\n */\n apiKeyFormatPatterns?: RegExp[];\n /**\n * Patterns tested against each observed platform-signal\n * header, as both `name` and `name: value` (so `/openai-organization/i` and\n * `/x-goog-user-project/i` match on presence).\n */\n headerPatterns?: RegExp[];\n /** Patterns tested against the observed JA4 TLS-client fingerprint. */\n ja4Patterns?: RegExp[];\n /** Exact ASN strings this signature matches (never regex). */\n asnList?: string[];\n}\n\n/**\n * A fingerprint whose vendor may be a dynamically-promoted (non-seed) key.\n * Structurally a superset of `PlatformFingerprint`.\n */\nexport interface DynamicPlatformFingerprint {\n vendor: string;\n fingerprintKey: string;\n displayName: string;\n}\n\n/**\n * Registry of known platform-agent signatures. Order matters — first match wins.\n *\n * Patterns are intentionally permissive (case-insensitive substring match) since\n * platform UA strings drift between versions. False positives are tolerable —\n * the orphan record is informational, not enforcement.\n */\nexport const PLATFORM_AGENT_SIGNATURES: ReadonlyArray<PlatformSignatureDef<PlatformAgentVendor>> = [\n {\n vendor: 'claude',\n displayName: 'Anthropic Claude',\n uaPatterns: [/claude/i, /anthropic/i],\n agentCardPatterns: [/anthropic\\.com/i, /claude\\.ai/i],\n apiKeyFormatPatterns: [/^sk-ant/i],\n headerPatterns: [/^anthropic-version/i, /^anthropic-beta/i, /^x-anthropic-/i],\n },\n {\n vendor: 'chatgpt',\n displayName: 'OpenAI ChatGPT',\n uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\\d/i],\n agentCardPatterns: [/openai\\.com/i, /chatgpt\\.com/i],\n apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],\n headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i],\n },\n {\n vendor: 'gemini',\n displayName: 'Google Gemini',\n uaPatterns: [/gemini/i, /google-gemini/i, /bard/i],\n agentCardPatterns: [/gemini\\.google\\.com/i, /ai\\.google\\.dev/i],\n apiKeyFormatPatterns: [/^aiza/i],\n headerPatterns: [/^x-goog-user-project/i, /^x-goog-api-client/i],\n },\n {\n vendor: 'cursor',\n displayName: 'Cursor',\n uaPatterns: [/cursor/i],\n agentCardPatterns: [/cursor\\.sh/i, /cursor\\.com/i],\n },\n {\n vendor: 'goose',\n displayName: 'Block Goose',\n uaPatterns: [/goose/i, /block-goose/i],\n agentCardPatterns: [/block\\.xyz\\/goose/i, /goose\\.tools/i],\n },\n {\n vendor: 'perplexity',\n displayName: 'Perplexity',\n uaPatterns: [/perplexity/i],\n agentCardPatterns: [/perplexity\\.ai/i],\n apiKeyFormatPatterns: [/^pplx-/i],\n },\n // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must\n // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the\n // browser product is a different surface than the assistant service, and\n // the DB registry mirrors one row per vendor. These sit after the primary\n // vendor entries so a UA carrying an explicit vendor token keeps its\n // primary attribution; a browser-shaped UA with the agentic token is\n // caught here BEFORE classify's human tier, which is what makes these\n // visits observable at all. Token drift is real (some builds mimic Chrome\n // outright) — re-verify against captured events when a new browser ships,\n // and check `sec-ch-ua` platformHeaders for the real brand.\n {\n vendor: 'chatgpt-atlas',\n displayName: 'ChatGPT Atlas browser',\n uaPatterns: [/\\batlas\\/\\d/i],\n headerPatterns: [/^sec-ch-ua: .*atlas/i],\n },\n {\n vendor: 'perplexity-comet',\n displayName: 'Perplexity Comet browser',\n uaPatterns: [/\\bcomet\\/\\d/i],\n headerPatterns: [/^sec-ch-ua: .*comet/i],\n },\n // Our own SDK self-identifies (sdkFetch stamps this UA) — first-party\n // traffic must never pollute the unknown/anonymous pool it exists to audit.\n {\n vendor: 'astrasync-sdk',\n displayName: 'AstraSync SDK',\n uaPatterns: [/^astrasync-sdk\\//i],\n },\n];\n\n/**\n * Generic first-match-wins matcher over an arbitrary signature set. The\n * backend's dynamic registry merges the seed constant with DB-promoted\n * signatures and calls this — ONE matching implementation everywhere\n * (fingerprint-key rule included: vendor + lowercased UA basename before\n * the first space/slash, falling back to the agent-card host).\n */\nexport function matchPlatformSignature(\n signatures: ReadonlyArray<PlatformSignatureDef>,\n input: PlatformDetectionInput\n): DynamicPlatformFingerprint | undefined {\n const ua = input.userAgent ?? '';\n const cardUrl = input.agentCardUrl ?? '';\n const apiKeyFormat = input.apiKeyFormat ?? '';\n const ja4 = input.ja4 ?? '';\n const asn = input.asn ?? '';\n // Flatten platform headers to `name` and `name: value` lines for matching.\n const headerLines: string[] = [];\n if (input.platformHeaders) {\n for (const [name, value] of Object.entries(input.platformHeaders)) {\n headerLines.push(name, `${name}: ${value}`);\n }\n }\n\n for (const sig of signatures) {\n const uaMatch = sig.uaPatterns?.some((p) => p.test(ua));\n const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));\n const keyMatch =\n apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));\n const headerMatch =\n headerLines.length > 0 &&\n sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));\n const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));\n const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);\n if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {\n const uaBase = ua.split(/[\\s/]/)[0]?.toLowerCase() ?? '';\n const cardHost = (() => {\n try {\n return cardUrl ? new URL(cardUrl).host : '';\n } catch {\n return '';\n }\n })();\n // Prefer a UA/card basename for the dedup key; fall back to the\n // credential format, then the connection fingerprint (still stable\n // per-vendor keys) when only a key/header/ja4/asn matched.\n const keyBasis =\n uaBase || cardHost || apiKeyFormat || ja4 || (asn && `asn${asn}`) || 'unknown';\n return {\n vendor: sig.vendor,\n fingerprintKey: `${sig.vendor}:${keyBasis}`,\n displayName: sig.displayName,\n };\n }\n }\n\n return undefined;\n}\n\n/**\n * Inspect caller metadata for a known SEED platform-agent fingerprint.\n * Returns undefined if no signature matches. (Backends with the dynamic\n * registry should call `matchPlatformSignature` over their merged set\n * instead — same semantics, live signature additions included.)\n */\nexport function detectPlatformFingerprint(\n input: PlatformDetectionInput\n): PlatformFingerprint | undefined {\n // Seed vendors are all members of the PlatformAgentVendor union, so the\n // narrowing cast is sound by construction.\n return matchPlatformSignature(PLATFORM_AGENT_SIGNATURES, input) as\n | PlatformFingerprint\n | undefined;\n}\n"],"mappings":"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","names":[]}
@@ -41,6 +41,27 @@ __export(registration_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(registration_exports);
43
43
 
44
+ // src/version.ts
45
+ var SDK_VERSION = "4.4.0";
46
+
47
+ // src/http.ts
48
+ var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
49
+ function withUserAgent(initHeaders) {
50
+ if (!initHeaders) return { "user-agent": SDK_USER_AGENT };
51
+ if (initHeaders instanceof Headers) {
52
+ const copy = new Headers(initHeaders);
53
+ if (!copy.has("user-agent")) copy.set("user-agent", SDK_USER_AGENT);
54
+ return copy;
55
+ }
56
+ if (Array.isArray(initHeaders)) {
57
+ const has2 = initHeaders.some(([name]) => name.toLowerCase() === "user-agent");
58
+ return has2 ? initHeaders : [...initHeaders, ["user-agent", SDK_USER_AGENT]];
59
+ }
60
+ const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === "user-agent");
61
+ return has ? initHeaders : { "user-agent": SDK_USER_AGENT, ...initHeaders };
62
+ }
63
+ var sdkFetch = (input, init) => fetch(input, { ...init, headers: withUserAgent(init?.headers) });
64
+
44
65
  // src/registration/errors.ts
45
66
  var AstraSyncError = class extends Error {
46
67
  constructor(message, statusCode, code) {
@@ -226,7 +247,7 @@ var AstraSync = class {
226
247
  */
227
248
  async pollRegistration(requestId) {
228
249
  const url = `${this.baseUrl}/api/agents/request-registration/${requestId}`;
229
- const res = await fetch(url, { headers: { Accept: "application/json" } });
250
+ const res = await sdkFetch(url, { headers: { Accept: "application/json" } });
230
251
  if (!res.ok) {
231
252
  const errBody = await res.json().catch(() => ({}));
232
253
  throw new AstraSyncError(
@@ -283,7 +304,7 @@ var AstraSync = class {
283
304
  * Check API health.
284
305
  */
285
306
  async health() {
286
- const res = await fetch(`${this.baseUrl}/api/health/`);
307
+ const res = await sdkFetch(`${this.baseUrl}/api/health/`);
287
308
  if (!res.ok) {
288
309
  throw new AstraSyncError(`Health check failed: ${res.status}`, res.status);
289
310
  }
@@ -310,7 +331,7 @@ var AstraSync = class {
310
331
  const signature = await this.signRequest(method, endpoint, body || {});
311
332
  headers["X-AstraSync-Signature"] = signature;
312
333
  }
313
- const res = await fetch(url, {
334
+ const res = await sdkFetch(url, {
314
335
  method,
315
336
  headers,
316
337
  ...body ? { body: JSON.stringify(body) } : {}
@@ -335,7 +356,7 @@ var AstraSync = class {
335
356
  if (this.cachedJwt && this.jwtExpiresAt && Date.now() < this.jwtExpiresAt) {
336
357
  return this.cachedJwt;
337
358
  }
338
- const res = await fetch(`${this.baseUrl}/api/auth/login`, {
359
+ const res = await sdkFetch(`${this.baseUrl}/api/auth/login`, {
339
360
  method: "POST",
340
361
  headers: { "Content-Type": "application/json" },
341
362
  body: JSON.stringify({ email: this.email, password: this.password })