@astrasyncai/verification-gateway 5.11.0 → 5.13.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 (98) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/express.js +394 -39
  5. package/dist/adapters/express.js.map +1 -1
  6. package/dist/adapters/express.mjs +394 -39
  7. package/dist/adapters/express.mjs.map +1 -1
  8. package/dist/adapters/mcp.d.mts +5 -1
  9. package/dist/adapters/mcp.d.ts +5 -1
  10. package/dist/adapters/mcp.js +193 -49
  11. package/dist/adapters/mcp.js.map +1 -1
  12. package/dist/adapters/mcp.mjs +193 -49
  13. package/dist/adapters/mcp.mjs.map +1 -1
  14. package/dist/adapters/nextjs.d.mts +1 -1
  15. package/dist/adapters/nextjs.d.ts +1 -1
  16. package/dist/adapters/nextjs.js +387 -22
  17. package/dist/adapters/nextjs.js.map +1 -1
  18. package/dist/adapters/nextjs.mjs +387 -22
  19. package/dist/adapters/nextjs.mjs.map +1 -1
  20. package/dist/adapters/sdk.d.mts +9 -1
  21. package/dist/adapters/sdk.d.ts +9 -1
  22. package/dist/adapters/sdk.js +125 -16
  23. package/dist/adapters/sdk.js.map +1 -1
  24. package/dist/adapters/sdk.mjs +125 -16
  25. package/dist/adapters/sdk.mjs.map +1 -1
  26. package/dist/agent/index.js +1 -1
  27. package/dist/agent/index.js.map +1 -1
  28. package/dist/agent/index.mjs +1 -1
  29. package/dist/agent/index.mjs.map +1 -1
  30. package/dist/bin/astrasync-claude-hook.js +309 -15
  31. package/dist/bin/astrasync-codex-hook.js +309 -15
  32. package/dist/bin/astrasync-guard.js +309 -15
  33. package/dist/bin/astrasync.js +322 -17
  34. package/dist/browser/background.js +309 -15
  35. package/dist/browser/background.js.map +1 -1
  36. package/dist/browser/background.mjs +309 -15
  37. package/dist/browser/background.mjs.map +1 -1
  38. package/dist/cli/index.js +1 -1
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/cli/index.mjs +1 -1
  41. package/dist/cli/index.mjs.map +1 -1
  42. package/dist/codex/index.js +309 -15
  43. package/dist/codex/index.js.map +1 -1
  44. package/dist/codex/index.mjs +309 -15
  45. package/dist/codex/index.mjs.map +1 -1
  46. package/dist/cursor/extension.js +309 -15
  47. package/dist/cursor/extension.js.map +1 -1
  48. package/dist/cursor/extension.mjs +309 -15
  49. package/dist/cursor/extension.mjs.map +1 -1
  50. package/dist/edge-config.d.mts +1 -1
  51. package/dist/edge-config.d.ts +1 -1
  52. package/dist/edge-config.js +1 -1
  53. package/dist/edge-config.js.map +1 -1
  54. package/dist/edge-config.mjs +1 -1
  55. package/dist/edge-config.mjs.map +1 -1
  56. package/dist/edge-core/index.d.mts +128 -13
  57. package/dist/edge-core/index.d.ts +128 -13
  58. package/dist/edge-core/index.js +362 -63
  59. package/dist/edge-core/index.js.map +1 -1
  60. package/dist/edge-core/index.mjs +359 -63
  61. package/dist/edge-core/index.mjs.map +1 -1
  62. package/dist/gateway/gateway.js +309 -15
  63. package/dist/gateway/gateway.js.map +1 -1
  64. package/dist/gateway/gateway.mjs +309 -15
  65. package/dist/gateway/gateway.mjs.map +1 -1
  66. package/dist/git-trigger/git-hooks.d.mts +1 -1
  67. package/dist/git-trigger/git-hooks.d.ts +1 -1
  68. package/dist/index.d.mts +118 -15
  69. package/dist/index.d.ts +118 -15
  70. package/dist/index.js +521 -72
  71. package/dist/index.js.map +1 -1
  72. package/dist/index.mjs +515 -72
  73. package/dist/index.mjs.map +1 -1
  74. package/dist/platform-signatures.js +4 -1
  75. package/dist/platform-signatures.js.map +1 -1
  76. package/dist/platform-signatures.mjs +4 -1
  77. package/dist/platform-signatures.mjs.map +1 -1
  78. package/dist/registration/index.js +1 -1
  79. package/dist/registration/index.js.map +1 -1
  80. package/dist/registration/index.mjs +1 -1
  81. package/dist/registration/index.mjs.map +1 -1
  82. package/dist/transport/index.js +1 -1
  83. package/dist/transport/index.js.map +1 -1
  84. package/dist/transport/index.mjs +1 -1
  85. package/dist/transport/index.mjs.map +1 -1
  86. package/dist/{types-CTmUpNpS.d.mts → types-BCkwBA84.d.mts} +12 -0
  87. package/dist/{types-CTmUpNpS.d.ts → types-BCkwBA84.d.ts} +12 -0
  88. package/dist/{types-CtFXmDzm.d.ts → types-C_b4QTyO.d.ts} +18 -0
  89. package/dist/{types-DQrn_kei.d.mts → types-DlshIIR-.d.mts} +18 -0
  90. package/dist/ui/index.d.mts +1 -1
  91. package/dist/ui/index.d.ts +1 -1
  92. package/dist/verify.d.mts +35 -14
  93. package/dist/verify.d.ts +35 -14
  94. package/dist/verify.js +165 -25
  95. package/dist/verify.js.map +1 -1
  96. package/dist/verify.mjs +164 -25
  97. package/dist/verify.mjs.map +1 -1
  98. package/package.json +3 -2
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "5.11.0";
2
+ var SDK_VERSION = "5.13.0";
3
3
 
