@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
package/README.md CHANGED
@@ -229,20 +229,39 @@ The CLI uses the same response logic — it will print a "Sign in to approve" me
229
229
 
230
230
  > A `PDLSSConfig` type exists in both `/registration` and `/agent`, with **different shapes**. `/registration`'s `PDLSSConfig` is the boundary declaration submitted at register time; `/agent`'s `PDLSSConfig` is the per-request runtime request shape. Disambiguate via the import path — the root export deliberately does not re-export either.
231
231
 
232
- ### Inner-hop verification (MCP → REST dedupe with X-Astra-Verified-Hop)
233
-
234
- When an MCP tool calls an inner REST endpoint that ALSO runs the
235
- verification gateway, two verify-access calls fire for the same agent in
236
- the same logical request — audit gets noisy and you pay a doubled
237
- round-trip. The `X-Astra-Verified-Hop` header marks the inner hop as
238
- already-verified by the upstream so the inner middleware skips a
239
- redundant verify-access call.
240
-
241
- The MCP middleware emits the marker automatically on its outbound
242
- response. Tool handlers calling inner REST hops need to forward it on
243
- outgoing fetches; the inner middleware reads it via `parseVerifiedHop` +
244
- `isVerifiedHopValidFor` and skips a duplicate verify-access if the
245
- marker is fresh AND matches the agent claimed on the inner hop.
232
+ ### Inner-hop verification (cross-hop dedupe with X-Astra-Verified-Hop)
233
+
234
+ When an upstream AstraSync hop and an inner gateway-protected endpoint
235
+ both verify the same agent in the same logical request, two verify-access
236
+ calls fire — audit gets noisy and you pay a doubled round-trip. The
237
+ `X-Astra-Verified-Hop` header marks the inner hop as already-verified by
238
+ the upstream so the inner middleware skips the redundant call.
239
+
240
+ Two upstreams emit the marker automatically (SDK 4.3.0+):
241
+
242
+ - the **MCP middleware**, on its outbound response (tool handlers calling
243
+ inner REST hops forward it on outgoing fetches — worked example below);
244
+ - the **edge gateway** (`adapter-lambda` / `adapter-edge`), toward the
245
+ origin whenever its verify-access call produced identity (authenticate /
246
+ authorize depth). An SDK-middleware origin behind the TAG then skips the
247
+ double verify by setting `trustVerifiedHop: true` — at the default
248
+ classify depth no marker is emitted and the origin verifies as usual.
249
+
250
+ `trustVerifiedHop` + `verifiedHopMaxAgeMs` are supported on the `express`,
251
+ `nextjs`, and `mcp` middleware options (all default off). The inner
252
+ middleware validates the marker via `parseVerifiedHop` +
253
+ `isVerifiedHopValidFor` and skips a duplicate verify-access only if the
254
+ marker is fresh AND matches the agent claimed on the inner hop. The edge
255
+ gateway strips any inbound `X-Astra-Verified-Hop` on evaluated passes, so
256
+ a viewer can never smuggle one through the TAG.
257
+
258
+ **Trust model (read before enabling):** the marker is UNSIGNED — a dedupe
259
+ advisory, not proof. Enable `trustVerifiedHop` only when every network
260
+ path to the inner hop crosses a stripping gateway (the TAG) or an
261
+ equivalent trusted boundary; inside that boundary a forger could reduce
262
+ verify-access from "every request" to "once per freshness window" for an
263
+ agent id it already presents. This is the same trust model as the
264
+ unsigned `X-AstraSync-*` attestation headers.
246
265
 
247
266
  **Worked example — upstream MCP → inner REST hop:**
248
267
 
@@ -589,13 +608,18 @@ The **Trusted Agent Gateway (TAG)** is AstraSync's verification gateway at the C
589
608
 
590
609
  Both consume the same dashboard-managed policy (`./edge-config`), the same platform fingerprint registry (`./platform-signatures`), and the same header sanitiser (`./metadata-capture`) as this SDK — one classification everywhere.
591
610
 
