@astrasyncai/verification-gateway 5.13.0 → 5.14.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 (73) hide show
  1. package/dist/adapters/express.js +28 -10
  2. package/dist/adapters/express.js.map +1 -1
  3. package/dist/adapters/express.mjs +28 -10
  4. package/dist/adapters/express.mjs.map +1 -1
  5. package/dist/adapters/mcp.js +1 -1
  6. package/dist/adapters/mcp.js.map +1 -1
  7. package/dist/adapters/mcp.mjs +1 -1
  8. package/dist/adapters/mcp.mjs.map +1 -1
  9. package/dist/adapters/nextjs.js +28 -10
  10. package/dist/adapters/nextjs.js.map +1 -1
  11. package/dist/adapters/nextjs.mjs +28 -10
  12. package/dist/adapters/nextjs.mjs.map +1 -1
  13. package/dist/adapters/sdk.js +1 -1
  14. package/dist/adapters/sdk.js.map +1 -1
  15. package/dist/adapters/sdk.mjs +1 -1
  16. package/dist/adapters/sdk.mjs.map +1 -1
  17. package/dist/agent/index.js +1 -1
  18. package/dist/agent/index.js.map +1 -1
  19. package/dist/agent/index.mjs +1 -1
  20. package/dist/agent/index.mjs.map +1 -1
  21. package/dist/bin/astrasync-claude-hook.js +28 -10
  22. package/dist/bin/astrasync-codex-hook.js +28 -10
  23. package/dist/bin/astrasync-guard.js +28 -10
  24. package/dist/bin/astrasync.js +29 -11
  25. package/dist/browser/background.js +28 -10
  26. package/dist/browser/background.js.map +1 -1
  27. package/dist/browser/background.mjs +28 -10
  28. package/dist/browser/background.mjs.map +1 -1
  29. package/dist/cli/index.js +1 -1
  30. package/dist/cli/index.js.map +1 -1
  31. package/dist/cli/index.mjs +1 -1
  32. package/dist/cli/index.mjs.map +1 -1
  33. package/dist/codex/index.js +28 -10
  34. package/dist/codex/index.js.map +1 -1
  35. package/dist/codex/index.mjs +28 -10
  36. package/dist/codex/index.mjs.map +1 -1
  37. package/dist/cursor/extension.js +28 -10
  38. package/dist/cursor/extension.js.map +1 -1
  39. package/dist/cursor/extension.mjs +28 -10
  40. package/dist/cursor/extension.mjs.map +1 -1
  41. package/dist/edge-config.js +1 -1
  42. package/dist/edge-config.js.map +1 -1
  43. package/dist/edge-config.mjs +1 -1
  44. package/dist/edge-config.mjs.map +1 -1
  45. package/dist/edge-core/index.d.mts +32 -4
  46. package/dist/edge-core/index.d.ts +32 -4
  47. package/dist/edge-core/index.js +28 -10
  48. package/dist/edge-core/index.js.map +1 -1
  49. package/dist/edge-core/index.mjs +28 -10
  50. package/dist/edge-core/index.mjs.map +1 -1
  51. package/dist/gateway/gateway.js +28 -10
  52. package/dist/gateway/gateway.js.map +1 -1
  53. package/dist/gateway/gateway.mjs +28 -10
  54. package/dist/gateway/gateway.mjs.map +1 -1
  55. package/dist/index.d.mts +2 -2
  56. package/dist/index.d.ts +2 -2
  57. package/dist/index.js +28 -10
  58. package/dist/index.js.map +1 -1
  59. package/dist/index.mjs +28 -10
  60. package/dist/index.mjs.map +1 -1
  61. package/dist/registration/index.js +1 -1
  62. package/dist/registration/index.js.map +1 -1
  63. package/dist/registration/index.mjs +1 -1
  64. package/dist/registration/index.mjs.map +1 -1
  65. package/dist/transport/index.js +1 -1
  66. package/dist/transport/index.js.map +1 -1
  67. package/dist/transport/index.mjs +1 -1
  68. package/dist/transport/index.mjs.map +1 -1
  69. package/dist/verify.js +1 -1
  70. package/dist/verify.js.map +1 -1
  71. package/dist/verify.mjs +1 -1
  72. package/dist/verify.mjs.map +1 -1
  73. package/package.json +1 -1
