@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}`;
@@ -92,6 +92,59 @@ function getCachedWellKnownUrls(apiBaseUrl) {
92
92
  return cache.get(apiBaseUrl)?.data;
93
93
  }
94
94
 
95
+ // src/failure-posture.ts
96
+ var VERIFICATION_STATUS_HEADER = "X-Astra-Verification";
97
+ var DEFAULT_UNAVAILABLE_RETRY_AFTER_S = 30;
98
+ var REJECTED_IDENTITY_DIMENSIONS = /* @__PURE__ */ new Set([
99
+ "identity.invalid_agent_id",
100
+ "identity.unknown_agent",
101
+ "agent.lookup"
102
+ ]);
103
+ 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.";
104
+ var UNAVAILABLE_DIMENSIONS = /* @__PURE__ */ new Set([
105
+ "verify_access.api_error",
106
+ "middleware.internal_error"
107
+ ]);
108
+ function isVerificationUnavailable(result) {
109
+ if (result.misconfigured) return false;
110
+ return (result.failures ?? []).some((f) => UNAVAILABLE_DIMENSIONS.has(f.dimension));
111
+ }
112
+ function unavailableRetryAfterSeconds(result) {
113
+ if (typeof result.retryAfterMs === "number" && Number.isFinite(result.retryAfterMs)) {
114
+ return Math.max(1, Math.ceil(result.retryAfterMs / 1e3));
115
+ }
116
+ return DEFAULT_UNAVAILABLE_RETRY_AFTER_S;
117
+ }
118
+ function isAnonymousRead(method, credentials) {
119
+ const m = (method ?? "").toUpperCase();
120
+ if (m !== "GET" && m !== "HEAD") return false;
121
+ return !credentials.astraId && !credentials.jwt;
122
+ }
123
+ function buildVerificationUnavailableBody(result) {
124
+ return {
125
+ success: false,
126
+ error: {
127
+ code: "VERIFICATION_UNAVAILABLE",
128
+ 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.",
129
+ retryable: true,
130
+ retryAfterSeconds: unavailableRetryAfterSeconds(result),
131
+ ...result.failures && { failures: result.failures },
132
+ ...result.correlationId && { correlationId: result.correlationId },
133
+ guidance: {
134
+ message: "Retry the request after the Retry-After interval, with exponential backoff; the condition is transient."
135
+ }
136
+ }
137
+ };
138
+ }
139
+ function isRejectedIdentityClaim(result) {
140
+ return (result.failures ?? []).some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
141
+ }
142
+ function denyStatusFor(result, policyDeniedCode) {
143
+ if (isRejectedIdentityClaim(result)) return { status: 403, code: "AGENT_IDENTITY_REJECTED" };
144
+ if (result.identityVerified) return { status: 403, code: policyDeniedCode };
145
+ return { status: 401, code: "UNAUTHORIZED" };
146
+ }
147
+
95
148
  // src/verify.ts
96
149
  var DEFAULT_CONFIG = {
97
150
  apiBaseUrl: "https://astrasync.ai/api",
@@ -104,7 +157,7 @@ var DEFAULT_CONFIG = {
104
157
  };
105
158
  var initCheckPerformed = false;
106
159
  var deprecationWarningShown = false;
107
- async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
160
+ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey, counterparty) {
108
161
  initCheckPerformed = true;
109
162
  try {
110
163
  const probeUrl = `${apiBaseUrl}/agents/verify-access`;
@@ -122,7 +175,8 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
122
175
  );
123
176
  }
124
177
  if (apiKey) {
125
- const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check`, {
178
+ const q = counterparty?.id ? `?counterpartyId=${encodeURIComponent(counterparty.id)}` : counterparty?.url ? `?counterpartyUrl=${encodeURIComponent(counterparty.url)}` : "";
179
+ const selfCheck = await sdkFetch(`${apiBaseUrl}/agents/verify-access/self-check${q}`, {
126
180
  headers: { Authorization: `Bearer ${apiKey}` }
127
181
  });
128
182
  if (selfCheck.status === 401) {
@@ -134,6 +188,13 @@ async function performInitCheck(apiBaseUrl, debug, strictInit, apiKey) {
134
188
  console.error(message);
135
189
  } else if (selfCheck.ok) {
136
190
  const body = await selfCheck.json().catch(() => ({}));
191
+ if (body.counterparty && body.counterparty.canVerify === false) {
192
+ 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.`;
193
+ if (strictInit) {
194
+ throw new Error(`${message} (strictInit=true)`);
195
+ }
196
+ console.error(message);
197
+ }
137
198
  if (typeof body.daysToExpiry === "number" && body.daysToExpiry <= 7) {
138
199
  console.warn(
139
200
  `[VerificationGateway] the configured AstraSync API key expires in ${body.daysToExpiry} day(s) \u2014 rotate it at Settings \u2192 Developer Tools before integrations start failing.`
@@ -272,11 +333,11 @@ function createGuidanceResponse(_config, reason, options = {}) {
272
333
  const isMisconfig = source === "merchant_misconfig";
273
334
  const urls = options.urls;
274
335
  const guidance = isMisconfig ? {
275
- 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.",
336
+ 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.",
276
337
  registrationUrl: urls?.registrationUrl ?? "",
277
338
  documentationUrl: options.remediation?.docsUrl ?? urls?.documentationUrl ?? "",
278
339
  steps: [
279
- "Report the failure to the site operator \u2014 their AstraSync API key needs rotating",
340
+ "Report the failure to the site operator \u2014 their AstraSync API key needs replacing",
280
341
  "Do not retry: the failure is deterministic until the operator updates their key"
281
342
  ]
282
343
  } : isApiError ? {
@@ -337,11 +398,44 @@ function createGuidanceResponse(_config, reason, options = {}) {
337
398
  // coercing the absent recommendation to a policy 'deny'. Misconfig is
338
399
  // deterministic and intentionally NOT marked retryable.
339
400
  ...isApiError && { retryable: true },
401
+ ...options.httpStatus !== void 0 && { httpStatus: options.httpStatus },
402
+ ...options.retryAfterMs !== void 0 && { retryAfterMs: options.retryAfterMs },
340
403
  correlationId: options.correlationId,
341
404
  attemptId: options.attemptId,
342
405
  verifiedAt: /* @__PURE__ */ new Date()
343
406
  };
344
407
  }
408
+ async function readJsonSafe(response) {
409
+ try {
410
+ const parsed = await response.json();
411
+ return parsed && typeof parsed === "object" ? parsed : {};
412
+ } catch {
413
+ return {};
414
+ }
415
+ }
416
+ function rejectedIdentityGuidance(failures) {
417
+ const f = failures?.find((x) => REJECTED_IDENTITY_DIMENSIONS.has(x.dimension));
418
+ if (!f) return void 0;
419
+ if (f.dimension === "agent.lookup") {
420
+ return `The presented agent identifier is not registered with AstraSync. ${UNKNOWN_AGENT_GUIDANCE}`;
421
+ }
422
+ return f.guidance ? `${f.message} ${f.guidance}` : f.message;
423
+ }
424
+ var MAX_RETRY_AFTER_MS = 15 * 60 * 1e3;
425
+ function parseRetryAfterMs(response) {
426
+ const raw = response.headers?.get?.("retry-after");
427
+ if (!raw) return void 0;
428
+ const trimmed = raw.trim();
429
+ let ms;
430
+ if (/^\d+(\.\d+)?$/.test(trimmed)) {
431
+ ms = Number(trimmed) * 1e3;
432
+ } else {
433
+ const at = Date.parse(trimmed);
434
+ if (Number.isNaN(at)) return void 0;
435
+ ms = at - Date.now();
436
+ }
437
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, Math.round(ms)));
438
+ }
345
439
  async function callVerifyAccessAPI(config, request) {
346
440
  const { credentials, ...requestData } = request;
347
441
  const body = {
@@ -398,7 +492,8 @@ async function callVerifyAccessAPI(config, request) {
398
492
  body: JSON.stringify(body),
399
493
  signal: AbortSignal.timeout(config.verifyTimeoutMs ?? 1e4)
400
494
  });
401
- const data = await response.json();
495
+ const data = await readJsonSafe(response);
496
+ const retryAfterMs = parseRetryAfterMs(response);
402
497
  if (response.status === 410) {
403
498
  return {
404
499
  success: true,
@@ -436,13 +531,31 @@ async function callVerifyAccessAPI(config, request) {
436
531
  }
437
532
  };
438
533
  }
534
+ if (response.status === 404 && credentials.astraId && (data?.error === "AGENT_NOT_FOUND" || typeof data?.message === "string" && /agent/i.test(data.message))) {
535
+ return {
536
+ success: true,
537
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
538
+ access: {
539
+ allowed: false,
540
+ reason: "unknown_agent",
541
+ failures: [
542
+ {
543
+ dimension: "identity.unknown_agent",
544
+ message: "The presented agent identifier is not registered with AstraSync.",
545
+ guidance: UNKNOWN_AGENT_GUIDANCE
546
+ }
547
+ ]
548
+ }
549
+ };
550
+ }
439
551
  if (!response.ok) {
440
- if (response.status === 401) {
552
+ const keyMismatch = response.status === 403 && data?.error === "COUNTERPARTY_KEY_MISMATCH";
553
+ if (response.status === 401 || keyMismatch) {
441
554
  return {
442
555
  success: false,
443
556
  error: data.message || data.error || "Integration API key rejected",
444
557
  misconfig: true,
445
- misconfigCode: typeof data?.code === "string" ? data.code : void 0,
558
+ misconfigCode: keyMismatch ? "counterparty_key_mismatch" : typeof data?.code === "string" ? data.code : void 0,
446
559
  remediation: data?.remediation && typeof data.remediation === "object" ? data.remediation : void 0,
447
560
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
448
561
  attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
@@ -450,9 +563,11 @@ async function callVerifyAccessAPI(config, request) {
450
563
  }
451
564
  return {
452
565
  success: false,
453
- error: data.message || data.error || `API returned ${response.status}`,
566
+ error: typeof data?.message === "string" && data.message || typeof data?.error === "string" && data.error || `API returned ${response.status}`,
454
567
  correlationId: typeof data?.correlationId === "string" ? data.correlationId : void 0,
455
- attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0
568
+ attemptId: typeof data?.attemptId === "string" ? data.attemptId : void 0,
569
+ httpStatus: response.status,
570
+ ...retryAfterMs !== void 0 && { retryAfterMs }
456
571
  };
457
572
  }
458
573
  return data;
@@ -473,14 +588,22 @@ async function verify(config, request, options) {
473
588
  mergedConfig.apiBaseUrl,
474
589
  mergedConfig.debug,
475
590
  true,
476
- mergedConfig.apiKey
591
+ mergedConfig.apiKey,
592
+ {
593
+ id: mergedConfig.counterpartyId,
594
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
595
+ }
477
596
  );
478
597
  } else {
479
598
  void performInitCheck(
480
599
  mergedConfig.apiBaseUrl,
481
600
  mergedConfig.debug,
482
601
  false,
483
- mergedConfig.apiKey
602
+ mergedConfig.apiKey,
603
+ {
604
+ id: mergedConfig.counterpartyId,
605
+ url: mergedConfig.counterpartyUrl ?? request.counterpartyUrl
606
+ }
484
607
  );
485
608
  }
486
609
  }
@@ -524,12 +647,14 @@ async function verify(config, request, options) {
524
647
  correlationId: apiResponse.correlationId,
525
648
  attemptId: apiResponse.attemptId,
526
649
  remediation: apiResponse.remediation,
650
+ httpStatus: apiResponse.httpStatus,
651
+ retryAfterMs: apiResponse.retryAfterMs,
527
652
  urls
528
653
  });
529
654
  }
530
655
  if (!apiResponse.access?.allowed) {
531
656
  const aggregatedFailures = apiResponse.access?.failures;
532
- const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true;
657
+ const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
533
658
  const result2 = {
534
659
  identityVerified: idVerifiedFromBackend,
535
660
  policyAllowed: false,
@@ -541,7 +666,10 @@ async function verify(config, request, options) {
541
666
  requiresStepUp: apiResponse.access?.requiresStepUp,
542
667
  requiresApproval: apiResponse.access?.requiresApproval,
543
668
  guidance: {
544
- message: apiResponse.access?.reason || "Access denied by PDLSS policy",
669
+ // 5.12.0: a presented-but-rejected id (malformed / unknown) carries
670
+ // actionable registration guidance on its failure — surface it
671
+ // instead of the bare machine reason ('unknown_agent').
672
+ message: rejectedIdentityGuidance(aggregatedFailures) ?? (apiResponse.access?.reason || "Access denied by PDLSS policy"),
545
673
  registrationUrl: urls?.registrationUrl ?? "",
546
674
  documentationUrl: urls?.documentationUrl ?? "",
547
675
  // 5.1.0: anonymous callers land on this branch — steer MCP-hosted
@@ -641,7 +769,7 @@ async function verify(config, request, options) {
641
769
  registrationUrl: urls?.registrationUrl ?? "",
642
770
  documentationUrl: urls?.documentationUrl ?? ""
643
771
  };
644
- } else if (result.recommendation === "step_up_required") {
772
+ } else if (result.recommendation === "step_up_required" && result.identityVerified) {
645
773
  result.requiresStepUp = true;
646
774
  result.denialReasons = serverDenialReasons ?? result.recommendationReasons ?? ["Step-up verification required"];
647
775
  }
@@ -651,6 +779,7 @@ async function verify(config, request, options) {
651
779
  }
652
780
  return result;
653
781
  }
782
+ var refusedPolicyReads = /* @__PURE__ */ new Set();
654
783
  async function fetchRoutes(config, counterpartyId) {
655
784
  if (!counterpartyId) return null;
656
785
  const headers = { "Content-Type": "application/json" };
@@ -663,7 +792,15 @@ async function fetchRoutes(config, counterpartyId) {
663
792
  `${config.apiBaseUrl}/endpoints/${encodeURIComponent(counterpartyId)}/routes`,
664
793
  { method: "GET", headers }
665
794
  );
666
- if (!response.ok) return null;
795
+ if (!response.ok) {
796
+ if ((response.status === 401 || response.status === 403 || response.status === 404) && !refusedPolicyReads.has(counterpartyId)) {
797
+ refusedPolicyReads.add(counterpartyId);
798
+ console.error(
799
+ `[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.`
800
+ );
801
+ }
802
+ return null;
803
+ }
667
804
  const body = await response.json();
668
805
  return body.data?.routes ?? [];
669
806
  } catch {
@@ -680,7 +817,10 @@ async function reportUnregisteredAttempt(config, data) {
680
817
  };
681
818
  await sdkFetch(`${apiBaseUrl}/verification-activity/unregistered-attempt`, {
682
819
  method: "POST",
683
- headers: { "Content-Type": "application/json" },
820
+ headers: {
821
+ "Content-Type": "application/json",
822
+ ...config.apiKey && { Authorization: `Bearer ${config.apiKey}` }
823
+ },
684
824
  body: JSON.stringify(body)
685
825
  }).catch(() => {
686
826
  });
@@ -1153,7 +1293,10 @@ var PLATFORM_AGENT_SIGNATURES = [
1153
1293
  {
1154
1294
  vendor: "chatgpt",
1155
1295
  displayName: "OpenAI ChatGPT",
1156
- uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i],
1296
+ // GPTBot (training crawler) and OAI-SearchBot (ChatGPT search) are
1297
+ // OpenAI's declared crawler UAs — `gpt-?\d` never matched them, so they
1298
+ // classified as unattributed crawlers (round-27.4 W5).
1299
+ uaPatterns: [/chatgpt/i, /openai/i, /gpt-?\d/i, /\bGPTBot\b/i, /\bOAI-SearchBot\b/i],
1157
1300
  agentCardPatterns: [/openai\.com/i, /chatgpt\.com/i],
1158
1301
  apiKeyFormatPatterns: [/^sk-proj/i, /^sk-svcacct/i],
1159
1302
  headerPatterns: [/^openai-organization/i, /^openai-project/i, /^openai-version/i]
@@ -1288,6 +1431,215 @@ var BROWSER_UA_PATTERN = /mozilla\/\d/i;
1288
1431
  var _patterns = { STATIC_EXTENSIONS, BOT_UA_PATTERN, BROWSER_UA_PATTERN, SELF_UA_PATTERN };
1289
1432
 
1290
1433
  // src/edge-core/events.ts
1434
+ async function withBudget(promise, ms) {
1435
+ let timer;
1436
+ try {
1437
+ return await Promise.race([
1438
+ promise,
1439
+ new Promise((resolve) => {
1440
+ timer = setTimeout(() => resolve(void 0), ms);
1441
+ })
1442
+ ]);
1443
+ } catch {
1444
+ return void 0;
1445
+ } finally {
1446
+ if (timer) clearTimeout(timer);
1447
+ }
1448
+ }
1449
+ var destKeyOf = (d) => `${d.apiBaseUrl}\0${d.apiKey ?? ""}\0${d.counterparty}\0${d.adapter ?? ""}`;
1450
+ var BeaconBuffer = class {
1451
+ constructor(options = {}) {
1452
+ this.queue = [];
1453
+ this.droppedSinceLast = 0;
1454
+ this.droppedTotal = 0;
1455
+ this.deliveredTotal = 0;
1456
+ this.backoffUntil = 0;
1457
+ this.collapse = /* @__PURE__ */ new Map();
1458
+ /** Keys the backend answered 401 for — later batches go unauthenticated. */
1459
+ this.rejectedKeys = /* @__PURE__ */ new Set();
1460
+ this.opts = {
1461
+ maxItems: options.maxItems ?? 1e3,
1462
+ maxBatch: options.maxBatch ?? 100,
1463
+ maxAttempts: options.maxAttempts ?? 3,
1464
+ collapseWindowMs: options.collapseWindowMs ?? 6e4,
1465
+ collapseMaxKeys: options.collapseMaxKeys ?? 1e3,
1466
+ requestTimeoutMs: options.requestTimeoutMs ?? 1e4,
1467
+ retryBackoffMs: options.retryBackoffMs ?? 1e3,
1468
+ now: options.now ?? Date.now
1469
+ };
1470
+ }
1471
+ /**
1472
+ * Queue one beacon. With a `collapseKey`, a repeat inside the window is
1473
+ * merged into a held aggregate (sent when the window closes) instead of
1474
+ * becoming its own item.
1475
+ */
1476
+ enqueue(dest, item, collapseKey) {
1477
+ const now = this.opts.now();
1478
+ if (collapseKey) {
1479
+ const entry = this.collapse.get(collapseKey);
1480
+ if (entry && now - entry.firstSeenAt < this.opts.collapseWindowMs) {
1481
+ for (const target of [entry.first, entry.held]) {
1482
+ if (target && !target.inFlight && this.queue.includes(target)) {
1483
+ target.item.repeatCount = (target.item.repeatCount ?? 1) + 1;
1484
+ return;
1485
+ }
1486
+ }
1487
+ const held = this.push(
1488
+ dest,
1489
+ { ...item, repeatCount: 1 },
1490
+ entry.firstSeenAt + this.opts.collapseWindowMs
1491
+ );
1492
+ entry.held = held;
1493
+ return;
1494
+ }
1495
+ this.collapse.delete(collapseKey);
1496
+ if (this.collapse.size >= this.opts.collapseMaxKeys) {
1497
+ const oldest = this.collapse.keys().next().value;
1498
+ if (oldest !== void 0) this.collapse.delete(oldest);
1499
+ }
1500
+ const fresh = { firstSeenAt: now };
1501
+ this.collapse.set(collapseKey, fresh);
1502
+ fresh.first = this.push(dest, item, now);
1503
+ return;
1504
+ }
1505
+ this.push(dest, item, now);
1506
+ }
1507
+ /**
1508
+ * Deliver every eligible queued item. Concurrent callers share ONE
1509
+ * in-flight drain (it survives the caller's budget and a Lambda freeze),
1510
+ * so nothing in flight is ever sent twice; items queued while it runs are
1511
+ * picked up by the same drain.
1512
+ */
1513
+ flush() {
1514
+ if (!this.inFlight) {
1515
+ this.inFlight = this.drain().finally(() => {
1516
+ this.inFlight = void 0;
1517
+ });
1518
+ }
1519
+ return this.inFlight;
1520
+ }
1521
+ /** Counters for tests and diagnostics. */
1522
+ stats() {
1523
+ return {
1524
+ queueDepth: this.queue.length,
1525
+ droppedSinceLast: this.droppedSinceLast,
1526
+ droppedTotal: this.droppedTotal,
1527
+ deliveredTotal: this.deliveredTotal,
1528
+ flushing: this.inFlight !== void 0
1529
+ };
1530
+ }
1531
+ push(dest, item, eligibleAt) {
1532
+ while (this.queue.length >= this.opts.maxItems) this.dropOldest();
1533
+ const entry = {
1534
+ dest,
1535
+ destKey: destKeyOf(dest),
1536
+ item,
1537
+ attempts: 0,
1538
+ eligibleAt,
1539
+ inFlight: false
1540
+ };
1541
+ this.queue.push(entry);
1542
+ return entry;
1543
+ }
1544
+ dropOldest() {
1545
+ if (this.queue.shift()) this.countDropped(1);
1546
+ }
1547
+ countDropped(n) {
1548
+ this.droppedSinceLast += n;
1549
+ this.droppedTotal += n;
1550
+ }
1551
+ async drain() {
1552
+ for (; ; ) {
1553
+ const now = this.opts.now();
1554
+ if (now < this.backoffUntil) return;
1555
+ const batch = this.takeBatch(now);
1556
+ if (batch.length === 0) return;
1557
+ const ok = await this.send(batch);
1558
+ if (!ok) return;
1559
+ }
1560
+ }
1561
+ /** Oldest eligible item + up to maxBatch-1 more for the same destination. */
1562
+ takeBatch(now) {
1563
+ const first = this.queue.find((q) => q.eligibleAt <= now);
1564
+ if (!first) return [];
1565
+ const batch = [];
1566
+ const rest = [];
1567
+ for (const q of this.queue) {
1568
+ if (batch.length < this.opts.maxBatch && q.destKey === first.destKey && q.eligibleAt <= now) {
1569
+ q.inFlight = true;
1570
+ batch.push(q);
1571
+ } else {
1572
+ rest.push(q);
1573
+ }
1574
+ }
1575
+ this.queue = rest;
1576
+ return batch;
1577
+ }
1578
+ /** POST one batch. Returns false when the drain should pause (backoff). */
1579
+ async send(batch) {
1580
+ const dest = batch[0].dest;
1581
+ const useKey = dest.apiKey && !this.rejectedKeys.has(dest.apiKey) ? dest.apiKey : void 0;
1582
+ const reportedDropped = this.droppedSinceLast;
1583
+ const meta = {
1584
+ droppedSinceLast: reportedDropped,
1585
+ queueDepth: this.queue.length,
1586
+ adapter: dest.adapter ?? "edge-core",
1587
+ version: SDK_VERSION
1588
+ };
1589
+ const controller = typeof AbortController !== "undefined" ? new AbortController() : void 0;
1590
+ const response = await withBudget(
1591
+ sdkFetch(`${dest.apiBaseUrl}/verification-activity/edge-events`, {
1592
+ method: "POST",
1593
+ headers: {
1594
+ "Content-Type": "application/json",
1595
+ ...useKey && { Authorization: `Bearer ${useKey}` }
1596
+ },
1597
+ body: JSON.stringify({ events: batch.map((q) => q.item), meta }),
1598
+ ...controller && { signal: controller.signal }
1599
+ }),
1600
+ this.opts.requestTimeoutMs
1601
+ );
1602
+ if (!response) controller?.abort();
1603
+ const status = response?.status;
1604
+ for (const q of batch) q.inFlight = false;
1605
+ if (status !== void 0 && status >= 200 && status < 300) {
1606
+ this.droppedSinceLast = Math.max(0, this.droppedSinceLast - reportedDropped);
1607
+ this.deliveredTotal += batch.length;
1608
+ return true;
1609
+ }
1610
+ if (status === 401 && useKey) {
1611
+ this.rejectedKeys.add(useKey);
1612
+ this.requeue(batch, false);
1613
+ return true;
1614
+ }
1615
+ if (status === 429) {
1616
+ const retryAfter = Number(response?.headers.get("retry-after"));
1617
+ const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? Math.min(retryAfter, 120) * 1e3 : 3e4;
1618
+ this.backoffUntil = this.opts.now() + waitMs;
1619
+ this.requeue(batch, false);
1620
+ return false;
1621
+ }
1622
+ if (status !== void 0 && status >= 400 && status < 500) {
1623
+ this.countDropped(batch.length);
1624
+ return true;
1625
+ }
1626
+ this.backoffUntil = this.opts.now() + this.opts.retryBackoffMs;
1627
+ this.requeue(batch, true);
1628
+ return false;
1629
+ }
1630
+ /** Put a failed batch back at the head of the queue (order preserved). */
1631
+ requeue(batch, countAttempt) {
1632
+ const keep = [];
1633
+ for (const q of batch) {
1634
+ if (countAttempt) q.attempts += 1;
1635
+ if (q.attempts >= this.opts.maxAttempts) this.countDropped(1);
1636
+ else keep.push(q);
1637
+ }
1638
+ this.queue = [...keep, ...this.queue];
1639
+ while (this.queue.length > this.opts.maxItems) this.dropOldest();
1640
+ }
1641
+ };
1642
+ var beaconBuffer = new BeaconBuffer();
1291
1643
  function sampled(rate, random = Math.random) {
1292
1644
  if (rate <= 0) return false;
1293
1645
  if (rate >= 1) return true;
@@ -1905,6 +2257,20 @@ function createMiddleware(options) {
1905
2257
  }
1906
2258
  const approvalRequired = result.policyAllowed && requiresHumanApproval(result);
1907
2259
  if (approvalRequired) annotateApprovalRequired(result);
2260
+ if (!result.policyAllowed && isVerificationUnavailable(result)) {
2261
+ if (isAnonymousRead(request.method, credentials)) {
2262
+ const passThrough = NextResponse.next();
2263
+ passThrough.headers.set(VERIFICATION_STATUS_HEADER, "unavailable");
2264
+ return passThrough;
2265
+ }
2266
+ return NextResponse.json(buildVerificationUnavailableBody(result), {
2267
+ status: 503,
2268
+ headers: {
2269
+ "Retry-After": String(unavailableRetryAfterSeconds(result)),
2270
+ [VERIFICATION_STATUS_HEADER]: "unavailable"
2271
+ }
2272
+ });
2273
+ }
1908
2274
  if (!result.policyAllowed || approvalRequired) {
1909
2275
  if (result.misconfigured && pathname.startsWith("/api/")) {
1910
2276
  return NextResponse.json(
@@ -1922,13 +2288,12 @@ function createMiddleware(options) {
1922
2288
  );
1923
2289
  }
1924
2290
  if (pathname.startsWith("/api/")) {
2291
+ const denyStatus = denyStatusFor(result, "POLICY_DENIED");
1925
2292
  return NextResponse.json(
1926
2293
  {
1927
2294
  success: false,
1928
2295
  error: {
1929
- // Round-18 G4: 401 → identity missing (re-auth); 403 → identity
1930
- // OK, policy denied (update PDLSS / step up).
1931
- code: !result.identityVerified ? "UNAUTHORIZED" : "POLICY_DENIED",
2296
+ code: denyStatus.code,
1932
2297
  message: result.denialReasons?.[0] || "Access denied",
1933
2298
  guidance: result.guidance,
1934
2299
  failures: result.failures,
@@ -1938,7 +2303,7 @@ function createMiddleware(options) {
1938
2303
  }
1939
2304
  },
1940
2305
  {
1941
- status: !result.identityVerified ? 401 : 403,
2306
+ status: denyStatus.status,
1942
2307
  ...stepUpHoldOutcome && {
1943
2308
  headers: { "X-Astra-Step-Up": stepUpHoldOutcome }
1944
2309
  }