@devlab.group/agent-commerce 1.3.0 → 1.4.1
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/README.md +96 -67
- package/dist/ap2.d.ts +164 -0
- package/dist/ap2.js +669 -0
- package/dist/ap2.js.map +1 -0
- package/dist/authorization-CPDcpJVa.d.ts +123 -0
- package/dist/chunk-GF6KHHKL.js +68 -0
- package/dist/chunk-GF6KHHKL.js.map +1 -0
- package/dist/{chunk-TBZA6Z5B.js → chunk-NWIQLJNU.js} +3 -3
- package/dist/{chunk-TBZA6Z5B.js.map → chunk-NWIQLJNU.js.map} +1 -1
- package/dist/{chunk-N3DNLXGC.js → chunk-ZEGFLRGV.js} +149 -66
- package/dist/chunk-ZEGFLRGV.js.map +1 -0
- package/dist/cli/index.js +419 -53
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +126 -8
- package/dist/index.js +622 -188
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -2
- package/dist/mcp.js +10 -12
- package/dist/mcp.js.map +1 -1
- package/dist/{protocol-adapter-DTFGWGXq.d.ts → protocol-adapter-Bebmggf_.d.ts} +42 -11
- package/dist/{runtime-Plk5Peqi.d.ts → runtime-CKq3d_3K.d.ts} +22 -2
- package/dist/types-CJtwLu1c.d.ts +99 -0
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +17 -7
- package/dist/x402.js.map +1 -1
- package/package.json +36 -7
- package/dist/chunk-N3DNLXGC.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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-GF6KHHKL.js';
|
|
2
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-NWIQLJNU.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-ZEGFLRGV.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-ZEGFLRGV.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
|
|
|
@@ -86,6 +85,7 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
|
86
85
|
|
|
87
86
|
// src/core/execution/backend-http.ts
|
|
88
87
|
var MAX_BODY_SNIPPET_LENGTH = 512;
|
|
88
|
+
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
89
89
|
var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
90
90
|
var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
|
|
91
91
|
var HttpBackendExecutor = class {
|
|
@@ -127,6 +127,10 @@ var HttpBackendExecutor = class {
|
|
|
127
127
|
if (error instanceof CommerceError) throw error;
|
|
128
128
|
}
|
|
129
129
|
const headers = { ...handler.headers ?? {} };
|
|
130
|
+
if (request.idempotencyKey !== void 0) {
|
|
131
|
+
deleteHeader(headers, IDEMPOTENCY_KEY_HEADER);
|
|
132
|
+
headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
|
|
133
|
+
}
|
|
130
134
|
let body;
|
|
131
135
|
checkQueryCollision(target, parts.query, context);
|
|
132
136
|
for (const [key, value] of Object.entries(parts.query)) {
|
|
@@ -335,6 +339,12 @@ function hasHeader(headers, name) {
|
|
|
335
339
|
const target = name.toLowerCase();
|
|
336
340
|
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
337
341
|
}
|
|
342
|
+
function deleteHeader(headers, name) {
|
|
343
|
+
const target = name.toLowerCase();
|
|
344
|
+
for (const key of Object.keys(headers)) {
|
|
345
|
+
if (key.toLowerCase() === target) delete headers[key];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
338
348
|
function headersToRecord(headers) {
|
|
339
349
|
const record = {};
|
|
340
350
|
headers.forEach((value, key) => {
|
|
@@ -599,6 +609,92 @@ function createExecutionPipeline(options) {
|
|
|
599
609
|
function buildEvent(partial) {
|
|
600
610
|
return { id: options.ids.next("evt"), at: options.clock.nowIso(), ...partial };
|
|
601
611
|
}
|
|
612
|
+
async function authorizeAndReserve(request, resource, providers, requirement, input) {
|
|
613
|
+
if (providers.length === 0) return void 0;
|
|
614
|
+
const submission = request.authorization;
|
|
615
|
+
const provider = providers.find((candidate) => candidate.name === submission?.method);
|
|
616
|
+
const unmet = providers.filter((candidate) => candidate !== provider).map((c) => c.name);
|
|
617
|
+
if (submission === void 0 || provider === void 0 || unmet.length > 0) {
|
|
618
|
+
await safeEmit(
|
|
619
|
+
buildEvent({
|
|
620
|
+
type: "authorization.rejected",
|
|
621
|
+
requestId: request.requestId,
|
|
622
|
+
resourceId: resource.id,
|
|
623
|
+
adapter: request.protocol,
|
|
624
|
+
status: "error",
|
|
625
|
+
data: { reason: "AUTHORIZATION_REQUIRED", methods: unmet }
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
throw new CommerceError(
|
|
629
|
+
"AUTHORIZATION_REQUIRED",
|
|
630
|
+
`Resource "${resource.id}" requires authorization: ${unmet.join(", ")}`,
|
|
631
|
+
{
|
|
632
|
+
requestId: request.requestId,
|
|
633
|
+
resourceId: resource.id,
|
|
634
|
+
details: { required: providers.map((candidate) => candidate.name), missing: unmet }
|
|
635
|
+
}
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
let verification;
|
|
639
|
+
try {
|
|
640
|
+
verification = await provider.verifyAndReserve({
|
|
641
|
+
requestId: request.requestId,
|
|
642
|
+
resourceId: resource.id,
|
|
643
|
+
input,
|
|
644
|
+
submission,
|
|
645
|
+
requirement
|
|
646
|
+
});
|
|
647
|
+
} catch (error) {
|
|
648
|
+
const mapped = isCommerceError(error) ? error : new CommerceError(
|
|
649
|
+
"AUTHORIZATION_PROVIDER_UNAVAILABLE",
|
|
650
|
+
"Authorization provider is unavailable",
|
|
651
|
+
{
|
|
652
|
+
requestId: request.requestId,
|
|
653
|
+
resourceId: resource.id,
|
|
654
|
+
cause: error
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
await safeEmit(
|
|
658
|
+
buildEvent({
|
|
659
|
+
type: "authorization.rejected",
|
|
660
|
+
requestId: request.requestId,
|
|
661
|
+
resourceId: resource.id,
|
|
662
|
+
adapter: request.protocol,
|
|
663
|
+
status: "error",
|
|
664
|
+
data: { reason: mapped.code, method: provider.name }
|
|
665
|
+
})
|
|
666
|
+
);
|
|
667
|
+
throw mapped;
|
|
668
|
+
}
|
|
669
|
+
await safeEmit(
|
|
670
|
+
buildEvent({
|
|
671
|
+
type: "authorization.verified",
|
|
672
|
+
requestId: request.requestId,
|
|
673
|
+
resourceId: resource.id,
|
|
674
|
+
adapter: request.protocol,
|
|
675
|
+
status: "ok",
|
|
676
|
+
data: { method: verification.method, reference: verification.reference }
|
|
677
|
+
})
|
|
678
|
+
);
|
|
679
|
+
const context = { requestId: request.requestId, resourceId: resource.id };
|
|
680
|
+
return {
|
|
681
|
+
record: {
|
|
682
|
+
method: verification.method,
|
|
683
|
+
reference: verification.reference,
|
|
684
|
+
...verification.metadata !== void 0 ? { metadata: verification.metadata } : {}
|
|
685
|
+
},
|
|
686
|
+
async finalize(action) {
|
|
687
|
+
try {
|
|
688
|
+
await provider[action](verification.reservationId, context);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
options.logger.error(
|
|
691
|
+
{ err: describeError2(error), requestId: request.requestId, action },
|
|
692
|
+
"Authorization finalization failed; the reservation stays reserved"
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
}
|
|
602
698
|
async function execute(request) {
|
|
603
699
|
const pipelineStart = options.clock.monotonicMs();
|
|
604
700
|
const resource = options.resources.get(request.resourceId);
|
|
@@ -620,7 +716,7 @@ function createExecutionPipeline(options) {
|
|
|
620
716
|
adapter: request.protocol
|
|
621
717
|
})
|
|
622
718
|
);
|
|
623
|
-
const strippedInput =
|
|
719
|
+
const strippedInput = stripReservedFields(request.input);
|
|
624
720
|
const validation = getValidator(resource)(strippedInput);
|
|
625
721
|
if (!validation.valid) {
|
|
626
722
|
throw new CommerceError(
|
|
@@ -648,7 +744,18 @@ function createExecutionPipeline(options) {
|
|
|
648
744
|
}
|
|
649
745
|
);
|
|
650
746
|
}
|
|
747
|
+
if (resource.pricing.type !== "fixed" && (resource.authorization?.required.length ?? 0) > 0) {
|
|
748
|
+
throw new CommerceError(
|
|
749
|
+
"CONFIG_INVALID",
|
|
750
|
+
`Resource "${resource.id}" requires authorization but is not a paid resource`,
|
|
751
|
+
{
|
|
752
|
+
requestId: request.requestId,
|
|
753
|
+
resourceId: resource.id
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
}
|
|
651
757
|
let paymentResult;
|
|
758
|
+
let authorization;
|
|
652
759
|
if (resource.pricing.type === "fixed") {
|
|
653
760
|
const pricing = resource.pricing;
|
|
654
761
|
const provider = pickProvider(options.paymentProviders, resource.paymentMethods);
|
|
@@ -663,6 +770,11 @@ function createExecutionPipeline(options) {
|
|
|
663
770
|
}
|
|
664
771
|
);
|
|
665
772
|
}
|
|
773
|
+
const authProviders = resolveAuthorizationProviders(
|
|
774
|
+
options.authorizationProviders ?? [],
|
|
775
|
+
resource,
|
|
776
|
+
request.requestId
|
|
777
|
+
);
|
|
666
778
|
let requirement;
|
|
667
779
|
try {
|
|
668
780
|
requirement = await provider.createRequirement({
|
|
@@ -693,7 +805,8 @@ function createExecutionPipeline(options) {
|
|
|
693
805
|
kind: "payment-required",
|
|
694
806
|
requestId: request.requestId,
|
|
695
807
|
resourceId: resource.id,
|
|
696
|
-
requirement
|
|
808
|
+
requirement,
|
|
809
|
+
...authProviders.length > 0 ? { authorization: authProviders.map((p) => p.requirement) } : {}
|
|
697
810
|
};
|
|
698
811
|
}
|
|
699
812
|
const payment = request.payment;
|
|
@@ -763,6 +876,14 @@ function createExecutionPipeline(options) {
|
|
|
763
876
|
});
|
|
764
877
|
}
|
|
765
878
|
const replayKey = verification.replayKey;
|
|
879
|
+
const hold = await authorizeAndReserve(
|
|
880
|
+
request,
|
|
881
|
+
resource,
|
|
882
|
+
authProviders,
|
|
883
|
+
requirement,
|
|
884
|
+
validInput
|
|
885
|
+
);
|
|
886
|
+
authorization = hold?.record;
|
|
766
887
|
try {
|
|
767
888
|
await options.store.reservePaymentAttempt({
|
|
768
889
|
requestId: request.requestId,
|
|
@@ -780,6 +901,7 @@ function createExecutionPipeline(options) {
|
|
|
780
901
|
"STORAGE_ERROR",
|
|
781
902
|
"Payment attempt could not be recorded"
|
|
782
903
|
);
|
|
904
|
+
await hold?.finalize("release");
|
|
783
905
|
await safeEmit(
|
|
784
906
|
buildEvent({
|
|
785
907
|
type: "payment.rejected",
|
|
@@ -813,16 +935,16 @@ function createExecutionPipeline(options) {
|
|
|
813
935
|
verification
|
|
814
936
|
});
|
|
815
937
|
} catch (error) {
|
|
816
|
-
const
|
|
938
|
+
const txHash = settlementTxHash(error);
|
|
939
|
+
await hold?.finalize("markUncertain");
|
|
817
940
|
await safePersist(
|
|
818
|
-
() => options.store.updatePaymentAttempt(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
),
|
|
825
|
-
uncertainTxHash !== void 0 ? "updatePaymentAttempt(settlement-uncertain)" : "updatePaymentAttempt(failed)",
|
|
941
|
+
() => options.store.updatePaymentAttempt({
|
|
942
|
+
replayKey,
|
|
943
|
+
status: "settlement-uncertain",
|
|
944
|
+
...txHash !== void 0 ? { externalReference: txHash } : {},
|
|
945
|
+
...error instanceof Error ? { rejectionReason: error.message } : {}
|
|
946
|
+
}),
|
|
947
|
+
"updatePaymentAttempt(settlement-uncertain)",
|
|
826
948
|
request.requestId
|
|
827
949
|
);
|
|
828
950
|
await safeEmit(
|
|
@@ -833,21 +955,24 @@ function createExecutionPipeline(options) {
|
|
|
833
955
|
adapter: request.protocol,
|
|
834
956
|
paymentProvider: provider.name,
|
|
835
957
|
status: "error",
|
|
836
|
-
data:
|
|
958
|
+
data: {
|
|
959
|
+
reason: "settlement-uncertain",
|
|
960
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
961
|
+
}
|
|
837
962
|
})
|
|
838
963
|
);
|
|
839
|
-
throw new CommerceError(
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
...uncertainTxHash !== void 0 ? { details: { settlementUncertain: true, transactionHash: uncertainTxHash } } : {}
|
|
964
|
+
throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
|
|
965
|
+
requestId: request.requestId,
|
|
966
|
+
resourceId: resource.id,
|
|
967
|
+
cause: error,
|
|
968
|
+
details: {
|
|
969
|
+
settlementUncertain: true,
|
|
970
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
847
971
|
}
|
|
848
|
-
);
|
|
972
|
+
});
|
|
849
973
|
}
|
|
850
974
|
if (settlement.status !== "settled") {
|
|
975
|
+
await hold?.finalize("release");
|
|
851
976
|
await safePersist(
|
|
852
977
|
() => options.store.updatePaymentAttempt({
|
|
853
978
|
replayKey,
|
|
@@ -877,6 +1002,7 @@ function createExecutionPipeline(options) {
|
|
|
877
1002
|
}
|
|
878
1003
|
);
|
|
879
1004
|
}
|
|
1005
|
+
await hold?.finalize("consume");
|
|
880
1006
|
await safePersist(
|
|
881
1007
|
() => options.store.updatePaymentAttempt({
|
|
882
1008
|
replayKey,
|
|
@@ -904,7 +1030,8 @@ function createExecutionPipeline(options) {
|
|
|
904
1030
|
backendResponse = await options.backend.call(resource.handler, {
|
|
905
1031
|
requestId: request.requestId,
|
|
906
1032
|
resourceId: resource.id,
|
|
907
|
-
input: validInput
|
|
1033
|
+
input: validInput,
|
|
1034
|
+
...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
|
|
908
1035
|
});
|
|
909
1036
|
} catch (error) {
|
|
910
1037
|
const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
|
|
@@ -928,6 +1055,7 @@ function createExecutionPipeline(options) {
|
|
|
928
1055
|
backendStatus: backendErrorStatus(commerceError),
|
|
929
1056
|
protocol: request.protocol,
|
|
930
1057
|
payment: paymentResult,
|
|
1058
|
+
...authorization !== void 0 ? { authorization } : {},
|
|
931
1059
|
metadata: { delivered: false, backendErrorCode: commerceError.code }
|
|
932
1060
|
};
|
|
933
1061
|
await safePersist(
|
|
@@ -976,7 +1104,8 @@ function createExecutionPipeline(options) {
|
|
|
976
1104
|
backendStatus: backendResponse.status,
|
|
977
1105
|
durationMs: backendResponse.durationMs,
|
|
978
1106
|
protocol: request.protocol,
|
|
979
|
-
...paymentResult !== void 0 ? { payment: paymentResult } : {}
|
|
1107
|
+
...paymentResult !== void 0 ? { payment: paymentResult } : {},
|
|
1108
|
+
...authorization !== void 0 ? { authorization } : {}
|
|
980
1109
|
};
|
|
981
1110
|
await safePersist(() => options.store.saveReceipt(receipt), "saveReceipt", request.requestId);
|
|
982
1111
|
const totalDurationMs = Math.round(options.clock.monotonicMs() - pipelineStart);
|
|
@@ -1008,11 +1137,24 @@ function backendErrorStatus(error) {
|
|
|
1008
1137
|
const status = error.details?.["status"];
|
|
1009
1138
|
return typeof status === "number" ? status : 0;
|
|
1010
1139
|
}
|
|
1011
|
-
function
|
|
1012
|
-
|
|
1013
|
-
const hash = error.details?.["transactionHash"];
|
|
1140
|
+
function settlementTxHash(error) {
|
|
1141
|
+
const hash = isCommerceError(error) ? error.details?.["transactionHash"] : void 0;
|
|
1014
1142
|
return typeof hash === "string" ? hash : void 0;
|
|
1015
1143
|
}
|
|
1144
|
+
function resolveAuthorizationProviders(providers, resource, requestId) {
|
|
1145
|
+
const required = resource.authorization?.required ?? [];
|
|
1146
|
+
return required.map((method) => {
|
|
1147
|
+
const found = providers.find((provider) => provider.name === method);
|
|
1148
|
+
if (!found) {
|
|
1149
|
+
throw new CommerceError(
|
|
1150
|
+
"CONFIG_INVALID",
|
|
1151
|
+
`Resource "${resource.id}" requires authorization method "${method}", which is not enabled`,
|
|
1152
|
+
{ requestId, resourceId: resource.id }
|
|
1153
|
+
);
|
|
1154
|
+
}
|
|
1155
|
+
return found;
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1016
1158
|
function pickProvider(providers, methods) {
|
|
1017
1159
|
for (const method of methods) {
|
|
1018
1160
|
const found = providers.find((provider) => provider.name === method);
|
|
@@ -1020,8 +1162,8 @@ function pickProvider(providers, methods) {
|
|
|
1020
1162
|
}
|
|
1021
1163
|
return void 0;
|
|
1022
1164
|
}
|
|
1023
|
-
var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([
|
|
1024
|
-
function
|
|
1165
|
+
var RESERVED_INPUT_KEYS = /* @__PURE__ */ new Set([...RESERVED_INPUT_FIELDS, "__proto__"]);
|
|
1166
|
+
function stripReservedFields(input) {
|
|
1025
1167
|
if (typeof input !== "object" || input === null || Array.isArray(input)) return input;
|
|
1026
1168
|
const record = input;
|
|
1027
1169
|
const hasReserved = [...RESERVED_INPUT_KEYS].some((key) => Object.hasOwn(record, key));
|
|
@@ -1234,7 +1376,8 @@ var ResourceEntrySchema = z.object({
|
|
|
1234
1376
|
backend: BackendHandlerSchema,
|
|
1235
1377
|
pricing: PricingSchema,
|
|
1236
1378
|
expose: z.array(z.string().min(1)).min(1),
|
|
1237
|
-
payments: z.array(z.string().min(1)).optional()
|
|
1379
|
+
payments: z.array(z.string().min(1)).optional(),
|
|
1380
|
+
authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
|
|
1238
1381
|
}).strict();
|
|
1239
1382
|
var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
1240
1383
|
var FacilitatorAuthSchema = z.discriminatedUnion("type", [
|
|
@@ -1282,6 +1425,32 @@ var X402Schema = z.object({
|
|
|
1282
1425
|
var PaymentsSchema = z.object({
|
|
1283
1426
|
x402: X402Schema.optional()
|
|
1284
1427
|
}).strict();
|
|
1428
|
+
var Ap2KeySchema = z.object({
|
|
1429
|
+
kid: z.string().min(1),
|
|
1430
|
+
jwk: z.record(z.string(), z.unknown())
|
|
1431
|
+
}).strict();
|
|
1432
|
+
var Ap2IssuerSchema = z.object({
|
|
1433
|
+
issuer: z.string().min(1),
|
|
1434
|
+
/**
|
|
1435
|
+
* Required, not defaulted. Without it, a mandate minted for another
|
|
1436
|
+
* merchant would verify here, and there is no value worth guessing for
|
|
1437
|
+
* something that decides that.
|
|
1438
|
+
*/
|
|
1439
|
+
audience: z.string().min(1),
|
|
1440
|
+
keys: z.array(Ap2KeySchema).min(1)
|
|
1441
|
+
}).strict();
|
|
1442
|
+
var Ap2Schema = z.object({
|
|
1443
|
+
enabled: BooleanOrString,
|
|
1444
|
+
specVersion: z.string().min(1).optional(),
|
|
1445
|
+
mode: z.string().min(1).optional(),
|
|
1446
|
+
trust: z.object({
|
|
1447
|
+
mandateIssuers: z.array(Ap2IssuerSchema).optional(),
|
|
1448
|
+
checkoutIssuers: z.array(Ap2IssuerSchema).optional()
|
|
1449
|
+
}).strict().optional(),
|
|
1450
|
+
clockSkewSeconds: NumberOrString.optional(),
|
|
1451
|
+
replay: z.object({ path: z.string().min(1) }).strict().optional()
|
|
1452
|
+
}).strict();
|
|
1453
|
+
var AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
|
|
1285
1454
|
var RawConfigSchema = z.object({
|
|
1286
1455
|
version: z.literal(SUPPORTED_CONFIG_VERSION),
|
|
1287
1456
|
merchant: MerchantSchema,
|
|
@@ -1289,7 +1458,8 @@ var RawConfigSchema = z.object({
|
|
|
1289
1458
|
storage: StorageSchema,
|
|
1290
1459
|
protocols: ProtocolsSchema,
|
|
1291
1460
|
resources: ResourcesMapSchema,
|
|
1292
|
-
payments: PaymentsSchema
|
|
1461
|
+
payments: PaymentsSchema,
|
|
1462
|
+
authorization: AuthorizationSchema.optional()
|
|
1293
1463
|
}).strict();
|
|
1294
1464
|
function parseConfig(raw, env) {
|
|
1295
1465
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
@@ -1390,6 +1560,7 @@ function toBoolean(value, path2) {
|
|
|
1390
1560
|
}
|
|
1391
1561
|
var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1392
1562
|
var SUPPORTED_PAYMENT_METHODS = /* @__PURE__ */ new Set(["x402"]);
|
|
1563
|
+
var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
1393
1564
|
function normalise(raw) {
|
|
1394
1565
|
const protocols = {
|
|
1395
1566
|
http: { enabled: toBoolean(raw.protocols.http.enabled, "protocols.http.enabled") },
|
|
@@ -1452,8 +1623,12 @@ function normalise(raw) {
|
|
|
1452
1623
|
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
1453
1624
|
});
|
|
1454
1625
|
}
|
|
1626
|
+
const ap2 = normaliseAp2(raw.authorization?.ap2);
|
|
1627
|
+
if (ap2?.enabled) {
|
|
1628
|
+
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
1629
|
+
}
|
|
1455
1630
|
const resources = Object.entries(raw.resources).map(
|
|
1456
|
-
([id, entry]) => normaliseResource(id, entry, protocols, x402)
|
|
1631
|
+
([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
|
|
1457
1632
|
);
|
|
1458
1633
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
1459
1634
|
return {
|
|
@@ -1476,7 +1651,8 @@ function normalise(raw) {
|
|
|
1476
1651
|
resources,
|
|
1477
1652
|
payments: {
|
|
1478
1653
|
...x402 !== void 0 ? { x402 } : {}
|
|
1479
|
-
}
|
|
1654
|
+
},
|
|
1655
|
+
...ap2 !== void 0 ? { authorization: { ap2 } } : {}
|
|
1480
1656
|
};
|
|
1481
1657
|
}
|
|
1482
1658
|
function validateMountPaths(protocols) {
|
|
@@ -1499,6 +1675,194 @@ function validateMountPaths(protocols) {
|
|
|
1499
1675
|
}
|
|
1500
1676
|
}
|
|
1501
1677
|
}
|
|
1678
|
+
function ap2Invalid(path2, message, extra = {}) {
|
|
1679
|
+
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1680
|
+
}
|
|
1681
|
+
function normaliseAp2(raw) {
|
|
1682
|
+
if (raw === void 0) return void 0;
|
|
1683
|
+
if (!toBoolean(raw.enabled, "authorization.ap2.enabled")) return { enabled: false };
|
|
1684
|
+
const specVersion = raw.specVersion ?? AP2_SPEC_VERSION;
|
|
1685
|
+
if (specVersion !== AP2_SPEC_VERSION) {
|
|
1686
|
+
throw ap2Invalid(
|
|
1687
|
+
"authorization.ap2.specVersion",
|
|
1688
|
+
`authorization.ap2.specVersion "${specVersion}" is not supported - this gateway verifies against the tagged AP2 release ${AP2_SPEC_VERSION} only`
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
const mode = raw.mode ?? AP2_MODES[0];
|
|
1692
|
+
if (!AP2_MODES.includes(mode)) {
|
|
1693
|
+
throw ap2Invalid(
|
|
1694
|
+
"authorization.ap2.mode",
|
|
1695
|
+
`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.`
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (raw.replay === void 0) {
|
|
1699
|
+
throw ap2Invalid(
|
|
1700
|
+
"authorization.ap2.replay",
|
|
1701
|
+
"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"
|
|
1702
|
+
);
|
|
1703
|
+
}
|
|
1704
|
+
const mandateIssuers = normaliseIssuers(
|
|
1705
|
+
raw.trust?.mandateIssuers,
|
|
1706
|
+
"authorization.ap2.trust.mandateIssuers"
|
|
1707
|
+
);
|
|
1708
|
+
const checkoutIssuers = normaliseIssuers(
|
|
1709
|
+
raw.trust?.checkoutIssuers,
|
|
1710
|
+
"authorization.ap2.trust.checkoutIssuers"
|
|
1711
|
+
);
|
|
1712
|
+
return {
|
|
1713
|
+
enabled: true,
|
|
1714
|
+
specVersion: AP2_SPEC_VERSION,
|
|
1715
|
+
mode,
|
|
1716
|
+
trust: { mandateIssuers, checkoutIssuers },
|
|
1717
|
+
clockSkewSeconds: toNumber(
|
|
1718
|
+
raw.clockSkewSeconds ?? AP2_DEFAULT_CLOCK_SKEW_SECONDS,
|
|
1719
|
+
"authorization.ap2.clockSkewSeconds",
|
|
1720
|
+
{ min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
|
|
1721
|
+
),
|
|
1722
|
+
replay: { path: raw.replay.path }
|
|
1723
|
+
};
|
|
1724
|
+
}
|
|
1725
|
+
function normaliseIssuers(raw, path2) {
|
|
1726
|
+
if (raw === void 0 || raw.length === 0) {
|
|
1727
|
+
throw ap2Invalid(
|
|
1728
|
+
path2,
|
|
1729
|
+
`${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`
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1733
|
+
return raw.map((entry, index) => {
|
|
1734
|
+
const entryPath = `${path2}.${index}`;
|
|
1735
|
+
if (seen.has(entry.issuer)) {
|
|
1736
|
+
throw ap2Invalid(
|
|
1737
|
+
`${entryPath}.issuer`,
|
|
1738
|
+
`${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).`,
|
|
1739
|
+
{ issuer: entry.issuer }
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
seen.add(entry.issuer);
|
|
1743
|
+
const kids = /* @__PURE__ */ new Set();
|
|
1744
|
+
const keys = entry.keys.map((key, keyIndex) => {
|
|
1745
|
+
const keyPath = `${entryPath}.keys.${keyIndex}`;
|
|
1746
|
+
if (kids.has(key.kid)) {
|
|
1747
|
+
throw ap2Invalid(
|
|
1748
|
+
`${keyPath}.kid`,
|
|
1749
|
+
`${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`,
|
|
1750
|
+
{ issuer: entry.issuer, kid: key.kid }
|
|
1751
|
+
);
|
|
1752
|
+
}
|
|
1753
|
+
kids.add(key.kid);
|
|
1754
|
+
return { kid: key.kid, jwk: validateJwk(key.jwk, key.kid, keyPath) };
|
|
1755
|
+
});
|
|
1756
|
+
return { issuer: entry.issuer, audience: entry.audience, keys };
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
function validateJwk(jwk, kid, path2) {
|
|
1760
|
+
const fail = (detail) => {
|
|
1761
|
+
throw ap2Invalid(`${path2}.jwk`, `${path2}.jwk (kid "${kid}") ${detail}`, { kid });
|
|
1762
|
+
};
|
|
1763
|
+
for (const member of Object.keys(jwk)) {
|
|
1764
|
+
if (AP2_JWK_MEMBERS.includes(member)) continue;
|
|
1765
|
+
if (member === "d" || member === "k") {
|
|
1766
|
+
fail(
|
|
1767
|
+
`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.`
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
fail(
|
|
1771
|
+
`has unsupported member "${member}". Allowed: ${AP2_JWK_MEMBERS.join(", ")}. Members naming a URL are refused on purpose - keys are configured inline and never fetched.`
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
const value = (member) => {
|
|
1775
|
+
const raw = jwk[member];
|
|
1776
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
1777
|
+
fail(`must give "${member}" as a non-empty string`);
|
|
1778
|
+
}
|
|
1779
|
+
return raw;
|
|
1780
|
+
};
|
|
1781
|
+
if (value("kty") !== AP2_KEY_TYPE) {
|
|
1782
|
+
fail(
|
|
1783
|
+
`must have kty "${AP2_KEY_TYPE}" (got "${value("kty")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
if (value("crv") !== AP2_JWK_CURVE) {
|
|
1787
|
+
fail(
|
|
1788
|
+
`must be on curve "${AP2_JWK_CURVE}" (got "${value("crv")}") - ${AP2_SIGNING_ALGORITHM} is the only accepted algorithm`
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
for (const coordinate of ["x", "y"]) {
|
|
1792
|
+
const encoded = value(coordinate);
|
|
1793
|
+
if (!/^[A-Za-z0-9_-]+$/.test(encoded)) {
|
|
1794
|
+
fail(`coordinate "${coordinate}" is not base64url (no padding, no "+" or "/")`);
|
|
1795
|
+
}
|
|
1796
|
+
if (Buffer.from(encoded, "base64url").length !== AP2_JWK_COORDINATE_BYTES) {
|
|
1797
|
+
fail(
|
|
1798
|
+
`coordinate "${coordinate}" decodes to ${Buffer.from(encoded, "base64url").length} bytes; a ${AP2_JWK_CURVE} coordinate is ${AP2_JWK_COORDINATE_BYTES}`
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (jwk["alg"] !== void 0 && value("alg") !== AP2_SIGNING_ALGORITHM) {
|
|
1803
|
+
fail(`declares alg "${value("alg")}"; only ${AP2_SIGNING_ALGORITHM} is accepted`);
|
|
1804
|
+
}
|
|
1805
|
+
if (jwk["use"] !== void 0 && value("use") !== "sig") {
|
|
1806
|
+
fail(`declares use "${value("use")}"; a verification key must be "sig"`);
|
|
1807
|
+
}
|
|
1808
|
+
if (jwk["kid"] !== void 0 && value("kid") !== kid) {
|
|
1809
|
+
fail(`declares kid "${value("kid")}", which disagrees with the configured kid "${kid}"`);
|
|
1810
|
+
}
|
|
1811
|
+
const normalised = {};
|
|
1812
|
+
for (const member of AP2_JWK_MEMBERS) {
|
|
1813
|
+
if (jwk[member] !== void 0) normalised[member] = value(member);
|
|
1814
|
+
}
|
|
1815
|
+
return normalised;
|
|
1816
|
+
}
|
|
1817
|
+
function validateReplayStoreIsolated(replayPath, receiptsPath, acp) {
|
|
1818
|
+
if (replayPath === ":memory:") return;
|
|
1819
|
+
const others = [["storage.receipts.path", receiptsPath]];
|
|
1820
|
+
if (acp.enabled) others.push(["protocols.acp.idempotency.path", acp.idempotency.path]);
|
|
1821
|
+
for (const [otherPath, other] of others) {
|
|
1822
|
+
if (replayPath !== other) continue;
|
|
1823
|
+
throw ap2Invalid(
|
|
1824
|
+
"authorization.ap2.replay.path",
|
|
1825
|
+
`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`
|
|
1826
|
+
);
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
function normaliseResourceAuthorization(id, entry, pricing, ap2) {
|
|
1830
|
+
const required = entry.authorization?.required;
|
|
1831
|
+
if (required === void 0) return void 0;
|
|
1832
|
+
const path2 = `resources.${id}.authorization.required`;
|
|
1833
|
+
const methods = [];
|
|
1834
|
+
for (const method of required) {
|
|
1835
|
+
if (!SUPPORTED_AUTHORIZATION_METHODS.has(method)) {
|
|
1836
|
+
throw ap2Invalid(
|
|
1837
|
+
path2,
|
|
1838
|
+
`Resource "${id}" requires unsupported authorization method "${method}". Supported: ${[...SUPPORTED_AUTHORIZATION_METHODS].join(", ")}.`,
|
|
1839
|
+
{ resourceId: id, method }
|
|
1840
|
+
);
|
|
1841
|
+
}
|
|
1842
|
+
if (methods.includes(method)) {
|
|
1843
|
+
throw ap2Invalid(path2, `Resource "${id}" lists authorization method "${method}" twice`, {
|
|
1844
|
+
resourceId: id,
|
|
1845
|
+
method
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
if (method === "ap2" && (ap2 === void 0 || !ap2.enabled)) {
|
|
1849
|
+
throw ap2Invalid(
|
|
1850
|
+
path2,
|
|
1851
|
+
`Resource "${id}" requires authorization method "ap2", which is not configured or not enabled under authorization.ap2`,
|
|
1852
|
+
{ resourceId: id, method }
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
methods.push(method);
|
|
1856
|
+
}
|
|
1857
|
+
if (pricing.type !== "fixed") {
|
|
1858
|
+
throw ap2Invalid(
|
|
1859
|
+
path2,
|
|
1860
|
+
`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`,
|
|
1861
|
+
{ resourceId: id }
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
return { required: methods };
|
|
1865
|
+
}
|
|
1502
1866
|
function acpInvalid(path2, message, extra = {}) {
|
|
1503
1867
|
return new CommerceError("CONFIG_INVALID", message, { details: { path: path2, ...extra } });
|
|
1504
1868
|
}
|
|
@@ -1638,20 +2002,23 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1638
2002
|
}
|
|
1639
2003
|
}
|
|
1640
2004
|
}
|
|
1641
|
-
function normaliseResource(id, entry, protocols, x402) {
|
|
2005
|
+
function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
1642
2006
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1643
2007
|
const inputProperties = entry.input?.["properties"];
|
|
1644
|
-
if (inputProperties && typeof inputProperties === "object"
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
2008
|
+
if (inputProperties && typeof inputProperties === "object") {
|
|
2009
|
+
for (const reserved of RESERVED_INPUT_FIELDS) {
|
|
2010
|
+
if (!(reserved in inputProperties)) continue;
|
|
2011
|
+
throw new CommerceError(
|
|
2012
|
+
"CONFIG_INVALID",
|
|
2013
|
+
`Resource "${id}" declares an input property "${reserved}", which is reserved by the gateway`,
|
|
2014
|
+
{
|
|
2015
|
+
details: {
|
|
2016
|
+
path: `resources.${id}.input.properties.${reserved}`,
|
|
2017
|
+
resourceId: id
|
|
2018
|
+
}
|
|
1652
2019
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
2020
|
+
);
|
|
2021
|
+
}
|
|
1655
2022
|
}
|
|
1656
2023
|
for (const protocol of entry.expose) {
|
|
1657
2024
|
if (!SUPPORTED_PROTOCOLS.has(protocol)) {
|
|
@@ -1740,6 +2107,7 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1740
2107
|
}
|
|
1741
2108
|
}
|
|
1742
2109
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
2110
|
+
const authorization = normaliseResourceAuthorization(id, entry, pricing, ap2);
|
|
1743
2111
|
return {
|
|
1744
2112
|
id,
|
|
1745
2113
|
name: entry.name,
|
|
@@ -1760,7 +2128,8 @@ function normaliseResource(id, entry, protocols, x402) {
|
|
|
1760
2128
|
},
|
|
1761
2129
|
pricing,
|
|
1762
2130
|
exposedVia: entry.expose,
|
|
1763
|
-
paymentMethods
|
|
2131
|
+
paymentMethods,
|
|
2132
|
+
...authorization !== void 0 ? { authorization } : {}
|
|
1764
2133
|
};
|
|
1765
2134
|
}
|
|
1766
2135
|
function defaultClosedObjectSchema(schema) {
|
|
@@ -1938,11 +2307,8 @@ function validateInputBindings(id, backend, input) {
|
|
|
1938
2307
|
Array.isArray(input?.["required"]) ? input["required"].filter((value) => typeof value === "string") : []
|
|
1939
2308
|
);
|
|
1940
2309
|
for (const [location, property] of entries) {
|
|
1941
|
-
if (property
|
|
1942
|
-
fail(
|
|
1943
|
-
`binds "${location}" to "${PAYMENT_INPUT_FIELD}", which is reserved for payment proofs`,
|
|
1944
|
-
{ location }
|
|
1945
|
-
);
|
|
2310
|
+
if (RESERVED_INPUT_FIELDS.includes(property)) {
|
|
2311
|
+
fail(`binds "${location}" to "${property}", which is reserved by the gateway`, { location });
|
|
1946
2312
|
}
|
|
1947
2313
|
const other = seen.get(property);
|
|
1948
2314
|
if (other !== void 0) {
|
|
@@ -2163,9 +2529,9 @@ var SECRET_FIELD_NAMES = [
|
|
|
2163
2529
|
"adminToken",
|
|
2164
2530
|
"token"
|
|
2165
2531
|
];
|
|
2532
|
+
var SECRET_HEADERS = ["authorization", PAYMENT_HEADER, AUTHORIZATION_HEADER];
|
|
2166
2533
|
var REDACT_PATHS = [
|
|
2167
|
-
|
|
2168
|
-
'req.headers["payment-signature"]',
|
|
2534
|
+
...SECRET_HEADERS.map((name) => `req.headers[${JSON.stringify(name)}]`),
|
|
2169
2535
|
...SECRET_FIELD_NAMES,
|
|
2170
2536
|
...SECRET_FIELD_NAMES.map((name) => `*.${name}`)
|
|
2171
2537
|
];
|
|
@@ -2506,6 +2872,33 @@ var PAYMENT_PROVIDER_DETAIL = {
|
|
|
2506
2872
|
warn: "payment-provider-degraded",
|
|
2507
2873
|
fail: "payment-provider-unreachable"
|
|
2508
2874
|
};
|
|
2875
|
+
var AUTHORIZATION_PROVIDER_DETAIL = {
|
|
2876
|
+
pass: void 0,
|
|
2877
|
+
warn: "authorization-provider-degraded",
|
|
2878
|
+
fail: "authorization-provider-unreachable"
|
|
2879
|
+
};
|
|
2880
|
+
async function probeProvider(provider, kind, details, options) {
|
|
2881
|
+
let health;
|
|
2882
|
+
try {
|
|
2883
|
+
health = await provider.health();
|
|
2884
|
+
} catch (error) {
|
|
2885
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2886
|
+
options.logger.error({ err: message, provider: provider.name }, `${kind} health() threw`);
|
|
2887
|
+
health = { status: "fail", checkedAt: options.clock.nowIso() };
|
|
2888
|
+
}
|
|
2889
|
+
if (health.detail !== void 0) {
|
|
2890
|
+
options.logger.debug(
|
|
2891
|
+
{ provider: provider.name, detail: health.detail },
|
|
2892
|
+
`${kind} health detail (not sent to the client)`
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
const detail = details[health.status];
|
|
2896
|
+
return {
|
|
2897
|
+
name: provider.name,
|
|
2898
|
+
status: health.status,
|
|
2899
|
+
...detail !== void 0 ? { detail } : {}
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2509
2902
|
async function checkReadiness(options) {
|
|
2510
2903
|
let storeHealth;
|
|
2511
2904
|
let storeThrew = false;
|
|
@@ -2541,45 +2934,32 @@ async function checkReadiness(options) {
|
|
|
2541
2934
|
})
|
|
2542
2935
|
);
|
|
2543
2936
|
const paymentProviderChecks = await Promise.all(
|
|
2544
|
-
options.paymentProviders.map(
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
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
|
-
})
|
|
2937
|
+
options.paymentProviders.map(
|
|
2938
|
+
(provider) => probeProvider(provider, "payment provider", PAYMENT_PROVIDER_DETAIL, options)
|
|
2939
|
+
)
|
|
2940
|
+
);
|
|
2941
|
+
const authorizationProviderChecks = await Promise.all(
|
|
2942
|
+
options.authorizationProviders.map(
|
|
2943
|
+
(provider) => probeProvider(provider, "authorization provider", AUTHORIZATION_PROVIDER_DETAIL, options)
|
|
2944
|
+
)
|
|
2569
2945
|
);
|
|
2570
2946
|
const storeReady = storeHealth.status !== "fail";
|
|
2571
2947
|
const adaptersReady = adapterChecks.every((check) => check.status !== "fail");
|
|
2572
2948
|
const paymentProvidersReady = paymentProviderChecks.every((check) => check.status !== "fail");
|
|
2949
|
+
const authorizationProvidersReady = authorizationProviderChecks.every(
|
|
2950
|
+
(check) => check.status !== "fail"
|
|
2951
|
+
);
|
|
2573
2952
|
const storeDetail = storeThrew ? "store-unreachable" : STORE_DETAIL[storeHealth.status];
|
|
2574
2953
|
return {
|
|
2575
|
-
ready: storeReady && adaptersReady && paymentProvidersReady,
|
|
2954
|
+
ready: storeReady && adaptersReady && paymentProvidersReady && authorizationProvidersReady,
|
|
2576
2955
|
store: {
|
|
2577
2956
|
name: "store",
|
|
2578
2957
|
status: storeHealth.status,
|
|
2579
2958
|
...storeDetail !== void 0 ? { detail: storeDetail } : {}
|
|
2580
2959
|
},
|
|
2581
2960
|
adapters: adapterChecks,
|
|
2582
|
-
paymentProviders: paymentProviderChecks
|
|
2961
|
+
paymentProviders: paymentProviderChecks,
|
|
2962
|
+
authorizationProviders: authorizationProviderChecks
|
|
2583
2963
|
};
|
|
2584
2964
|
}
|
|
2585
2965
|
var READINESS_TTL_MS = 2e3;
|
|
@@ -2633,6 +3013,7 @@ async function buildWellKnownDocument(options) {
|
|
|
2633
3013
|
protocols: publicProtocols(options.config.protocols),
|
|
2634
3014
|
adapters,
|
|
2635
3015
|
paymentProviders: options.paymentProviders.map((provider) => provider.descriptor),
|
|
3016
|
+
authorizationProviders: options.authorizationProviders.map((provider) => provider.descriptor),
|
|
2636
3017
|
store: options.store.descriptor,
|
|
2637
3018
|
payments: {
|
|
2638
3019
|
...x402 !== void 0 ? {
|
|
@@ -2672,6 +3053,7 @@ function registerRoutes(options) {
|
|
|
2672
3053
|
store: options.store,
|
|
2673
3054
|
adapterRuntimes: options.adapterRuntimes,
|
|
2674
3055
|
paymentProviders: options.paymentProviders,
|
|
3056
|
+
authorizationProviders: options.authorizationProviders,
|
|
2675
3057
|
clock: options.clock,
|
|
2676
3058
|
logger: options.logger
|
|
2677
3059
|
});
|
|
@@ -2686,6 +3068,7 @@ function registerRoutes(options) {
|
|
|
2686
3068
|
async () => buildWellKnownDocument({
|
|
2687
3069
|
config: options.config,
|
|
2688
3070
|
paymentProviders: options.paymentProviders,
|
|
3071
|
+
authorizationProviders: options.authorizationProviders,
|
|
2689
3072
|
store: options.store,
|
|
2690
3073
|
adapterRuntimes: options.adapterRuntimes,
|
|
2691
3074
|
clock: options.clock
|
|
@@ -2781,13 +3164,18 @@ async function handleInvoke(request, reply, options) {
|
|
|
2781
3164
|
const paymentValue = Array.isArray(paymentHeader) ? paymentHeader[0] : paymentHeader;
|
|
2782
3165
|
const paymentMethod = resource.paymentMethods[0];
|
|
2783
3166
|
const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
|
|
3167
|
+
const authorization = parseAuthorizationHeader(
|
|
3168
|
+
request.headers[AUTHORIZATION_HEADER],
|
|
3169
|
+
request.id
|
|
3170
|
+
);
|
|
2784
3171
|
const canonicalRequest = {
|
|
2785
3172
|
requestId: request.id,
|
|
2786
3173
|
resourceId,
|
|
2787
3174
|
input: request.body ?? {},
|
|
2788
3175
|
protocol: "http",
|
|
2789
3176
|
receivedAt: options.clock.nowIso(),
|
|
2790
|
-
...payment !== void 0 ? { payment } : {}
|
|
3177
|
+
...payment !== void 0 ? { payment } : {},
|
|
3178
|
+
...authorization !== void 0 ? { authorization } : {}
|
|
2791
3179
|
};
|
|
2792
3180
|
const outcome = await options.pipeline.execute(canonicalRequest);
|
|
2793
3181
|
if (outcome.kind === "payment-required") {
|
|
@@ -2873,11 +3261,13 @@ async function createGateway(options) {
|
|
|
2873
3261
|
const ids = options.ids ?? createDefaultIdGenerator();
|
|
2874
3262
|
const logger = options.logger ?? createGatewayLogger({ name: options.config.merchant.id }).core;
|
|
2875
3263
|
const backend = options.backend ?? new HttpBackendExecutor({ logger });
|
|
3264
|
+
const authorizationProviders = options.authorizationProviders ?? [];
|
|
2876
3265
|
const resources = createResourceRegistry(options.config.resources);
|
|
2877
3266
|
const eventBus = createEventBus({ store: options.store, logger });
|
|
2878
3267
|
const pipeline = createExecutionPipeline({
|
|
2879
3268
|
resources,
|
|
2880
3269
|
paymentProviders: options.paymentProviders,
|
|
3270
|
+
authorizationProviders,
|
|
2881
3271
|
store: options.store,
|
|
2882
3272
|
backend,
|
|
2883
3273
|
events: eventBus,
|
|
@@ -2912,6 +3302,7 @@ async function createGateway(options) {
|
|
|
2912
3302
|
resources,
|
|
2913
3303
|
store: options.store,
|
|
2914
3304
|
paymentProviders: options.paymentProviders,
|
|
3305
|
+
authorizationProviders,
|
|
2915
3306
|
eventBus,
|
|
2916
3307
|
clock,
|
|
2917
3308
|
adapterRuntimes,
|
|
@@ -2955,13 +3346,14 @@ async function createGateway(options) {
|
|
|
2955
3346
|
|
|
2956
3347
|
// src/protocols/acp/checkout-mapping.ts
|
|
2957
3348
|
function toCanonicalRequest(options) {
|
|
2958
|
-
const { request, resourceId, requestId, receivedAt } = options;
|
|
3349
|
+
const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
|
|
2959
3350
|
return {
|
|
2960
3351
|
requestId,
|
|
2961
3352
|
resourceId,
|
|
2962
3353
|
input: canonicalInput(request),
|
|
2963
3354
|
protocol: "acp",
|
|
2964
|
-
receivedAt
|
|
3355
|
+
receivedAt,
|
|
3356
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
2965
3357
|
};
|
|
2966
3358
|
}
|
|
2967
3359
|
function canonicalInput(request) {
|
|
@@ -3069,8 +3461,8 @@ function writeAcpJson(res, status, body, headers = {}) {
|
|
|
3069
3461
|
function writeAcpFailure(res, failure, headers = {}) {
|
|
3070
3462
|
writeAcpJson(res, failure.status, failure.error, headers);
|
|
3071
3463
|
}
|
|
3072
|
-
function
|
|
3073
|
-
return sha256(`acp-
|
|
3464
|
+
function operationKey(scope) {
|
|
3465
|
+
return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
3074
3466
|
}
|
|
3075
3467
|
function requestFingerprint(body) {
|
|
3076
3468
|
return sha256(canonicalize(body));
|
|
@@ -3087,54 +3479,9 @@ function canonicalize(value) {
|
|
|
3087
3479
|
function sha256(value) {
|
|
3088
3480
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
3089
3481
|
}
|
|
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
3482
|
|
|
3137
3483
|
// src/protocols/acp/idempotency/store.ts
|
|
3484
|
+
var SCHEMA_VERSION = 2;
|
|
3138
3485
|
var ACP_MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
3139
3486
|
function createAcpIdempotencyStore(options) {
|
|
3140
3487
|
const logger = options.logger ?? NOOP_LOGGER;
|
|
@@ -3145,33 +3492,39 @@ function createAcpIdempotencyStore(options) {
|
|
|
3145
3492
|
label: "ACP idempotency database",
|
|
3146
3493
|
logger
|
|
3147
3494
|
});
|
|
3148
|
-
migrate(db);
|
|
3495
|
+
migrate(db, logger);
|
|
3149
3496
|
const selectStmt = db.prepare(
|
|
3150
3497
|
`SELECT state, fingerprint, status, body_json FROM acp_idempotency
|
|
3151
|
-
WHERE
|
|
3498
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3152
3499
|
);
|
|
3153
3500
|
const insertStmt = db.prepare(
|
|
3154
3501
|
`INSERT INTO acp_idempotency
|
|
3155
|
-
(
|
|
3156
|
-
VALUES (@
|
|
3502
|
+
(deployment, endpoint, idempotency_key, fingerprint, state, created_at, expires_at)
|
|
3503
|
+
VALUES (@deployment, @endpoint, @idempotency_key, @fingerprint, 'in_flight', @created_at, @expires_at)`
|
|
3157
3504
|
);
|
|
3158
3505
|
const completeStmt = db.prepare(
|
|
3159
3506
|
`UPDATE acp_idempotency SET state = 'completed', status = @status, body_json = @body_json
|
|
3160
|
-
WHERE
|
|
3507
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3161
3508
|
);
|
|
3162
3509
|
const deleteStmt = db.prepare(
|
|
3163
3510
|
`DELETE FROM acp_idempotency
|
|
3164
|
-
WHERE
|
|
3511
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3512
|
+
);
|
|
3513
|
+
const unresolvedStmt = db.prepare(
|
|
3514
|
+
`UPDATE acp_idempotency SET state = 'unresolved'
|
|
3515
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3516
|
+
);
|
|
3517
|
+
const expireStmt = db.prepare(
|
|
3518
|
+
"DELETE FROM acp_idempotency WHERE expires_at <= ? AND state = 'completed'"
|
|
3165
3519
|
);
|
|
3166
|
-
const expireStmt = db.prepare("DELETE FROM acp_idempotency WHERE expires_at <= ?");
|
|
3167
3520
|
const claimTx = db.transaction(
|
|
3168
3521
|
(scope, fingerprint) => {
|
|
3169
3522
|
const at = now();
|
|
3170
3523
|
expireStmt.run(new Date(at).toISOString());
|
|
3171
|
-
const existing = selectStmt.get(scope.
|
|
3524
|
+
const existing = selectStmt.get(scope.deployment, scope.endpoint, scope.key);
|
|
3172
3525
|
if (existing === void 0) {
|
|
3173
3526
|
insertStmt.run({
|
|
3174
|
-
|
|
3527
|
+
deployment: scope.deployment,
|
|
3175
3528
|
endpoint: scope.endpoint,
|
|
3176
3529
|
idempotency_key: scope.key,
|
|
3177
3530
|
fingerprint,
|
|
@@ -3181,6 +3534,7 @@ function createAcpIdempotencyStore(options) {
|
|
|
3181
3534
|
return { kind: "reserved" };
|
|
3182
3535
|
}
|
|
3183
3536
|
if (existing.fingerprint !== fingerprint) return { kind: "conflict" };
|
|
3537
|
+
if (existing.state === "unresolved") return { kind: "unresolved" };
|
|
3184
3538
|
if (existing.state !== "completed") return { kind: "in-flight" };
|
|
3185
3539
|
return {
|
|
3186
3540
|
kind: "replay",
|
|
@@ -3196,15 +3550,22 @@ function createAcpIdempotencyStore(options) {
|
|
|
3196
3550
|
},
|
|
3197
3551
|
complete(scope, response) {
|
|
3198
3552
|
completeStmt.run({
|
|
3199
|
-
|
|
3553
|
+
deployment: scope.deployment,
|
|
3200
3554
|
endpoint: scope.endpoint,
|
|
3201
3555
|
idempotency_key: scope.key,
|
|
3202
3556
|
status: response.status,
|
|
3203
3557
|
body_json: JSON.stringify(response.body ?? null)
|
|
3204
3558
|
});
|
|
3205
3559
|
},
|
|
3560
|
+
markUnresolved(scope) {
|
|
3561
|
+
unresolvedStmt.run({
|
|
3562
|
+
deployment: scope.deployment,
|
|
3563
|
+
endpoint: scope.endpoint,
|
|
3564
|
+
idempotency_key: scope.key
|
|
3565
|
+
});
|
|
3566
|
+
},
|
|
3206
3567
|
release(scope) {
|
|
3207
|
-
deleteStmt.run(scope.
|
|
3568
|
+
deleteStmt.run(scope.deployment, scope.endpoint, scope.key);
|
|
3208
3569
|
},
|
|
3209
3570
|
close() {
|
|
3210
3571
|
if (closed) return;
|
|
@@ -3213,13 +3574,23 @@ function createAcpIdempotencyStore(options) {
|
|
|
3213
3574
|
}
|
|
3214
3575
|
};
|
|
3215
3576
|
}
|
|
3216
|
-
function migrate(db) {
|
|
3577
|
+
function migrate(db, logger) {
|
|
3217
3578
|
const current = db.pragma("user_version", { simple: true });
|
|
3218
|
-
if (current >=
|
|
3579
|
+
if (current >= SCHEMA_VERSION) return;
|
|
3219
3580
|
const apply = db.transaction(() => {
|
|
3581
|
+
if (current === 1) {
|
|
3582
|
+
const carried = db.prepare("SELECT COUNT(*) AS count FROM acp_idempotency WHERE state != 'completed'").get();
|
|
3583
|
+
if (carried.count > 0) {
|
|
3584
|
+
logger.warn(
|
|
3585
|
+
{ unresolved: carried.count },
|
|
3586
|
+
"acp idempotency: schema v1 -> v2 discards claims keyed by the old bearer-token digest; reconcile these operations against the merchant before trusting a retry"
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
db.exec("DROP TABLE acp_idempotency");
|
|
3590
|
+
}
|
|
3220
3591
|
db.exec(`
|
|
3221
3592
|
CREATE TABLE IF NOT EXISTS acp_idempotency (
|
|
3222
|
-
|
|
3593
|
+
deployment TEXT NOT NULL,
|
|
3223
3594
|
endpoint TEXT NOT NULL,
|
|
3224
3595
|
idempotency_key TEXT NOT NULL,
|
|
3225
3596
|
fingerprint TEXT NOT NULL,
|
|
@@ -3228,12 +3599,12 @@ function migrate(db) {
|
|
|
3228
3599
|
body_json TEXT,
|
|
3229
3600
|
created_at TEXT NOT NULL,
|
|
3230
3601
|
expires_at TEXT NOT NULL,
|
|
3231
|
-
PRIMARY KEY (
|
|
3602
|
+
PRIMARY KEY (deployment, endpoint, idempotency_key)
|
|
3232
3603
|
);
|
|
3233
3604
|
CREATE INDEX IF NOT EXISTS idx_acp_idempotency_expires_at
|
|
3234
3605
|
ON acp_idempotency(expires_at);
|
|
3235
3606
|
`);
|
|
3236
|
-
db.pragma(
|
|
3607
|
+
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
3237
3608
|
});
|
|
3238
3609
|
apply();
|
|
3239
3610
|
}
|
|
@@ -7641,6 +8012,28 @@ function asResponse(failure) {
|
|
|
7641
8012
|
|
|
7642
8013
|
// src/protocols/acp/adapter.ts
|
|
7643
8014
|
var DISCOVERY_CACHE_CONTROL = "public, max-age=3600";
|
|
8015
|
+
var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
8016
|
+
"CONFIG_INVALID",
|
|
8017
|
+
"RESOURCE_NOT_FOUND",
|
|
8018
|
+
"INPUT_INVALID",
|
|
8019
|
+
"PROTOCOL_UNSUPPORTED",
|
|
8020
|
+
"GATEWAY_BUSY",
|
|
8021
|
+
"PAYMENT_REQUIRED",
|
|
8022
|
+
"PAYMENT_INVALID",
|
|
8023
|
+
"PAYMENT_REPLAYED",
|
|
8024
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
8025
|
+
"PAYMENT_SETTLEMENT_FAILED",
|
|
8026
|
+
"AUTHORIZATION_REQUIRED",
|
|
8027
|
+
"AUTHORIZATION_INVALID",
|
|
8028
|
+
"AUTHORIZATION_REPLAYED",
|
|
8029
|
+
"AUTHORIZATION_PROVIDER_UNAVAILABLE"
|
|
8030
|
+
]);
|
|
8031
|
+
function reachedFor(code) {
|
|
8032
|
+
return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
|
|
8033
|
+
}
|
|
8034
|
+
function notReached(failure) {
|
|
8035
|
+
return { response: asResponse(failure), reached: "no" };
|
|
8036
|
+
}
|
|
7644
8037
|
var AcpProtocolAdapter = class {
|
|
7645
8038
|
name = "acp";
|
|
7646
8039
|
mountPath;
|
|
@@ -7649,8 +8042,14 @@ var AcpProtocolAdapter = class {
|
|
|
7649
8042
|
token;
|
|
7650
8043
|
operations;
|
|
7651
8044
|
idempotencyOptions;
|
|
7652
|
-
/**
|
|
7653
|
-
|
|
8045
|
+
/**
|
|
8046
|
+
* What an idempotency claim is scoped to, from `context.publicBaseUrl`.
|
|
8047
|
+
*
|
|
8048
|
+
* Not readonly because, unlike the token, it is known only once the adapter
|
|
8049
|
+
* starts. Worth that: scoping by the credential meant a rotation freed
|
|
8050
|
+
* every outstanding claim.
|
|
8051
|
+
*/
|
|
8052
|
+
deployment;
|
|
7654
8053
|
discoveryMetadata;
|
|
7655
8054
|
idempotency;
|
|
7656
8055
|
resources = /* @__PURE__ */ new Map();
|
|
@@ -7665,7 +8064,6 @@ var AcpProtocolAdapter = class {
|
|
|
7665
8064
|
this.token = options.token;
|
|
7666
8065
|
this.operations = options.operations;
|
|
7667
8066
|
this.idempotencyOptions = options.idempotency;
|
|
7668
|
-
this.identityHash = identityHash(options.token);
|
|
7669
8067
|
this.discoveryMetadata = options.discovery;
|
|
7670
8068
|
this.descriptor = buildDescriptor(PACKAGE_VERSION);
|
|
7671
8069
|
this.additionalHttpRoutes = [
|
|
@@ -7709,6 +8107,7 @@ var AcpProtocolAdapter = class {
|
|
|
7709
8107
|
resolved.set(operation, resource);
|
|
7710
8108
|
}
|
|
7711
8109
|
this.resources = resolved;
|
|
8110
|
+
this.deployment = context.publicBaseUrl;
|
|
7712
8111
|
this.idempotency = createAcpIdempotencyStore({
|
|
7713
8112
|
path: this.idempotencyOptions.path,
|
|
7714
8113
|
retentionHours: this.idempotencyOptions.retentionHours,
|
|
@@ -7797,22 +8196,33 @@ var AcpProtocolAdapter = class {
|
|
|
7797
8196
|
*/
|
|
7798
8197
|
async dispatch(request) {
|
|
7799
8198
|
const store = this.idempotency;
|
|
8199
|
+
const deployment = this.deployment;
|
|
7800
8200
|
const key = request.idempotencyKey;
|
|
7801
|
-
if (store === void 0 || key === void 0)
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
key
|
|
7806
|
-
};
|
|
8201
|
+
if (store === void 0 || deployment === void 0 || key === void 0) {
|
|
8202
|
+
return (await this.runCheckout(request)).response;
|
|
8203
|
+
}
|
|
8204
|
+
const scope = { deployment, endpoint: request.route.path, key };
|
|
7807
8205
|
const claim = store.claim(scope, requestFingerprint(request.body));
|
|
7808
8206
|
switch (claim.kind) {
|
|
7809
8207
|
case "in-flight":
|
|
8208
|
+
return {
|
|
8209
|
+
...asResponse(
|
|
8210
|
+
acpFailure(
|
|
8211
|
+
409,
|
|
8212
|
+
"invalid_request",
|
|
8213
|
+
"idempotency_in_flight",
|
|
8214
|
+
"A request with this Idempotency-Key is still being processed."
|
|
8215
|
+
)
|
|
8216
|
+
),
|
|
8217
|
+
retryAfterSeconds: ACP_IN_FLIGHT_RETRY_AFTER_SECONDS
|
|
8218
|
+
};
|
|
8219
|
+
case "unresolved":
|
|
7810
8220
|
return asResponse(
|
|
7811
8221
|
acpFailure(
|
|
7812
8222
|
409,
|
|
7813
|
-
"
|
|
7814
|
-
"
|
|
7815
|
-
"
|
|
8223
|
+
"processing_error",
|
|
8224
|
+
"idempotency_unresolved",
|
|
8225
|
+
"An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
|
|
7816
8226
|
)
|
|
7817
8227
|
);
|
|
7818
8228
|
case "conflict":
|
|
@@ -7827,16 +8237,21 @@ var AcpProtocolAdapter = class {
|
|
|
7827
8237
|
case "replay":
|
|
7828
8238
|
return { status: claim.status, body: claim.body, replayed: true };
|
|
7829
8239
|
}
|
|
7830
|
-
let
|
|
8240
|
+
let outcome;
|
|
7831
8241
|
try {
|
|
7832
|
-
|
|
8242
|
+
outcome = await this.runCheckout(request, operationKey(scope));
|
|
7833
8243
|
} catch (err) {
|
|
7834
|
-
store.
|
|
8244
|
+
store.markUnresolved(scope);
|
|
7835
8245
|
throw err;
|
|
7836
8246
|
}
|
|
7837
|
-
if (response.status
|
|
7838
|
-
|
|
7839
|
-
|
|
8247
|
+
if (outcome.response.status < 500) {
|
|
8248
|
+
store.complete(scope, outcome.response);
|
|
8249
|
+
} else if (outcome.reached === "no") {
|
|
8250
|
+
store.release(scope);
|
|
8251
|
+
} else {
|
|
8252
|
+
store.markUnresolved(scope);
|
|
8253
|
+
}
|
|
8254
|
+
return outcome.response;
|
|
7840
8255
|
}
|
|
7841
8256
|
/**
|
|
7842
8257
|
* One accepted request, one `pipeline.execute()`.
|
|
@@ -7845,11 +8260,11 @@ var AcpProtocolAdapter = class {
|
|
|
7845
8260
|
* merchant backend: the adapter builds a canonical request and reads back
|
|
7846
8261
|
* what the pipeline decided.
|
|
7847
8262
|
*/
|
|
7848
|
-
async runCheckout(request) {
|
|
8263
|
+
async runCheckout(request, idempotencyKey) {
|
|
7849
8264
|
const context = this.context;
|
|
7850
8265
|
const resource = this.resources.get(request.route.operation);
|
|
7851
8266
|
if (context === void 0 || resource === void 0) {
|
|
7852
|
-
return
|
|
8267
|
+
return notReached(
|
|
7853
8268
|
acpFailure(
|
|
7854
8269
|
503,
|
|
7855
8270
|
"service_unavailable",
|
|
@@ -7862,7 +8277,8 @@ var AcpProtocolAdapter = class {
|
|
|
7862
8277
|
request,
|
|
7863
8278
|
resourceId: resource.id,
|
|
7864
8279
|
requestId: context.ids.next("acp"),
|
|
7865
|
-
receivedAt: context.clock.nowIso()
|
|
8280
|
+
receivedAt: context.clock.nowIso(),
|
|
8281
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
7866
8282
|
});
|
|
7867
8283
|
try {
|
|
7868
8284
|
const outcome = await context.pipeline.execute(canonical);
|
|
@@ -7871,7 +8287,7 @@ var AcpProtocolAdapter = class {
|
|
|
7871
8287
|
{ resourceId: resource.id, requestId: canonical.requestId },
|
|
7872
8288
|
"acp adapter: mapped checkout resource returned payment-required - it must be priced free"
|
|
7873
8289
|
);
|
|
7874
|
-
return
|
|
8290
|
+
return notReached(
|
|
7875
8291
|
acpFailure(
|
|
7876
8292
|
500,
|
|
7877
8293
|
"processing_error",
|
|
@@ -7887,14 +8303,22 @@ var AcpProtocolAdapter = class {
|
|
|
7887
8303
|
"acp adapter: refusing to forward a non-conformant merchant response"
|
|
7888
8304
|
);
|
|
7889
8305
|
}
|
|
7890
|
-
return mapped.response;
|
|
8306
|
+
return { response: mapped.response, reached: "yes" };
|
|
7891
8307
|
} catch (err) {
|
|
7892
8308
|
const error = toCommerceError(err);
|
|
7893
8309
|
context.logger.warn(
|
|
7894
|
-
{
|
|
8310
|
+
{
|
|
8311
|
+
resourceId: resource.id,
|
|
8312
|
+
requestId: canonical.requestId,
|
|
8313
|
+
reached: reachedFor(error.code),
|
|
8314
|
+
err: error.toInfo()
|
|
8315
|
+
},
|
|
7895
8316
|
"acp adapter: checkout execution failed"
|
|
7896
8317
|
);
|
|
7897
|
-
return
|
|
8318
|
+
return {
|
|
8319
|
+
response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
|
|
8320
|
+
reached: reachedFor(error.code)
|
|
8321
|
+
};
|
|
7898
8322
|
}
|
|
7899
8323
|
}
|
|
7900
8324
|
/**
|
|
@@ -7907,7 +8331,7 @@ var AcpProtocolAdapter = class {
|
|
|
7907
8331
|
...request.requestId !== void 0 ? { [ACP_REQUEST_ID_HEADER]: request.requestId } : {},
|
|
7908
8332
|
...request.idempotencyKey !== void 0 ? { [ACP_IDEMPOTENCY_KEY_HEADER]: request.idempotencyKey } : {},
|
|
7909
8333
|
...replayed ? { [ACP_IDEMPOTENT_REPLAYED_HEADER]: "true" } : {},
|
|
7910
|
-
...response?.
|
|
8334
|
+
...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
|
|
7911
8335
|
};
|
|
7912
8336
|
}
|
|
7913
8337
|
fail(res, err, message) {
|
|
@@ -7934,6 +8358,7 @@ var AcpProtocolAdapter = class {
|
|
|
7934
8358
|
this.resources = /* @__PURE__ */ new Map();
|
|
7935
8359
|
this.idempotency?.close();
|
|
7936
8360
|
this.idempotency = void 0;
|
|
8361
|
+
this.deployment = void 0;
|
|
7937
8362
|
this.context = void 0;
|
|
7938
8363
|
}
|
|
7939
8364
|
};
|
|
@@ -7975,12 +8400,14 @@ function receiptToRow(receipt) {
|
|
|
7975
8400
|
backend_status: receipt.backendStatus,
|
|
7976
8401
|
duration_ms: receipt.durationMs !== void 0 ? receipt.durationMs : null,
|
|
7977
8402
|
protocol: receipt.protocol !== void 0 ? receipt.protocol : null,
|
|
7978
|
-
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null
|
|
8403
|
+
metadata_json: receipt.metadata !== void 0 ? JSON.stringify(redact(receipt.metadata)) : null,
|
|
8404
|
+
authorization_json: receipt.authorization !== void 0 ? JSON.stringify(redact(receipt.authorization)) : null
|
|
7979
8405
|
};
|
|
7980
8406
|
}
|
|
7981
8407
|
function rowToReceipt(row) {
|
|
7982
8408
|
const payment = row.payment_json !== null ? JSON.parse(row.payment_json) : void 0;
|
|
7983
8409
|
const metadata = row.metadata_json !== null ? JSON.parse(row.metadata_json) : void 0;
|
|
8410
|
+
const authorization = row.authorization_json !== null ? JSON.parse(row.authorization_json) : void 0;
|
|
7984
8411
|
return {
|
|
7985
8412
|
id: row.id,
|
|
7986
8413
|
requestId: row.request_id,
|
|
@@ -7990,7 +8417,8 @@ function rowToReceipt(row) {
|
|
|
7990
8417
|
...payment !== void 0 ? { payment } : {},
|
|
7991
8418
|
...row.duration_ms !== null ? { durationMs: row.duration_ms } : {},
|
|
7992
8419
|
...row.protocol !== null ? { protocol: row.protocol } : {},
|
|
7993
|
-
...metadata !== void 0 ? { metadata } : {}
|
|
8420
|
+
...metadata !== void 0 ? { metadata } : {},
|
|
8421
|
+
...authorization !== void 0 ? { authorization } : {}
|
|
7994
8422
|
};
|
|
7995
8423
|
}
|
|
7996
8424
|
function eventToRow(event) {
|
|
@@ -8099,9 +8527,15 @@ var MIGRATIONS = [
|
|
|
8099
8527
|
CREATE INDEX IF NOT EXISTS idx_payment_attempts_created_at ON payment_attempts(created_at);
|
|
8100
8528
|
`);
|
|
8101
8529
|
}
|
|
8530
|
+
},
|
|
8531
|
+
{
|
|
8532
|
+
version: 2,
|
|
8533
|
+
up(db) {
|
|
8534
|
+
db.exec(`ALTER TABLE receipts ADD COLUMN authorization_json TEXT;`);
|
|
8535
|
+
}
|
|
8102
8536
|
}
|
|
8103
8537
|
];
|
|
8104
|
-
var
|
|
8538
|
+
var SCHEMA_VERSION2 = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
8105
8539
|
function migrate2(db) {
|
|
8106
8540
|
const current = db.pragma("user_version", { simple: true });
|
|
8107
8541
|
for (const migration of MIGRATIONS) {
|
|
@@ -8136,8 +8570,8 @@ function createSqliteReceiptStore(options) {
|
|
|
8136
8570
|
const db = openSqliteDatabase({ path: path2, label: "Receipt database", logger });
|
|
8137
8571
|
migrate2(db);
|
|
8138
8572
|
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)`
|
|
8573
|
+
`INSERT INTO receipts (id, request_id, resource_id, payment_json, delivered_at, backend_status, duration_ms, protocol, metadata_json, authorization_json)
|
|
8574
|
+
VALUES (@id, @request_id, @resource_id, @payment_json, @delivered_at, @backend_status, @duration_ms, @protocol, @metadata_json, @authorization_json)`
|
|
8141
8575
|
);
|
|
8142
8576
|
const getReceiptStmt = db.prepare("SELECT * FROM receipts WHERE id = ?");
|
|
8143
8577
|
const listReceiptsStmt = db.prepare(
|
|
@@ -8187,17 +8621,17 @@ function createSqliteReceiptStore(options) {
|
|
|
8187
8621
|
name: "sqlite-receipt-store",
|
|
8188
8622
|
kind: "storage",
|
|
8189
8623
|
implementationVersion: PACKAGE_VERSION,
|
|
8190
|
-
supportedSpec: `sqlite-schema-v${
|
|
8624
|
+
supportedSpec: `sqlite-schema-v${SCHEMA_VERSION2}`,
|
|
8191
8625
|
capabilities: ["receipts", "events", "payment-attempts"],
|
|
8192
8626
|
status: "stable"
|
|
8193
8627
|
};
|
|
8194
8628
|
const store = {
|
|
8195
8629
|
async init() {
|
|
8196
8630
|
const version = db.pragma("user_version", { simple: true });
|
|
8197
|
-
if (version !==
|
|
8631
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8198
8632
|
throw new CommerceError(
|
|
8199
8633
|
"STORAGE_ERROR",
|
|
8200
|
-
`Receipt store schema version mismatch (found ${version}, expected ${
|
|
8634
|
+
`Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION2})`
|
|
8201
8635
|
);
|
|
8202
8636
|
}
|
|
8203
8637
|
},
|
|
@@ -8331,9 +8765,9 @@ function createSqliteReceiptStore(options) {
|
|
|
8331
8765
|
}
|
|
8332
8766
|
try {
|
|
8333
8767
|
const version = db.pragma("user_version", { simple: true });
|
|
8334
|
-
if (version !==
|
|
8768
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8335
8769
|
logger.warn(
|
|
8336
|
-
{ found: version, expected:
|
|
8770
|
+
{ found: version, expected: SCHEMA_VERSION2 },
|
|
8337
8771
|
"receipt-store: health check failed (schema version mismatch)"
|
|
8338
8772
|
);
|
|
8339
8773
|
return {
|
|
@@ -8345,7 +8779,7 @@ function createSqliteReceiptStore(options) {
|
|
|
8345
8779
|
}
|
|
8346
8780
|
return {
|
|
8347
8781
|
status: "pass",
|
|
8348
|
-
detail: `sqlite schema v${
|
|
8782
|
+
detail: `sqlite schema v${SCHEMA_VERSION2} writable`,
|
|
8349
8783
|
checkedAt: clock.nowIso(),
|
|
8350
8784
|
durationMs: clock.monotonicMs() - startedAt
|
|
8351
8785
|
};
|