@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.
- package/README.md +45 -21
- package/dist/adapter-interface/interface.d.mts +2 -2
- package/dist/adapter-interface/interface.d.ts +2 -2
- package/dist/adapters/express.d.mts +2 -2
- package/dist/adapters/express.d.ts +2 -2
- package/dist/adapters/express.js +145 -21
- package/dist/adapters/express.js.map +1 -1
- package/dist/adapters/express.mjs +145 -21
- package/dist/adapters/express.mjs.map +1 -1
- package/dist/adapters/mcp.d.mts +38 -30
- package/dist/adapters/mcp.d.ts +38 -30
- package/dist/adapters/mcp.js +72 -16
- package/dist/adapters/mcp.js.map +1 -1
- package/dist/adapters/mcp.mjs +72 -16
- package/dist/adapters/mcp.mjs.map +1 -1
- package/dist/adapters/nextjs.d.mts +2 -2
- package/dist/adapters/nextjs.d.ts +2 -2
- package/dist/adapters/nextjs.js +141 -19
- package/dist/adapters/nextjs.js.map +1 -1
- package/dist/adapters/nextjs.mjs +141 -19
- package/dist/adapters/nextjs.mjs.map +1 -1
- package/dist/adapters/sdk.d.mts +2 -2
- package/dist/adapters/sdk.d.ts +2 -2
- package/dist/adapters/sdk.js +24 -6
- package/dist/adapters/sdk.js.map +1 -1
- package/dist/adapters/sdk.mjs +24 -6
- package/dist/adapters/sdk.mjs.map +1 -1
- package/dist/agent/index.d.mts +2 -2
- package/dist/agent/index.d.ts +2 -2
- package/dist/agent/index.js +24 -3
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/index.mjs +24 -3
- package/dist/agent/index.mjs.map +1 -1
- package/dist/bin/astrasync-claude-hook.js +22 -4
- package/dist/bin/astrasync-codex-hook.js +22 -4
- package/dist/bin/astrasync-guard.js +26 -8
- package/dist/bin/astrasync.js +76 -23
- package/dist/browser/background.js +22 -4
- package/dist/browser/background.js.map +1 -1
- package/dist/browser/background.mjs +22 -4
- package/dist/browser/background.mjs.map +1 -1
- package/dist/browser/browser-adapter.d.mts +2 -2
- package/dist/browser/browser-adapter.d.ts +2 -2
- package/dist/claude-code/claude-code-adapter.d.mts +2 -2
- package/dist/claude-code/claude-code-adapter.d.ts +2 -2
- package/dist/cli/index.d.mts +2 -2
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.js +23 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +23 -2
- package/dist/cli/index.mjs.map +1 -1
- package/dist/codex/index.d.mts +2 -2
- package/dist/codex/index.d.ts +2 -2
- package/dist/codex/index.js +22 -4
- package/dist/codex/index.js.map +1 -1
- package/dist/codex/index.mjs +22 -4
- package/dist/codex/index.mjs.map +1 -1
- package/dist/cursor/cursor-adapter.d.mts +2 -2
- package/dist/cursor/cursor-adapter.d.ts +2 -2
- package/dist/cursor/extension.d.mts +2 -2
- package/dist/cursor/extension.d.ts +2 -2
- package/dist/cursor/extension.js +22 -4
- package/dist/cursor/extension.js.map +1 -1
- package/dist/cursor/extension.mjs +22 -4
- package/dist/cursor/extension.mjs.map +1 -1
- package/dist/edge-config.d.mts +1 -1
- package/dist/edge-config.d.ts +1 -1
- package/dist/edge-config.js +24 -1
- package/dist/edge-config.js.map +1 -1
- package/dist/edge-config.mjs +22 -1
- package/dist/edge-config.mjs.map +1 -1
- package/dist/edge-core/index.d.mts +1 -1
- package/dist/edge-core/index.d.ts +1 -1
- package/dist/edge-core/index.js +121 -16
- package/dist/edge-core/index.js.map +1 -1
- package/dist/edge-core/index.mjs +121 -16
- package/dist/edge-core/index.mjs.map +1 -1
- package/dist/{express-D_4hTn5Z.d.mts → express-CR6F3njD.d.mts} +1 -1
- package/dist/{express-BVd1_3FE.d.ts → express-DaqL11tx.d.ts} +1 -1
- package/dist/gateway/gateway.d.mts +2 -2
- package/dist/gateway/gateway.d.ts +2 -2
- package/dist/gateway/gateway.js +22 -4
- package/dist/gateway/gateway.js.map +1 -1
- package/dist/gateway/gateway.mjs +22 -4
- package/dist/gateway/gateway.mjs.map +1 -1
- package/dist/git-trigger/git-hooks.d.mts +2 -2
- package/dist/git-trigger/git-hooks.d.ts +2 -2
- package/dist/{index-BqHKlLOU.d.ts → index-B5Ts1WtM.d.ts} +1 -1
- package/dist/{index-T1aBoUcc.d.ts → index-BfIxCYIZ.d.ts} +1 -1
- package/dist/{index-BTeQ9fgV.d.mts → index-Cub7lK-T.d.mts} +1 -1
- package/dist/{index-DQb5-_1X.d.mts → index-CwAh7Tln.d.mts} +1 -1
- package/dist/index.d.mts +58 -9
- package/dist/index.d.ts +58 -9
- package/dist/index.js +218 -54
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +214 -54
- package/dist/index.mjs.map +1 -1
- package/dist/local-evaluator/evaluator.d.mts +2 -2
- package/dist/local-evaluator/evaluator.d.ts +2 -2
- package/dist/metadata-capture.d.mts +35 -5
- package/dist/metadata-capture.d.ts +35 -5
- package/dist/metadata-capture.js +44 -6
- package/dist/metadata-capture.js.map +1 -1
- package/dist/metadata-capture.mjs +43 -6
- package/dist/metadata-capture.mjs.map +1 -1
- package/dist/{nextjs-w7RTbP1P.d.ts → nextjs-DL6uE1ye.d.ts} +1 -1
- package/dist/{nextjs-WyeVr2Kp.d.mts → nextjs-DlX6xwbU.d.mts} +1 -1
- package/dist/platform-signatures.d.mts +18 -1
- package/dist/platform-signatures.d.ts +18 -1
- package/dist/platform-signatures.js +35 -2
- package/dist/platform-signatures.js.map +1 -1
- package/dist/platform-signatures.mjs +35 -2
- package/dist/platform-signatures.mjs.map +1 -1
- package/dist/registration/index.js +25 -4
- package/dist/registration/index.js.map +1 -1
- package/dist/registration/index.mjs +25 -4
- package/dist/registration/index.mjs.map +1 -1
- package/dist/{sdk-CbTNIkAa.d.mts → sdk-Bbh9B_dw.d.mts} +1 -1
- package/dist/{sdk-QeX0Z8Ho.d.ts → sdk-C6SASmsh.d.ts} +1 -1
- package/dist/transport/index.d.mts +2 -2
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.js +22 -1
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/index.mjs +22 -1
- package/dist/transport/index.mjs.map +1 -1
- package/dist/{types-r850cOt0.d.mts → types-C0zfSXMB.d.mts} +22 -0
- package/dist/{types-DqfPU5Bl.d.mts → types-C7Rb-w0d.d.mts} +1 -1
- package/dist/{types-sMxBT-nM.d.ts → types-DwSfO7Sb.d.ts} +22 -0
- package/dist/{types-BLUx92FJ.d.ts → types-nIh3HtuA.d.ts} +1 -1
- package/dist/ui/index.d.mts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/dist/verify.d.mts +1 -1
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +29 -11
- package/dist/verify.js.map +1 -1
- package/dist/verify.mjs +29 -11
- package/dist/verify.mjs.map +1 -1
- 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 (
|
|
233
|
-
|
|
234
|
-
When an
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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,
|
|
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-
|
|
3
|
-
import '../types-
|
|
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-
|
|
3
|
-
import '../types-
|
|
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-
|
|
3
|
-
export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-
|
|
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-
|
|
3
|
-
export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-
|
|
2
|
+
import '../types-DwSfO7Sb.js';
|
|
3
|
+
export { c as createMiddleware, a as extractAstraSyncCredentials } from '../express-DaqL11tx.js';
|
|
4
4
|
import '../metadata-capture.js';
|
package/dist/adapters/express.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = [
|
|
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
|
-
|
|
1071
|
-
|
|
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
|
|
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
|
-
|
|
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,
|