592
- ### What each integration captures
611
+ ### What each integration captures (canonical capture matrix)
612
+
613
+ Header-level capture (`ObservedMetadata.headers` etc.) is identical everywhere: **automatic on every verify** for the edge adapters and the `express` / `nextjs` / `mcp` SDK adapters (no config, no opt-out). What differs is the **connection layer** — and the deciding variable is _who operates the CDN in front of your origin_:
614
+
615
+ | Integration | Connection layer | TLS fingerprint (JA3/JA4) |
616
+ | -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
617
+ | **Edge adapter** (`adapter-lambda`, `adapter-edge`) | **Full** — native platform signals (IP, ASN, geo, TLS, HTTP version, device class, header structure) | CloudFront: **JA3 + JA4** (2.2.0+, via the template's origin request policy). Cloudflare Workers: Bot Management zones only. Vercel: JA4 with BotID. Fastly: none |
618
+ | **SDK behind a CDN you control** (your own Cloudflare / Fastly / Vercel / CloudFront distribution) | **Derived** — geo/ASN/TLS promoted from the CDN's injected headers (`deriveConnectionFromHeaders`) | Cloudflare: JA3 + JA4 (`cf-ja3-hash` / `cf-ja4`, plan-dependent). CloudFront: JA3 + JA4 — add the `CloudFront-Viewer-JA3/JA4-Fingerprint` headers to your origin request policy |
619
+ | **SDK behind a platform-managed CDN** (Railway, Render, Fly, Heroku, …) | **IP only** — platform CDNs forward `x-forwarded-for` but NOT their provider-specific geo/TLS headers to customer origins | None — no fingerprint header reaches your origin |
620
+ | **SDK, no CDN** | Absent (IP from `x-forwarded-for` when a proxy sets it) | None |
593
621
 
594
- | Integration | Header-level capture (`ObservedMetadata`) | Connection layer (IP / ASN / country / TLS / HTTP version / device class) |
595
- | ------------------------------------------------ | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
596
- | Edge adapters (`adapter-lambda`, `adapter-edge`) | Yes | **Yes** — native platform signals (the authoritative source) |
597
- | `express` + `nextjs` SDK adapters | Yes — **automatic on every verify** (no config, no opt-out) | Derived from CDN-injected headers when your origin sits behind Cloudflare / Fastly / Vercel / CloudFront (`deriveConnectionFromHeaders`, from 4.1.0); absent otherwise |
598
- | `mcp` SDK adapter | Yes — from 4.1.0 | Same CDN-header derivation as express/nextjs |
622
+ > "Behind Fastly" ≠ "receives Fastly's headers": platform-managed CDNs are operated by the platform, and they do not forward `Fastly-Client-IP`-style headers to your origin. If connection-layer signals or fingerprints matter to you on a managed platform, deploy the edge adapter in front (bucket 1) — that is the whole point of the TAG.
599
623
 
600
624
  > **Note:** `runCommercePipeline` (on the `./transport` subpath) is the **server-side** orchestrator of commerce-protocol verification — it runs inside the AstraSync verify-access service. The edge adapters forward raw commerce artifacts to verify-access and never bundle or call the pipeline themselves.
601
625
 
@@ -622,7 +646,7 @@ The ONE sanitiser deciding what is safe to store from an inbound agent's request
622
646
  - `sanitizeHeaders(raw)` → `{ headers, apiKeyFormat?, platformHeaders? }` — drops genuine secrets (cookies, signatures, AstraSync credential headers), reduces credential headers to a safe **format prefix** (e.g. `sk-ant-api03` — a platform signal, never the key), keeps everything else verbatim, and enforces size caps (`MAX_HEADERS`, `MAX_HEADER_VALUE_BYTES`, `MAX_TOTAL_BYTES`).
623
647
  - `extractApiKeyFormat(value)` — the safe key-format prefix of a credential header value, or `undefined` for unrecognised/opaque tokens.
624
648
  - `extractPlatformHeaders(sanitized)` — the platform-signal subset (`openai-organization`, `anthropic-version`, `x-goog-user-project`, …) of an already-sanitised map.
625
- - `deriveConnectionFromHeaders(headers)` — promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront geo, client-IP, and TLS-fingerprint headers, incl. `cf-ja3-hash` / `cf-ja4`) into the `connection` block, so SDK-only deployments behind a CDN still get connection-layer attribution. Native edge signals remain authoritative; this is the SDK-level fallback.
649
+ - `deriveConnectionFromHeaders(headers)` — promotes CDN-injected headers (Cloudflare / Fastly / Vercel / CloudFront geo, client-IP, TLS-fingerprint, and header-structure headers — incl. `cf-ja3-hash` / `cf-ja4` and `cloudfront-viewer-ja3-fingerprint` / `-ja4-fingerprint` from 4.3.0) into the `connection` block, so SDK-only deployments behind a CDN **you control** still get connection-layer attribution (see the capture matrix for the platform-managed-CDN caveat). Values are truncated to `MAX_CONNECTION_VALUE_CHARS` (256 — the platform bound). Native edge signals remain authoritative; this is the SDK-level fallback.
626
650
  - `buildSdkObservedMetadata(rawHeaders)` — the one builder the express / nextjs / mcp adapters share: sanitised headers + `apiKeyFormat` + `platformHeaders` + derived `connection` + `schemaVersion`.
