@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,5 +1,5 @@
1
- import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-DqfPU5Bl.mjs';
2
- import '../types-r850cOt0.mjs';
1
+ import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-C7Rb-w0d.mjs';
2
+ import '../types-C0zfSXMB.mjs';
3
3
  import '../metadata-capture.mjs';
4
4
 
5
5
  /**
@@ -1,5 +1,5 @@
1
- import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-BLUx92FJ.js';
2
- import '../types-sMxBT-nM.js';
1
+ import { L as LocalPolicy, P as PDLSSContext, V as VerificationDecision, b as LocalPurposeRule } from '../types-nIh3HtuA.js';
2
+ import '../types-DwSfO7Sb.js';
3
3
  import '../metadata-capture.js';
4
4
 
5
5
  /**
@@ -62,6 +62,21 @@ interface ObservedMetadata {
62
62
  * absent when neither source had anything.
63
63
  */
64
64
  connection?: Record<string, string>;
65
+ /**
66
+ * Number of cookie pairs the caller sent. The `cookie` header VALUE is a
67
+ * secret and is always dropped; the COUNT is a client-shape signal
68
+ * (browsers carry cookies, most automation carries none) that would
69
+ * otherwise be unobservable downstream. Absent when no cookie header
70
+ * arrived.
71
+ */
72
+ cookieCount?: number;
73
+ /**
74
+ * Query-string parameter NAMES (deduped, lowercased) — values are NEVER
75
+ * captured (they routinely carry OAuth codes, tokens, and PII, and no
76
+ * deny-list is complete). Names alone fingerprint client shape. Populated
77
+ * by the edge path, which sees the raw query string.
78
+ */
79
+ queryKeys?: string[];
65
80
  /**
66
81
  * Version of the capture semantics this object was produced under — see
67
82
  * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with
@@ -77,6 +92,8 @@ interface SanitizeHeadersResult {
77
92
  headers: Record<string, string>;
78
93
  apiKeyFormat?: string;
79
94
  platformHeaders?: Record<string, string>;
95
+ /** Cookie-pair count; the cookie value itself is always dropped. */
96
+ cookieCount?: number;
80
97
  }
81
98
  /** Size caps — the only guard against jsonb bloat from a hostile POST. */
82
99
  declare const MAX_HEADERS = 64;
