@devlab.group/agent-commerce 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,16 +1,15 @@
1
- import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-TBZA6Z5B.js';
2
- import { PROTOCOL_NAMES, CommerceError, PAYMENT_INPUT_FIELD, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, PAYMENT_HEADER, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-N3DNLXGC.js';
3
- export { COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RETRYABLE_ERROR_CODES, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-N3DNLXGC.js';
4
- import { promises, accessSync, constants, existsSync, mkdirSync, closeSync, openSync, chmodSync } from 'fs';
1
+ import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-7NKWFGBY.js';
2
+ import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-IUR3Q7DE.js';
3
+ import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-ZDVJ7EVL.js';
4
+ export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-ZDVJ7EVL.js';
5
+ import { promises, accessSync, constants } from 'fs';
5
6
  import * as path from 'path';
6
- import { dirname } from 'path';
7
7
  import { parse, YAMLError } from 'yaml';
8
8
  import { z } from 'zod';
9
9
  import { randomUUID, timingSafeEqual, createHash } from 'crypto';
10
10
  import { createRequire } from 'module';
11
11
  import pino from 'pino';
12
12
  import Fastify from 'fastify';
13
- import Database from 'better-sqlite3';
14
13
  import Ajv2020 from 'ajv/dist/2020.js';
15
14
  import addFormats from 'ajv-formats';
16
15
 
@@ -599,6 +598,92 @@ function createExecutionPipeline(options) {
599
598
  function buildEvent(partial) {
600
599
  return { id: options.ids.next("evt"), at: options.clock.nowIso(), ...partial };
601
600
  }
601
+ async function authorizeAndReserve(request, resource, providers, requirement, input) {
602
+ if (providers.length === 0) return void 0;
603
+ const submission = request.authorization;
604
+ const provider = providers.find((candidate) => candidate.name === submission?.method);
605
+ const unmet = providers.filter((candidate) => candidate !== provider).map((c) => c.name);
606
+ if (submission === void 0 || provider === void 0 || unmet.length > 0) {
607
+ await safeEmit(
608
+ buildEvent({
609
+ type: "authorization.rejected",
610
+ requestId: request.requestId,
611
+ resourceId: resource.id,
612
+ adapter: request.protocol,
613
+ status: "error",
614
+ data: { reason: "AUTHORIZATION_REQUIRED", methods: unmet }
615
+ })
616
+ );
617
+ throw new CommerceError(
618
+ "AUTHORIZATION_REQUIRED",
619
+ `Resource "${resource.id}" requires authorization: ${unmet.join(", ")}`,
620
+ {
621
+ requestId: request.requestId,
622
+ resourceId: resource.id,
623
+ details: { required: providers.map((candidate) => candidate.name), missing: unmet }
624
+ }
625
+ );
626
+ }
627
+ let verification;
628
+ try {
629
+ verification = await provider.verifyAndReserve({
630
+ requestId: request.requestId,
631
+ resourceId: resource.id,
632
+ input,
633
+ submission,
634
+ requirement
635
+ });
636
+ } catch (error) {
637
+ const mapped = isCommerceError(error) ? error : new CommerceError(
638
+ "AUTHORIZATION_PROVIDER_UNAVAILABLE",
639
+ "Authorization provider is unavailable",
640
+ {
641
+ requestId: request.requestId,
642
+ resourceId: resource.id,
643
+ cause: error
644
+ }
645
+ );
646
+ await safeEmit(
647
+ buildEvent({
648
+ type: "authorization.rejected",
649
+ requestId: request.requestId,
650
+ resourceId: resource.id,
651
+ adapter: request.protocol,
652
+ status: "error",
653
+ data: { reason: mapped.code, method: provider.name }
654
+ })
655
+ );
656
+ throw mapped;
657
+ }
658
+ await safeEmit(
659
+ buildEvent({
660
+ type: "authorization.verified",
661
+ requestId: request.requestId,
662
+ resourceId: resource.id,
663
+ adapter: request.protocol,
664
+ status: "ok",
665
+ data: { method: verification.method, reference: verification.reference }
666
+ })
667
+ );
668
+ const context = { requestId: request.requestId, resourceId: resource.id };
669
+ return {
670
+ record: {
671
+ method: verification.method,
672
+ reference: verification.reference,
673
+ ...verification.metadata !== void 0 ? { metadata: verification.metadata } : {}
674
+ },
675
+ async finalize(action) {
676
+ try {
677
+ await provider[action](verification.reservationId, context);
678
+ } catch (error) {
679
+ options.logger.error(
680
+ { err: describeError2(error), requestId: request.requestId, action },
681
+ "Authorization finalization failed; the reservation stays reserved"
682
+ );
683
+ }
684
+ }
685
+ };
686
+ }
602
687
  async function execute(request) {
603
688
  const pipelineStart = options.clock.monotonicMs();
604
689
  const resource = options.resources.get(request.resourceId);
@@ -620,7 +705,7 @@ function createExecutionPipeline(options) {
620
705
  adapter: request.protocol
621
706
  })
622
707
  );
623
- const strippedInput = stripPaymentField(request.input);
708
+ const strippedInput = stripReservedFields(request.input);
624
709
  const validation = getValidator(resource)(strippedInput);
625
710
  if (!validation.valid) {
626
711
  throw new CommerceError(
@@ -648,7 +733,18 @@ function createExecutionPipeline(options) {
648
733
  }
649
734
  );
650
735
  }
736
+ if (resource.pricing.type !== "fixed" && (resource.authorization?.required.length ?? 0) > 0) {
737
+ throw new CommerceError(
738
+ "CONFIG_INVALID",
739
+ `Resource "${resource.id}" requires authorization but is not a paid resource`,
740
+ {
741
+ requestId: request.requestId,
742
+ resourceId: resource.id
743
+ }
744
+ );
745
+ }
651
746
  let paymentResult;
747
+ let authorization;
652
748
  if (resource.pricing.type === "fixed") {
653
749
  const pricing = resource.pricing;
654
750
  const provider = pickProvider(options.paymentProviders, resource.paymentMethods);
@@ -663,6 +759,11 @@ function createExecutionPipeline(options) {
663
759
  }
664
760
  );
665
761
  }
762
+ const authProviders = resolveAuthorizationProviders(
763
+ options.authorizationProviders ?? [],
764
+ resource,
765
+ request.requestId
766
+ );
666
767
  let requirement;
667
768
  try {
668
769
  requirement = await provider.createRequirement({
@@ -693,7 +794,8 @@ function createExecutionPipeline(options) {
693
794
  kind: "payment-required",
694
795
  requestId: request.requestId,
695
796
  resourceId: resource.id,
696
- requirement
797
+ requirement,
798
+ ...authProviders.length > 0 ? { authorization: authProviders.map((p) => p.requirement) } : {}
697
799
  };
698
800
  }
699
801
  const payment = request.payment;
@@ -763,6 +865,14 @@ function createExecutionPipeline(options) {
763
865
  });
764
866
  }
765
867
  const replayKey = verification.replayKey;
868
+ const hold = await authorizeAndReserve(
869
+ request,
870
+ resource,
871
+ authProviders,
872
+ requirement,
873
+ validInput
874
+ );
875
+ authorization = hold?.record;
766
876
  try {
767
877
  await options.store.reservePaymentAttempt({
768
878
  requestId: request.requestId,
@@ -780,6 +890,7 @@ function createExecutionPipeline(options) {
780
890
  "STORAGE_ERROR",
781
891
  "Payment attempt could not be recorded"
782
892
  );
893
+ await hold?.finalize("release");
783
894
  await safeEmit(
784
895
  buildEvent({
785
896
  type: "payment.rejected",
@@ -814,6 +925,7 @@ function createExecutionPipeline(options) {
814
925
  });
815
926
  } catch (error) {
816
927
  const uncertainTxHash = uncertainSettlementTxHash(error);
928
+ await hold?.finalize(uncertainTxHash !== void 0 ? "markUncertain" : "release");
817
929
  await safePersist(
818
930
  () => options.store.updatePaymentAttempt(
819
931
  uncertainTxHash !== void 0 ? { replayKey, status: "settlement-uncertain", externalReference: uncertainTxHash } : {
@@ -848,6 +960,7 @@ function createExecutionPipeline(options) {
848
960
  );
849
961
  }
850
962
  if (settlement.status !== "settled") {
963
+ await hold?.finalize("release");
851
964
  await safePersist(
852
965
  () => options.store.updatePaymentAttempt({
853
966
  replayKey,
@@ -877,6 +990,7 @@ function createExecutionPipeline(options) {
877
990
  }
878
991
  );
879
992
  }
993
+ await hold?.finalize("consume");
880
994
  await safePersist(
881
995
  () => options.store.updatePaymentAttempt({
882
996
  replayKey,
@@ -928,6 +1042,7 @@ function createExecutionPipeline(options) {
928
1042
  backendStatus: backendErrorStatus(commerceError),
929
1043
  protocol: request.protocol,
930
1044
  payment: paymentResult,
1045
+ ...authorization !== void 0 ? { authorization } : {},
931
1046
  metadata: { delivered: false, backendErrorCode: commerceError.code }
932
1047
  };
933
1048
  await safePersist(
@@ -976,7 +1091,8 @@ function createExecutionPipeline(options) {
976
1091
  backendStatus: backendResponse.status,
977
1092
  durationMs: backendResponse.durationMs,
978
1093
  protocol: request.protocol,
979
- ...paymentResult !== void 0 ? { payment: paymentResult } : {}
1094
+ ...paymentResult !== void 0 ? { payment: paymentResult } : {},
1095
+ ...authorization !== void 0 ? { authorization } : {}
980
1096
  };
981
1097
  await safePersist(() => options.store.saveReceipt(receipt), "saveReceipt", request.requestId);
982
1098
  const totalDurationMs = Math.round(options.clock.monotonicMs() - pipelineStart);
@@ -1013,6 +1129,20 @@ function uncertainSettlementTxHash(error) {
1013
1129
  const hash = error.details?.["transactionHash"];
1014
1130
  return typeof hash === "string" ? hash : void 0;
1015
1131
  }
1132
+ function resolveAuthorizationProviders(providers, resource, requestId) {
1133
+ const required = resource.authorization?.required ?? [];
1134
+ return required.map((method) => {
1135
+ const found = providers.find((provider) => provider.name === method);
1136
+ if (!found) {
1137
+ throw new CommerceError(
1138
+ "CONFIG_INVALID",
1139
+ `Resource "${resource.id}" requires authorization method "${method}", which is not enabled`,
1140
+ { requestId, resourceId: resource.id }
1141
+ );
1142
+ }
1143
+ return found;
1144
+ });
1145
+ }
1016
1146
  function pickProvider(providers, methods) {
1017
1147
  for (const method of methods) {
1018
1148
  const found = providers.find((provider) => provider.name === method);
@@ -1020,8 +1150,8 @@ function pickProvider(providers, methods) {
1020
1150
  }
1021
1151
  return void 0;
1022
1152
  }
1023
- var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([PAYMENT_INPUT_FIELD, "__proto__"]);
1024
- function stripPaymentField(input) {
1153
+ var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
1154
+ function stripReservedFields(input) {
1025
1155
  if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
1026
1156
  const record = input;
1027
1157
  const hasReserved = [...RESERVED_INPUT_KEYS].some((key) => Object.hasOwn(record, key));
@@ -1234,7 +1364,8 @@ var ResourceEntrySchema = z.object({
1234
1364
  backend: BackendHandlerSchema,
1235
1365
  pricing: PricingSchema,
1236
1366
  expose: z.array(z.string().min(1)).min(1),
1237
- payments: z.array(z.string().min(1)).optional()
1367
+ payments: z.array(z.string().min(1)).optional(),
1368
+ authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
1238
1369
  }).strict();
1239
1370
  var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
1240
1371
  var FacilitatorAuthSchema = z.discriminatedUnion("type", [
@@ -1282,6 +1413,32 @@ var X402Schema = z.object({
1282
1413
  var PaymentsSchema = z.object({
1283
1414
  x402: X402Schema.optional()
1284
1415
  }).strict();
1416
+ var Ap2KeySchema = z.object({
1417
+ kid: z.string().min(1),
1418
+ jwk: z.record(z.string(), z.unknown())
1419
+ }).strict();
1420
+ var Ap2IssuerSchema = z.object({
1421
+ issuer: z.string().min(1),
1422
+ /**
1423
+ * Required, not defaulted. Without it, a mandate minted for another
1424
+ * merchant would verify here, and there is no value worth guessing for
1425
+ * something that decides that.
1426
+ */
1427
+ audience: z.string().min(1),
1428
+ keys: z.array(Ap2KeySchema).min(1)
1429
+ }).strict();
1430
+ var Ap2Schema = z.object({
1431
+ enabled: BooleanOrString,
1432
+ specVersion: z.string().min(1).optional(),
1433
+ mode: z.string().min(1).optional(),
1434
+ trust: z.object({
1435
+ mandateIssuers: z.array(Ap2IssuerSchema).optional(),
1436
+ checkoutIssuers: z.array(Ap2IssuerSchema).optional()
1437
+ }).strict().optional(),
1438
+ clockSkewSeconds: NumberOrString.optional(),
1439
+ replay: z.object({ path: z.string().min(1) }).strict().optional()
1440
+ }).strict();
1441
+ var AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
1285
1442
  var RawConfigSchema = z.object({
1286
1443
  version: z.literal(SUPPORTED_CONFIG_VERSION),
1287
1444
  merchant: MerchantSchema,
@@ -1289,7 +1446,8 @@ var RawConfigSchema = z.object({
1289
1446
  storage: StorageSchema,
1290
1447
  protocols: ProtocolsSchema,
1291
1448
  resources: ResourcesMapSchema,
1292
- payments: PaymentsSchema
1449
+ payments: PaymentsSchema,
1450
+ authorization: AuthorizationSchema.optional()
1293
1451
  }).strict();
1294
1452
  function parseConfig(raw, env) {
1295
1453
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -1390,6 +1548,7 @@ function toBoolean(value, path2) {
1390
1548
  }
1391
1549
  var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
1392
1550
  var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
1551
+ var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
1393
1552
  function normalise(raw) {
1394
1553
  const protocols = {
1395
1554
  http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
@@ -1452,8 +1611,12 @@ function normalise(raw) {
1452
1611
  ...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
1453
1612
  });
1454
1613
  }
1614
+ const ap2 = normaliseAp2(raw.authorization?.ap2);
1615
+ if (ap2?.enabled) {
1616
+ validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
1617
+ }
1455
1618
  const resources = Object.entries(raw.resources).map(
1456
- ([id, entry]) => normaliseResource(id, entry, protocols, x402)
1619
+ ([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
1457
1620
  );
1458
1621
  if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
1459
1622
  return {
@@ -1476,7 +1639,8 @@ function normalise(raw) {
1476
1639
  resources,
1477
1640
  payments: {
1478
1641
  ...x402 !== void 0 ? { x402 } : {}
1479
- }
1642
+ },
1643
+ ...ap2 !== void 0 ? { authorization: { ap2 } } : {}
1480
1644
  };
1481
1645
  }
1482
1646
  function validateMountPaths(protocols) {
@@ -1499,6 +1663,194 @@ function validateMountPaths(protocols) {
1499
1663
  }
1500
1664
  }
1501
1665
  }
1666
+ function ap2Invalid(path2, message, extra = {}) {
1667
+ return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1668
+ }
1669
+ function normaliseAp2(raw) {
1670
+ if (raw === void 0) return void 0;
1671
+ if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
1672
+ const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
1673
+ if (specVersion !== AP2_SPEC_VERSION) {
1674
+ throw ap2Invalid(
1675
+ "authorization.ap2.specVersion",
1676
+ `authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
1677
+ );
1678
+ }
1679
+ const mode = raw.mode ?? AP2_MODES[0];
1680
+ if (!AP2_MODES.includes(mode)) {
1681
+ throw ap2Invalid(
1682
+ "authorization.ap2.mode",
1683
+ `authorization.ap2.mode "${mode}" is not supported. Supported: ${AP2_MODES.join(", ")}. Autonomous mode needs open mandates, agent key binding and constraint evaluation, none of which this release implements.`
1684
+ );
1685
+ }
1686
+ if (raw.replay === void 0) {
1687
+ throw ap2Invalid(
1688
+ "authorization.ap2.replay",
1689
+ "authorization.ap2.replay is required when AP2 is enabled - it names the SQLite file recording which mandates have been spent, and without it a verified mandate could authorise a second settlement"
1690
+ );
1691
+ }
1692
+ const mandateIssuers = normaliseIssuers(
1693
+ raw.trust?.mandateIssuers,
1694
+ "authorization.ap2.trust.mandateIssuers"
1695
+ );
1696
+ const checkoutIssuers = normaliseIssuers(
1697
+ raw.trust?.checkoutIssuers,
1698
+ "authorization.ap2.trust.checkoutIssuers"
1699
+ );
1700
+ return {
1701
+ enabled: true,
1702
+ specVersion: AP2_SPEC_VERSION,
1703
+ mode,
1704
+ trust: { mandateIssuers, checkoutIssuers },
1705
+ clockSkewSeconds: toNumber(
1706
+ raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
1707
+ "authorization.ap2.clockSkewSeconds",
1708
+ { min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
1709
+ ),
1710
+ replay: { path: raw.replay.path }
1711
+ };
1712
+ }
1713
+ function normaliseIssuers(raw, path2) {
1714
+ if (raw === void 0 || raw.length === 0) {
1715
+ throw ap2Invalid(
1716
+ path2,
1717
+ `${path2} is required when AP2 is enabled and must name at least one issuer - trust is operator-configured and static, so an empty list means no mandate can ever verify`
1718
+ );
1719
+ }
1720
+ const seen = /* @__PURE__ */ new Set();
1721
+ return raw.map((entry, index) => {
1722
+ const entryPath = `${path2}.${index}`;
1723
+ if (seen.has(entry.issuer)) {
1724
+ throw ap2Invalid(
1725
+ `${entryPath}.issuer`,
1726
+ `${entryPath}.issuer "${entry.issuer}" is listed twice - key lookup resolves by issuer, so a second entry for the same one is silently unreachable. Merge the keys into one entry instead (which is also how a rotation overlaps an old and a new key).`,
1727
+ { issuer: entry.issuer }
1728
+ );
1729
+ }
1730
+ seen.add(entry.issuer);
1731
+ const kids = /* @__PURE__ */ new Set();
1732
+ const keys = entry.keys.map((key, keyIndex) => {
1733
+ const keyPath = `${entryPath}.keys.${keyIndex}`;
1734
+ if (kids.has(key.kid)) {
1735
+ throw ap2Invalid(
1736
+ `${keyPath}.kid`,
1737
+ `${keyPath}.kid "${key.kid}" is listed twice for issuer "${entry.issuer}" - a kid selects exactly one key, so which of the two verifies a signature would be undefined`,
1738
+ { issuer: entry.issuer, kid: key.kid }
1739
+ );
1740
+ }
1741
+ kids.add(key.kid);
1742
+ return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
1743
+ });
1744
+ return { issuer: entry.issuer, audience: entry.audience, keys };
1745
+ });
1746
+ }
1747
+ function validateJwk(jwk, kid, path2) {
1748
+ const fail = (detail) => {
1749
+ throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
1750
+ };
1751
+ for (const member of Object.keys(jwk)) {
1752
+ if (AP2_JWK_MEMBERS.includes(member)) continue;
1753
+ if (member === "d" || member === "k") {
1754
+ fail(
1755
+ `carries private key material ("${member}"). The gateway verifies signatures and never produces them; publish only the public half. Treat the pasted key as compromised and rotate it.`
1756
+ );
1757
+ }
1758
+ fail(
1759
+ `has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
1760
+ );
1761
+ }
1762
+ const value = (member) => {
1763
+ const raw = jwk[member];
1764
+ if (typeof raw !== "string" || raw.length === 0) {
1765
+ fail(`must give "${member}" as a non-empty string`);
1766
+ }
1767
+ return raw;
1768
+ };
1769
+ if (value("kty") !== AP2_KEY_TYPE) {
1770
+ fail(
1771
+ `must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
1772
+ );
1773
+ }
1774
+ if (value("crv") !== AP2_JWK_CURVE) {
1775
+ fail(
1776
+ `must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
1777
+ );
1778
+ }
1779
+ for (const coordinate of ["x", "y"]) {
1780
+ const encoded = value(coordinate);
1781
+ if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
1782
+ fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
1783
+ }
1784
+ if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
1785
+ fail(
1786
+ `coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
1787
+ );
1788
+ }
1789
+ }
1790
+ if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
1791
+ fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
1792
+ }
1793
+ if (jwk["use"] !== void 0 && value("use") !== "sig") {
1794
+ fail(`declares use "${value("use")}"; a verification key must be "sig"`);
1795
+ }
1796
+ if (jwk["kid"] !== void 0 && value("kid") !== kid) {
1797
+ fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
1798
+ }
1799
+ const normalised = {};
1800
+ for (const member of AP2_JWK_MEMBERS) {
1801
+ if (jwk[member] !== void 0) normalised[member] = value(member);
1802
+ }
1803
+ return normalised;
1804
+ }
1805
+ function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
1806
+ if (replayPath === ":memory:") return;
1807
+ const others = [["storage.receipts.path", receiptsPath]];
1808
+ if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
1809
+ for (const [otherPath, other] of others) {
1810
+ if (replayPath !== other) continue;
1811
+ throw ap2Invalid(
1812
+ "authorization.ap2.replay.path",
1813
+ `authorization.ap2.replay.path is the same file as ${otherPath} ("${replayPath}") - the AP2 replay store keeps its own schema and must not share a database with another store`
1814
+ );
1815
+ }
1816
+ }
1817
+ function normaliseResourceAuthorization(id, entry, pricing, ap2) {
1818
+ const required = entry.authorization?.required;
1819
+ if (required === void 0) return void 0;
1820
+ const path2 = `resources.${id}.authorization.required`;
1821
+ const methods = [];
1822
+ for (const method of required) {
1823
+ if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
1824
+ throw ap2Invalid(
1825
+ path2,
1826
+ `Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
1827
+ { resourceId: id, method }
1828
+ );
1829
+ }
1830
+ if (methods.includes(method)) {
1831
+ throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
1832
+ resourceId: id,
1833
+ method
1834
+ });
1835
+ }
1836
+ if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
1837
+ throw ap2Invalid(
1838
+ path2,
1839
+ `Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
1840
+ { resourceId: id, method }
1841
+ );
1842
+ }
1843
+ methods.push(method);
1844
+ }
1845
+ if (pricing.type !== "fixed") {
1846
+ throw ap2Invalid(
1847
+ path2,
1848
+ `Resource "${id}" requires authorization but its pricing is "${pricing.type}" - authorization proves a purchase was approved and never replaces payment, so it applies to fixed-price paid resources only in this release`,
1849
+ { resourceId: id }
1850
+ );
1851
+ }
1852
+ return { required: methods };
1853
+ }
1502
1854
  function acpInvalid(path2, message, extra = {}) {
1503
1855
  return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
1504
1856
  }
@@ -1638,20 +1990,23 @@ function validateAcpOperationInput(path2, operation, resource) {
1638
1990
  }
1639
1991
  }
1640
1992
  }
1641
- function normaliseResource(id, entry, protocols, x402) {
1993
+ function normaliseResource(id, entry, protocols, x402, ap2) {
1642
1994
  if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
1643
1995
  const inputProperties = entry.input?.["properties"];
1644
- if (inputProperties && typeof inputProperties === "object" && PAYMENT_INPUT_FIELD in inputProperties) {
1645
- throw new CommerceError(
1646
- "CONFIG_INVALID",
1647
- `Resource "${id}" declares an input property "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1648
- {
1649
- details: {
1650
- path: `resources.${id}.input.properties.${PAYMENT_INPUT_FIELD}`,
1651
- resourceId: id
1996
+ if (inputProperties && typeof inputProperties === "object") {
1997
+ for (const reserved of RESERVED_INPUT_FIELDS) {
1998
+ if (!(reserved in inputProperties)) continue;
1999
+ throw new CommerceError(
2000
+ "CONFIG_INVALID",
2001
+ `Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
2002
+ {
2003
+ details: {
2004
+ path: `resources.${id}.input.properties.${reserved}`,
2005
+ resourceId: id
2006
+ }
1652
2007
  }
1653
- }
1654
- );
2008
+ );
2009
+ }
1655
2010
  }
1656
2011
  for (const protocol of entry.expose) {
1657
2012
  if (!SUPPORTED_PROTOCOLS.has(protocol)) {
@@ -1740,6 +2095,7 @@ function normaliseResource(id, entry, protocols, x402) {
1740
2095
  }
1741
2096
  }
1742
2097
  const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
2098
+ const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
1743
2099
  return {
1744
2100
  id,
1745
2101
  name: entry.name,
@@ -1760,7 +2116,8 @@ function normaliseResource(id, entry, protocols, x402) {
1760
2116
  },
1761
2117
  pricing,
1762
2118
  exposedVia: entry.expose,
1763
- paymentMethods
2119
+ paymentMethods,
2120
+ ...authorization !== void 0 ? { authorization } : {}
1764
2121
  };
1765
2122
  }
1766
2123
  function defaultClosedObjectSchema(schema) {
@@ -1938,11 +2295,8 @@ function validateInputBindings(id, backend, input) {
1938
2295
  Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
1939
2296
  );
1940
2297
  for (const [location, property] of entries) {
1941
- if (property === PAYMENT_INPUT_FIELD) {
1942
- fail(
1943
- `binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
1944
- { location }
1945
- );
2298
+ if (RESERVED_INPUT_FIELDS.includes(property)) {
2299
+ fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
1946
2300
  }
1947
2301
  const other = seen.get(property);
1948
2302
  if (other !== void 0) {
@@ -2163,9 +2517,9 @@ var SECRET_FIELD_NAMES = [
2163
2517
  "adminToken",
2164
2518
  "token"
2165
2519
  ];
2520
+ var SECRET_HEADERS = ["authorization", PAYMENT_HEADER, AUTHORIZATION_HEADER];
2166
2521
  var REDACT_PATHS = [
2167
- "req.headers.authorization",
2168
- 'req.headers["payment-signature"]',
2522
+ ...SECRET_HEADERS.map((name) => `req.headers[${JSON.stringify(name)}]`),
2169
2523
  ...SECRET_FIELD_NAMES,
2170
2524
  ...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
2171
2525
  ];
@@ -2506,6 +2860,33 @@ var PAYMENT_PROVIDER_DETAIL = {
2506
2860
  warn: "payment-provider-degraded",
2507
2861
  fail: "payment-provider-unreachable"
2508
2862
  };
2863
+ var AUTHORIZATION_PROVIDER_DETAIL = {
2864
+ pass: void 0,
2865
+ warn: "authorization-provider-degraded",
2866
+ fail: "authorization-provider-unreachable"
2867
+ };
2868
+ async function probeProvider(provider, kind, details, options) {
2869
+ let health;
2870
+ try {
2871
+ health = await provider.health();
2872
+ } catch (error) {
2873
+ const message = error instanceof Error ? error.message : String(error);
2874
+ options.logger.error({ err: message, provider: provider.name }, `${kind} health() threw`);
2875
+ health = { status: "fail", checkedAt: options.clock.nowIso() };
2876
+ }
2877
+ if (health.detail !== void 0) {
2878
+ options.logger.debug(
2879
+ { provider: provider.name, detail: health.detail },
2880
+ `${kind} health detail (not sent to the client)`
2881
+ );
2882
+ }
2883
+ const detail = details[health.status];
2884
+ return {
2885
+ name: provider.name,
2886
+ status: health.status,
2887
+ ...detail !== void 0 ? { detail } : {}
2888
+ };
2889
+ }
2509
2890
  async function checkReadiness(options) {
2510
2891
  let storeHealth;
2511
2892
  let storeThrew = false;
@@ -2541,45 +2922,32 @@ async function checkReadiness(options) {
2541
2922
  })
2542
2923
  );
2543
2924
  const paymentProviderChecks = await Promise.all(
2544
- options.paymentProviders.map(async (provider) => {
2545
- let health;
2546
- try {
2547
- health = await provider.health();
2548
- } catch (error) {
2549
- const message = error instanceof Error ? error.message : String(error);
2550
- options.logger.error(
2551
- { err: message, provider: provider.name },
2552
- "payment provider health() threw"
2553
- );
2554
- health = { status: "fail", checkedAt: options.clock.nowIso() };
2555
- }
2556
- if (health.detail !== void 0) {
2557
- options.logger.debug(
2558
- { provider: provider.name, detail: health.detail },
2559
- "payment provider health detail (not sent to the client)"
2560
- );
2561
- }
2562
- const detail = PAYMENT_PROVIDER_DETAIL[health.status];
2563
- return {
2564
- name: provider.name,
2565
- status: health.status,
2566
- ...detail !== void 0 ? { detail } : {}
2567
- };
2568
- })
2925
+ options.paymentProviders.map(
2926
+ (provider) => probeProvider(provider, "payment provider", PAYMENT_PROVIDER_DETAIL, options)
2927
+ )
2928
+ );
2929
+ const authorizationProviderChecks = await Promise.all(
2930
+ options.authorizationProviders.map(
2931
+ (provider) => probeProvider(provider, "authorization provider", AUTHORIZATION_PROVIDER_DETAIL, options)
2932
+ )
2569
2933
  );
2570
2934
  const storeReady = storeHealth.status !== "fail";
2571
2935
  const adaptersReady = adapterChecks.every((check) => check.status !== "fail");
2572
2936
  const paymentProvidersReady = paymentProviderChecks.every((check) => check.status !== "fail");
2937
+ const authorizationProvidersReady = authorizationProviderChecks.every(
2938
+ (check) => check.status !== "fail"
2939
+ );
2573
2940
  const storeDetail = storeThrew ? "store-unreachable" : STORE_DETAIL[storeHealth.status];
2574
2941
  return {
2575
- ready: storeReady && adaptersReady && paymentProvidersReady,
2942
+ ready: storeReady && adaptersReady && paymentProvidersReady && authorizationProvidersReady,
2576
2943
  store: {
2577
2944
  name: "store",
2578
2945
  status: storeHealth.status,
2579
2946
  ...storeDetail !== void 0 ? { detail: storeDetail } : {}
2580
2947
  },
2581
2948
  adapters: adapterChecks,
2582
- paymentProviders: paymentProviderChecks
2949
+ paymentProviders: paymentProviderChecks,
2950
+ authorizationProviders: authorizationProviderChecks
2583
2951
  };
2584
2952
  }
2585
2953
  var READINESS_TTL_MS = 2e3;
@@ -2633,6 +3001,7 @@ async function buildWellKnownDocument(options) {
2633
3001
  protocols: publicProtocols(options.config.protocols),
2634
3002
  adapters,
2635
3003
  paymentProviders: options.paymentProviders.map((provider) => provider.descriptor),
3004
+ authorizationProviders: options.authorizationProviders.map((provider) => provider.descriptor),
2636
3005
  store: options.store.descriptor,
2637
3006
  payments: {
2638
3007
  ...x402 !== void 0 ? {
@@ -2672,6 +3041,7 @@ function registerRoutes(options) {
2672
3041
  store: options.store,
2673
3042
  adapterRuntimes: options.adapterRuntimes,
2674
3043
  paymentProviders: options.paymentProviders,
3044
+ authorizationProviders: options.authorizationProviders,
2675
3045
  clock: options.clock,
2676
3046
  logger: options.logger
2677
3047
  });
@@ -2686,6 +3056,7 @@ function registerRoutes(options) {
2686
3056
  async () => buildWellKnownDocument({
2687
3057
  config: options.config,
2688
3058
  paymentProviders: options.paymentProviders,
3059
+ authorizationProviders: options.authorizationProviders,
2689
3060
  store: options.store,
2690
3061
  adapterRuntimes: options.adapterRuntimes,
2691
3062
  clock: options.clock
@@ -2781,13 +3152,18 @@ async function handleInvoke(request, reply, options) {
2781
3152
  const paymentValue = Array.isArray(paymentHeader) ? paymentHeader[0] : paymentHeader;
2782
3153
  const paymentMethod = resource.paymentMethods[0];
2783
3154
  const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
3155
+ const authorization = parseAuthorizationHeader(
3156
+ request.headers[AUTHORIZATION_HEADER],
3157
+ request.id
3158
+ );
2784
3159
  const canonicalRequest = {
2785
3160
  requestId: request.id,
2786
3161
  resourceId,
2787
3162
  input: request.body ?? {},
2788
3163
  protocol: "http",
2789
3164
  receivedAt: options.clock.nowIso(),
2790
- ...payment !== void 0 ? { payment } : {}
3165
+ ...payment !== void 0 ? { payment } : {},
3166
+ ...authorization !== void 0 ? { authorization } : {}
2791
3167
  };
2792
3168
  const outcome = await options.pipeline.execute(canonicalRequest);
2793
3169
  if (outcome.kind === "payment-required") {
@@ -2873,11 +3249,13 @@ async function createGateway(options) {
2873
3249
  const ids = options.ids ?? createDefaultIdGenerator();
2874
3250
  const logger = options.logger ?? createGatewayLogger({ name: options.config.merchant.id }).core;
2875
3251
  const backend = options.backend ?? new HttpBackendExecutor({ logger });
3252
+ const authorizationProviders = options.authorizationProviders ?? [];
2876
3253
  const resources = createResourceRegistry(options.config.resources);
2877
3254
  const eventBus = createEventBus({ store: options.store, logger });
2878
3255
  const pipeline = createExecutionPipeline({
2879
3256
  resources,
2880
3257
  paymentProviders: options.paymentProviders,
3258
+ authorizationProviders,
2881
3259
  store: options.store,
2882
3260
  backend,
2883
3261
  events: eventBus,
@@ -2912,6 +3290,7 @@ async function createGateway(options) {
2912
3290
  resources,
2913
3291
  store: options.store,
2914
3292
  paymentProviders: options.paymentProviders,
3293
+ authorizationProviders,
2915
3294
  eventBus,
2916
3295
  clock,
2917
3296
  adapterRuntimes,
@@ -3087,52 +3466,6 @@ function canonicalize(value) {
3087
3466
  function sha256(value) {
3088
3467
  return createHash("sha256").update(value, "utf8").digest("hex");
3089
3468
  }
3090
- function openSqliteDatabase(options) {
3091
- const { path: path2, label, logger } = options;
3092
- const isFileBacked = path2 !== ":memory:";
3093
- if (isFileBacked) {
3094
- const dir = dirname(path2);
3095
- if (dir !== "" && dir !== "." && !existsSync(dir)) {
3096
- mkdirSync(dir, { recursive: true, mode: 448 });
3097
- }
3098
- }
3099
- if (isFileBacked && existsSync(path2)) {
3100
- try {
3101
- accessSync(path2, constants.W_OK);
3102
- } catch {
3103
- throw new CommerceError(
3104
- "STORAGE_ERROR",
3105
- `${label} "${path2}" exists but is not writable by this process (uid ${typeof process.getuid === "function" ? process.getuid() : "unknown"}). If this is a container that recently changed user, an existing volume may still be owned by the previous one - recreate it (docker compose down -v) or fix its ownership.`,
3106
- { details: { path: path2 } }
3107
- );
3108
- }
3109
- }
3110
- if (isFileBacked) {
3111
- try {
3112
- closeSync(openSync(path2, "a", 384));
3113
- } catch {
3114
- }
3115
- }
3116
- const db = new Database(path2);
3117
- db.pragma("journal_mode = WAL");
3118
- db.pragma("foreign_keys = ON");
3119
- if (isFileBacked) restrictDatabasePermissions(path2, label, logger);
3120
- return db;
3121
- }
3122
- function restrictDatabasePermissions(path2, label, logger) {
3123
- for (const file of [path2, `${path2}-wal`, `${path2}-shm`]) {
3124
- try {
3125
- chmodSync(file, 384);
3126
- } catch (error) {
3127
- const code = error.code;
3128
- if (code === "ENOENT") continue;
3129
- logger.warn(
3130
- { file, label, error: code ?? "unknown" },
3131
- "could not restrict database permissions to owner-only"
3132
- );
3133
- }
3134
- }
3135
- }
3136
3469
 
3137
3470
  // src/protocols/acp/idempotency/store.ts
3138
3471
  var ACP_MAX_IDEMPOTENCY_KEY_LENGTH = 255;
@@ -7975,12 +8308,14 @@ function receiptToRow(receipt) {
7975
8308
  backend_status: receipt.backendStatus,
7976
8309
  duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
7977
8310
  protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
7978
- metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null
8311
+ metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
8312
+ authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
7979
8313
  };
7980
8314
  }
7981
8315
  function rowToReceipt(row) {
7982
8316
  const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
7983
8317
  const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
8318
+ const authorization = row.authorization_json !== null ? JSON.parse(row.authorization_json) : void 0;
7984
8319
  return {
7985
8320
  id: row.id,
7986
8321
  requestId: row.request_id,
@@ -7990,7 +8325,8 @@ function rowToReceipt(row) {
7990
8325
  ...payment !== void 0 ? { payment } : {},
7991
8326
  ...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
7992
8327
  ...row.protocol !== null ? { protocol: row.protocol } : {},
7993
- ...metadata !== void 0 ? { metadata } : {}
8328
+ ...metadata !== void 0 ? { metadata } : {},
8329
+ ...authorization !== void 0 ? { authorization } : {}
7994
8330
  };
7995
8331
  }
7996
8332
  function eventToRow(event) {
@@ -8099,6 +8435,12 @@ var MIGRATIONS = [
8099
8435
  CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
8100
8436
  `);
8101
8437
  }
8438
+ },
8439
+ {
8440
+ version: 2,
8441
+ up(db) {
8442
+ db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
8443
+ }
8102
8444
  }
8103
8445
  ];
8104
8446
  var SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
@@ -8136,8 +8478,8 @@ function createSqliteReceiptStore(options) {
8136
8478
  const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
8137
8479
  migrate2(db);
8138
8480
  const insertReceiptStmt = db.prepare(
8139
- `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json)
8140
- VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json)`
8481
+ `INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
8482
+ VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
8141
8483
  );
8142
8484
  const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
8143
8485
  const listReceiptsStmt = db.prepare(