@astrasyncai/verification-gateway 5.18.1 → 5.20.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 (129) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/README.md +217 -4
  3. package/dist/adapter-interface/interface.d.mts +2 -2
  4. package/dist/adapter-interface/interface.d.ts +2 -2
  5. package/dist/adapters/express.d.mts +9 -1
  6. package/dist/adapters/express.d.ts +9 -1
  7. package/dist/adapters/express.js +160 -52
  8. package/dist/adapters/express.js.map +1 -1
  9. package/dist/adapters/express.mjs +160 -52
  10. package/dist/adapters/express.mjs.map +1 -1
  11. package/dist/adapters/http-pdlss.d.mts +7 -1
  12. package/dist/adapters/http-pdlss.d.ts +7 -1
  13. package/dist/adapters/http-pdlss.js +18 -2
  14. package/dist/adapters/http-pdlss.js.map +1 -1
  15. package/dist/adapters/http-pdlss.mjs +16 -1
  16. package/dist/adapters/http-pdlss.mjs.map +1 -1
  17. package/dist/adapters/kit-express.d.mts +93 -1
  18. package/dist/adapters/kit-express.d.ts +93 -1
  19. package/dist/adapters/kit-express.js +181 -2
  20. package/dist/adapters/kit-express.js.map +1 -1
  21. package/dist/adapters/kit-express.mjs +180 -2
  22. package/dist/adapters/kit-express.mjs.map +1 -1
  23. package/dist/adapters/mcp.d.mts +102 -9
  24. package/dist/adapters/mcp.d.ts +102 -9
  25. package/dist/adapters/mcp.js +242 -19
  26. package/dist/adapters/mcp.js.map +1 -1
  27. package/dist/adapters/mcp.mjs +240 -19
  28. package/dist/adapters/mcp.mjs.map +1 -1
  29. package/dist/adapters/nextjs.d.mts +1 -1
  30. package/dist/adapters/nextjs.d.ts +1 -1
  31. package/dist/adapters/nextjs.js +97 -14
  32. package/dist/adapters/nextjs.js.map +1 -1
  33. package/dist/adapters/nextjs.mjs +97 -14
  34. package/dist/adapters/nextjs.mjs.map +1 -1
  35. package/dist/adapters/sdk.d.mts +101 -2
  36. package/dist/adapters/sdk.d.ts +101 -2
  37. package/dist/adapters/sdk.js +199 -12
  38. package/dist/adapters/sdk.js.map +1 -1
  39. package/dist/adapters/sdk.mjs +199 -12
  40. package/dist/adapters/sdk.mjs.map +1 -1
  41. package/dist/agent/index.d.mts +1 -1
  42. package/dist/agent/index.d.ts +1 -1
  43. package/dist/agent/index.js +1 -1
  44. package/dist/agent/index.js.map +1 -1
  45. package/dist/agent/index.mjs +1 -1
  46. package/dist/agent/index.mjs.map +1 -1
  47. package/dist/bin/astrasync-claude-hook.js +94 -16
  48. package/dist/bin/astrasync-codex-hook.js +94 -16
  49. package/dist/bin/astrasync-guard.js +94 -16
  50. package/dist/bin/astrasync.js +127 -16
  51. package/dist/browser/background.js +94 -16
  52. package/dist/browser/background.js.map +1 -1
  53. package/dist/browser/background.mjs +94 -16
  54. package/dist/browser/background.mjs.map +1 -1
  55. package/dist/cli/index.js +1 -1
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/index.mjs +1 -1
  58. package/dist/cli/index.mjs.map +1 -1
  59. package/dist/codex/index.js +94 -16
  60. package/dist/codex/index.js.map +1 -1
  61. package/dist/codex/index.mjs +94 -16
  62. package/dist/codex/index.mjs.map +1 -1
  63. package/dist/cursor/extension.js +94 -16
  64. package/dist/cursor/extension.js.map +1 -1
  65. package/dist/cursor/extension.mjs +94 -16
  66. package/dist/cursor/extension.mjs.map +1 -1
  67. package/dist/edge-config.d.mts +1 -1
  68. package/dist/edge-config.d.ts +1 -1
  69. package/dist/edge-config.js +1 -1
  70. package/dist/edge-config.js.map +1 -1
  71. package/dist/edge-config.mjs +1 -1
  72. package/dist/edge-config.mjs.map +1 -1
  73. package/dist/edge-core/index.d.mts +1 -1
  74. package/dist/edge-core/index.d.ts +1 -1
  75. package/dist/edge-core/index.js +69 -12
  76. package/dist/edge-core/index.js.map +1 -1
  77. package/dist/edge-core/index.mjs +69 -12
  78. package/dist/edge-core/index.mjs.map +1 -1
  79. package/dist/gateway/gateway.d.mts +2 -2
  80. package/dist/gateway/gateway.d.ts +2 -2
  81. package/dist/gateway/gateway.js +94 -16
  82. package/dist/gateway/gateway.js.map +1 -1
  83. package/dist/gateway/gateway.mjs +94 -16
  84. package/dist/gateway/gateway.mjs.map +1 -1
  85. package/dist/git-trigger/git-hooks.d.mts +1 -1
  86. package/dist/git-trigger/git-hooks.d.ts +1 -1
  87. package/dist/index.d.mts +706 -34
  88. package/dist/index.d.ts +706 -34
  89. package/dist/index.js +1391 -205
  90. package/dist/index.js.map +1 -1
  91. package/dist/index.mjs +1374 -195
  92. package/dist/index.mjs.map +1 -1
  93. package/dist/local-evaluator/evaluator.d.mts +2 -2
  94. package/dist/local-evaluator/evaluator.d.ts +2 -2
  95. package/dist/registration/index.d.mts +11 -2
  96. package/dist/registration/index.d.ts +11 -2
  97. package/dist/registration/index.js +1 -1
  98. package/dist/registration/index.js.map +1 -1
  99. package/dist/registration/index.mjs +1 -1
  100. package/dist/registration/index.mjs.map +1 -1
  101. package/dist/transport/index.d.mts +1 -1
  102. package/dist/transport/index.d.ts +1 -1
  103. package/dist/transport/index.js +3 -2
  104. package/dist/transport/index.js.map +1 -1
  105. package/dist/transport/index.mjs +3 -2
  106. package/dist/transport/index.mjs.map +1 -1
  107. package/dist/{types-BRz2U0Pn.d.ts → types-B_HLhZ9n.d.ts} +1 -1
  108. package/dist/{types-BfILnheI.d.mts → types-CU17R4Nr.d.mts} +11 -0
  109. package/dist/{types-BfILnheI.d.ts → types-CU17R4Nr.d.ts} +11 -0
  110. package/dist/{types-Cns4l8gX.d.mts → types-CxjqmPl-.d.mts} +115 -13
  111. package/dist/{types-Cns4l8gX.d.ts → types-CxjqmPl-.d.ts} +115 -13
  112. package/dist/{types-3ObBpsnf.d.mts → types-D37aSQ9c.d.mts} +106 -3
  113. package/dist/{types-CfBpm3w5.d.mts → types-Djpz-L0w.d.mts} +1 -1
  114. package/dist/{types-BoxZEYPS.d.ts → types-Dn8tXnei.d.ts} +106 -3
  115. package/dist/ui/index.d.mts +1 -1
  116. package/dist/ui/index.d.ts +1 -1
  117. package/dist/verify.d.mts +50 -4
  118. package/dist/verify.d.ts +50 -4
  119. package/dist/verify.js +93 -14
  120. package/dist/verify.js.map +1 -1
  121. package/dist/verify.mjs +91 -14
  122. package/dist/verify.mjs.map +1 -1
  123. package/dist/webhooks.d.mts +16 -3
  124. package/dist/webhooks.d.ts +16 -3
  125. package/dist/webhooks.js +24 -13
  126. package/dist/webhooks.js.map +1 -1
  127. package/dist/webhooks.mjs +24 -13
  128. package/dist/webhooks.mjs.map +1 -1
  129. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "5.18.1";