4
4
  // src/http.ts
5
5
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -144,6 +144,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
144
144
  return cache.get(apiBaseUrl)?.data;
145
145
  }
146
146
 
147
+ // src/failure-posture.ts
148
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
149
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
150
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
151
+ "identity.invalid_agent_id",
152
+ "identity.unknown_agent",
153
+ "agent.lookup"
154
+ ]);
155
+ var UNKNOWN_AGENT_GUIDANCE = "Register your agent with AstraSync (or, inside an MCP host, add the AstraSync MCP connector for keyless, human-approved registration), then present the canonical ASTRA-* id issued after owner approval.";
156
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
157
+ "verify_access.api_error",
158
+ "middleware.internal_error"
159
+ ]);
160
+ function isVerificationUnavailable(result) {
161
+ if (result.misconfigured) return false;
162
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
163
+ }
164
+ function unavailableRetryAfterSeconds(result) {
165
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
166
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
167
+ }
168
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
169
+ }
170
+ function isAnonymousRead(method, credentials) {
171
+ const m = (method ?? "").toUpperCase();
172
+ if (m !== "GET" && m !== "HEAD") return false;
173
+ return !credentials.astraId && !credentials.jwt;
174
+ }
175
+ function buildVerificationUnavailableBody(result) {
176
+ return {
177
+ success: false,
178
+ error: {
179
+ code: "VERIFICATION_UNAVAILABLE",
180
+ message: "Agent verification is temporarily unavailable and this path enforces verification. Your identity was not evaluated and no denial was recorded \u2014 retry after the Retry-After interval.",
181
+ retryable: true,
182
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
183
+ ...result.failures && { failures: result.failures },
184
+ ...result.correlationId && { correlationId: result.correlationId },
185
+ guidance: {
186
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
187
+ }
188
+ }
189
+ };
190
+ }
191
+ function isRejectedIdentityClaim(result) {
192
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
193
+ }
194
+ function denyStatusFor(result, policyDeniedCode) {
195
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
196
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
197
+ return { status: 401, code: "UNAUTHORIZED" };
198
+ }
199
+
147
200
  // src/verify.ts
148
201
  var DEFAULT_CONFIG = {
149
202
  apiBaseUrl: "https://astrasync.ai/api",
@@ -156,7 +209,7 @@ var DEFAULT_CONFIG = {
156
209
  };
157
210
  var initCheckPerformed = false;
158
211
  var deprecationWarningShown = false;
159
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
212
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
160
213
  initCheckPerformed = true;
161
214
  try {
162
215
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -174,7 +227,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
174
227
  );
175
228
  }