627
651
  - `CAPTURE_SCHEMA_VERSION` — the capture-semantics version stamped on every `ObservedMetadata` (currently `1`). Capture semantics are **frozen within a major version**: `schemaVersion` bumps only on a semantic change to what is kept/dropped/derived, never for additive signals. The verify body's `sdkVersion` disambiguates builds.
628
652
  - Types: `ObservedMetadata` (`{ headers, apiKeyFormat?, platformHeaders?, connection?, schemaVersion? }` — `connection` is populated natively by edge adapters and via CDN-header derivation by SDK adapters), `SanitizeHeadersResult`.
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.mjs';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-DqfPU5Bl.mjs';
3
- import '../types-r850cOt0.mjs';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-C7Rb-w0d.mjs';
3
+ import '../types-C0zfSXMB.mjs';
4
4
  import '../metadata-capture.mjs';
5
5
 
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { AstraSyncGateway } from '../gateway/gateway.js';
2
- import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-BLUx92FJ.js';
3
- import '../types-sMxBT-nM.js';
2
+ import { A as AgentAction, I as InterceptResult, P as PDLSSContext, V as VerificationDecision } from '../types-nIh3HtuA.js';
3
+ import '../types-DwSfO7Sb.js';
4
4
  import '../metadata-capture.js';
5
5
 
6
6
  /**
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- import '../types-r850cOt0.mjs';
3
- export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-D_4hTn5Z.mjs';
2
+ import '../types-C0zfSXMB.mjs';
3
+ export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-CR6F3njD.mjs';
4
4
  import '../metadata-capture.mjs';
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- import '../types-sMxBT-nM.js';
3
- export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-BVd1_3FE.js';
2
+ import '../types-DwSfO7Sb.js';
3
+ export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-DaqL11tx.js';
4
4
  import '../metadata-capture.js';
@@ -25,6 +25,27 @@ __export(express_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(express_exports);
27
27
 
28
+ // src/version.ts
29
+ var SDK_VERSION = "4.4.0";
30
+
31
+ // src/http.ts
32
+ var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
33
+ function withUserAgent(initHeaders) {
34
+ if (!initHeaders) return { "user-agent": SDK_USER_AGENT };
35
+ if (initHeaders instanceof Headers) {
36
+ const copy = new Headers(initHeaders);
37
+ if (!copy.has("user-agent")) copy.set("user-agent", SDK_USER_AGENT);
38
+ return copy;
39
+ }
40
+ if (Array.isArray(initHeaders)) {
41
+ const has2 = initHeaders.some(([name]) => name.toLowerCase() === "user-agent");
42
+ return has2 ? initHeaders : [...initHeaders, ["user-agent", SDK_USER_AGENT]];
43
+ }
44
+ const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === "user-agent");
45
+ return has ? initHeaders : { "user-agent": SDK_USER_AGENT, ...initHeaders };
46
+ }
47
+ var sdkFetch = (input, init) => fetch(input, { ...init, headers: withUserAgent(init?.headers) });
48
+
28
49
  // src/adapters/stepup-poll.ts
29
50
  var DEFAULT_POLL_INTERVAL_MS = 3e3;
30
51
  var MIN_POLL_INTERVAL_MS = 250;
@@ -39,7 +60,7 @@ function resolvePollUrl(config, pollUrl) {
39
60
  async function pollStepUpStatus(config, approval) {
40
61
  if (!approval.pollUrl) return null;
41
62
  try {
42
- const res = await fetch(resolvePollUrl(config, approval.pollUrl), {
63
+ const res = await sdkFetch(resolvePollUrl(config, approval.pollUrl), {
43
64
  method: "GET",
44
65
  signal: AbortSignal.timeout(POLL_REQUEST_TIMEOUT_MS)
45
66
  });
@@ -91,9 +112,6 @@ function getTrustLevel(score) {
91
112
  return "BRONZE";
92
113
  }
93
114
 
94
- // src/version.ts
95
- var SDK_VERSION = "4.1.0";
96
-
97
115
  // src/well-known.ts
98
116
  var CACHE_TTL_MS = 60 * 60 * 1e3;
99
117
  var cache = /* @__PURE__ */ new Map();