2
+ var SDK_VERSION = "5.20.0";
3
3
 
4
4
  // src/http.ts
5
5
  var SDK_USER_AGENT = `astrasync-sdk/${SDK_VERSION}`;
@@ -177,7 +177,12 @@ function getCacheKey(request, counterpartyId) {
177
177
  // its start_checkout quote was served the cached quote-leg grant (no
178
178
  // settlementOutcome) and never reached the backend. Same F-A1-07 bug class.
179
179
  request.commercePhase || "",
180
- request.checkoutSessionId || ""
180
+ request.checkoutSessionId || "",
181
+ // 5.19.0: a route_config purpose claim is checked server-side against
182
+ // the stored rule for this method + path, so it changes the verdict.
183
+ request.purposeSource || "",
184
+ request.requestMethod || "",
185
+ request.requestPath || ""
181
186
  ].join("|");
182
187
  }
183
188
  function getCachedResult(request, counterpartyId) {
@@ -202,6 +207,7 @@ function cacheResult(request, result, configuredTtl, counterpartyId) {
202
207
  }
203
208
  function clearCache() {
204
209
  verificationCache.clear();
210
+ inflightVerifies.clear();
205
211
  }
206
212
  function mcpGuidance(urls) {
207
213
  if (!urls?.mcpEndpoint) return void 0;
@@ -338,6 +344,9 @@ async function callVerifyAccessAPI(config, request) {
338
344
  if (requestData.enableRuntimeChallenge)
339
345
  body.enableRuntimeChallenge = requestData.enableRuntimeChallenge;
340
346
  if (requestData.routePolicy) body.routePolicy = requestData.routePolicy;
347
+ if (config.actingProof?.connectorToken) {
348
+ body.actingProof = { connectorToken: config.actingProof.connectorToken };
349
+ }
341
350
  if (requestData.approvalHeldByRoute) body.approvalHeldByRoute = true;
342
351
  if (requestData.createSession) body.createSession = requestData.createSession;
343
352
  if (requestData.durationRequired) body.durationRequired = requestData.durationRequired;
@@ -354,6 +363,11 @@ async function callVerifyAccessAPI(config, request) {
354
363
  if (requestData.buyerEmail) body.buyerEmail = requestData.buyerEmail;
355
364
  if (requestData.commerceArtifacts) body.commerceArtifacts = requestData.commerceArtifacts;
356
365
  if (requestData.attemptId) body.attemptId = requestData.attemptId;
366
+ if (requestData.purposeSource) body.purposeSource = requestData.purposeSource;
367
+ if (requestData.agentArtifacts) body.agentArtifacts = requestData.agentArtifacts;
368
+ if (requestData.metadata) body.metadata = requestData.metadata;
369
+ if (requestData.requestMethod) body.requestMethod = requestData.requestMethod;
370
+ if (requestData.requestPath) body.requestPath = requestData.requestPath;
357
371
  if (requestData.considerationSet) body.considerationSet = requestData.considerationSet;
358
372
  body.sdkVersion = SDK_VERSION;
359
373
  if (requestData.callerMetadata || requestData.clientIp || requestData.userAgent) {
@@ -500,22 +514,43 @@ async function verify(config, request, options) {
500
514
  "[VerificationGateway] minTrustScore / minTrustScoreForFull are deprecated in v2.3.0 and have no effect. Server is now the single source of truth for access decisions (verified ID + runtime challenge + PDLSS + trust score). To gate access to an endpoint, configure the endpoint's trust_score_requirement server-side."
501
515
  );
502
516
  }
503
- const uncacheable = Boolean(request.commerceArtifacts);
517
+ const uncacheable = Boolean(request.commerceArtifacts) || Boolean(mergedConfig.actingProof);
504
518
  if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
505
519
  const cached = getCachedResult(request, mergedConfig.counterpartyId);
506
520
  if (cached) {
507
521
  if (mergedConfig.debug) {
508
522
  console.log("[VerificationGateway] Returning cached result");
509
523
  }
510
- const hit = { ...cached };
511
- if (request.attemptId) {
512
- hit.attemptId = request.attemptId;
513
- } else {
514
- delete hit.attemptId;
515
- }
516
- return hit;
524
+ return sharedVerdict(cached, request);
517
525
  }
518
526
  }
527
+ if (mergedConfig.cacheTtl !== 0 && !options?.bypassCache && !uncacheable) {
528
+ const key = getCacheKey(request, mergedConfig.counterpartyId);
529
+ const pending = inflightVerifies.get(key);
530
+ if (pending) return sharedVerdict(await pending, request);
531
+ const flight = verifyViaApi(mergedConfig, request, urls, uncacheable);
532
+ inflightVerifies.set(key, flight);
533
+ try {
534
+ return await flight;
535
+ } finally {
536
+ inflightVerifies.delete(key);
537
+ }
538
+ }
539
+ return verifyViaApi(mergedConfig, request, urls, uncacheable);
540
+ }
541
+ var inflightVerifies = /* @__PURE__ */ new Map();
542
+ var sharedVerdicts = /* @__PURE__ */ new WeakSet();
543
+ function sharedVerdict(source, request) {
544
+ const hit = { ...source };
545
+ if (request.attemptId) {
546
+ hit.attemptId = request.attemptId;
547
+ } else {
548
+ delete hit.attemptId;
549
+ }
550
+ sharedVerdicts.add(hit);
551
+ return hit;
552
+ }
553
+ async function verifyViaApi(mergedConfig, request, urls, uncacheable) {
519
554
  const enrichedRequest = { ...request };
520
555
  if (!enrichedRequest.counterpartyUrl && mergedConfig.counterpartyUrl) {
521
556
  enrichedRequest.counterpartyUrl = mergedConfig.counterpartyUrl;
@@ -543,9 +578,18 @@ async function verify(config, request, options) {
543
578
  if (!apiResponse.access?.allowed) {
544
579
  const aggregatedFailures = apiResponse.access?.failures;
545
580
  const idVerifiedFromBackend = apiResponse.verificationContext?.idVerified === true && !aggregatedFailures?.some((f) => REJECTED_IDENTITY_DIMENSIONS.has(f.dimension));
581
+ const denyAgent = idVerifiedFromBackend && apiResponse.agent ? {
582
+ astraId: apiResponse.agent.astraId,
583
+ name: apiResponse.agent.name,
584
+ trustScore: apiResponse.agent.trustScore,
585
+ trustLevel: getTrustLevel(apiResponse.agent.trustScore),
586
+ blockchainAnchored: apiResponse.agent.blockchainStatus === "anchored",
587
+ status: apiResponse.agent.agentStatus
588
+ } : void 0;
546
589
  const result2 = {
547
590
  identityVerified: idVerifiedFromBackend,
548
591
  policyAllowed: false,
592
+ ...denyAgent && { agent: denyAgent },
549
593
  // 3.6.0: the server now emits top-level denialReasons on every deny
550
594
  // path — prefer it verbatim; the failure-message reconstruction remains
551
595
  // as the fallback for older backends.
@@ -585,7 +629,9 @@ async function verify(config, request, options) {
585
629
  fulfilment: apiResponse.fulfilment,
586
630
  mandates: apiResponse.mandates,
587
631
  kyaToken: apiResponse.kyaToken,
588
- aep: apiResponse.aep
632
+ aep: apiResponse.aep,
633
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
634
+ warnings: apiResponse.warnings
589
635
  };
590
636
  return result2;
591
637
  }
@@ -653,7 +699,9 @@ async function verify(config, request, options) {
653
699
  // 5.15.0: merchant-lane AP2 mandates + KYAPay identity token.
654
700
  mandates: apiResponse.mandates,
655
701
  kyaToken: apiResponse.kyaToken,
656
- aep: apiResponse.aep
702
+ aep: apiResponse.aep,
703
+ // 5.20.0 (round 29.1): non-fatal integration warnings (e.g. events_url_missing).
704
+ warnings: apiResponse.warnings
657
705
  };
658
706
  if (result.recommendation === "deny") {
659
707
  result.policyAllowed = false;
@@ -679,8 +727,18 @@ async function verify(config, request, options) {
679
727
  cacheResult(request, result, mergedConfig.cacheTtl, mergedConfig.counterpartyId);
680
728
  }
681
729
  }
730
+ notifyVerificationListeners(request, result);
682
731
  return result;
683
732
  }
733
+ var verificationListeners = /* @__PURE__ */ new Set();
734
+ function notifyVerificationListeners(request, result) {
735
+ for (const listener of verificationListeners) {
736
+ try {
737
+ listener(result, request);
738
+ } catch {
739
+ }
740
+ }
741
+ }
684
742
  async function quickVerify(config, credentials) {
685
743
  const result = await verify(config, {
686
744
  credentials,
@@ -716,6 +774,7 @@ var SettlementReportError = class extends AstraSyncError {
716
774
  };
717
775
 
718
776
  // src/adapters/sdk.ts
777
+ var warnedPhaselessValue = false;
719
778
  var VerificationGatewayClient = class {
720
779
  constructor(options) {
721
780
  this.config = {
@@ -747,9 +806,16 @@ var VerificationGatewayClient = class {
747
806
  apiKey: options.apiKey,
748
807
  jwt: options.jwt
749
808
  };
809
+ if (options.transactionValue && !options.commercePhase && !warnedPhaselessValue) {
810
+ warnedPhaselessValue = true;
811
+ console.warn(
812
+ "[astrasync] verify() with a transactionValue and no commercePhase is a purchase leg: it records a pending order and redeems a matching owner approval. Use quoteCheckout() for a price / limit check (commercePhase 'quote') and confirmCheckout() for the purchase. The default becomes 'quote' in 6.0."
813
+ );
814
+ }
750
815
  return this.executeWithRetry(
751
816
  () => verify(this.config, {
752
817
  credentials,
818
+ ...options.commercePhase && { commercePhase: options.commercePhase },
753
819
  purpose: options.purpose,
754
820
  action: options.action,
755
821
  resourceType: options.resourceType,
@@ -765,6 +831,43 @@ var VerificationGatewayClient = class {
765
831
  })
766
832
  );
767
833
  }
834
+ /**
835
+ * 5.19.0: the QUOTE leg of a checkout — a price / limit check before the
836
+ * purchase. Sends `commercePhase: 'quote'`: verify-access evaluates the
837
+ * agent's policy at this value and tells you whether a purchase would be
838
+ * granted, held for the owner's approval, or denied, but records no order,
839
+ * moves no money and never redeems an approval. Always reaches the backend
840
+ * (no verdict cache).
841
+ *
842
+ * Quotes count toward limit-probing detection: a run of rising quotes is
843
+ * how an agent (or a merchant) maps a spending limit. Quote what the buyer
844
+ * asked for, not a ladder of values.
845
+ */
846
+ async quoteCheckout(options) {
847
+ const credentials = {
848
+ astraId: options.astraId,
849
+ apiKey: options.apiKey,
850
+ jwt: options.jwt
851
+ };
852
+ return this.executeWithRetry(
853
+ () => verify(
854
+ this.config,
855
+ {
856
+ credentials,
857
+ purpose: options.purpose ?? "shopping",
858
+ action: options.action ?? "shopping.purchase",
859
+ transactionValue: options.transactionValue,
860
+ currency: options.currency,
861
+ commercePhase: "quote",
862
+ ...options.checkoutSessionId && { checkoutSessionId: options.checkoutSessionId },
863
+ ...options.attemptId && { attemptId: options.attemptId },
864
+ counterpartyUrl: options.counterpartyUrl,
865
+ counterpartyType: options.counterpartyType
866
+ },
867
+ { bypassCache: true }
868
+ )
869
+ );
870
+ }
768
871
  /**
769
872
  * 5.4.0 (astra-pay): confirm-leg of a checkout — the ONLY call that may fire
770
873
  * first-party charge-at-redeem (server-side settlement on the owner's saved
@@ -803,6 +906,14 @@ var VerificationGatewayClient = class {
803
906
  * is matched to — and only to — this confirm. A held cart's approval is
804
907
  * bound to its checkout session and exact value; a confirm for any other
805
908
  * cart is held for its own approval.
909
+ *
910
+ * 5.19.0 checkout kits: pass the kit's `agentArtifacts` (the inbound agent
911
+ * request — signed RFC 9421 headers with the public URL and raw-body
912
+ * digest, or `{ unsigned: true }`) and `metadata` (`{ kitSessionId,
913
+ * protocol }`) from the `verifyPurchase` context. The platform attributes
914
+ * the confirm to the agent from the artifacts, and stores the metadata on
915
+ * any approval this confirm creates so the approval event
916
+ * (`handleApprovalEvents`) can find the kit session.
806
917
  */
807
918
  async confirmCheckout(options) {
808
919
  const credentials = {
@@ -825,6 +936,8 @@ var VerificationGatewayClient = class {
825
936
  ...options.checkoutJwt && { checkoutJwt: options.checkoutJwt },
826
937
  ...options.attemptId && { attemptId: options.attemptId },
827
938
  buyerEmail: options.buyerEmail,
939
+ ...options.agentArtifacts && { agentArtifacts: options.agentArtifacts },
940
+ ...options.metadata && { metadata: options.metadata },
828
941
  counterpartyUrl: options.counterpartyUrl,
829
942
  counterpartyType: options.counterpartyType
830
943
  },
@@ -885,6 +998,80 @@ var VerificationGatewayClient = class {
885
998
  }
886
999
  return json.data;
887
1000
  }
1001
+ /**
1002
+ * 5.20.0 (round 29.1): change an endpoint's OPERATIONAL URLs with this
1003
+ * client's API key — `PATCH /api/endpoints/:id`. Accepts the key of the
1004
+ * endpoint's owner, or a personal key of an individual with an active seat
1005
+ * in the owning organisation. Policy (rules, trust floor, challenge, edge
1006
+ * config) stays dashboard-only. `null` clears a field. The owner is alerted
1007
+ * when `eventsUrl` changes.
1008
+ *
1009
+ * @param endpointId the ASTRAE id (defaults to the client's `counterpartyId`)
1010
+ * @returns the endpoint as stored, plus `changed` (the fields that changed)
1011
+ * @throws AstraSyncError — 400 `UNKNOWN_FIELDS` / validation, 401/403, 404
1012
+ */
1013
+ async updateEndpoint(endpointId, patch) {
1014
+ return this.endpointCall("PATCH", endpointId, "", patch);
1015
+ }
1016
+ /**
1017
+ * 5.20.0 (round 29.1): ask AstraSync to send ONE signed step-up event to the
1018
+ * endpoint's Events URL now — `POST /api/endpoints/:id/events/test`.
1019
+ * Delivered synchronously, outside the event outbox (random `eventId`, no
1020
+ * retries), signed with the endpoint's webhook secret like a real event.
1021
+ * Without `approvalId` the payload is synthetic (a kit acknowledges and
1022
+ * ignores it). With the id of a held kit approval at this endpoint the
1023
+ * payload is the real one, so your resume path runs: `approved` makes the
1024
+ * kit re-confirm (held again — nothing is charged); `denied` / `expired` /
1025
+ * `lapsed` release the session.
1026
+ *
1027
+ * Resolves even when your endpoint refused the event (`delivered: false`,
1028
+ * with `responseCode` / `error`).
1029
+ */
1030
+ async sendTestEvent(endpointId, options) {
1031
+ return this.endpointCall("POST", endpointId, "/events/test", options);
1032
+ }
1033
+ async endpointCall(method, endpointId, suffix, body) {
1034
+ const id = endpointId ?? this.config.counterpartyId;
1035
+ if (!id) {
1036
+ throw new AstraSyncError(
1037
+ "An endpoint id (ASTRAE-\u2026) is required: pass it, or set counterpartyId on the client",
1038
+ 400,
1039
+ "VALIDATION"
1040
+ );
1041
+ }
1042
+ if (!this.config.apiKey) {
1043
+ throw new AstraSyncError("This call needs the client apiKey", 401, "AUTH_REQUIRED");
1044
+ }
1045
+ const base = this.config.apiBaseUrl.replace(/\/+$/, "");
1046
+ const url = `${base}/endpoints/${encodeURIComponent(id)}${suffix}`;
1047
+ let res;
1048
+ try {
1049
+ res = await sdkFetch(url, {
1050
+ method,
1051
+ headers: {
1052
+ "Content-Type": "application/json",
1053
+ Authorization: `Bearer ${this.config.apiKey}`,
1054
+ "X-API-Key": this.config.apiKey
1055
+ },
1056
+ body: JSON.stringify(body),
1057
+ signal: AbortSignal.timeout(Math.max(this.timeout, 15e3))
1058
+ });
1059
+ } catch (err) {
1060
+ throw new AstraSyncError(
1061
+ `${method} ${suffix || "/endpoints/:id"} failed: ${err instanceof Error ? err.message : String(err)}`,
1062
+ 0,
1063
+ "network_error"
1064
+ );
1065
+ }
1066
+ const json = await res.json().catch(() => ({}));
1067
+ if (!res.ok || json.success === false || json.data === void 0) {
1068
+ const errObj = typeof json.error === "object" ? json.error : void 0;
1069
+ const code = json.code ?? errObj?.code ?? (typeof json.error === "string" ? json.error : "unknown_error");
1070
+ const message = json.message ?? errObj?.message ?? `HTTP ${res.status}`;
1071
+ throw new AstraSyncError(`${message} (${code})`, res.status, code);
1072
+ }
1073
+ return json.data;
1074
+ }
888
1075
  /**
889
1076
  * Quick verification — checks credentials and policy in one call.
890
1077
  *