176
229
  if (apiKey) {
177
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
230
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
231
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
178
232
  headers: { Authorization: `Bearer ${apiKey}` }
179
233
  });
180
234
  if (selfCheck.status === 401) {
@@ -186,6 +240,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
186
240
  console.error(message);
187
241
  } else if (selfCheck.ok) {
188
242
  const body = await selfCheck.json().catch(() => ({}));
243
+ if (body.counterparty && body.counterparty.canVerify === false) {
244
+ const message = `[VerificationGateway] the configured AstraSync API key cannot verify for this endpoint (${body.counterparty.message ?? "endpoint not found or owned by another account"}). Every inbound agent will be turned away until the key or counterparty is corrected.`;
245
+ if (strictInit) {
246
+ throw new Error(`${message} (strictInit=true)`);
247
+ }
248
+ console.error(message);
249
+ }
189
250
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
190
251
  console.warn(
191
252
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -324,11 +385,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
324
385
  const isMisconfig = source === "merchant_misconfig";
325
386
  const urls = options.urls;
326
387
  const guidance = isMisconfig ? {
327
- message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, or rotated). Your identity and the AstraSync platform are fine \u2014 contact the site operator. Retrying will not help.",
388
+ message: "This site's AstraSync integration is misconfigured (its API key was revoked, expired, rotated, or belongs to another account). Your identity and the AstraSync platform are fine \u2014 contact the site operator. Retrying will not help.",
328
389
  registrationUrl: urls?.registrationUrl ?? "",
329
390
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
330
391
  steps: [
331
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
392
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
332
393
  "Do not retry: the failure is deterministic until the operator updates their key"
333
394
  ]
334
395
  } : isApiError ? {
@@ -389,11 +450,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
389
450
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
390
451
  // deterministic and intentionally NOT marked retryable.
391
452
  ...isApiError && { retryable: true },
453
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
454
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
392
455
  correlationId: options.correlationId,
393
456
  attemptId: options.attemptId,
394
457
  verifiedAt: /* @__PURE__ */ new Date()
395
458
  };
396
459
  }