@@ -106,6 +123,12 @@ declare function sanitizeHeaders(raw: Record<string, string | string[] | undefin
106
123
  * want only the platform subset. Never re-run over raw (unsanitised) headers.
107
124
  */
108
125
  declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
126
+ /**
127
+ * Backend bound: observedMetadata.connection values longer than this fail
128
+ * the platform's zod parse outright (boundedStringRecord(32, 256)), so every
129
+ * emitter truncates — headerOrder routinely exceeds it.
130
+ */
131
+ declare const MAX_CONNECTION_VALUE_CHARS = 256;
109
132
  /**
110
133
  * Derive the connection-layer block from CDN-injected request headers.
111
134
  *
@@ -116,12 +139,19 @@ declare function extractPlatformHeaders(sanitized: Record<string, string> | unde
116
139
  * deployments still get IP / geo / TLS attribution. Precedence per key:
117
140
  *
118
141
  * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
119
- * `x-real-ip` > leftmost `x-forwarded-for`
142
+ * `x-real-ip` > `cloudfront-viewer-address` (port stripped) > leftmost
143
+ * `x-forwarded-for`
120
144
  * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
121
145
  * `cloudfront-viewer-country` > `fastly-geo-country`
122
- * - `region` / `city`: Vercel then CloudFront viewer headers
123
- * - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`
124
- * - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`
146
+ * - `region` / `city` / `timeZone`: Vercel then CloudFront viewer headers
147
+ * - `countryRegionName` / `asn` / `tlsVersion` / `httpVersion` /
148
+ * `metroCode` / `headerOrder` / `headerCount`: `cloudfront-viewer-*`
149
+ * - `tlsFingerprint`: `cf-ja3-hash` > `cloudfront-viewer-ja3-fingerprint`
150
+ * - `ja4`: `cf-ja4` > `cloudfront-viewer-ja4-fingerprint`
151
+ *
152
+ * All keys are additive under `schemaVersion` 1 (the frozen-capture doctrine
153
+ * bumps only for changed semantics, never for additive signals). Values are
154
+ * truncated to MAX_CONNECTION_VALUE_CHARS — the platform's hard bound.
125
155
  *
126
156
  * Edge adapters keep their richer native collectors — native platform
127
157
  * signals are authoritative; this header derivation is the SDK-level
@@ -143,4 +173,4 @@ declare function deriveConnectionFromHeaders(headers: Record<string, string> | u
143
173
  */
144
174
  declare function buildSdkObservedMetadata(rawHeaders: Record<string, string | string[] | undefined> | undefined | null): ObservedMetadata;
145
175
 
146
- export { CAPTURE_SCHEMA_VERSION, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
176
+ export { CAPTURE_SCHEMA_VERSION, MAX_CONNECTION_VALUE_CHARS, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
@@ -62,6 +62,21 @@ interface ObservedMetadata {
62
62
  * absent when neither source had anything.
63
63
  */
64
64
  connection?: Record<string, string>;
65
+ /**
66
+ * Number of cookie pairs the caller sent. The `cookie` header VALUE is a
67
+ * secret and is always dropped; the COUNT is a client-shape signal
68
+ * (browsers carry cookies, most automation carries none) that would
69
+ * otherwise be unobservable downstream. Absent when no cookie header
70
+ * arrived.
71
+ */
72
+ cookieCount?: number;
73
+ /**
74
+ * Query-string parameter NAMES (deduped, lowercased) — values are NEVER
75
+ * captured (they routinely carry OAuth codes, tokens, and PII, and no
76
+ * deny-list is complete). Names alone fingerprint client shape. Populated
77
+ * by the edge path, which sees the raw query string.
78
+ */
79
+ queryKeys?: string[];
65
80
  /**
66
81
  * Version of the capture semantics this object was produced under — see
67
82
  * {@link CAPTURE_SCHEMA_VERSION}. Optional for backward compatibility with
@@ -77,6 +92,8 @@ interface SanitizeHeadersResult {
77
92
  headers: Record<string, string>;
78
93
  apiKeyFormat?: string;
79
94
  platformHeaders?: Record<string, string>;
95
+ /** Cookie-pair count; the cookie value itself is always dropped. */
96
+ cookieCount?: number;
80
97
  }
81
98
  /** Size caps — the only guard against jsonb bloat from a hostile POST. */
82
99
  declare const MAX_HEADERS = 64;
@@ -106,6 +123,12 @@ declare function sanitizeHeaders(raw: Record<string, string | string[] | undefin
106
123
  * want only the platform subset. Never re-run over raw (unsanitised) headers.
107
124
  */
108
125
  declare function extractPlatformHeaders(sanitized: Record<string, string> | undefined | null): Record<string, string>;
126
+ /**
127
+ * Backend bound: observedMetadata.connection values longer than this fail
128
+ * the platform's zod parse outright (boundedStringRecord(32, 256)), so every
129
+ * emitter truncates — headerOrder routinely exceeds it.
130
+ */
131
+ declare const MAX_CONNECTION_VALUE_CHARS = 256;
109
132
  /**
110
133
  * Derive the connection-layer block from CDN-injected request headers.
111
134
  *
@@ -116,12 +139,19 @@ declare function extractPlatformHeaders(sanitized: Record<string, string> | unde
116
139
  * deployments still get IP / geo / TLS attribution. Precedence per key:
117
140
  *
118
141
  * - `ip`: `cf-connecting-ip` > `fastly-client-ip` > `true-client-ip` >
119
- * `x-real-ip` > leftmost `x-forwarded-for`
142
+ * `x-real-ip` > `cloudfront-viewer-address` (port stripped) > leftmost
143
+ * `x-forwarded-for`
120
144
  * - `country`: `cf-ipcountry` > `x-vercel-ip-country` >
121
145
  * `cloudfront-viewer-country` > `fastly-geo-country`
122
- * - `region` / `city`: Vercel then CloudFront viewer headers
123
- * - `asn` / `tlsVersion` / `httpVersion`: `cloudfront-viewer-*`
124
- * - `tlsFingerprint` / `ja4`: Cloudflare `cf-ja3-hash` / `cf-ja4`
146
+ * - `region` / `city` / `timeZone`: Vercel then CloudFront viewer headers
147
+ * - `countryRegionName` / `asn` / `tlsVersion` / `httpVersion` /
148
+ * `metroCode` / `headerOrder` / `headerCount`: `cloudfront-viewer-*`
149
+ * - `tlsFingerprint`: `cf-ja3-hash` > `cloudfront-viewer-ja3-fingerprint`
150
+ * - `ja4`: `cf-ja4` > `cloudfront-viewer-ja4-fingerprint`
151
+ *
152
+ * All keys are additive under `schemaVersion` 1 (the frozen-capture doctrine
153
+ * bumps only for changed semantics, never for additive signals). Values are
154
+ * truncated to MAX_CONNECTION_VALUE_CHARS — the platform's hard bound.
125
155
  *
126
156
  * Edge adapters keep their richer native collectors — native platform
127
157
  * signals are authoritative; this header derivation is the SDK-level
@@ -143,4 +173,4 @@ declare function deriveConnectionFromHeaders(headers: Record<string, string> | u
143
173
  */
144
174
  declare function buildSdkObservedMetadata(rawHeaders: Record<string, string | string[] | undefined> | undefined | null): ObservedMetadata;
145
175
 
146
- export { CAPTURE_SCHEMA_VERSION, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
176
+ export { CAPTURE_SCHEMA_VERSION, MAX_CONNECTION_VALUE_CHARS, MAX_HEADERS, MAX_HEADER_VALUE_BYTES, MAX_TOTAL_BYTES, type ObservedMetadata, type SanitizeHeadersResult, buildSdkObservedMetadata, deriveConnectionFromHeaders, extractApiKeyFormat, extractPlatformHeaders, sanitizeHeaders };
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var metadata_capture_exports = {};
22
22
  __export(metadata_capture_exports, {
23
23
  CAPTURE_SCHEMA_VERSION: () => CAPTURE_SCHEMA_VERSION,
24
+ MAX_CONNECTION_VALUE_CHARS: () => MAX_CONNECTION_VALUE_CHARS,
24
25
  MAX_HEADERS: () => MAX_HEADERS,
25
26
  MAX_HEADER_VALUE_BYTES: () => MAX_HEADER_VALUE_BYTES,
26
27
  MAX_TOTAL_BYTES: () => MAX_TOTAL_BYTES,
@@ -40,7 +41,9 @@ var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
40
41
  "x-astrasync-signature",
41
42
  "x-astrasync-secret",
42
43
  "x-hub-signature",
43
- "x-hub-signature-256"
44
+ "x-hub-signature-256",
45
+ // Short-lived verify dedupe token — must never persist in stored metadata.
46
+ "x-astra-verified-hop"
44
47
  ]);
45
48
  var SECRET_HEADER_PREFIXES = ["x-astrasync-", "kya-", "kya_"];
46
49
  var CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -65,7 +68,13 @@ var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
65
68
  "x-agent-card-url",
66
69
  "x-agent-provider"
67
70
  ]);
68
- var PLATFORM_HEADER_PREFIXES = ["x-goog-", "x-stainless-", "x-anthropic-", "x-openai-"];
71
+ var PLATFORM_HEADER_PREFIXES = [
72
+ "x-goog-",
73
+ "x-stainless-",
74
+ "x-anthropic-",
75
+ "x-openai-",
76
+ "sec-ch-"
77
+ ];
69
78
  var KEY_FORMAT_PREFIXES = [
70
79
  "sk-ant-api03",
71
80
  "sk-ant-api",
@@ -137,6 +146,7 @@ function sanitizeHeaders(raw) {
137
146
  const headers = {};
138
147
  const platformHeaders = {};
139
148
  let apiKeyFormat;
149
+ let cookieCount;
140
150
  let totalBytes = 0;
141
151
  let count = 0;
142
152
  if (!raw) return { headers };
@@ -151,6 +161,9 @@ function sanitizeHeaders(raw) {
151
161
  if (fmt && !apiKeyFormat) apiKeyFormat = fmt;
152
162
  continue;
153
163
  }
164
+ if (name === "cookie") {
165
+ cookieCount = joined.split(";").filter((s) => s.trim().length > 0).length;
166
+ }
154
167
  if (isSecretHeader(name)) continue;
155
168
  let value = joined;
156
169
  if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {
@@ -166,6 +179,7 @@ function sanitizeHeaders(raw) {
166
179
  const result = { headers };
167
180
  if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;
168
181
  if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;
182
+ if (cookieCount !== void 0) result.cookieCount = cookieCount;
169
183
  return result;
170
184
  }
171
185
  function extractPlatformHeaders(sanitized) {
@@ -183,16 +197,28 @@ var CONNECTION_HEADER_SOURCES = [
183
197
  ["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
184
198
  ],
185
199
  ["region", ["x-vercel-ip-country-region", "cloudfront-viewer-country-region"]],
200
+ ["countryRegionName", ["cloudfront-viewer-country-region-name"]],
186
201
  ["city", ["x-vercel-ip-city", "cloudfront-viewer-city"]],
202
+ ["latitude", ["x-vercel-ip-latitude", "cloudfront-viewer-latitude"]],
203
+ ["longitude", ["x-vercel-ip-longitude", "cloudfront-viewer-longitude"]],
204
+ ["postalCode", ["x-vercel-ip-postal-code", "cloudfront-viewer-postal-code"]],
187
205
  ["asn", ["cloudfront-viewer-asn"]],
188
206
  ["tlsVersion", ["cloudfront-viewer-tls"]],
189
207
  ["httpVersion", ["cloudfront-viewer-http-version"]],
208
+ ["timeZone", ["x-vercel-ip-timezone", "cloudfront-viewer-time-zone"]],
209
+ ["metroCode", ["cloudfront-viewer-metro-code"]],
190
210
  // TLS fingerprints can only be computed where TLS terminates. When a CDN
191
211
  // terminates TLS in front of the merchant, its fingerprint headers ARE the
192
212
  // connection-layer truth — normalising them here is the permanent design.
193
- ["tlsFingerprint", ["cf-ja3-hash"]],
194
- ["ja4", ["cf-ja4"]]
213
+ // CloudFront delivers JA3/JA4 only via an origin request policy and only
214
+ // for HTTPS viewer connections.
215
+ ["tlsFingerprint", ["cf-ja3-hash", "cloudfront-viewer-ja3-fingerprint"]],
216
+ ["ja4", ["cf-ja4", "cloudfront-viewer-ja4-fingerprint"]],
217
+ // Header-structure fingerprint: browser/SDK-distinctive header ordering.
218
+ ["headerOrder", ["cloudfront-viewer-header-order"]],
219
+ ["headerCount", ["cloudfront-viewer-header-count"]]
195
220
  ];
221
+ var MAX_CONNECTION_VALUE_CHARS = 256;
196
222
  function deriveConnectionFromHeaders(headers) {
197
223
  if (!headers) return void 0;
198
224
  const lower = {};
@@ -205,20 +231,30 @@ function deriveConnectionFromHeaders(headers) {
205
231
  for (const source of sources) {
206
232
  const value = lower[source]?.trim();
207
233
  if (value) {
208
- connection[key] = value;
234
+ connection[key] = value.slice(0, MAX_CONNECTION_VALUE_CHARS);
209
235
  break;
210
236
  }
211
237
  }
212
238
  }
239
+ {
240
+ const addr = lower["cloudfront-viewer-address"]?.trim();
241
+ const portMatch = addr?.match(/:(\d+)$/);
242
+ if (portMatch) connection.sourcePort = portMatch[1];
243
+ if (!connection.ip && addr) connection.ip = addr.replace(/:\d+$/, "");
244
+ }
213
245
  if (!connection.ip) {
214
246
  const xff = lower["x-forwarded-for"];
215
247
  const client = xff?.split(",")[0]?.trim();
216
248
  if (client) connection.ip = client;
217
249
  }
250
+ if (!connection.tlsCipher) {
251
+ const cipher = connection.tlsVersion?.split(":")[1];
252
+ if (cipher) connection.tlsCipher = cipher;
253
+ }
218
254
  return Object.keys(connection).length > 0 ? connection : void 0;
219
255
  }
220
256
  function buildSdkObservedMetadata(rawHeaders) {
221
- const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);
257
+ const { headers, apiKeyFormat, platformHeaders, cookieCount } = sanitizeHeaders(rawHeaders);
222
258
  const flat = {};
223
259
  if (rawHeaders) {
224
260
  for (const name of Object.keys(rawHeaders)) {
@@ -233,12 +269,14 @@ function buildSdkObservedMetadata(rawHeaders) {
233
269
  ...apiKeyFormat && { apiKeyFormat },
234
270
  ...platformHeaders && { platformHeaders },
235
271
  ...connection && { connection },
272
+ ...cookieCount !== void 0 && { cookieCount },
236
273
  schemaVersion: CAPTURE_SCHEMA_VERSION
237
274
  };
238
275
  }
239
276
  // Annotate the CommonJS export names for ESM import in node:
240
277
  0 && (module.exports = {
241
278
  CAPTURE_SCHEMA_VERSION,
279
+ MAX_CONNECTION_VALUE_CHARS,
242
280
  MAX_HEADERS,
243
281
  MAX_HEADER_VALUE_BYTES,
244
282
  MAX_TOTAL_BYTES,
@@ -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":[]}
@@ -8,7 +8,9 @@ var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
8
8
  "x-astrasync-signature",
9
9
  "x-astrasync-secret",
10
10
  "x-hub-signature",
11
- "x-hub-signature-256"
11
+ "x-hub-signature-256",
12
+ // Short-lived verify dedupe token — must never persist in stored metadata.
13
+ "x-astra-verified-hop"
12
14
  ]);
13
15
  var SECRET_HEADER_PREFIXES = ["x-astrasync-", "kya-", "kya_"];
14
16
  var CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -33,7 +35,13 @@ var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
33
35
  "x-agent-card-url",
34
36
  "x-agent-provider"
35
37
  ]);
36
- var PLATFORM_HEADER_PREFIXES = ["x-goog-", "x-stainless-", "x-anthropic-", "x-openai-"];
38
+ var PLATFORM_HEADER_PREFIXES = [
39
+ "x-goog-",
40
+ "x-stainless-",
41
+ "x-anthropic-",
42
+ "x-openai-",
43
+ "sec-ch-"
44
+ ];
37
45
  var KEY_FORMAT_PREFIXES = [
38
46
  "sk-ant-api03",
39
47
  "sk-ant-api",
@@ -105,6 +113,7 @@ function sanitizeHeaders(raw) {
105
113
  const headers = {};
106
114
  const platformHeaders = {};
107
115
  let apiKeyFormat;
116
+ let cookieCount;
108
117
  let totalBytes = 0;
109
118
  let count = 0;
110
119
  if (!raw) return { headers };
@@ -119,6 +128,9 @@ function sanitizeHeaders(raw) {
119
128
  if (fmt && !apiKeyFormat) apiKeyFormat = fmt;
120
129
  continue;
121
130
  }
131
+ if (name === "cookie") {
132
+ cookieCount = joined.split(";").filter((s) => s.trim().length > 0).length;
133
+ }
122
134
  if (isSecretHeader(name)) continue;
123
135
  let value = joined;
124
136
  if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {
@@ -134,6 +146,7 @@ function sanitizeHeaders(raw) {
134
146
  const result = { headers };
135
147
  if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;
136
148
  if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;
149
+ if (cookieCount !== void 0) result.cookieCount = cookieCount;
137
150
  return result;
138
151
  }
139
152
  function extractPlatformHeaders(sanitized) {
@@ -151,16 +164,28 @@ var CONNECTION_HEADER_SOURCES = [
151
164
  ["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
152
165
  ],
153
166
  ["region", ["x-vercel-ip-country-region", "cloudfront-viewer-country-region"]],
167
+ ["countryRegionName", ["cloudfront-viewer-country-region-name"]],
154
168
  ["city", ["x-vercel-ip-city", "cloudfront-viewer-city"]],
169
+ ["latitude", ["x-vercel-ip-latitude", "cloudfront-viewer-latitude"]],
170
+ ["longitude", ["x-vercel-ip-longitude", "cloudfront-viewer-longitude"]],
171
+ ["postalCode", ["x-vercel-ip-postal-code", "cloudfront-viewer-postal-code"]],
155
172
  ["asn", ["cloudfront-viewer-asn"]],
156
173
  ["tlsVersion", ["cloudfront-viewer-tls"]],
157
174
  ["httpVersion", ["cloudfront-viewer-http-version"]],
175
+ ["timeZone", ["x-vercel-ip-timezone", "cloudfront-viewer-time-zone"]],
176
+ ["metroCode", ["cloudfront-viewer-metro-code"]],
158
177
  // TLS fingerprints can only be computed where TLS terminates. When a CDN
159
178
  // terminates TLS in front of the merchant, its fingerprint headers ARE the
160
179
  // connection-layer truth — normalising them here is the permanent design.
161
- ["tlsFingerprint", ["cf-ja3-hash"]],
162
- ["ja4", ["cf-ja4"]]
180
+ // CloudFront delivers JA3/JA4 only via an origin request policy and only
181
+ // for HTTPS viewer connections.
182
+ ["tlsFingerprint", ["cf-ja3-hash", "cloudfront-viewer-ja3-fingerprint"]],
183
+ ["ja4", ["cf-ja4", "cloudfront-viewer-ja4-fingerprint"]],
184
+ // Header-structure fingerprint: browser/SDK-distinctive header ordering.
185
+ ["headerOrder", ["cloudfront-viewer-header-order"]],
186
+ ["headerCount", ["cloudfront-viewer-header-count"]]
163
187
  ];
188
+ var MAX_CONNECTION_VALUE_CHARS = 256;
164
189
  function deriveConnectionFromHeaders(headers) {
165
190
  if (!headers) return void 0;
166
191
  const lower = {};
@@ -173,20 +198,30 @@ function deriveConnectionFromHeaders(headers) {
173
198
  for (const source of sources) {
174
199
  const value = lower[source]?.trim();
175
200
  if (value) {
176
- connection[key] = value;
201
+ connection[key] = value.slice(0, MAX_CONNECTION_VALUE_CHARS);
177
202
  break;
178
203
  }
179
204
  }
180
205
  }
206
+ {
207
+ const addr = lower["cloudfront-viewer-address"]?.trim();
208
+ const portMatch = addr?.match(/:(\d+)$/);
209
+ if (portMatch) connection.sourcePort = portMatch[1];
210
+ if (!connection.ip && addr) connection.ip = addr.replace(/:\d+$/, "");
211
+ }
181
212
  if (!connection.ip) {
182
213
  const xff = lower["x-forwarded-for"];
183
214
  const client = xff?.split(",")[0]?.trim();
184
215
  if (client) connection.ip = client;
185
216
  }
217
+ if (!connection.tlsCipher) {
218
+ const cipher = connection.tlsVersion?.split(":")[1];
219
+ if (cipher) connection.tlsCipher = cipher;
220
+ }
186
221
  return Object.keys(connection).length > 0 ? connection : void 0;
187
222
  }
188
223
  function buildSdkObservedMetadata(rawHeaders) {
189
- const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);
224
+ const { headers, apiKeyFormat, platformHeaders, cookieCount } = sanitizeHeaders(rawHeaders);
190
225
  const flat = {};
191
226
  if (rawHeaders) {
192
227
  for (const name of Object.keys(rawHeaders)) {
@@ -201,11 +236,13 @@ function buildSdkObservedMetadata(rawHeaders) {
201
236
  ...apiKeyFormat && { apiKeyFormat },
202
237
  ...platformHeaders && { platformHeaders },
203
238
  ...connection && { connection },
239
+ ...cookieCount !== void 0 && { cookieCount },
204
240
  schemaVersion: CAPTURE_SCHEMA_VERSION
205
241
  };
206
242
  }
207
243
  export {
208
244
  CAPTURE_SCHEMA_VERSION,
245
+ MAX_CONNECTION_VALUE_CHARS,
209
246
  MAX_HEADERS,
210
247
  MAX_HEADER_VALUE_BYTES,
211
248
  MAX_TOTAL_BYTES,