@@ -31,7 +31,7 @@ __export(edge_config_exports, {
31
31
  module.exports = __toCommonJS(edge_config_exports);
32
32
 
33
33
  // src/version.ts
34
- var SDK_VERSION = "5.13.0";
34
+ var SDK_VERSION = "5.14.0";
35
35
 
36
36
  // src/http.ts
37
37
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/edge-config.ts","../src/version.ts","../src/http.ts"],"sourcesContent":["/**\n * Edge verification config — the per-endpoint policy the Trusted Agent\n * Gateway edge adapters (e.g. `@astrasyncai/adapter-lambda`) fetch from the\n * AstraSync dashboard to decide how much verification friction to apply\n * to inbound traffic, and where.\n *\n * Fetch pattern mirrors `well-known.ts`: TTL cache + stale-while-revalidate\n * + in-flight dedupe, with a `_reset*` hook for tests. Fail posture is\n * deliberately asymmetric: this module can NEVER put a site into enforce\n * mode on its own — enforce only ever originates from a successfully\n * fetched config, and a config that has gone stale past `maxStaleMs`\n * degrades back to observe.\n *\n * Which EdgeConfig fields each integration consumes:\n *\n * | Field | Edge adapters | SDK adapters (express/nextjs/mcp) |\n * | ------------------------------ | ------------- | --------------------------------- |\n * | `mode` (observe/enforce) | yes | — |\n * | `depth` | yes | — |\n * | `pathRules` | yes | — |\n * | `sampling.anonymousBeaconRate` | yes | yes — the SDK anonymous beacon |\n * | `failurePosture` | yes | — |\n *\n * The observe/enforce × depth posture is EDGE-ONLY by design: SDK adapters\n * enforce via the dashboard route policy (`fetchRoutes`), not via this\n * config, and consume only the `sampling` block for their anonymous\n * traffic beacon.\n */\n\nimport { sdkFetch } from './http';\nimport type { GatewayConfig } from './types';\n\n/**\n * Edge posture. Canonical Trusted Agent Gateway vocabulary:\n * `observe` ≈ the gateway's `passive` posture (verify +\n * record, enforce nothing), `enforce` ≈ `active`. New value set on a new\n * field name — do NOT reuse `GatewayPosture`'s `active`/`passive` here\n * (different field, different meaning: posture governs an agent-side\n * gateway; mode governs counterparty-side edge interception).\n */\nexport type EdgeMode = 'observe' | 'enforce';\n\n/**\n * How deep the edge checks each request — what is it → who is it → are\n * they allowed:\n * - `classify` — UA / platform-fingerprint classification only; no\n * credential handling.\n * - `authenticate` — establish who is calling: detect and parse agent\n * credentials (X-Astra-*, VI SD-JWT, RFC 9421, ACP,\n * UCP, AP2…) and verify presented AstraSync identities.\n * - `authorize` — full policy decision: credentials cryptographically\n * proven and PDLSS-evaluated via a verify-access round\n * trip. (US spelling on the wire, matching\n * `authorizeSettlement` and HTTP `Authorization`.)\n */\nexport type EdgeVerificationDepth = 'classify' | 'authenticate' | 'authorize';\n\nexport interface EdgePathRule {\n /**\n * `*`-wildcard glob matched against the request path. Matching is\n * case-INsensitive and all regex metacharacters except `*` are\n * escaped. (Deliberately diverges from the express adapter's\n * `matchRoute`, which is historically case-sensitive by default —\n * new surface ships case-insensitive matching as the default.)\n */\n pattern: string;\n /** Pass the request through untouched (static assets); mode/depth ignored. */\n skip?: boolean;\n /** Override the top-level mode for this path. */\n mode?: EdgeMode;\n /** Override the top-level depth for this path. */\n depth?: EdgeVerificationDepth;\n /**\n * Per-path beacon sampling opt-DOWN (VI uplift 1.2). The default is full\n * capture; put a fractional rate on explicitly high-volume/low-value paths\n * (static mirrors, health checks) instead of sampling commerce traffic.\n */\n sampling?: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n}\n\nexport interface EdgeConfig {\n /** Shape version — bump on breaking changes. */\n version: 1;\n /** Default posture for paths no rule matches. */\n mode: EdgeMode;\n /** Default check depth for paths no rule matches. */\n depth: EdgeVerificationDepth;\n /** Ordered, first-match-wins. */\n pathRules: EdgePathRule[];\n sampling: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n /**\n * Query-string parameter names whose VALUES may be captured into\n * `observedMetadata.queryParams` (VI uplift Part 4 — campaign/UTM\n * attribution). Lowercase exact names or a single trailing-`*` glob\n * (`utm_*`); ≤16 entries × ≤64 chars. ABSENT by default — the privacy\n * stance (query values dropped at capture) is unchanged unless the\n * endpoint owner explicitly configures this.\n */\n queryValueAllowlist?: string[];\n /**\n * 4.5.0: the EFFECTIVE failure posture is derived from the effective\n * mode — fail posture follows success posture. Observe fails open\n * (unreachable backend = pass-through, site never breaks); enforce fails\n * closed on verification-infrastructure failure (503\n * `VERIFICATION_UNAVAILABLE`, retry-later — see edge-core/pipeline). This\n * field is retained on the wire for adapter compatibility and as a future\n * explicit override; it is not consulted by the 4.5.x pipeline. Note the\n * distinction from CONFIG staleness: a config stale past 24h still has\n * enforce degraded to observe (`degradeToObserve`) — that is config-trust\n * hygiene, independent of runtime verification availability.\n */\n failurePosture: 'open' | 'closed';\n}\n\n/**\n * The safe default: observe-only, classification depth, nothing enforced.\n * Served by the backend when an endpoint has never been configured, and\n * used by `getEdgeConfig` whenever no config can be fetched.\n *\n * Beacon rate 1.0 (VI uplift 1.2): every anonymous hit beacons by default —\n * a sampled denominator understates anonymous traffic in every dashboard\n * tile and no toggle can reveal the dropped rows. Sampling is a deliberate\n * per-path opt-down (`EdgePathRule.sampling`) for static/high-volume paths.\n */\nexport const DEFAULT_EDGE_CONFIG: EdgeConfig = Object.freeze({\n version: 1,\n mode: 'observe',\n depth: 'classify',\n pathRules: [],\n sampling: Object.freeze({ anonymousBeaconRate: 1.0 }),\n failurePosture: 'open',\n}) as EdgeConfig;\n\nconst EDGE_MODES: readonly string[] = ['observe', 'enforce'];\nconst EDGE_DEPTHS: readonly string[] = ['classify', 'authenticate', 'authorize'];\n\nfunction isSamplingShape(value: unknown): value is { anonymousBeaconRate: number } {\n if (typeof value !== 'object' || value === null) return false;\n const rate = (value as Record<string, unknown>).anonymousBeaconRate;\n return typeof rate === 'number' && rate >= 0 && rate <= 1;\n}\n\n/** Structural guard for configs arriving over the wire. */\nexport function isEdgeConfig(value: unknown): value is EdgeConfig {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.version !== 1) return false;\n if (!EDGE_MODES.includes(v.mode as string)) return false;\n if (!EDGE_DEPTHS.includes(v.depth as string)) return false;\n if (!Array.isArray(v.pathRules)) return false;\n for (const rule of v.pathRules) {\n if (typeof rule !== 'object' || rule === null) return false;\n const r = rule as Record<string, unknown>;\n if (typeof r.pattern !== 'string' || r.pattern.length === 0) return false;\n if (r.mode !== undefined && !EDGE_MODES.includes(r.mode as string)) return false;\n if (r.depth !== undefined && !EDGE_DEPTHS.includes(r.depth as string)) return false;\n if (r.sampling !== undefined && !isSamplingShape(r.sampling)) return false;\n }\n if (!isSamplingShape(v.sampling)) return false;\n if (v.queryValueAllowlist !== undefined) {\n if (\n !Array.isArray(v.queryValueAllowlist) ||\n v.queryValueAllowlist.length > 16 ||\n !v.queryValueAllowlist.every((e) => typeof e === 'string' && e.length > 0 && e.length <= 64)\n ) {\n return false;\n }\n }\n return v.failurePosture === 'open';\n}\n\nexport interface FetchEdgeConfigSuccess {\n edgeConfig: EdgeConfig;\n etag?: string;\n}\n\nexport type FetchEdgeConfigResult = FetchEdgeConfigSuccess | { notModified: true } | null;\n\n/**\n * Fetch the edge config for an endpoint from the AstraSync backend.\n * `GET {apiBaseUrl}/endpoints/:counterpartyId/edge-config` with the same\n * auth headers as `fetchRoutes`. Pass the cached `etag` to get a cheap\n * `{ notModified: true }` on 304. Returns `null` on any failure —\n * the caller decides how to fall back.\n */\nexport async function fetchEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { etag?: string; timeoutMs?: number }\n): Promise<FetchEdgeConfigResult> {\n if (!counterpartyId) return null;\n const headers: Record<string, string> = { Accept: 'application/json' };\n if (config.apiKey) {\n headers['Authorization'] = `Bearer ${config.apiKey}`;\n headers['X-API-Key'] = config.apiKey;\n }\n if (opts?.etag) headers['If-None-Match'] = opts.etag;\n try {\n const response = await sdkFetch(\n `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/edge-config`,\n { method: 'GET', headers, signal: AbortSignal.timeout(opts?.timeoutMs ?? 5000) }\n );\n if (response.status === 304) return { notModified: true };\n if (!response.ok) return null;\n const body = (await response.json()) as { data?: { edgeConfig?: unknown } };\n const edgeConfig = body.data?.edgeConfig;\n if (!isEdgeConfig(edgeConfig)) return null;\n return { edgeConfig, etag: response.headers.get('etag') ?? undefined };\n } catch {\n return null;\n }\n}\n\ninterface CacheEntry {\n config: EdgeConfig;\n etag?: string;\n /** When the entry was last confirmed fresh (fetch or 304). */\n lastSuccessAt: number;\n}\n\nconst DEFAULT_TTL_MS = 60 * 1000;\nconst DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1000;\n\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<void>>();\n\nfunction cacheKey(apiBaseUrl: string, counterpartyId: string): string {\n return `${apiBaseUrl}|${counterpartyId}`;\n}\n\n/** Copy of `config` with every enforce (top-level and per-rule) degraded to observe. */\nexport function degradeToObserve(config: EdgeConfig): EdgeConfig {\n if (config.mode !== 'enforce' && !config.pathRules.some((r) => r.mode === 'enforce')) {\n return config;\n }\n return {\n ...config,\n mode: 'observe',\n pathRules: config.pathRules.map((r) => (r.mode === 'enforce' ? { ...r, mode: 'observe' } : r)),\n };\n}\n\nasync function revalidate(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string\n): Promise<void> {\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n const existing = inflight.get(key);\n if (existing) return existing;\n\n const promise = fetchEdgeConfig(config, counterpartyId, { etag: cache.get(key)?.etag })\n .then((result) => {\n if (result === null) return; // failure — cache untouched, caller falls back\n const now = Date.now();\n if ('notModified' in result) {\n const entry = cache.get(key);\n if (entry) entry.lastSuccessAt = now;\n return;\n }\n cache.set(key, { config: result.edgeConfig, etag: result.etag, lastSuccessAt: now });\n })\n .finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, promise);\n return promise;\n}\n\n/**\n * Get the edge config for an endpoint, cached. Never throws, never blocks\n * longer than one fetch.\n *\n * - Fresh cache (within `ttlMs`) → cached config, no network.\n * - Stale cache → cached config immediately + background revalidate\n * (stale-while-revalidate). If the last successful fetch is older than\n * `maxStaleMs` (default 24h), any enforce is degraded to observe — a\n * long-dead backend can never keep a site enforcing.\n * - No cache → await one fetch; on failure return `DEFAULT_EDGE_CONFIG`\n * (observe-only). Defaults never enforce.\n */\nexport async function getEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { ttlMs?: number; maxStaleMs?: number }\n): Promise<EdgeConfig> {\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts?.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n\n const entry = cache.get(key);\n if (entry) {\n const age = Date.now() - entry.lastSuccessAt;\n if (age > ttlMs) {\n revalidate(config, counterpartyId).catch(() => {\n // background refresh failed — stale entry remains until next call\n });\n }\n return age > maxStaleMs ? degradeToObserve(entry.config) : entry.config;\n }\n\n await revalidate(config, counterpartyId).catch(() => {\n // fetch failed — fall through to defaults\n });\n return cache.get(key)?.config ?? DEFAULT_EDGE_CONFIG;\n}\n\n/**\n * First-match-wins path-rule lookup. Case-insensitive `*` globs with all\n * other regex metacharacters escaped (a literal `.` in a pattern matches\n * only `.`). Returns `undefined` when no rule matches — the caller applies\n * the config's top-level mode/depth.\n */\nexport function matchEdgePathRule(rules: EdgePathRule[], path: string): EdgePathRule | undefined {\n for (const rule of rules) {\n if (globToRegExp(rule.pattern).test(path)) return rule;\n }\n return undefined;\n}\n\nfunction globToRegExp(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`, 'i');\n}\n\n/** Reset cache — for testing only. */\nexport function _resetEdgeConfigCache(): void {\n cache.clear();\n inflight.clear();\n}\n","/**\n * Single source-of-truth for the SDK's\n * package version emitted on verify-access bodies (and any future\n * telemetry). Bumped alongside `package.json#version` on every release.\n *\n * Why a constant rather than `import pkg from '../package.json'`:\n * - `tsconfig.json` sets `rootDir: ./src`; importing the sibling\n * package.json fails the build with \"outside rootDir\".\n * - Build-time string replacement (tsup `define`, esbuild banner, etc.)\n * adds toolchain coupling for a trivial gain.\n * - Embedded readonly constant works in every environment (Node, browser,\n * bundlers, Deno) without runtime fs / network access.\n *\n * Release discipline: a CI lint can grep `package.json#version` against\n * this constant if the two ever diverge in the wild. A manual bump\n * is fine — bumping both in the release-ceremony commit keeps them\n * lockstep.\n */\nexport const SDK_VERSION = '5.13.0';\n","/**\n * Shared outbound-HTTP wrapper — every request the SDK makes to the\n * AstraSync backend (or anywhere else) goes through `sdkFetch` so it\n * carries an identifying User-Agent.\n *\n * Why: Node's global fetch defaults to `user-agent: node`, which is\n * indistinguishable from every other Node client on the wire. Visit\n * Intelligence observed our own SDK/beacon traffic as anonymous \"node\"\n * visits. Self-identification is the platform's own medicine.\n *\n * Browser note: some browsers filter the User-Agent request header\n * silently (never throwing), so this is a no-op there — which is correct:\n * in a browser the page's real UA is the honest identity.\n */\n\nimport { SDK_VERSION } from './version';\n\nexport const SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;\n\n/**\n * Add the SDK User-Agent without changing the SHAPE of the caller's headers\n * (plain records stay plain records) — interceptors, adapters, and tests\n * read `init.headers` as a record, and `Headers` would also lowercase names.\n */\nfunction withUserAgent(initHeaders: HeadersInit | undefined): HeadersInit {\n if (!initHeaders) return { 'user-agent': SDK_USER_AGENT };\n if (initHeaders instanceof Headers) {\n const copy = new Headers(initHeaders);\n if (!copy.has('user-agent')) copy.set('user-agent', SDK_USER_AGENT);\n return copy;\n }\n if (Array.isArray(initHeaders)) {\n const has = initHeaders.some(([name]) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : [...initHeaders, ['user-agent', SDK_USER_AGENT]];\n }\n const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : { 'user-agent': SDK_USER_AGENT, ...initHeaders };\n}\n\nexport const sdkFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, headers: withUserAgent(init?.headers) });\n"],"mappings":"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","names":["has"]}
1
+ {"version":3,"sources":["../src/edge-config.ts","../src/version.ts","../src/http.ts"],"sourcesContent":["/**\n * Edge verification config — the per-endpoint policy the Trusted Agent\n * Gateway edge adapters (e.g. `@astrasyncai/adapter-lambda`) fetch from the\n * AstraSync dashboard to decide how much verification friction to apply\n * to inbound traffic, and where.\n *\n * Fetch pattern mirrors `well-known.ts`: TTL cache + stale-while-revalidate\n * + in-flight dedupe, with a `_reset*` hook for tests. Fail posture is\n * deliberately asymmetric: this module can NEVER put a site into enforce\n * mode on its own — enforce only ever originates from a successfully\n * fetched config, and a config that has gone stale past `maxStaleMs`\n * degrades back to observe.\n *\n * Which EdgeConfig fields each integration consumes:\n *\n * | Field | Edge adapters | SDK adapters (express/nextjs/mcp) |\n * | ------------------------------ | ------------- | --------------------------------- |\n * | `mode` (observe/enforce) | yes | — |\n * | `depth` | yes | — |\n * | `pathRules` | yes | — |\n * | `sampling.anonymousBeaconRate` | yes | yes — the SDK anonymous beacon |\n * | `failurePosture` | yes | — |\n *\n * The observe/enforce × depth posture is EDGE-ONLY by design: SDK adapters\n * enforce via the dashboard route policy (`fetchRoutes`), not via this\n * config, and consume only the `sampling` block for their anonymous\n * traffic beacon.\n */\n\nimport { sdkFetch } from './http';\nimport type { GatewayConfig } from './types';\n\n/**\n * Edge posture. Canonical Trusted Agent Gateway vocabulary:\n * `observe` ≈ the gateway's `passive` posture (verify +\n * record, enforce nothing), `enforce` ≈ `active`. New value set on a new\n * field name — do NOT reuse `GatewayPosture`'s `active`/`passive` here\n * (different field, different meaning: posture governs an agent-side\n * gateway; mode governs counterparty-side edge interception).\n */\nexport type EdgeMode = 'observe' | 'enforce';\n\n/**\n * How deep the edge checks each request — what is it → who is it → are\n * they allowed:\n * - `classify` — UA / platform-fingerprint classification only; no\n * credential handling.\n * - `authenticate` — establish who is calling: detect and parse agent\n * credentials (X-Astra-*, VI SD-JWT, RFC 9421, ACP,\n * UCP, AP2…) and verify presented AstraSync identities.\n * - `authorize` — full policy decision: credentials cryptographically\n * proven and PDLSS-evaluated via a verify-access round\n * trip. (US spelling on the wire, matching\n * `authorizeSettlement` and HTTP `Authorization`.)\n */\nexport type EdgeVerificationDepth = 'classify' | 'authenticate' | 'authorize';\n\nexport interface EdgePathRule {\n /**\n * `*`-wildcard glob matched against the request path. Matching is\n * case-INsensitive and all regex metacharacters except `*` are\n * escaped. (Deliberately diverges from the express adapter's\n * `matchRoute`, which is historically case-sensitive by default —\n * new surface ships case-insensitive matching as the default.)\n */\n pattern: string;\n /** Pass the request through untouched (static assets); mode/depth ignored. */\n skip?: boolean;\n /** Override the top-level mode for this path. */\n mode?: EdgeMode;\n /** Override the top-level depth for this path. */\n depth?: EdgeVerificationDepth;\n /**\n * Per-path beacon sampling opt-DOWN (VI uplift 1.2). The default is full\n * capture; put a fractional rate on explicitly high-volume/low-value paths\n * (static mirrors, health checks) instead of sampling commerce traffic.\n */\n sampling?: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n}\n\nexport interface EdgeConfig {\n /** Shape version — bump on breaking changes. */\n version: 1;\n /** Default posture for paths no rule matches. */\n mode: EdgeMode;\n /** Default check depth for paths no rule matches. */\n depth: EdgeVerificationDepth;\n /** Ordered, first-match-wins. */\n pathRules: EdgePathRule[];\n sampling: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n /**\n * Query-string parameter names whose VALUES may be captured into\n * `observedMetadata.queryParams` (VI uplift Part 4 — campaign/UTM\n * attribution). Lowercase exact names or a single trailing-`*` glob\n * (`utm_*`); ≤16 entries × ≤64 chars. ABSENT by default — the privacy\n * stance (query values dropped at capture) is unchanged unless the\n * endpoint owner explicitly configures this.\n */\n queryValueAllowlist?: string[];\n /**\n * 4.5.0: the EFFECTIVE failure posture is derived from the effective\n * mode — fail posture follows success posture. Observe fails open\n * (unreachable backend = pass-through, site never breaks); enforce fails\n * closed on verification-infrastructure failure (503\n * `VERIFICATION_UNAVAILABLE`, retry-later — see edge-core/pipeline). This\n * field is retained on the wire for adapter compatibility and as a future\n * explicit override; it is not consulted by the 4.5.x pipeline. Note the\n * distinction from CONFIG staleness: a config stale past 24h still has\n * enforce degraded to observe (`degradeToObserve`) — that is config-trust\n * hygiene, independent of runtime verification availability.\n */\n failurePosture: 'open' | 'closed';\n}\n\n/**\n * The safe default: observe-only, classification depth, nothing enforced.\n * Served by the backend when an endpoint has never been configured, and\n * used by `getEdgeConfig` whenever no config can be fetched.\n *\n * Beacon rate 1.0 (VI uplift 1.2): every anonymous hit beacons by default —\n * a sampled denominator understates anonymous traffic in every dashboard\n * tile and no toggle can reveal the dropped rows. Sampling is a deliberate\n * per-path opt-down (`EdgePathRule.sampling`) for static/high-volume paths.\n */\nexport const DEFAULT_EDGE_CONFIG: EdgeConfig = Object.freeze({\n version: 1,\n mode: 'observe',\n depth: 'classify',\n pathRules: [],\n sampling: Object.freeze({ anonymousBeaconRate: 1.0 }),\n failurePosture: 'open',\n}) as EdgeConfig;\n\nconst EDGE_MODES: readonly string[] = ['observe', 'enforce'];\nconst EDGE_DEPTHS: readonly string[] = ['classify', 'authenticate', 'authorize'];\n\nfunction isSamplingShape(value: unknown): value is { anonymousBeaconRate: number } {\n if (typeof value !== 'object' || value === null) return false;\n const rate = (value as Record<string, unknown>).anonymousBeaconRate;\n return typeof rate === 'number' && rate >= 0 && rate <= 1;\n}\n\n/** Structural guard for configs arriving over the wire. */\nexport function isEdgeConfig(value: unknown): value is EdgeConfig {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.version !== 1) return false;\n if (!EDGE_MODES.includes(v.mode as string)) return false;\n if (!EDGE_DEPTHS.includes(v.depth as string)) return false;\n if (!Array.isArray(v.pathRules)) return false;\n for (const rule of v.pathRules) {\n if (typeof rule !== 'object' || rule === null) return false;\n const r = rule as Record<string, unknown>;\n if (typeof r.pattern !== 'string' || r.pattern.length === 0) return false;\n if (r.mode !== undefined && !EDGE_MODES.includes(r.mode as string)) return false;\n if (r.depth !== undefined && !EDGE_DEPTHS.includes(r.depth as string)) return false;\n if (r.sampling !== undefined && !isSamplingShape(r.sampling)) return false;\n }\n if (!isSamplingShape(v.sampling)) return false;\n if (v.queryValueAllowlist !== undefined) {\n if (\n !Array.isArray(v.queryValueAllowlist) ||\n v.queryValueAllowlist.length > 16 ||\n !v.queryValueAllowlist.every((e) => typeof e === 'string' && e.length > 0 && e.length <= 64)\n ) {\n return false;\n }\n }\n return v.failurePosture === 'open';\n}\n\nexport interface FetchEdgeConfigSuccess {\n edgeConfig: EdgeConfig;\n etag?: string;\n}\n\nexport type FetchEdgeConfigResult = FetchEdgeConfigSuccess | { notModified: true } | null;\n\n/**\n * Fetch the edge config for an endpoint from the AstraSync backend.\n * `GET {apiBaseUrl}/endpoints/:counterpartyId/edge-config` with the same\n * auth headers as `fetchRoutes`. Pass the cached `etag` to get a cheap\n * `{ notModified: true }` on 304. Returns `null` on any failure —\n * the caller decides how to fall back.\n */\nexport async function fetchEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { etag?: string; timeoutMs?: number }\n): Promise<FetchEdgeConfigResult> {\n if (!counterpartyId) return null;\n const headers: Record<string, string> = { Accept: 'application/json' };\n if (config.apiKey) {\n headers['Authorization'] = `Bearer ${config.apiKey}`;\n headers['X-API-Key'] = config.apiKey;\n }\n if (opts?.etag) headers['If-None-Match'] = opts.etag;\n try {\n const response = await sdkFetch(\n `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/edge-config`,\n { method: 'GET', headers, signal: AbortSignal.timeout(opts?.timeoutMs ?? 5000) }\n );\n if (response.status === 304) return { notModified: true };\n if (!response.ok) return null;\n const body = (await response.json()) as { data?: { edgeConfig?: unknown } };\n const edgeConfig = body.data?.edgeConfig;\n if (!isEdgeConfig(edgeConfig)) return null;\n return { edgeConfig, etag: response.headers.get('etag') ?? undefined };\n } catch {\n return null;\n }\n}\n\ninterface CacheEntry {\n config: EdgeConfig;\n etag?: string;\n /** When the entry was last confirmed fresh (fetch or 304). */\n lastSuccessAt: number;\n}\n\nconst DEFAULT_TTL_MS = 60 * 1000;\nconst DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1000;\n\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<void>>();\n\nfunction cacheKey(apiBaseUrl: string, counterpartyId: string): string {\n return `${apiBaseUrl}|${counterpartyId}`;\n}\n\n/** Copy of `config` with every enforce (top-level and per-rule) degraded to observe. */\nexport function degradeToObserve(config: EdgeConfig): EdgeConfig {\n if (config.mode !== 'enforce' && !config.pathRules.some((r) => r.mode === 'enforce')) {\n return config;\n }\n return {\n ...config,\n mode: 'observe',\n pathRules: config.pathRules.map((r) => (r.mode === 'enforce' ? { ...r, mode: 'observe' } : r)),\n };\n}\n\nasync function revalidate(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string\n): Promise<void> {\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n const existing = inflight.get(key);\n if (existing) return existing;\n\n const promise = fetchEdgeConfig(config, counterpartyId, { etag: cache.get(key)?.etag })\n .then((result) => {\n if (result === null) return; // failure — cache untouched, caller falls back\n const now = Date.now();\n if ('notModified' in result) {\n const entry = cache.get(key);\n if (entry) entry.lastSuccessAt = now;\n return;\n }\n cache.set(key, { config: result.edgeConfig, etag: result.etag, lastSuccessAt: now });\n })\n .finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, promise);\n return promise;\n}\n\n/**\n * Get the edge config for an endpoint, cached. Never throws, never blocks\n * longer than one fetch.\n *\n * - Fresh cache (within `ttlMs`) → cached config, no network.\n * - Stale cache → cached config immediately + background revalidate\n * (stale-while-revalidate). If the last successful fetch is older than\n * `maxStaleMs` (default 24h), any enforce is degraded to observe — a\n * long-dead backend can never keep a site enforcing.\n * - No cache → await one fetch; on failure return `DEFAULT_EDGE_CONFIG`\n * (observe-only). Defaults never enforce.\n */\nexport async function getEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { ttlMs?: number; maxStaleMs?: number }\n): Promise<EdgeConfig> {\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts?.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n\n const entry = cache.get(key);\n if (entry) {\n const age = Date.now() - entry.lastSuccessAt;\n if (age > ttlMs) {\n revalidate(config, counterpartyId).catch(() => {\n // background refresh failed — stale entry remains until next call\n });\n }\n return age > maxStaleMs ? degradeToObserve(entry.config) : entry.config;\n }\n\n await revalidate(config, counterpartyId).catch(() => {\n // fetch failed — fall through to defaults\n });\n return cache.get(key)?.config ?? DEFAULT_EDGE_CONFIG;\n}\n\n/**\n * First-match-wins path-rule lookup. Case-insensitive `*` globs with all\n * other regex metacharacters escaped (a literal `.` in a pattern matches\n * only `.`). Returns `undefined` when no rule matches — the caller applies\n * the config's top-level mode/depth.\n */\nexport function matchEdgePathRule(rules: EdgePathRule[], path: string): EdgePathRule | undefined {\n for (const rule of rules) {\n if (globToRegExp(rule.pattern).test(path)) return rule;\n }\n return undefined;\n}\n\nfunction globToRegExp(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`, 'i');\n}\n\n/** Reset cache — for testing only. */\nexport function _resetEdgeConfigCache(): void {\n cache.clear();\n inflight.clear();\n}\n","/**\n * Single source-of-truth for the SDK's\n * package version emitted on verify-access bodies (and any future\n * telemetry). Bumped alongside `package.json#version` on every release.\n *\n * Why a constant rather than `import pkg from '../package.json'`:\n * - `tsconfig.json` sets `rootDir: ./src`; importing the sibling\n * package.json fails the build with \"outside rootDir\".\n * - Build-time string replacement (tsup `define`, esbuild banner, etc.)\n * adds toolchain coupling for a trivial gain.\n * - Embedded readonly constant works in every environment (Node, browser,\n * bundlers, Deno) without runtime fs / network access.\n *\n * Release discipline: a CI lint can grep `package.json#version` against\n * this constant if the two ever diverge in the wild. A manual bump\n * is fine — bumping both in the release-ceremony commit keeps them\n * lockstep.\n */\nexport const SDK_VERSION = '5.14.0';\n","/**\n * Shared outbound-HTTP wrapper — every request the SDK makes to the\n * AstraSync backend (or anywhere else) goes through `sdkFetch` so it\n * carries an identifying User-Agent.\n *\n * Why: Node's global fetch defaults to `user-agent: node`, which is\n * indistinguishable from every other Node client on the wire. Visit\n * Intelligence observed our own SDK/beacon traffic as anonymous \"node\"\n * visits. Self-identification is the platform's own medicine.\n *\n * Browser note: some browsers filter the User-Agent request header\n * silently (never throwing), so this is a no-op there — which is correct:\n * in a browser the page's real UA is the honest identity.\n */\n\nimport { SDK_VERSION } from './version';\n\nexport const SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;\n\n/**\n * Add the SDK User-Agent without changing the SHAPE of the caller's headers\n * (plain records stay plain records) — interceptors, adapters, and tests\n * read `init.headers` as a record, and `Headers` would also lowercase names.\n */\nfunction withUserAgent(initHeaders: HeadersInit | undefined): HeadersInit {\n if (!initHeaders) return { 'user-agent': SDK_USER_AGENT };\n if (initHeaders instanceof Headers) {\n const copy = new Headers(initHeaders);\n if (!copy.has('user-agent')) copy.set('user-agent', SDK_USER_AGENT);\n return copy;\n }\n if (Array.isArray(initHeaders)) {\n const has = initHeaders.some(([name]) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : [...initHeaders, ['user-agent', SDK_USER_AGENT]];\n }\n const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : { 'user-agent': SDK_USER_AGENT, ...initHeaders };\n}\n\nexport const sdkFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, headers: withUserAgent(init?.headers) });\n"],"mappings":"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","names":["has"]}
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "5.13.0";
2
+ var SDK_VERSION = "5.14.0";
3
3
 
4
4
  // src/http.ts
5
5
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/version.ts","../src/http.ts","../src/edge-config.ts"],"sourcesContent":["/**\n * Single source-of-truth for the SDK's\n * package version emitted on verify-access bodies (and any future\n * telemetry). Bumped alongside `package.json#version` on every release.\n *\n * Why a constant rather than `import pkg from '../package.json'`:\n * - `tsconfig.json` sets `rootDir: ./src`; importing the sibling\n * package.json fails the build with \"outside rootDir\".\n * - Build-time string replacement (tsup `define`, esbuild banner, etc.)\n * adds toolchain coupling for a trivial gain.\n * - Embedded readonly constant works in every environment (Node, browser,\n * bundlers, Deno) without runtime fs / network access.\n *\n * Release discipline: a CI lint can grep `package.json#version` against\n * this constant if the two ever diverge in the wild. A manual bump\n * is fine — bumping both in the release-ceremony commit keeps them\n * lockstep.\n */\nexport const SDK_VERSION = '5.13.0';\n","/**\n * Shared outbound-HTTP wrapper — every request the SDK makes to the\n * AstraSync backend (or anywhere else) goes through `sdkFetch` so it\n * carries an identifying User-Agent.\n *\n * Why: Node's global fetch defaults to `user-agent: node`, which is\n * indistinguishable from every other Node client on the wire. Visit\n * Intelligence observed our own SDK/beacon traffic as anonymous \"node\"\n * visits. Self-identification is the platform's own medicine.\n *\n * Browser note: some browsers filter the User-Agent request header\n * silently (never throwing), so this is a no-op there — which is correct:\n * in a browser the page's real UA is the honest identity.\n */\n\nimport { SDK_VERSION } from './version';\n\nexport const SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;\n\n/**\n * Add the SDK User-Agent without changing the SHAPE of the caller's headers\n * (plain records stay plain records) — interceptors, adapters, and tests\n * read `init.headers` as a record, and `Headers` would also lowercase names.\n */\nfunction withUserAgent(initHeaders: HeadersInit | undefined): HeadersInit {\n if (!initHeaders) return { 'user-agent': SDK_USER_AGENT };\n if (initHeaders instanceof Headers) {\n const copy = new Headers(initHeaders);\n if (!copy.has('user-agent')) copy.set('user-agent', SDK_USER_AGENT);\n return copy;\n }\n if (Array.isArray(initHeaders)) {\n const has = initHeaders.some(([name]) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : [...initHeaders, ['user-agent', SDK_USER_AGENT]];\n }\n const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : { 'user-agent': SDK_USER_AGENT, ...initHeaders };\n}\n\nexport const sdkFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, headers: withUserAgent(init?.headers) });\n","/**\n * Edge verification config — the per-endpoint policy the Trusted Agent\n * Gateway edge adapters (e.g. `@astrasyncai/adapter-lambda`) fetch from the\n * AstraSync dashboard to decide how much verification friction to apply\n * to inbound traffic, and where.\n *\n * Fetch pattern mirrors `well-known.ts`: TTL cache + stale-while-revalidate\n * + in-flight dedupe, with a `_reset*` hook for tests. Fail posture is\n * deliberately asymmetric: this module can NEVER put a site into enforce\n * mode on its own — enforce only ever originates from a successfully\n * fetched config, and a config that has gone stale past `maxStaleMs`\n * degrades back to observe.\n *\n * Which EdgeConfig fields each integration consumes:\n *\n * | Field | Edge adapters | SDK adapters (express/nextjs/mcp) |\n * | ------------------------------ | ------------- | --------------------------------- |\n * | `mode` (observe/enforce) | yes | — |\n * | `depth` | yes | — |\n * | `pathRules` | yes | — |\n * | `sampling.anonymousBeaconRate` | yes | yes — the SDK anonymous beacon |\n * | `failurePosture` | yes | — |\n *\n * The observe/enforce × depth posture is EDGE-ONLY by design: SDK adapters\n * enforce via the dashboard route policy (`fetchRoutes`), not via this\n * config, and consume only the `sampling` block for their anonymous\n * traffic beacon.\n */\n\nimport { sdkFetch } from './http';\nimport type { GatewayConfig } from './types';\n\n/**\n * Edge posture. Canonical Trusted Agent Gateway vocabulary:\n * `observe` ≈ the gateway's `passive` posture (verify +\n * record, enforce nothing), `enforce` ≈ `active`. New value set on a new\n * field name — do NOT reuse `GatewayPosture`'s `active`/`passive` here\n * (different field, different meaning: posture governs an agent-side\n * gateway; mode governs counterparty-side edge interception).\n */\nexport type EdgeMode = 'observe' | 'enforce';\n\n/**\n * How deep the edge checks each request — what is it → who is it → are\n * they allowed:\n * - `classify` — UA / platform-fingerprint classification only; no\n * credential handling.\n * - `authenticate` — establish who is calling: detect and parse agent\n * credentials (X-Astra-*, VI SD-JWT, RFC 9421, ACP,\n * UCP, AP2…) and verify presented AstraSync identities.\n * - `authorize` — full policy decision: credentials cryptographically\n * proven and PDLSS-evaluated via a verify-access round\n * trip. (US spelling on the wire, matching\n * `authorizeSettlement` and HTTP `Authorization`.)\n */\nexport type EdgeVerificationDepth = 'classify' | 'authenticate' | 'authorize';\n\nexport interface EdgePathRule {\n /**\n * `*`-wildcard glob matched against the request path. Matching is\n * case-INsensitive and all regex metacharacters except `*` are\n * escaped. (Deliberately diverges from the express adapter's\n * `matchRoute`, which is historically case-sensitive by default —\n * new surface ships case-insensitive matching as the default.)\n */\n pattern: string;\n /** Pass the request through untouched (static assets); mode/depth ignored. */\n skip?: boolean;\n /** Override the top-level mode for this path. */\n mode?: EdgeMode;\n /** Override the top-level depth for this path. */\n depth?: EdgeVerificationDepth;\n /**\n * Per-path beacon sampling opt-DOWN (VI uplift 1.2). The default is full\n * capture; put a fractional rate on explicitly high-volume/low-value paths\n * (static mirrors, health checks) instead of sampling commerce traffic.\n */\n sampling?: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n}\n\nexport interface EdgeConfig {\n /** Shape version — bump on breaking changes. */\n version: 1;\n /** Default posture for paths no rule matches. */\n mode: EdgeMode;\n /** Default check depth for paths no rule matches. */\n depth: EdgeVerificationDepth;\n /** Ordered, first-match-wins. */\n pathRules: EdgePathRule[];\n sampling: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n /**\n * Query-string parameter names whose VALUES may be captured into\n * `observedMetadata.queryParams` (VI uplift Part 4 — campaign/UTM\n * attribution). Lowercase exact names or a single trailing-`*` glob\n * (`utm_*`); ≤16 entries × ≤64 chars. ABSENT by default — the privacy\n * stance (query values dropped at capture) is unchanged unless the\n * endpoint owner explicitly configures this.\n */\n queryValueAllowlist?: string[];\n /**\n * 4.5.0: the EFFECTIVE failure posture is derived from the effective\n * mode — fail posture follows success posture. Observe fails open\n * (unreachable backend = pass-through, site never breaks); enforce fails\n * closed on verification-infrastructure failure (503\n * `VERIFICATION_UNAVAILABLE`, retry-later — see edge-core/pipeline). This\n * field is retained on the wire for adapter compatibility and as a future\n * explicit override; it is not consulted by the 4.5.x pipeline. Note the\n * distinction from CONFIG staleness: a config stale past 24h still has\n * enforce degraded to observe (`degradeToObserve`) — that is config-trust\n * hygiene, independent of runtime verification availability.\n */\n failurePosture: 'open' | 'closed';\n}\n\n/**\n * The safe default: observe-only, classification depth, nothing enforced.\n * Served by the backend when an endpoint has never been configured, and\n * used by `getEdgeConfig` whenever no config can be fetched.\n *\n * Beacon rate 1.0 (VI uplift 1.2): every anonymous hit beacons by default —\n * a sampled denominator understates anonymous traffic in every dashboard\n * tile and no toggle can reveal the dropped rows. Sampling is a deliberate\n * per-path opt-down (`EdgePathRule.sampling`) for static/high-volume paths.\n */\nexport const DEFAULT_EDGE_CONFIG: EdgeConfig = Object.freeze({\n version: 1,\n mode: 'observe',\n depth: 'classify',\n pathRules: [],\n sampling: Object.freeze({ anonymousBeaconRate: 1.0 }),\n failurePosture: 'open',\n}) as EdgeConfig;\n\nconst EDGE_MODES: readonly string[] = ['observe', 'enforce'];\nconst EDGE_DEPTHS: readonly string[] = ['classify', 'authenticate', 'authorize'];\n\nfunction isSamplingShape(value: unknown): value is { anonymousBeaconRate: number } {\n if (typeof value !== 'object' || value === null) return false;\n const rate = (value as Record<string, unknown>).anonymousBeaconRate;\n return typeof rate === 'number' && rate >= 0 && rate <= 1;\n}\n\n/** Structural guard for configs arriving over the wire. */\nexport function isEdgeConfig(value: unknown): value is EdgeConfig {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.version !== 1) return false;\n if (!EDGE_MODES.includes(v.mode as string)) return false;\n if (!EDGE_DEPTHS.includes(v.depth as string)) return false;\n if (!Array.isArray(v.pathRules)) return false;\n for (const rule of v.pathRules) {\n if (typeof rule !== 'object' || rule === null) return false;\n const r = rule as Record<string, unknown>;\n if (typeof r.pattern !== 'string' || r.pattern.length === 0) return false;\n if (r.mode !== undefined && !EDGE_MODES.includes(r.mode as string)) return false;\n if (r.depth !== undefined && !EDGE_DEPTHS.includes(r.depth as string)) return false;\n if (r.sampling !== undefined && !isSamplingShape(r.sampling)) return false;\n }\n if (!isSamplingShape(v.sampling)) return false;\n if (v.queryValueAllowlist !== undefined) {\n if (\n !Array.isArray(v.queryValueAllowlist) ||\n v.queryValueAllowlist.length > 16 ||\n !v.queryValueAllowlist.every((e) => typeof e === 'string' && e.length > 0 && e.length <= 64)\n ) {\n return false;\n }\n }\n return v.failurePosture === 'open';\n}\n\nexport interface FetchEdgeConfigSuccess {\n edgeConfig: EdgeConfig;\n etag?: string;\n}\n\nexport type FetchEdgeConfigResult = FetchEdgeConfigSuccess | { notModified: true } | null;\n\n/**\n * Fetch the edge config for an endpoint from the AstraSync backend.\n * `GET {apiBaseUrl}/endpoints/:counterpartyId/edge-config` with the same\n * auth headers as `fetchRoutes`. Pass the cached `etag` to get a cheap\n * `{ notModified: true }` on 304. Returns `null` on any failure —\n * the caller decides how to fall back.\n */\nexport async function fetchEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { etag?: string; timeoutMs?: number }\n): Promise<FetchEdgeConfigResult> {\n if (!counterpartyId) return null;\n const headers: Record<string, string> = { Accept: 'application/json' };\n if (config.apiKey) {\n headers['Authorization'] = `Bearer ${config.apiKey}`;\n headers['X-API-Key'] = config.apiKey;\n }\n if (opts?.etag) headers['If-None-Match'] = opts.etag;\n try {\n const response = await sdkFetch(\n `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/edge-config`,\n { method: 'GET', headers, signal: AbortSignal.timeout(opts?.timeoutMs ?? 5000) }\n );\n if (response.status === 304) return { notModified: true };\n if (!response.ok) return null;\n const body = (await response.json()) as { data?: { edgeConfig?: unknown } };\n const edgeConfig = body.data?.edgeConfig;\n if (!isEdgeConfig(edgeConfig)) return null;\n return { edgeConfig, etag: response.headers.get('etag') ?? undefined };\n } catch {\n return null;\n }\n}\n\ninterface CacheEntry {\n config: EdgeConfig;\n etag?: string;\n /** When the entry was last confirmed fresh (fetch or 304). */\n lastSuccessAt: number;\n}\n\nconst DEFAULT_TTL_MS = 60 * 1000;\nconst DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1000;\n\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<void>>();\n\nfunction cacheKey(apiBaseUrl: string, counterpartyId: string): string {\n return `${apiBaseUrl}|${counterpartyId}`;\n}\n\n/** Copy of `config` with every enforce (top-level and per-rule) degraded to observe. */\nexport function degradeToObserve(config: EdgeConfig): EdgeConfig {\n if (config.mode !== 'enforce' && !config.pathRules.some((r) => r.mode === 'enforce')) {\n return config;\n }\n return {\n ...config,\n mode: 'observe',\n pathRules: config.pathRules.map((r) => (r.mode === 'enforce' ? { ...r, mode: 'observe' } : r)),\n };\n}\n\nasync function revalidate(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string\n): Promise<void> {\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n const existing = inflight.get(key);\n if (existing) return existing;\n\n const promise = fetchEdgeConfig(config, counterpartyId, { etag: cache.get(key)?.etag })\n .then((result) => {\n if (result === null) return; // failure — cache untouched, caller falls back\n const now = Date.now();\n if ('notModified' in result) {\n const entry = cache.get(key);\n if (entry) entry.lastSuccessAt = now;\n return;\n }\n cache.set(key, { config: result.edgeConfig, etag: result.etag, lastSuccessAt: now });\n })\n .finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, promise);\n return promise;\n}\n\n/**\n * Get the edge config for an endpoint, cached. Never throws, never blocks\n * longer than one fetch.\n *\n * - Fresh cache (within `ttlMs`) → cached config, no network.\n * - Stale cache → cached config immediately + background revalidate\n * (stale-while-revalidate). If the last successful fetch is older than\n * `maxStaleMs` (default 24h), any enforce is degraded to observe — a\n * long-dead backend can never keep a site enforcing.\n * - No cache → await one fetch; on failure return `DEFAULT_EDGE_CONFIG`\n * (observe-only). Defaults never enforce.\n */\nexport async function getEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { ttlMs?: number; maxStaleMs?: number }\n): Promise<EdgeConfig> {\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts?.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n\n const entry = cache.get(key);\n if (entry) {\n const age = Date.now() - entry.lastSuccessAt;\n if (age > ttlMs) {\n revalidate(config, counterpartyId).catch(() => {\n // background refresh failed — stale entry remains until next call\n });\n }\n return age > maxStaleMs ? degradeToObserve(entry.config) : entry.config;\n }\n\n await revalidate(config, counterpartyId).catch(() => {\n // fetch failed — fall through to defaults\n });\n return cache.get(key)?.config ?? DEFAULT_EDGE_CONFIG;\n}\n\n/**\n * First-match-wins path-rule lookup. Case-insensitive `*` globs with all\n * other regex metacharacters escaped (a literal `.` in a pattern matches\n * only `.`). Returns `undefined` when no rule matches — the caller applies\n * the config's top-level mode/depth.\n */\nexport function matchEdgePathRule(rules: EdgePathRule[], path: string): EdgePathRule | undefined {\n for (const rule of rules) {\n if (globToRegExp(rule.pattern).test(path)) return rule;\n }\n return undefined;\n}\n\nfunction globToRegExp(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`, 'i');\n}\n\n/** Reset cache — for testing only. */\nexport function _resetEdgeConfigCache(): void {\n cache.clear();\n inflight.clear();\n}\n"],"mappings":"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","names":["has"]}
1
+ {"version":3,"sources":["../src/version.ts","../src/http.ts","../src/edge-config.ts"],"sourcesContent":["/**\n * Single source-of-truth for the SDK's\n * package version emitted on verify-access bodies (and any future\n * telemetry). Bumped alongside `package.json#version` on every release.\n *\n * Why a constant rather than `import pkg from '../package.json'`:\n * - `tsconfig.json` sets `rootDir: ./src`; importing the sibling\n * package.json fails the build with \"outside rootDir\".\n * - Build-time string replacement (tsup `define`, esbuild banner, etc.)\n * adds toolchain coupling for a trivial gain.\n * - Embedded readonly constant works in every environment (Node, browser,\n * bundlers, Deno) without runtime fs / network access.\n *\n * Release discipline: a CI lint can grep `package.json#version` against\n * this constant if the two ever diverge in the wild. A manual bump\n * is fine — bumping both in the release-ceremony commit keeps them\n * lockstep.\n */\nexport const SDK_VERSION = '5.14.0';\n","/**\n * Shared outbound-HTTP wrapper — every request the SDK makes to the\n * AstraSync backend (or anywhere else) goes through `sdkFetch` so it\n * carries an identifying User-Agent.\n *\n * Why: Node's global fetch defaults to `user-agent: node`, which is\n * indistinguishable from every other Node client on the wire. Visit\n * Intelligence observed our own SDK/beacon traffic as anonymous \"node\"\n * visits. Self-identification is the platform's own medicine.\n *\n * Browser note: some browsers filter the User-Agent request header\n * silently (never throwing), so this is a no-op there — which is correct:\n * in a browser the page's real UA is the honest identity.\n */\n\nimport { SDK_VERSION } from './version';\n\nexport const SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;\n\n/**\n * Add the SDK User-Agent without changing the SHAPE of the caller's headers\n * (plain records stay plain records) — interceptors, adapters, and tests\n * read `init.headers` as a record, and `Headers` would also lowercase names.\n */\nfunction withUserAgent(initHeaders: HeadersInit | undefined): HeadersInit {\n if (!initHeaders) return { 'user-agent': SDK_USER_AGENT };\n if (initHeaders instanceof Headers) {\n const copy = new Headers(initHeaders);\n if (!copy.has('user-agent')) copy.set('user-agent', SDK_USER_AGENT);\n return copy;\n }\n if (Array.isArray(initHeaders)) {\n const has = initHeaders.some(([name]) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : [...initHeaders, ['user-agent', SDK_USER_AGENT]];\n }\n const has = Object.keys(initHeaders).some((name) => name.toLowerCase() === 'user-agent');\n return has ? initHeaders : { 'user-agent': SDK_USER_AGENT, ...initHeaders };\n}\n\nexport const sdkFetch: typeof fetch = (input, init) =>\n fetch(input, { ...init, headers: withUserAgent(init?.headers) });\n","/**\n * Edge verification config — the per-endpoint policy the Trusted Agent\n * Gateway edge adapters (e.g. `@astrasyncai/adapter-lambda`) fetch from the\n * AstraSync dashboard to decide how much verification friction to apply\n * to inbound traffic, and where.\n *\n * Fetch pattern mirrors `well-known.ts`: TTL cache + stale-while-revalidate\n * + in-flight dedupe, with a `_reset*` hook for tests. Fail posture is\n * deliberately asymmetric: this module can NEVER put a site into enforce\n * mode on its own — enforce only ever originates from a successfully\n * fetched config, and a config that has gone stale past `maxStaleMs`\n * degrades back to observe.\n *\n * Which EdgeConfig fields each integration consumes:\n *\n * | Field | Edge adapters | SDK adapters (express/nextjs/mcp) |\n * | ------------------------------ | ------------- | --------------------------------- |\n * | `mode` (observe/enforce) | yes | — |\n * | `depth` | yes | — |\n * | `pathRules` | yes | — |\n * | `sampling.anonymousBeaconRate` | yes | yes — the SDK anonymous beacon |\n * | `failurePosture` | yes | — |\n *\n * The observe/enforce × depth posture is EDGE-ONLY by design: SDK adapters\n * enforce via the dashboard route policy (`fetchRoutes`), not via this\n * config, and consume only the `sampling` block for their anonymous\n * traffic beacon.\n */\n\nimport { sdkFetch } from './http';\nimport type { GatewayConfig } from './types';\n\n/**\n * Edge posture. Canonical Trusted Agent Gateway vocabulary:\n * `observe` ≈ the gateway's `passive` posture (verify +\n * record, enforce nothing), `enforce` ≈ `active`. New value set on a new\n * field name — do NOT reuse `GatewayPosture`'s `active`/`passive` here\n * (different field, different meaning: posture governs an agent-side\n * gateway; mode governs counterparty-side edge interception).\n */\nexport type EdgeMode = 'observe' | 'enforce';\n\n/**\n * How deep the edge checks each request — what is it → who is it → are\n * they allowed:\n * - `classify` — UA / platform-fingerprint classification only; no\n * credential handling.\n * - `authenticate` — establish who is calling: detect and parse agent\n * credentials (X-Astra-*, VI SD-JWT, RFC 9421, ACP,\n * UCP, AP2…) and verify presented AstraSync identities.\n * - `authorize` — full policy decision: credentials cryptographically\n * proven and PDLSS-evaluated via a verify-access round\n * trip. (US spelling on the wire, matching\n * `authorizeSettlement` and HTTP `Authorization`.)\n */\nexport type EdgeVerificationDepth = 'classify' | 'authenticate' | 'authorize';\n\nexport interface EdgePathRule {\n /**\n * `*`-wildcard glob matched against the request path. Matching is\n * case-INsensitive and all regex metacharacters except `*` are\n * escaped. (Deliberately diverges from the express adapter's\n * `matchRoute`, which is historically case-sensitive by default —\n * new surface ships case-insensitive matching as the default.)\n */\n pattern: string;\n /** Pass the request through untouched (static assets); mode/depth ignored. */\n skip?: boolean;\n /** Override the top-level mode for this path. */\n mode?: EdgeMode;\n /** Override the top-level depth for this path. */\n depth?: EdgeVerificationDepth;\n /**\n * Per-path beacon sampling opt-DOWN (VI uplift 1.2). The default is full\n * capture; put a fractional rate on explicitly high-volume/low-value paths\n * (static mirrors, health checks) instead of sampling commerce traffic.\n */\n sampling?: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n}\n\nexport interface EdgeConfig {\n /** Shape version — bump on breaking changes. */\n version: 1;\n /** Default posture for paths no rule matches. */\n mode: EdgeMode;\n /** Default check depth for paths no rule matches. */\n depth: EdgeVerificationDepth;\n /** Ordered, first-match-wins. */\n pathRules: EdgePathRule[];\n sampling: {\n /** Fraction (0..1) of anonymous-bot requests that emit a beacon. */\n anonymousBeaconRate: number;\n };\n /**\n * Query-string parameter names whose VALUES may be captured into\n * `observedMetadata.queryParams` (VI uplift Part 4 — campaign/UTM\n * attribution). Lowercase exact names or a single trailing-`*` glob\n * (`utm_*`); ≤16 entries × ≤64 chars. ABSENT by default — the privacy\n * stance (query values dropped at capture) is unchanged unless the\n * endpoint owner explicitly configures this.\n */\n queryValueAllowlist?: string[];\n /**\n * 4.5.0: the EFFECTIVE failure posture is derived from the effective\n * mode — fail posture follows success posture. Observe fails open\n * (unreachable backend = pass-through, site never breaks); enforce fails\n * closed on verification-infrastructure failure (503\n * `VERIFICATION_UNAVAILABLE`, retry-later — see edge-core/pipeline). This\n * field is retained on the wire for adapter compatibility and as a future\n * explicit override; it is not consulted by the 4.5.x pipeline. Note the\n * distinction from CONFIG staleness: a config stale past 24h still has\n * enforce degraded to observe (`degradeToObserve`) — that is config-trust\n * hygiene, independent of runtime verification availability.\n */\n failurePosture: 'open' | 'closed';\n}\n\n/**\n * The safe default: observe-only, classification depth, nothing enforced.\n * Served by the backend when an endpoint has never been configured, and\n * used by `getEdgeConfig` whenever no config can be fetched.\n *\n * Beacon rate 1.0 (VI uplift 1.2): every anonymous hit beacons by default —\n * a sampled denominator understates anonymous traffic in every dashboard\n * tile and no toggle can reveal the dropped rows. Sampling is a deliberate\n * per-path opt-down (`EdgePathRule.sampling`) for static/high-volume paths.\n */\nexport const DEFAULT_EDGE_CONFIG: EdgeConfig = Object.freeze({\n version: 1,\n mode: 'observe',\n depth: 'classify',\n pathRules: [],\n sampling: Object.freeze({ anonymousBeaconRate: 1.0 }),\n failurePosture: 'open',\n}) as EdgeConfig;\n\nconst EDGE_MODES: readonly string[] = ['observe', 'enforce'];\nconst EDGE_DEPTHS: readonly string[] = ['classify', 'authenticate', 'authorize'];\n\nfunction isSamplingShape(value: unknown): value is { anonymousBeaconRate: number } {\n if (typeof value !== 'object' || value === null) return false;\n const rate = (value as Record<string, unknown>).anonymousBeaconRate;\n return typeof rate === 'number' && rate >= 0 && rate <= 1;\n}\n\n/** Structural guard for configs arriving over the wire. */\nexport function isEdgeConfig(value: unknown): value is EdgeConfig {\n if (typeof value !== 'object' || value === null) return false;\n const v = value as Record<string, unknown>;\n if (v.version !== 1) return false;\n if (!EDGE_MODES.includes(v.mode as string)) return false;\n if (!EDGE_DEPTHS.includes(v.depth as string)) return false;\n if (!Array.isArray(v.pathRules)) return false;\n for (const rule of v.pathRules) {\n if (typeof rule !== 'object' || rule === null) return false;\n const r = rule as Record<string, unknown>;\n if (typeof r.pattern !== 'string' || r.pattern.length === 0) return false;\n if (r.mode !== undefined && !EDGE_MODES.includes(r.mode as string)) return false;\n if (r.depth !== undefined && !EDGE_DEPTHS.includes(r.depth as string)) return false;\n if (r.sampling !== undefined && !isSamplingShape(r.sampling)) return false;\n }\n if (!isSamplingShape(v.sampling)) return false;\n if (v.queryValueAllowlist !== undefined) {\n if (\n !Array.isArray(v.queryValueAllowlist) ||\n v.queryValueAllowlist.length > 16 ||\n !v.queryValueAllowlist.every((e) => typeof e === 'string' && e.length > 0 && e.length <= 64)\n ) {\n return false;\n }\n }\n return v.failurePosture === 'open';\n}\n\nexport interface FetchEdgeConfigSuccess {\n edgeConfig: EdgeConfig;\n etag?: string;\n}\n\nexport type FetchEdgeConfigResult = FetchEdgeConfigSuccess | { notModified: true } | null;\n\n/**\n * Fetch the edge config for an endpoint from the AstraSync backend.\n * `GET {apiBaseUrl}/endpoints/:counterpartyId/edge-config` with the same\n * auth headers as `fetchRoutes`. Pass the cached `etag` to get a cheap\n * `{ notModified: true }` on 304. Returns `null` on any failure —\n * the caller decides how to fall back.\n */\nexport async function fetchEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { etag?: string; timeoutMs?: number }\n): Promise<FetchEdgeConfigResult> {\n if (!counterpartyId) return null;\n const headers: Record<string, string> = { Accept: 'application/json' };\n if (config.apiKey) {\n headers['Authorization'] = `Bearer ${config.apiKey}`;\n headers['X-API-Key'] = config.apiKey;\n }\n if (opts?.etag) headers['If-None-Match'] = opts.etag;\n try {\n const response = await sdkFetch(\n `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/edge-config`,\n { method: 'GET', headers, signal: AbortSignal.timeout(opts?.timeoutMs ?? 5000) }\n );\n if (response.status === 304) return { notModified: true };\n if (!response.ok) return null;\n const body = (await response.json()) as { data?: { edgeConfig?: unknown } };\n const edgeConfig = body.data?.edgeConfig;\n if (!isEdgeConfig(edgeConfig)) return null;\n return { edgeConfig, etag: response.headers.get('etag') ?? undefined };\n } catch {\n return null;\n }\n}\n\ninterface CacheEntry {\n config: EdgeConfig;\n etag?: string;\n /** When the entry was last confirmed fresh (fetch or 304). */\n lastSuccessAt: number;\n}\n\nconst DEFAULT_TTL_MS = 60 * 1000;\nconst DEFAULT_MAX_STALE_MS = 24 * 60 * 60 * 1000;\n\nconst cache = new Map<string, CacheEntry>();\nconst inflight = new Map<string, Promise<void>>();\n\nfunction cacheKey(apiBaseUrl: string, counterpartyId: string): string {\n return `${apiBaseUrl}|${counterpartyId}`;\n}\n\n/** Copy of `config` with every enforce (top-level and per-rule) degraded to observe. */\nexport function degradeToObserve(config: EdgeConfig): EdgeConfig {\n if (config.mode !== 'enforce' && !config.pathRules.some((r) => r.mode === 'enforce')) {\n return config;\n }\n return {\n ...config,\n mode: 'observe',\n pathRules: config.pathRules.map((r) => (r.mode === 'enforce' ? { ...r, mode: 'observe' } : r)),\n };\n}\n\nasync function revalidate(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string\n): Promise<void> {\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n const existing = inflight.get(key);\n if (existing) return existing;\n\n const promise = fetchEdgeConfig(config, counterpartyId, { etag: cache.get(key)?.etag })\n .then((result) => {\n if (result === null) return; // failure — cache untouched, caller falls back\n const now = Date.now();\n if ('notModified' in result) {\n const entry = cache.get(key);\n if (entry) entry.lastSuccessAt = now;\n return;\n }\n cache.set(key, { config: result.edgeConfig, etag: result.etag, lastSuccessAt: now });\n })\n .finally(() => {\n inflight.delete(key);\n });\n\n inflight.set(key, promise);\n return promise;\n}\n\n/**\n * Get the edge config for an endpoint, cached. Never throws, never blocks\n * longer than one fetch.\n *\n * - Fresh cache (within `ttlMs`) → cached config, no network.\n * - Stale cache → cached config immediately + background revalidate\n * (stale-while-revalidate). If the last successful fetch is older than\n * `maxStaleMs` (default 24h), any enforce is degraded to observe — a\n * long-dead backend can never keep a site enforcing.\n * - No cache → await one fetch; on failure return `DEFAULT_EDGE_CONFIG`\n * (observe-only). Defaults never enforce.\n */\nexport async function getEdgeConfig(\n config: Pick<GatewayConfig, 'apiBaseUrl' | 'apiKey'>,\n counterpartyId: string,\n opts?: { ttlMs?: number; maxStaleMs?: number }\n): Promise<EdgeConfig> {\n const ttlMs = opts?.ttlMs ?? DEFAULT_TTL_MS;\n const maxStaleMs = opts?.maxStaleMs ?? DEFAULT_MAX_STALE_MS;\n const key = cacheKey(config.apiBaseUrl, counterpartyId);\n\n const entry = cache.get(key);\n if (entry) {\n const age = Date.now() - entry.lastSuccessAt;\n if (age > ttlMs) {\n revalidate(config, counterpartyId).catch(() => {\n // background refresh failed — stale entry remains until next call\n });\n }\n return age > maxStaleMs ? degradeToObserve(entry.config) : entry.config;\n }\n\n await revalidate(config, counterpartyId).catch(() => {\n // fetch failed — fall through to defaults\n });\n return cache.get(key)?.config ?? DEFAULT_EDGE_CONFIG;\n}\n\n/**\n * First-match-wins path-rule lookup. Case-insensitive `*` globs with all\n * other regex metacharacters escaped (a literal `.` in a pattern matches\n * only `.`). Returns `undefined` when no rule matches — the caller applies\n * the config's top-level mode/depth.\n */\nexport function matchEdgePathRule(rules: EdgePathRule[], path: string): EdgePathRule | undefined {\n for (const rule of rules) {\n if (globToRegExp(rule.pattern).test(path)) return rule;\n }\n return undefined;\n}\n\nfunction globToRegExp(pattern: string): RegExp {\n const escaped = pattern.replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&').replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`, 'i');\n}\n\n/** Reset cache — for testing only. */\nexport function _resetEdgeConfigCache(): void {\n cache.clear();\n inflight.clear();\n}\n"],"mappings":"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","names":["has"]}
@@ -369,9 +369,14 @@ declare function applyOutcome(options: {
369
369
  * without spending an attempt; a 401 on the key falls back to
370
370
  * unauthenticated delivery);
371
371
  * - collapses identical requests (same counterparty, IP, UA, method, path,
372
- * attempt id) inside a 60s window into one item carrying `repeatCount`;
372
+ * attempt id) inside a 60s window into one item carrying `repeatCount`
373
+ * — but only while that item is still queued (5.14.0, round 27.6: the
374
+ * old 60s held aggregate waited for a later invocation that a reclaimed
375
+ * Lambda container never gets; S1003 measured ~68% delivery);
373
376
  * - stamps each batch with `meta { droppedSinceLast, queueDepth, adapter,
374
- * version }` so the backend can report delivery loss.
377
+ * version, containerId, enqueuedTotal, deliveredTotal, droppedTotal }`
378
+ * so the backend can report delivery loss — including what a container
379
+ * that stops reporting left behind (5.14.0).
375
380
  * Delivery is at-least-once: a POST that times out after the backend
376
381
  * recorded it is retried.
377
382
  */
@@ -398,6 +403,16 @@ interface BeaconBatchMeta {
398
403
  queueDepth: number;
399
404
  adapter: string;
400
405
  version: string;
406
+ /**
407
+ * 5.14.0 (round 27.6): this container's id (one per cold start) and its
408
+ * cumulative item counters. The backend keeps the last report per
409
+ * container, so items stranded in a container that stops reporting (a
410
+ * reclaimed Lambda) are counted instead of silently vanishing.
411
+ */
412
+ containerId: string;
413
+ enqueuedTotal: number;
414
+ deliveredTotal: number;
415
+ droppedTotal: number;
401
416
  }
402
417
  interface BeaconBufferOptions {
403
418
  /** Max queued items; the oldest is dropped (and counted) beyond it. Default 1000. */
@@ -408,6 +423,16 @@ interface BeaconBufferOptions {
408
423
  maxAttempts?: number;
409
424
  /** Repeat-collapse window. Default 60s. */
410
425
  collapseWindowMs?: number;
426
+ /**
427
+ * Pre-5.14 behaviour: hold a repeat aggregate until its collapse window
428
+ * closes. Off by default — a held item waits up to 60s for a later
429
+ * invocation of the same container, and a container that is never
430
+ * invoked again loses it (round 27.6, S1003: ~32% loss on staging). Kept
431
+ * only so the delivery simulator can compare the two.
432
+ */
433
+ holdRepeats?: boolean;
434
+ /** Container id stamped on batch meta. Default: one per module load. */
435
+ containerId?: string;
411
436
  /** Max distinct repeat keys remembered (LRU). Default 1000. */
412
437
  collapseMaxKeys?: number;
413
438
  /** Per-POST timeout (the budget is the caller's; this bounds a hung POST). Default 10s. */
@@ -422,6 +447,7 @@ declare class BeaconBuffer {
422
447
  private droppedSinceLast;
423
448
  private droppedTotal;
424
449
  private deliveredTotal;
450
+ private enqueuedTotal;
425
451
  private inFlight;
426
452
  private backoffUntil;
427
453
  private readonly collapse;
@@ -431,8 +457,9 @@ declare class BeaconBuffer {
431
457
  constructor(options?: BeaconBufferOptions);
432
458
  /**
433
459
  * Queue one beacon. With a `collapseKey`, a repeat inside the window is
434
- * merged into a held aggregate (sent when the window closes) instead of
435
- * becoming its own item.
460
+ * merged into this key's item while that item is still queued (not yet in
461
+ * flight). Once it has left, the repeat becomes a new item, sendable on
462
+ * this flush — nothing waits for a later invocation (round 27.6).
436
463
  */
437
464
  enqueue(dest: BeaconDestination, item: BeaconItem, collapseKey?: string): void;
438
465
  /**
@@ -448,6 +475,7 @@ declare class BeaconBuffer {
448
475
  droppedSinceLast: number;
449
476
  droppedTotal: number;
450
477
  deliveredTotal: number;
478
+ enqueuedTotal: number;
451
479
  flushing: boolean;
452
480
  };
453
481
  private push;
@@ -369,9 +369,14 @@ declare function applyOutcome(options: {
369
369
  * without spending an attempt; a 401 on the key falls back to
370
370
  * unauthenticated delivery);
371
371
  * - collapses identical requests (same counterparty, IP, UA, method, path,
372
- * attempt id) inside a 60s window into one item carrying `repeatCount`;
372
+ * attempt id) inside a 60s window into one item carrying `repeatCount`
373
+ * — but only while that item is still queued (5.14.0, round 27.6: the
374
+ * old 60s held aggregate waited for a later invocation that a reclaimed
375
+ * Lambda container never gets; S1003 measured ~68% delivery);
373
376
  * - stamps each batch with `meta { droppedSinceLast, queueDepth, adapter,
374
- * version }` so the backend can report delivery loss.
377
+ * version, containerId, enqueuedTotal, deliveredTotal, droppedTotal }`
378
+ * so the backend can report delivery loss — including what a container
379
+ * that stops reporting left behind (5.14.0).
375
380
  * Delivery is at-least-once: a POST that times out after the backend
376
381
  * recorded it is retried.
377
382
  */
@@ -398,6 +403,16 @@ interface BeaconBatchMeta {
398
403
  queueDepth: number;
399
404
  adapter: string;
400
405
  version: string;
406
+ /**
407
+ * 5.14.0 (round 27.6): this container's id (one per cold start) and its
408
+ * cumulative item counters. The backend keeps the last report per
409
+ * container, so items stranded in a container that stops reporting (a
410
+ * reclaimed Lambda) are counted instead of silently vanishing.
411
+ */
412
+ containerId: string;
413
+ enqueuedTotal: number;
414
+ deliveredTotal: number;
415
+ droppedTotal: number;
401
416
  }
402
417
  interface BeaconBufferOptions {
403
418
  /** Max queued items; the oldest is dropped (and counted) beyond it. Default 1000. */
@@ -408,6 +423,16 @@ interface BeaconBufferOptions {
408
423
  maxAttempts?: number;
409
424
  /** Repeat-collapse window. Default 60s. */
410
425
  collapseWindowMs?: number;
426
+ /**
427
+ * Pre-5.14 behaviour: hold a repeat aggregate until its collapse window
428
+ * closes. Off by default — a held item waits up to 60s for a later
429
+ * invocation of the same container, and a container that is never
430
+ * invoked again loses it (round 27.6, S1003: ~32% loss on staging). Kept
431
+ * only so the delivery simulator can compare the two.
432
+ */
433
+ holdRepeats?: boolean;
434
+ /** Container id stamped on batch meta. Default: one per module load. */
435
+ containerId?: string;
411
436
  /** Max distinct repeat keys remembered (LRU). Default 1000. */
412
437
  collapseMaxKeys?: number;
413
438
  /** Per-POST timeout (the budget is the caller's; this bounds a hung POST). Default 10s. */
@@ -422,6 +447,7 @@ declare class BeaconBuffer {
422
447
  private droppedSinceLast;
423
448
  private droppedTotal;
424
449
  private deliveredTotal;
450
+ private enqueuedTotal;
425
451
  private inFlight;
426
452
  private backoffUntil;
427
453
  private readonly collapse;
@@ -431,8 +457,9 @@ declare class BeaconBuffer {
431
457
  constructor(options?: BeaconBufferOptions);
432
458
  /**
433
459
  * Queue one beacon. With a `collapseKey`, a repeat inside the window is
434
- * merged into a held aggregate (sent when the window closes) instead of
435
- * becoming its own item.
460
+ * merged into this key's item while that item is still queued (not yet in
461
+ * flight). Once it has left, the repeat becomes a new item, sendable on
462
+ * this flush — nothing waits for a later invocation (round 27.6).
436
463
  */
437
464
  enqueue(dest: BeaconDestination, item: BeaconItem, collapseKey?: string): void;
438
465
  /**
@@ -448,6 +475,7 @@ declare class BeaconBuffer {
448
475
  droppedSinceLast: number;
449
476
  droppedTotal: number;
450
477
  deliveredTotal: number;
478
+ enqueuedTotal: number;
451
479
  flushing: boolean;
452
480
  };
453
481
  private push;
@@ -46,7 +46,7 @@ __export(edge_core_exports, {
46
46
  module.exports = __toCommonJS(edge_core_exports);
47
47
 
48
48
  // src/version.ts
49
- var SDK_VERSION = "5.13.0";
49
+ var SDK_VERSION = "5.14.0";
50
50
 
51
51
  // src/http.ts
52
52
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -1581,6 +1581,11 @@ async function withBudget(promise, ms) {
1581
1581
  if (timer) clearTimeout(timer);
1582
1582
  }
1583
1583
  }
1584
+ var CONTAINER_ID = (() => {
1585
+ const c = globalThis.crypto;
1586
+ if (c?.randomUUID) return c.randomUUID();
1587
+ return `c-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
1588
+ })();
1584
1589
  var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1585
1590
  var BeaconBuffer = class {
1586
1591
  constructor(options = {}) {
@@ -1588,6 +1593,7 @@ var BeaconBuffer = class {
1588
1593
  this.droppedSinceLast = 0;
1589
1594
  this.droppedTotal = 0;
1590
1595
  this.deliveredTotal = 0;
1596
+ this.enqueuedTotal = 0;
1591
1597
  this.backoffUntil = 0;
1592
1598
  this.collapse = /* @__PURE__ */ new Map();
1593
1599
  /** Keys the backend answered 401 for — later batches go unauthenticated. */
@@ -1600,13 +1606,16 @@ var BeaconBuffer = class {
1600
1606
  collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1601
1607
  requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1602
1608
  retryBackoffMs: options.retryBackoffMs ?? 1e3,
1609
+ holdRepeats: options.holdRepeats ?? false,
1610
+ containerId: options.containerId ?? CONTAINER_ID,
1603
1611
  now: options.now ?? Date.now
1604
1612
  };
1605
1613
  }
1606
1614
  /**
1607
1615
  * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1608
- * merged into a held aggregate (sent when the window closes) instead of
1609
- * becoming its own item.
1616
+ * merged into this key's item while that item is still queued (not yet in
1617
+ * flight). Once it has left, the repeat becomes a new item, sendable on
1618
+ * this flush — nothing waits for a later invocation (round 27.6).
1610
1619
  */
1611
1620
  enqueue(dest, item, collapseKey) {
1612
1621
  const now = this.opts.now();
@@ -1619,12 +1628,15 @@ var BeaconBuffer = class {
1619
1628
  return;
1620
1629
  }
1621
1630
  }
1622
- const held = this.push(
1623
- dest,
1624
- { ...item, repeatCount: 1 },
1625
- entry.firstSeenAt + this.opts.collapseWindowMs
1626
- );
1627
- entry.held = held;
1631
+ if (this.opts.holdRepeats) {
1632
+ entry.held = this.push(
1633
+ dest,
1634
+ { ...item, repeatCount: 1 },
1635
+ entry.firstSeenAt + this.opts.collapseWindowMs
1636
+ );
1637
+ return;
1638
+ }
1639
+ entry.first = this.push(dest, { ...item, repeatCount: 1 }, now);
1628
1640
  return;
1629
1641
  }
1630
1642
  this.collapse.delete(collapseKey);
@@ -1660,6 +1672,7 @@ var BeaconBuffer = class {
1660
1672
  droppedSinceLast: this.droppedSinceLast,
1661
1673
  droppedTotal: this.droppedTotal,
1662
1674
  deliveredTotal: this.deliveredTotal,
1675
+ enqueuedTotal: this.enqueuedTotal,
1663
1676
  flushing: this.inFlight !== void 0
1664
1677
  };
1665
1678
  }
@@ -1674,6 +1687,7 @@ var BeaconBuffer = class {
1674
1687
  inFlight: false
1675
1688
  };
1676
1689
  this.queue.push(entry);
1690
+ this.enqueuedTotal += 1;
1677
1691
  return entry;
1678
1692
  }
1679
1693
  dropOldest() {
@@ -1719,7 +1733,11 @@ var BeaconBuffer = class {
1719
1733
  droppedSinceLast: reportedDropped,
1720
1734
  queueDepth: this.queue.length,
1721
1735
  adapter: dest.adapter ?? "edge-core",
1722
- version: SDK_VERSION
1736
+ version: SDK_VERSION,
1737
+ containerId: this.opts.containerId,
1738
+ enqueuedTotal: this.enqueuedTotal,
1739
+ deliveredTotal: this.deliveredTotal,
1740
+ droppedTotal: this.droppedTotal
1723
1741
  };
1724
1742
  const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1725
1743
  const response = await withBudget(