460
+ async function readJsonSafe(response) {
461
+ try {
462
+ const parsed = await response.json();
463
+ return parsed && typeof parsed === "object" ? parsed : {};
464
+ } catch {
465
+ return {};
466
+ }
467
+ }
468
+ function rejectedIdentityGuidance(failures) {
469
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
470
+ if (!f) return void 0;
471
+ if (f.dimension === "agent.lookup") {
472
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
473
+ }
474
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
475
+ }
476
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
477
+ function parseRetryAfterMs(response) {
478
+ const raw = response.headers?.get?.("retry-after");
479
+ if (!raw) return void 0;
480
+ const trimmed = raw.trim();
481
+ let ms;
482
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
483
+ ms = Number(trimmed) * 1e3;
484
+ } else {
485
+ const at = Date.parse(trimmed);
486
+ if (Number.isNaN(at)) return void 0;
487
+ ms = at - Date.now();
488
+ }
489
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
490
+ }
397
491
  async function callVerifyAccessAPI(config, request) {
398
492
  const { credentials, ...requestData } = request;
399
493
  const body = {
@@ -450,7 +544,8 @@ async function callVerifyAccessAPI(config, request) {
450
544
  body: JSON.stringify(body),
451
545
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
452
546
  });
453
- const data = await response.json();
547
+ const data = await readJsonSafe(response);
548
+ const retryAfterMs = parseRetryAfterMs(response);
454
549
  if (response.status === 410) {
455
550
  return {
456
551
  success: true,
@@ -488,13 +583,31 @@ async function callVerifyAccessAPI(config, request) {
488
583
  }
489
584
  };
490
585
  }
586
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
587
+ return {
588
+ success: true,
589
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
590
+ access: {
591
+ allowed: false,
592
+ reason: "unknown_agent",
593
+ failures: [
594
+ {
595
+ dimension: "identity.unknown_agent",
596
+ message: "The presented agent identifier is not registered with AstraSync.",
597
+ guidance: UNKNOWN_AGENT_GUIDANCE
598
+ }
599
+ ]
600
+ }
601
+ };
602
+ }
491
603
  if (!response.ok) {
492
- if (response.status === 401) {
604
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
605
+ if (response.status === 401 || keyMismatch) {
493
606
  return {
494
607
  success: false,
495
608
  error: data.message || data.error || "Integration API key rejected",
496
609
  misconfig: true,
497
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
610
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
498
611
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
499
612
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
500
613
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -502,9 +615,11 @@ async function callVerifyAccessAPI(config, request) {
502
615
  }
503
616
  return {
504
617
  success: false,
505
- error: data.message || data.error || `API returned ${response.status}`,
618
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
506
619
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
507
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
620
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
621
+ httpStatus: response.status,
622
+ ...retryAfterMs !== void 0 && { retryAfterMs }
508
623
  };
509
624
  }
510
625
  return data;
@@ -525,14 +640,22 @@ async function verify(config, request, options) {
525
640
  mergedConfig.apiBaseUrl,
526
641
  mergedConfig.debug,
527
642
  true,
528
- mergedConfig.apiKey
643
+ mergedConfig.apiKey,
644
+ {
645
+ id: mergedConfig.counterpartyId,
646
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
647
+ }
529
648
  );
530
649
  } else {
531
650
  void performInitCheck(
532
651
  mergedConfig.apiBaseUrl,
533
652
  mergedConfig.debug,
534
653
  false,
535
- mergedConfig.apiKey
654
+ mergedConfig.apiKey,
655
+ {
656
+ id: mergedConfig.counterpartyId,
657
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
658
+ }
536
659
  );
537
660
  }
538
661
  }
@@ -576,12 +699,14 @@ async function verify(config, request, options) {
576
699
  correlationId: apiResponse.correlationId,
577
700
  attemptId: apiResponse.attemptId,
578
701
  remediation: apiResponse.remediation,
702
+ httpStatus: apiResponse.httpStatus,
703
+ retryAfterMs: apiResponse.retryAfterMs,
579
704
  urls
580
705
  });
581
706
  }