@@ -104,7 +122,7 @@ function wellKnownUrl(apiBaseUrl) {
104
122
  }
105
123
  async function fetchWellKnown(apiBaseUrl) {
106
124
  const url = wellKnownUrl(apiBaseUrl);
107
- const response = await fetch(url, {
125
+ const response = await sdkFetch(url, {
108
126
  method: "GET",
109
127
  headers: { Accept: "application/json" },
110
128
  signal: AbortSignal.timeout(5e3)
@@ -174,7 +192,7 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
174
192
  initCheckPerformed = true;
175
193
  try {
176
194
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
177
- const response = await fetch(probeUrl, { method: "HEAD" });
195
+ const response = await sdkFetch(probeUrl, { method: "HEAD" });
178
196
  const contentType = response.headers.get("content-type") ?? "";
179
197
  if (contentType.startsWith("text/html")) {
180
198
  const message = `[VerificationGateway] apiBaseUrl '${apiBaseUrl}' returned HTML (content-type: ${contentType}). This usually means apiBaseUrl is pointing at a marketing site instead of the API. Expected: 'https://astrasync.ai/api' (prod) or 'https://staging.astrasync.ai/api' (staging).`;
@@ -188,7 +206,7 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
188
206
  );
189
207
  }
190
208
  if (apiKey) {
191
- const selfCheck = await fetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
209
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
192
210
  headers: { Authorization: `Bearer ${apiKey}` }
193
211
  });
194
212
  if (selfCheck.status === 401) {
@@ -435,7 +453,7 @@ async function callVerifyAccessAPI(config, request) {
435
453
  headers["X-API-Key"] = config.apiKey;
436
454
  }
437
455
  try {
438
- const response = await fetch(`${config.apiBaseUrl}/agents/verify-access`, {
456
+ const response = await sdkFetch(`${config.apiBaseUrl}/agents/verify-access`, {
439
457
  method: "POST",
440
458
  headers,
441
459
  body: JSON.stringify(body),
@@ -678,7 +696,7 @@ async function recordDecision(config, sessionId, decision, reason) {
678
696
  headers["Authorization"] = `Bearer ${config.apiKey}`;
679
697
  headers["X-API-Key"] = config.apiKey;
680
698
  }
681
- await fetch(`${config.apiBaseUrl}/agents/verify-access/${sessionId}/decision`, {
699
+ await sdkFetch(`${config.apiBaseUrl}/agents/verify-access/${sessionId}/decision`, {
682
700
  method: "POST",
683
701
  headers,
684
702
  body: JSON.stringify({ decision, reason })
@@ -693,7 +711,7 @@ async function fetchRoutes(config, counterpartyId) {
693
711
  headers["X-API-Key"] = config.apiKey;
694
712
  }
695
713
  try {
696
- const response = await fetch(
714
+ const response = await sdkFetch(
697
715
  `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
698
716
  { method: "GET", headers }
699
717
  );
@@ -712,7 +730,7 @@ async function reportUnregisteredAttempt(config, data) {
712
730
  ...counterpartyUrl && { counterpartyUrl },
713
731
  ...config.counterpartyId && { counterpartyId: config.counterpartyId }
714
732
  };
715
- await fetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
733
+ await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
716
734
  method: "POST",
717
735
  headers: { "Content-Type": "application/json" },
718
736
  body: JSON.stringify(body)
@@ -721,7 +739,7 @@ async function reportUnregisteredAttempt(config, data) {
721
739
  }
722
740
  async function reportCounterpartyPreCheckFailure(config, data) {
723
741
  const apiBaseUrl = config.apiBaseUrl || DEFAULT_CONFIG.apiBaseUrl;
724
- await fetch(`${apiBaseUrl}/verification-activity/counterparty-pre-check-failure`, {
742
+ await sdkFetch(`${apiBaseUrl}/verification-activity/counterparty-pre-check-failure`, {
725
743
  method: "POST",
726
744
  headers: { "Content-Type": "application/json" },
727
745
  body: JSON.stringify(data)
@@ -775,6 +793,31 @@ function extractHttpCredentials(headers) {
775
793
  return credentials;
776
794
  }
777
795
 
796
+ // src/verified-hop.ts
797
+ var MCP_VERIFIED_HOP_HEADER = "X-Astra-Verified-Hop";
798
+ var MCP_VERIFIED_HOP_MAX_AGE_MS = 6e4;
799
+ function parseVerifiedHop(value) {
800
+ if (!value) return null;
801
+ const parts = value.split(";");
802
+ if (parts.length !== 3) return null;
803
+ const [astraId, sessionId, checkedAtRaw] = parts;
804
+ if (!astraId) return null;
805
+ const checkedAt = Number(checkedAtRaw);
806
+ if (!Number.isFinite(checkedAt) || checkedAt <= 0) return null;
807
+ return {
808
+ astraId,
809
+ ...sessionId ? { sessionId } : {},
810
+ checkedAt
811
+ };
812
+ }
813
+ function isVerifiedHopValidFor(marker, expectedAstraId, opts = {}) {
814
+ if (!marker) return false;
815
+ if (marker.astraId !== expectedAstraId) return false;
816
+ const maxAge = opts.maxAgeMs ?? MCP_VERIFIED_HOP_MAX_AGE_MS;
817
+ const now = opts.now ?? Date.now();
818
+ return now - marker.checkedAt <= maxAge && now >= marker.checkedAt;
819
+ }
820
+
778
821
  // src/adapters/http-pdlss.ts
779
822
  var HTTP_METHOD_ACTION_TABLE = {
780
823
  GET: "data.read",
@@ -925,7 +968,9 @@ var SECRET_HEADER_NAMES = /* @__PURE__ */ new Set([
925
968
  "x-astrasync-signature",
926
969
  "x-astrasync-secret",
927
970
  "x-hub-signature",
928
- "x-hub-signature-256"
971
+ "x-hub-signature-256",
972
+ // Short-lived verify dedupe token — must never persist in stored metadata.
973
+ "x-astra-verified-hop"
929
974
  ]);
930
975
  var SECRET_HEADER_PREFIXES = ["x-astrasync-", "kya-", "kya_"];
931
976
  var CREDENTIAL_HEADER_NAMES = /* @__PURE__ */ new Set([
@@ -950,7 +995,13 @@ var PLATFORM_HEADER_NAMES = /* @__PURE__ */ new Set([
950
995
  "x-agent-card-url",
951
996
  "x-agent-provider"
952
997
  ]);
953
- var PLATFORM_HEADER_PREFIXES = ["x-goog-", "x-stainless-", "x-anthropic-", "x-openai-"];
998
+ var PLATFORM_HEADER_PREFIXES = [
999
+ "x-goog-",
1000
+ "x-stainless-",
1001
+ "x-anthropic-",
1002
+ "x-openai-",
1003
+ "sec-ch-"
1004
+ ];
954
1005
  var KEY_FORMAT_PREFIXES = [
955
1006
  "sk-ant-api03",
956
1007
  "sk-ant-api",
@@ -1022,6 +1073,7 @@ function sanitizeHeaders(raw) {
1022
1073
  const headers = {};
1023
1074
  const platformHeaders = {};
1024
1075
  let apiKeyFormat;
1076
+ let cookieCount;
1025
1077
  let totalBytes = 0;
1026
1078
  let count = 0;
1027
1079
  if (!raw) return { headers };
@@ -1036,6 +1088,9 @@ function sanitizeHeaders(raw) {
1036
1088
  if (fmt && !apiKeyFormat) apiKeyFormat = fmt;
1037
1089
  continue;
1038
1090
  }
1091
+ if (name === "cookie") {
1092
+ cookieCount = joined.split(";").filter((s) => s.trim().length > 0).length;
1093
+ }
1039
1094
  if (isSecretHeader(name)) continue;
1040
1095
  let value = joined;
1041
1096
  if (byteLength(value) > MAX_HEADER_VALUE_BYTES) {
@@ -1051,6 +1106,7 @@ function sanitizeHeaders(raw) {
1051
1106
  const result = { headers };
1052
1107
  if (apiKeyFormat) result.apiKeyFormat = apiKeyFormat;
1053
1108
  if (Object.keys(platformHeaders).length > 0) result.platformHeaders = platformHeaders;
1109
+ if (cookieCount !== void 0) result.cookieCount = cookieCount;
1054
1110
  return result;
1055
1111
  }
1056
1112
  var CONNECTION_HEADER_SOURCES = [
@@ -1060,16 +1116,28 @@ var CONNECTION_HEADER_SOURCES = [
1060
1116
  ["cf-ipcountry", "x-vercel-ip-country", "cloudfront-viewer-country", "fastly-geo-country"]
1061
1117
  ],
1062
1118
  ["region", ["x-vercel-ip-country-region", "cloudfront-viewer-country-region"]],
1119
+ ["countryRegionName", ["cloudfront-viewer-country-region-name"]],
1063
1120
  ["city", ["x-vercel-ip-city", "cloudfront-viewer-city"]],
1121
+ ["latitude", ["x-vercel-ip-latitude", "cloudfront-viewer-latitude"]],
1122
+ ["longitude", ["x-vercel-ip-longitude", "cloudfront-viewer-longitude"]],
1123
+ ["postalCode", ["x-vercel-ip-postal-code", "cloudfront-viewer-postal-code"]],
1064
1124
  ["asn", ["cloudfront-viewer-asn"]],
1065
1125
  ["tlsVersion", ["cloudfront-viewer-tls"]],
1066
1126
  ["httpVersion", ["cloudfront-viewer-http-version"]],
1127
+ ["timeZone", ["x-vercel-ip-timezone", "cloudfront-viewer-time-zone"]],
1128
+ ["metroCode", ["cloudfront-viewer-metro-code"]],
1067
1129
  // TLS fingerprints can only be computed where TLS terminates. When a CDN
1068
1130
  // terminates TLS in front of the merchant, its fingerprint headers ARE the
1069
1131
  // connection-layer truth — normalising them here is the permanent design.
1070
- ["tlsFingerprint", ["cf-ja3-hash"]],
1071
- ["ja4", ["cf-ja4"]]
1132
+ // CloudFront delivers JA3/JA4 only via an origin request policy and only
1133
+ // for HTTPS viewer connections.
1134
+ ["tlsFingerprint", ["cf-ja3-hash", "cloudfront-viewer-ja3-fingerprint"]],
1135
+ ["ja4", ["cf-ja4", "cloudfront-viewer-ja4-fingerprint"]],
1136
+ // Header-structure fingerprint: browser/SDK-distinctive header ordering.
1137
+ ["headerOrder", ["cloudfront-viewer-header-order"]],
1138
+ ["headerCount", ["cloudfront-viewer-header-count"]]
1072
1139
  ];
1140
+ var MAX_CONNECTION_VALUE_CHARS = 256;
1073
1141
  function deriveConnectionFromHeaders(headers) {
1074
1142
  if (!headers) return void 0;
1075
1143
  const lower = {};
@@ -1082,20 +1150,30 @@ function deriveConnectionFromHeaders(headers) {
1082
1150
  for (const source of sources) {
1083
1151
  const value = lower[source]?.trim();
1084
1152
  if (value) {
1085
- connection[key] = value;
1153
+ connection[key] = value.slice(0, MAX_CONNECTION_VALUE_CHARS);
1086
1154
  break;
1087
1155
  }
1088
1156
  }
1089
1157
  }
1158
+ {
1159
+ const addr = lower["cloudfront-viewer-address"]?.trim();
1160
+ const portMatch = addr?.match(/:(\d+)$/);
1161
+ if (portMatch) connection.sourcePort = portMatch[1];
1162
+ if (!connection.ip && addr) connection.ip = addr.replace(/:\d+$/, "");
1163
+ }
1090
1164
  if (!connection.ip) {
1091
1165
  const xff = lower["x-forwarded-for"];
1092
1166
  const client = xff?.split(",")[0]?.trim();
1093
1167
  if (client) connection.ip = client;
1094
1168
  }
1169
+ if (!connection.tlsCipher) {
1170
+ const cipher = connection.tlsVersion?.split(":")[1];
1171
+ if (cipher) connection.tlsCipher = cipher;
1172
+ }
1095
1173
  return Object.keys(connection).length > 0 ? connection : void 0;
1096
1174
  }
1097
1175
  function buildSdkObservedMetadata(rawHeaders) {
1098
- const { headers, apiKeyFormat, platformHeaders } = sanitizeHeaders(rawHeaders);
1176
+ const { headers, apiKeyFormat, platformHeaders, cookieCount } = sanitizeHeaders(rawHeaders);
1099
1177
  const flat = {};
1100
1178
  if (rawHeaders) {
1101
1179
  for (const name of Object.keys(rawHeaders)) {
@@ -1110,6 +1188,7 @@ function buildSdkObservedMetadata(rawHeaders) {
1110
1188
  ...apiKeyFormat && { apiKeyFormat },
1111
1189
  ...platformHeaders && { platformHeaders },
1112
1190
  ...connection && { connection },
1191
+ ...cookieCount !== void 0 && { cookieCount },
1113
1192
  schemaVersion: CAPTURE_SCHEMA_VERSION
1114
1193
  };
1115
1194
  }
@@ -1154,7 +1233,7 @@ async function fetchEdgeConfig(config, counterpartyId, opts) {
1154
1233
  }
1155
1234
  if (opts?.etag) headers["If-None-Match"] = opts.etag;
1156
1235
  try {
1157
- const response = await fetch(
1236
+ const response = await sdkFetch(
1158
1237
  `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/edge-config`,
1159
1238
  { method: "GET", headers, signal: AbortSignal.timeout(opts?.timeoutMs ?? 5e3) }
1160
1239
  );
@@ -1266,12 +1345,43 @@ var PLATFORM_AGENT_SIGNATURES = [
1266
1345
  uaPatterns: [/perplexity/i],
1267
1346
  agentCardPatterns: [/perplexity\.ai/i],
1268
1347
  apiKeyFormatPatterns: [/^pplx-/i]
1348
+ },
1349
+ // Agentic browsers — versioned-token patterns ONLY (a stock Chrome UA must
1350
+ // never match). Distinct vendor keys (not 'chatgpt'/'perplexity'): the
1351
+ // browser product is a different surface than the assistant service, and
1352
+ // the DB registry mirrors one row per vendor. These sit after the primary
1353
+ // vendor entries so a UA carrying an explicit vendor token keeps its
1354
+ // primary attribution; a browser-shaped UA with the agentic token is
1355
+ // caught here BEFORE classify's human tier, which is what makes these
1356
+ // visits observable at all. Token drift is real (some builds mimic Chrome
1357
+ // outright) — re-verify against captured events when a new browser ships,
1358
+ // and check `sec-ch-ua` platformHeaders for the real brand.
1359
+ {
1360
+ vendor: "chatgpt-atlas",
1361
+ displayName: "ChatGPT Atlas browser",
1362
+ uaPatterns: [/\batlas\/\d/i],
1363
+ headerPatterns: [/^sec-ch-ua: .*atlas/i]
1364
+ },
1365
+ {
1366
+ vendor: "perplexity-comet",
1367
+ displayName: "Perplexity Comet browser",
1368
+ uaPatterns: [/\bcomet\/\d/i],
1369
+ headerPatterns: [/^sec-ch-ua: .*comet/i]
1370
+ },
1371
+ // Our own SDK self-identifies (sdkFetch stamps this UA) — first-party
1372
+ // traffic must never pollute the unknown/anonymous pool it exists to audit.
1373
+ {
1374
+ vendor: "astrasync-sdk",
1375
+ displayName: "AstraSync SDK",
1376
+ uaPatterns: [/^astrasync-sdk\//i]
1269
1377
  }
1270
1378
  ];
1271
1379
  function matchPlatformSignature(signatures, input) {
1272
1380
  const ua = input.userAgent ?? "";
1273
1381
  const cardUrl = input.agentCardUrl ?? "";
1274
1382
  const apiKeyFormat = input.apiKeyFormat ?? "";
1383
+ const ja4 = input.ja4 ?? "";
1384
+ const asn = input.asn ?? "";
1275
1385
  const headerLines = [];
1276
1386
  if (input.platformHeaders) {
1277
1387
  for (const [name, value] of Object.entries(input.platformHeaders)) {
@@ -1283,7 +1393,9 @@ function matchPlatformSignature(signatures, input) {
1283
1393
  const cardMatch = sig.agentCardPatterns?.some((p) => p.test(cardUrl));
1284
1394
  const keyMatch = apiKeyFormat.length > 0 && sig.apiKeyFormatPatterns?.some((p) => p.test(apiKeyFormat));
1285
1395
  const headerMatch = headerLines.length > 0 && sig.headerPatterns?.some((p) => headerLines.some((line) => p.test(line)));
1286
- if (uaMatch || cardMatch || keyMatch || headerMatch) {
1396
+ const ja4Match = ja4.length > 0 && sig.ja4Patterns?.some((p) => p.test(ja4));
1397
+ const asnMatch = asn.length > 0 && sig.asnList?.includes(asn);
1398
+ if (uaMatch || cardMatch || keyMatch || headerMatch || ja4Match || asnMatch) {
1287
1399
  const uaBase = ua.split(/[\s/]/)[0]?.toLowerCase() ?? "";
1288
1400
  const cardHost = (() => {
1289
1401
  try {
@@ -1292,7 +1404,7 @@ function matchPlatformSignature(signatures, input) {
1292
1404
  return "";
1293
1405
  }
1294
1406
  })();
1295
- const keyBasis = uaBase || cardHost || apiKeyFormat || "unknown";
1407
+ const keyBasis = uaBase || cardHost || apiKeyFormat || ja4 || asn && `asn${asn}` || "unknown";
1296
1408
  return {
1297
1409
  vendor: sig.vendor,
1298
1410
  fingerprintKey: `${sig.vendor}:${keyBasis}`,
@@ -1527,6 +1639,8 @@ function createMiddleware(options) {
1527
1639
  failOnError = "open",
1528
1640
  caseInsensitiveRouteMatch = false,
1529
1641
  anonymousBeacon = true,
1642
+ trustVerifiedHop = false,
1643
+ verifiedHopMaxAgeMs,
1530
1644
  ...config
1531
1645
  } = options;
1532
1646
  if (config.apiBaseUrl) {
@@ -1619,6 +1733,16 @@ function createMiddleware(options) {
1619
1733
  beaconPassThrough?.(req);
1620
1734
  return next();
1621
1735
  }
1736
+ if (trustVerifiedHop && credentials.astraId) {
1737
+ const hopRaw = req.headers[MCP_VERIFIED_HOP_HEADER.toLowerCase()];
1738
+ const hopValue = typeof hopRaw === "string" ? hopRaw : Array.isArray(hopRaw) ? hopRaw[0] : void 0;
1739
+ const marker = parseVerifiedHop(hopValue);
1740
+ if (isVerifiedHopValidFor(marker, credentials.astraId, {
1741
+ ...verifiedHopMaxAgeMs !== void 0 ? { maxAgeMs: verifiedHopMaxAgeMs } : {}
1742
+ })) {
1743
+ return next();
1744
+ }
1745
+ }
1622
1746
  const pdlssPair = resolveRequestPdlss(
1623
1747
  req,
1624
1748
  routeConfig,