582
707
  if (!apiResponse.access?.allowed) {
583
708
  const aggregatedFailures = apiResponse.access?.failures;
584
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
709
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
585
710
  const result2 = {
586
711
  identityVerified: idVerifiedFromBackend,
587
712
  policyAllowed: false,
@@ -593,7 +718,10 @@ async function verify(config, request, options) {
593
718
  requiresStepUp: apiResponse.access?.requiresStepUp,
594
719
  requiresApproval: apiResponse.access?.requiresApproval,
595
720
  guidance: {
596
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
721
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
722
+ // actionable registration guidance on its failure — surface it
723
+ // instead of the bare machine reason ('unknown_agent').
724
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
597
725
  registrationUrl: urls?.registrationUrl ?? "",
598
726
  documentationUrl: urls?.documentationUrl ?? "",
599
727
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -693,7 +821,7 @@ async function verify(config, request, options) {
693
821
  registrationUrl: urls?.registrationUrl ?? "",
694
822
  documentationUrl: urls?.documentationUrl ?? ""
695
823
  };
696
- } else if (result.recommendation === "step_up_required") {
824
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
697
825
  result.requiresStepUp = true;
698
826
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
699
827
  }
@@ -716,6 +844,7 @@ async function recordDecision(config, sessionId, decision, reason) {
716
844
  }).catch(() => {
717
845
  });
718
846
  }
847
+ var refusedPolicyReads = /* @__PURE__ */ new Set();
719
848
  async function fetchRoutes(config, counterpartyId) {
720
849
  if (!counterpartyId) return null;
721
850
  const headers = { "Content-Type": "application/json" };
@@ -728,7 +857,15 @@ async function fetchRoutes(config, counterpartyId) {
728
857
  `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
729
858
  { method: "GET", headers }
730
859
  );
731
- if (!response.ok) return null;
860
+ if (!response.ok) {
861
+ if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
862
+ refusedPolicyReads.add(counterpartyId);
863
+ console.error(
864
+ `[VerificationGateway] the configured API key cannot read the route policy for ${counterpartyId} (HTTP ${response.status}). The key belongs to an account that does not own this endpoint, or the counterpartyId is wrong. The gateway is in PASS-THROUGH mode: no agent is being verified.`
865
+ );
866
+ }
867
+ return null;
868
+ }
732
869
  const body = await response.json();
733
870
  return body.data?.routes ?? [];
734
871
  } catch {
@@ -745,7 +882,10 @@ async function reportUnregisteredAttempt(config, data) {
745
882
  };
746
883
  await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
747
884
  method: "POST",
748
- headers: { "Content-Type": "application/json" },
885
+ headers: {
886
+ "Content-Type": "application/json",
887
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
888
+ },
749
889
  body: JSON.stringify(body)
750
890
  }).catch(() => {
751
891
  });
@@ -1335,7 +1475,10 @@ var PLATFORM_AGENT_SIGNATURES = [
1335
1475
  {
1336
1476
  vendor: "chatgpt",
1337
1477
  displayName: "OpenAI ChatGPT",
1338
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
1478
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
1479
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
1480
+ // classified as unattributed crawlers (round-27.4 W5).
1481
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
1339
1482
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
1340
1483
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
1341
1484
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -1470,6 +1613,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
1470
1613
  var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
1471
1614
 
1472
1615
  // src/edge-core/events.ts
1616
+ async function withBudget(promise, ms) {
1617
+ let timer;
1618
+ try {
1619
+ return await Promise.race([
1620
+ promise,
1621
+ new Promise((resolve) => {
1622
+ timer = setTimeout(() => resolve(void 0), ms);
1623
+ })
1624
+ ]);
1625
+ } catch {
1626
+ return void 0;
1627
+ } finally {
1628
+ if (timer) clearTimeout(timer);
1629
+ }
1630
+ }
1631
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1632
+ var BeaconBuffer = class {
1633
+ constructor(options = {}) {
1634
+ this.queue = [];
1635
+ this.droppedSinceLast = 0;
1636
+ this.droppedTotal = 0;
1637
+ this.deliveredTotal = 0;
1638
+ this.backoffUntil = 0;
1639
+ this.collapse = /* @__PURE__ */ new Map();
1640
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1641
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1642
+ this.opts = {
1643
+ maxItems: options.maxItems ?? 1e3,
1644
+ maxBatch: options.maxBatch ?? 100,
1645
+ maxAttempts: options.maxAttempts ?? 3,
1646
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1647
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1648
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1649
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1650
+ now: options.now ?? Date.now
1651
+ };
1652
+ }
1653
+ /**
1654
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1655
+ * merged into a held aggregate (sent when the window closes) instead of
1656
+ * becoming its own item.
1657
+ */
1658
+ enqueue(dest, item, collapseKey) {
1659
+ const now = this.opts.now();
1660
+ if (collapseKey) {
1661
+ const entry = this.collapse.get(collapseKey);
1662
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1663
+ for (const target of [entry.first, entry.held]) {
1664
+ if (target && !target.inFlight && this.queue.includes(target)) {
1665
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1666
+ return;
1667
+ }
1668
+ }
1669
+ const held = this.push(
1670
+ dest,
1671
+ { ...item, repeatCount: 1 },
1672
+ entry.firstSeenAt + this.opts.collapseWindowMs
1673
+ );
1674
+ entry.held = held;
1675
+ return;
1676
+ }
1677
+ this.collapse.delete(collapseKey);
1678
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1679
+ const oldest = this.collapse.keys().next().value;
1680
+ if (oldest !== void 0) this.collapse.delete(oldest);
1681
+ }
1682
+ const fresh = { firstSeenAt: now };
1683
+ this.collapse.set(collapseKey, fresh);
1684
+ fresh.first = this.push(dest, item, now);
1685
+ return;
1686
+ }
1687
+ this.push(dest, item, now);
1688
+ }
1689
+ /**
1690
+ * Deliver every eligible queued item. Concurrent callers share ONE
1691
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1692
+ * so nothing in flight is ever sent twice; items queued while it runs are
1693
+ * picked up by the same drain.
1694
+ */
1695
+ flush() {
1696
+ if (!this.inFlight) {
1697
+ this.inFlight = this.drain().finally(() => {
1698
+ this.inFlight = void 0;
1699
+ });
1700
+ }
1701
+ return this.inFlight;
1702
+ }
1703
+ /** Counters for tests and diagnostics. */
1704
+ stats() {
1705
+ return {
1706
+ queueDepth: this.queue.length,
1707
+ droppedSinceLast: this.droppedSinceLast,
1708
+ droppedTotal: this.droppedTotal,
1709
+ deliveredTotal: this.deliveredTotal,
1710
+ flushing: this.inFlight !== void 0
1711
+ };
1712
+ }
1713
+ push(dest, item, eligibleAt) {
1714
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1715
+ const entry = {
1716
+ dest,
1717
+ destKey: destKeyOf(dest),
1718
+ item,
1719
+ attempts: 0,
1720
+ eligibleAt,
1721
+ inFlight: false
1722
+ };
1723
+ this.queue.push(entry);
1724
+ return entry;
1725
+ }
1726
+ dropOldest() {
1727
+ if (this.queue.shift()) this.countDropped(1);
1728
+ }
1729
+ countDropped(n) {
1730
+ this.droppedSinceLast += n;
1731
+ this.droppedTotal += n;
1732
+ }
1733
+ async drain() {
1734
+ for (; ; ) {
1735
+ const now = this.opts.now();
1736
+ if (now < this.backoffUntil) return;
1737
+ const batch = this.takeBatch(now);
1738
+ if (batch.length === 0) return;
1739
+ const ok = await this.send(batch);
1740
+ if (!ok) return;
1741
+ }
1742
+ }
1743
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1744
+ takeBatch(now) {
1745
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1746
+ if (!first) return [];
1747
+ const batch = [];
1748
+ const rest = [];
1749
+ for (const q of this.queue) {
1750
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1751
+ q.inFlight = true;
1752
+ batch.push(q);
1753
+ } else {
1754
+ rest.push(q);
1755
+ }
1756
+ }
1757
+ this.queue = rest;
1758
+ return batch;
1759
+ }
1760
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1761
+ async send(batch) {
1762
+ const dest = batch[0].dest;
1763
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1764
+ const reportedDropped = this.droppedSinceLast;
1765
+ const meta = {
1766
+ droppedSinceLast: reportedDropped,
1767
+ queueDepth: this.queue.length,
1768
+ adapter: dest.adapter ?? "edge-core",
1769
+ version: SDK_VERSION
1770
+ };
1771
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1772
+ const response = await withBudget(
1773
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1774
+ method: "POST",
1775
+ headers: {
1776
+ "Content-Type": "application/json",
1777
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1778
+ },
1779
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1780
+ ...controller && { signal: controller.signal }
1781
+ }),
1782
+ this.opts.requestTimeoutMs
1783
+ );
1784
+ if (!response) controller?.abort();
1785
+ const status = response?.status;
1786
+ for (const q of batch) q.inFlight = false;
1787
+ if (status !== void 0 && status >= 200 && status < 300) {
1788
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1789
+ this.deliveredTotal += batch.length;
1790
+ return true;
1791
+ }
1792
+ if (status === 401 && useKey) {
1793
+ this.rejectedKeys.add(useKey);
1794
+ this.requeue(batch, false);
1795
+ return true;
1796
+ }
1797
+ if (status === 429) {
1798
+ const retryAfter = Number(response?.headers.get("retry-after"));
1799
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1800
+ this.backoffUntil = this.opts.now() + waitMs;
1801
+ this.requeue(batch, false);
1802
+ return false;
1803
+ }
1804
+ if (status !== void 0 && status >= 400 && status < 500) {
1805
+ this.countDropped(batch.length);
1806
+ return true;
1807
+ }
1808
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1809
+ this.requeue(batch, true);
1810
+ return false;
1811
+ }
1812
+ /** Put a failed batch back at the head of the queue (order preserved). */
1813
+ requeue(batch, countAttempt) {
1814
+ const keep = [];
1815
+ for (const q of batch) {
1816
+ if (countAttempt) q.attempts += 1;
1817
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1818
+ else keep.push(q);
1819
+ }
1820
+ this.queue = [...keep, ...this.queue];
1821
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1822
+ }
1823
+ };
1824
+ var beaconBuffer = new BeaconBuffer();
1473
1825
  function sampled(rate, random = Math.random) {
1474
1826
  if (rate <= 0) return false;
1475
1827
  if (rate >= 1) return true;
@@ -1646,12 +1998,19 @@ function defaultOnDenied(result, _req, res) {
1646
1998
  });
1647
1999
  return;
1648
2000
  }
1649
- const statusCode = !result.identityVerified ? 401 : 403;
2001
+ if (isVerificationUnavailable(result)) {
2002
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2003
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2004
+ res.setHeader("Retry-After", String(unavailableRetryAfterSeconds(result)));
2005
+ res.status(503).json(buildVerificationUnavailableBody(result));
2006
+ return;
2007
+ }
2008
+ const { status: statusCode, code } = denyStatusFor(result, "INSUFFICIENT_ACCESS");
1650
2009
  res.setHeader("X-Astra-Gateway-Mode", "enforced");
1651
2010
  res.status(statusCode).json({
1652
2011
  success: false,
1653
2012
  error: {
1654
- code: !result.identityVerified ? "UNAUTHORIZED" : "INSUFFICIENT_ACCESS",
2013
+ code,
1655
2014
  message: result.denialReasons?.[0] || "Access denied",
1656
2015
  guidance: result.guidance,
1657
2016
  // Round-10: aggregated per-dimension detail + correlation handle.
@@ -1899,6 +2258,21 @@ function createMiddleware(options) {
1899
2258
  result.stepUpOutcome = outcome;
1900
2259
  }
1901
2260
  }
2261
+ if (!result.policyAllowed && isVerificationUnavailable(result) && (!shouldEnforce || isAnonymousRead(req.method, credentials))) {
2262
+ res.setHeader(VERIFICATION_STATUS_HEADER, "unavailable");
2263
+ return next();
2264
+ }
2265
+ if (!shouldEnforce) {
2266
+ if (config.setPassThroughHeader) {
2267
+ res.setHeader("X-Astra-Gateway-Mode", "enforced");
2268
+ res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
2269
+ }
2270
+ if (shouldRecordDecisions && sessionId) {
2271
+ recordDecision(config, sessionId, "granted").catch(() => {
2272
+ });
2273
+ }
2274
+ return next();
2275
+ }
1902
2276
  if (!result.policyAllowed) {
1903
2277
  if (shouldRecordDecisions && sessionId) {
1904
2278
  recordDecision(config, sessionId, "denied", result.denialReasons?.[0]).catch(() => {
@@ -1918,17 +2292,6 @@ function createMiddleware(options) {
1918
2292
  onDenied(result, req, res);
1919
2293
  return;
1920
2294
  }
1921
- if (!shouldEnforce) {
1922
- if (config.setPassThroughHeader) {
1923
- res.setHeader("X-Astra-Gateway-Mode", "enforced");
1924
- res.setHeader("X-Astra-Gateway-Reason", "evaluated-not-enforced");
1925
- }
1926
- if (shouldRecordDecisions && sessionId) {
1927
- recordDecision(config, sessionId, "granted").catch(() => {
1928
- });
1929
- }
1930
- return next();
1931
- }
1932
2295
  if (routeConfig.minTrustScore && result.agent) {
1933
2296
  if (result.agent.trustScore < routeConfig.minTrustScore) {
1934
2297
  const trustFailure = {
@@ -1984,14 +2347,6 @@ function createMiddleware(options) {
1984
2347
  verifiedAt: /* @__PURE__ */ new Date(),
1985
2348
  correlationId
1986
2349
  };
1987
- const catchUrls = config.apiBaseUrl ? await getWellKnownUrls(config.apiBaseUrl).catch(() => void 0) : void 0;
1988
- if (catchUrls) {
1989
- result.guidance = {
1990
- message: `Middleware threw ${errorClass} \u2014 failing closed`,
1991
- registrationUrl: catchUrls.registrationUrl,
1992
- documentationUrl: catchUrls.documentationUrl
1993
- };
1994
- }
1995
2350
  dedupeFailures(result);
1996
2351
  return onDenied(result, req, res);
1997
2352
  }