@devlab.group/agent-commerce 1.4.0 → 1.5.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/README.md +115 -126
- package/dist/ap2.d.ts +5 -5
- package/dist/ap2.js +19 -11
- package/dist/ap2.js.map +1 -1
- package/dist/{authorization-CPDcpJVa.d.ts → authorization-DA7mjy_O.d.ts} +4 -4
- package/dist/{chunk-ZDVJ7EVL.js → chunk-5GEWKNIH.js} +5 -4
- package/dist/chunk-5GEWKNIH.js.map +1 -0
- package/dist/chunk-6D2EZ5ME.js +27 -0
- package/dist/chunk-6D2EZ5ME.js.map +1 -0
- package/dist/chunk-NKNXVDHN.js +701 -0
- package/dist/chunk-NKNXVDHN.js.map +1 -0
- package/dist/{chunk-IUR3Q7DE.js → chunk-PBYN7EK2.js} +33 -31
- package/dist/chunk-PBYN7EK2.js.map +1 -0
- package/dist/{chunk-7NKWFGBY.js → chunk-US622BKF.js} +3 -3
- package/dist/{chunk-7NKWFGBY.js.map → chunk-US622BKF.js.map} +1 -1
- package/dist/cli/index.js +339 -107
- package/dist/cli/index.js.map +1 -1
- package/dist/constants-6mypC60h.d.ts +50 -0
- package/dist/index.d.ts +44 -13
- package/dist/index.js +358 -113
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +3 -3
- package/dist/mcp.js +4 -1
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +50 -0
- package/dist/mpp.js +351 -0
- package/dist/mpp.js.map +1 -0
- package/dist/{protocol-adapter-CaMZj6Wm.d.ts → protocol-adapter-APRjs1E2.d.ts} +25 -11
- package/dist/{runtime-CKq3d_3K.d.ts → runtime-DC5XQ9Cc.d.ts} +24 -13
- package/dist/x402.d.ts +1 -1
- package/dist/x402.js +7 -678
- package/dist/x402.js.map +1 -1
- package/package.json +22 -4
- package/dist/chunk-IUR3Q7DE.js.map +0 -1
- package/dist/chunk-ZDVJ7EVL.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
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-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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-US622BKF.js';
|
|
2
|
+
import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-6D2EZ5ME.js';
|
|
3
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-PBYN7EK2.js';
|
|
4
|
+
import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_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-5GEWKNIH.js';
|
|
5
|
+
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_METHOD_NAMES, 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-5GEWKNIH.js';
|
|
5
6
|
import { promises, accessSync, constants } from 'fs';
|
|
6
7
|
import * as path from 'path';
|
|
7
8
|
import { parse, YAMLError } from 'yaml';
|
|
@@ -85,6 +86,7 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
|
85
86
|
|
|
86
87
|
// src/core/execution/backend-http.ts
|
|
87
88
|
var MAX_BODY_SNIPPET_LENGTH = 512;
|
|
89
|
+
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
88
90
|
var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
89
91
|
var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
|
|
90
92
|
var HttpBackendExecutor = class {
|
|
@@ -126,6 +128,10 @@ var HttpBackendExecutor = class {
|
|
|
126
128
|
if (error instanceof CommerceError) throw error;
|
|
127
129
|
}
|
|
128
130
|
const headers = { ...handler.headers ?? {} };
|
|
131
|
+
if (request.idempotencyKey !== void 0) {
|
|
132
|
+
deleteHeader(headers, IDEMPOTENCY_KEY_HEADER);
|
|
133
|
+
headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
|
|
134
|
+
}
|
|
129
135
|
let body;
|
|
130
136
|
checkQueryCollision(target, parts.query, context);
|
|
131
137
|
for (const [key, value] of Object.entries(parts.query)) {
|
|
@@ -334,6 +340,12 @@ function hasHeader(headers, name) {
|
|
|
334
340
|
const target = name.toLowerCase();
|
|
335
341
|
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
336
342
|
}
|
|
343
|
+
function deleteHeader(headers, name) {
|
|
344
|
+
const target = name.toLowerCase();
|
|
345
|
+
for (const key of Object.keys(headers)) {
|
|
346
|
+
if (key.toLowerCase() === target) delete headers[key];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
337
349
|
function headersToRecord(headers) {
|
|
338
350
|
const record = {};
|
|
339
351
|
headers.forEach((value, key) => {
|
|
@@ -811,7 +823,8 @@ function createExecutionPipeline(options) {
|
|
|
811
823
|
const mapped = isCommerceError(error) ? error : new CommerceError("PAYMENT_INVALID", "Payment verification failed", {
|
|
812
824
|
requestId: request.requestId,
|
|
813
825
|
resourceId: resource.id,
|
|
814
|
-
cause: error
|
|
826
|
+
cause: error,
|
|
827
|
+
...retryChallenge(requirement)
|
|
815
828
|
});
|
|
816
829
|
await safeEmit(
|
|
817
830
|
buildEvent({
|
|
@@ -843,7 +856,8 @@ function createExecutionPipeline(options) {
|
|
|
843
856
|
verification.rejectionReason ?? "Payment was rejected",
|
|
844
857
|
{
|
|
845
858
|
requestId: request.requestId,
|
|
846
|
-
resourceId: resource.id
|
|
859
|
+
resourceId: resource.id,
|
|
860
|
+
...retryChallenge(requirement)
|
|
847
861
|
}
|
|
848
862
|
);
|
|
849
863
|
}
|
|
@@ -924,17 +938,16 @@ function createExecutionPipeline(options) {
|
|
|
924
938
|
verification
|
|
925
939
|
});
|
|
926
940
|
} catch (error) {
|
|
927
|
-
const
|
|
928
|
-
await hold?.finalize(
|
|
941
|
+
const txHash = settlementTxHash(error);
|
|
942
|
+
await hold?.finalize("markUncertain");
|
|
929
943
|
await safePersist(
|
|
930
|
-
() => options.store.updatePaymentAttempt(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
),
|
|
937
|
-
uncertainTxHash !== void 0 ? "updatePaymentAttempt(settlement-uncertain)" : "updatePaymentAttempt(failed)",
|
|
944
|
+
() => options.store.updatePaymentAttempt({
|
|
945
|
+
replayKey,
|
|
946
|
+
status: "settlement-uncertain",
|
|
947
|
+
...txHash !== void 0 ? { externalReference: txHash } : {},
|
|
948
|
+
...error instanceof Error ? { rejectionReason: error.message } : {}
|
|
949
|
+
}),
|
|
950
|
+
"updatePaymentAttempt(settlement-uncertain)",
|
|
938
951
|
request.requestId
|
|
939
952
|
);
|
|
940
953
|
await safeEmit(
|
|
@@ -945,19 +958,21 @@ function createExecutionPipeline(options) {
|
|
|
945
958
|
adapter: request.protocol,
|
|
946
959
|
paymentProvider: provider.name,
|
|
947
960
|
status: "error",
|
|
948
|
-
data:
|
|
961
|
+
data: {
|
|
962
|
+
reason: "settlement-uncertain",
|
|
963
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
964
|
+
}
|
|
949
965
|
})
|
|
950
966
|
);
|
|
951
|
-
throw new CommerceError(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
...uncertainTxHash !== void 0 ? { details: { settlementUncertain: true, transactionHash: uncertainTxHash } } : {}
|
|
967
|
+
throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
|
|
968
|
+
requestId: request.requestId,
|
|
969
|
+
resourceId: resource.id,
|
|
970
|
+
cause: error,
|
|
971
|
+
details: {
|
|
972
|
+
settlementUncertain: true,
|
|
973
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
959
974
|
}
|
|
960
|
-
);
|
|
975
|
+
});
|
|
961
976
|
}
|
|
962
977
|
if (settlement.status !== "settled") {
|
|
963
978
|
await hold?.finalize("release");
|
|
@@ -1018,7 +1033,8 @@ function createExecutionPipeline(options) {
|
|
|
1018
1033
|
backendResponse = await options.backend.call(resource.handler, {
|
|
1019
1034
|
requestId: request.requestId,
|
|
1020
1035
|
resourceId: resource.id,
|
|
1021
|
-
input: validInput
|
|
1036
|
+
input: validInput,
|
|
1037
|
+
...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
|
|
1022
1038
|
});
|
|
1023
1039
|
} catch (error) {
|
|
1024
1040
|
const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
|
|
@@ -1064,7 +1080,8 @@ function createExecutionPipeline(options) {
|
|
|
1064
1080
|
amount: paymentResult.amount,
|
|
1065
1081
|
currency: paymentResult.currency,
|
|
1066
1082
|
...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
|
|
1067
|
-
...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {}
|
|
1083
|
+
...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {},
|
|
1084
|
+
...typeof paymentResult.metadata?.["receipt"] === "string" ? { receipt: paymentResult.metadata["receipt"] } : {}
|
|
1068
1085
|
}
|
|
1069
1086
|
}
|
|
1070
1087
|
}
|
|
@@ -1124,9 +1141,8 @@ function backendErrorStatus(error) {
|
|
|
1124
1141
|
const status = error.details?.["status"];
|
|
1125
1142
|
return typeof status === "number" ? status : 0;
|
|
1126
1143
|
}
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1129
|
-
const hash = error.details?.["transactionHash"];
|
|
1144
|
+
function settlementTxHash(error) {
|
|
1145
|
+
const hash = isCommerceError(error) ? error.details?.["transactionHash"] : void 0;
|
|
1130
1146
|
return typeof hash === "string" ? hash : void 0;
|
|
1131
1147
|
}
|
|
1132
1148
|
function resolveAuthorizationProviders(providers, resource, requestId) {
|
|
@@ -1143,6 +1159,10 @@ function resolveAuthorizationProviders(providers, resource, requestId) {
|
|
|
1143
1159
|
return found;
|
|
1144
1160
|
});
|
|
1145
1161
|
}
|
|
1162
|
+
function retryChallenge(requirement) {
|
|
1163
|
+
const envelope = requirement.challenge.envelope;
|
|
1164
|
+
return envelope === void 0 ? {} : { details: { challenge: envelope } };
|
|
1165
|
+
}
|
|
1146
1166
|
function pickProvider(providers, methods) {
|
|
1147
1167
|
for (const method of methods) {
|
|
1148
1168
|
const found = providers.find((provider) => provider.name === method);
|
|
@@ -1410,8 +1430,26 @@ var X402Schema = z.object({
|
|
|
1410
1430
|
/** Accepts a mainnet facilitator that takes no credential. Never defaulted. */
|
|
1411
1431
|
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1412
1432
|
}).strict();
|
|
1433
|
+
var MppSchema = z.object({
|
|
1434
|
+
enabled: BooleanOrString,
|
|
1435
|
+
network: z.string().min(1).optional(),
|
|
1436
|
+
rpcUrl: z.string().min(1),
|
|
1437
|
+
asset: z.string().min(1),
|
|
1438
|
+
assetName: z.string().min(1),
|
|
1439
|
+
assetVersion: z.string().min(1),
|
|
1440
|
+
recipient: z.string().min(1),
|
|
1441
|
+
realm: z.string().min(1),
|
|
1442
|
+
challengeSecret: z.string().min(1),
|
|
1443
|
+
challengeTtlSeconds: NumberOrString.optional(),
|
|
1444
|
+
facilitator: FacilitatorSchema,
|
|
1445
|
+
// Real funds. Never defaulted - see src/payments/x402/guardrails.ts
|
|
1446
|
+
allowMainnet: BooleanOrString.optional(),
|
|
1447
|
+
// Accepts a mainnet facilitator that takes no credential. Never defaulted
|
|
1448
|
+
allowUnauthenticatedFacilitator: BooleanOrString.optional()
|
|
1449
|
+
}).strict();
|
|
1413
1450
|
var PaymentsSchema = z.object({
|
|
1414
|
-
x402: X402Schema.optional()
|
|
1451
|
+
x402: X402Schema.optional(),
|
|
1452
|
+
mpp: MppSchema.optional()
|
|
1415
1453
|
}).strict();
|
|
1416
1454
|
var Ap2KeySchema = z.object({
|
|
1417
1455
|
kid: z.string().min(1),
|
|
@@ -1547,7 +1585,7 @@ function toBoolean(value, path2) {
|
|
|
1547
1585
|
);
|
|
1548
1586
|
}
|
|
1549
1587
|
var SUPPORTED_PROTOCOLS = new Set(PROTOCOL_NAMES);
|
|
1550
|
-
var SUPPORTED_PAYMENT_METHODS =
|
|
1588
|
+
var SUPPORTED_PAYMENT_METHODS = new Set(PAYMENT_METHOD_NAMES);
|
|
1551
1589
|
var SUPPORTED_AUTHORIZATION_METHODS = /* @__PURE__ */ new Set(["ap2"]);
|
|
1552
1590
|
function normalise(raw) {
|
|
1553
1591
|
const protocols = {
|
|
@@ -1564,12 +1602,7 @@ function normalise(raw) {
|
|
|
1564
1602
|
};
|
|
1565
1603
|
validateMountPaths(protocols);
|
|
1566
1604
|
const x402Raw = raw.payments.x402;
|
|
1567
|
-
const
|
|
1568
|
-
mode: "remote",
|
|
1569
|
-
url: x402Raw.facilitator.url,
|
|
1570
|
-
auth: x402Raw.facilitator.auth ?? { type: "none" }
|
|
1571
|
-
};
|
|
1572
|
-
const x402 = x402Raw && facilitator ? {
|
|
1605
|
+
const x402 = x402Raw !== void 0 ? {
|
|
1573
1606
|
enabled: toBoolean(x402Raw.enabled, "payments.x402.enabled"),
|
|
1574
1607
|
network: x402Raw.network,
|
|
1575
1608
|
rpcUrl: x402Raw.rpcUrl,
|
|
@@ -1588,7 +1621,7 @@ function normalise(raw) {
|
|
|
1588
1621
|
min: 1
|
|
1589
1622
|
}
|
|
1590
1623
|
),
|
|
1591
|
-
facilitator,
|
|
1624
|
+
facilitator: normaliseFacilitator(x402Raw.facilitator),
|
|
1592
1625
|
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
1593
1626
|
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
1594
1627
|
allowUnauthenticatedFacilitator: toBoolean(
|
|
@@ -1611,12 +1644,13 @@ function normalise(raw) {
|
|
|
1611
1644
|
...x402.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: x402.allowUnauthenticatedFacilitator } : {}
|
|
1612
1645
|
});
|
|
1613
1646
|
}
|
|
1647
|
+
const mpp = normaliseMpp(raw.payments.mpp);
|
|
1614
1648
|
const ap2 = normaliseAp2(raw.authorization?.ap2);
|
|
1615
1649
|
if (ap2?.enabled) {
|
|
1616
1650
|
validateReplayStoreIsolated(ap2.replay.path, raw.storage.receipts.path, protocols.acp);
|
|
1617
1651
|
}
|
|
1618
1652
|
const resources = Object.entries(raw.resources).map(
|
|
1619
|
-
([id, entry]) => normaliseResource(id, entry, protocols, x402, ap2)
|
|
1653
|
+
([id, entry]) => normaliseResource(id, entry, protocols, x402, mpp, ap2)
|
|
1620
1654
|
);
|
|
1621
1655
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
1622
1656
|
return {
|
|
@@ -1638,7 +1672,8 @@ function normalise(raw) {
|
|
|
1638
1672
|
protocols,
|
|
1639
1673
|
resources,
|
|
1640
1674
|
payments: {
|
|
1641
|
-
...x402 !== void 0 ? { x402 } : {}
|
|
1675
|
+
...x402 !== void 0 ? { x402 } : {},
|
|
1676
|
+
...mpp !== void 0 ? { mpp } : {}
|
|
1642
1677
|
},
|
|
1643
1678
|
...ap2 !== void 0 ? { authorization: { ap2 } } : {}
|
|
1644
1679
|
};
|
|
@@ -1990,7 +2025,74 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1990
2025
|
}
|
|
1991
2026
|
}
|
|
1992
2027
|
}
|
|
1993
|
-
function
|
|
2028
|
+
function normaliseFacilitator(raw) {
|
|
2029
|
+
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
2030
|
+
}
|
|
2031
|
+
function normaliseMpp(raw) {
|
|
2032
|
+
if (raw === void 0) return void 0;
|
|
2033
|
+
const network = raw.network ?? MPP_DEFAULT_NETWORK;
|
|
2034
|
+
if (!isMppNetwork(network)) {
|
|
2035
|
+
throw new CommerceError(
|
|
2036
|
+
"CONFIG_INVALID",
|
|
2037
|
+
`payments.mpp.network must be one of ${MPP_NETWORKS.join(", ")}`,
|
|
2038
|
+
{ details: { path: "payments.mpp.network" } }
|
|
2039
|
+
);
|
|
2040
|
+
}
|
|
2041
|
+
const mpp = {
|
|
2042
|
+
enabled: toBoolean(raw.enabled, "payments.mpp.enabled"),
|
|
2043
|
+
network,
|
|
2044
|
+
rpcUrl: raw.rpcUrl,
|
|
2045
|
+
asset: raw.asset,
|
|
2046
|
+
assetName: raw.assetName,
|
|
2047
|
+
assetVersion: raw.assetVersion,
|
|
2048
|
+
recipient: raw.recipient,
|
|
2049
|
+
realm: raw.realm,
|
|
2050
|
+
challengeSecret: raw.challengeSecret,
|
|
2051
|
+
...raw.challengeTtlSeconds !== void 0 ? {
|
|
2052
|
+
challengeTtlSeconds: toNumber(
|
|
2053
|
+
raw.challengeTtlSeconds,
|
|
2054
|
+
"payments.mpp.challengeTtlSeconds",
|
|
2055
|
+
{ min: 1 }
|
|
2056
|
+
)
|
|
2057
|
+
} : {},
|
|
2058
|
+
facilitator: normaliseFacilitator(raw.facilitator),
|
|
2059
|
+
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
2060
|
+
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
2061
|
+
allowUnauthenticatedFacilitator: toBoolean(
|
|
2062
|
+
raw.allowUnauthenticatedFacilitator,
|
|
2063
|
+
"payments.mpp.allowUnauthenticatedFacilitator"
|
|
2064
|
+
)
|
|
2065
|
+
} : {}
|
|
2066
|
+
};
|
|
2067
|
+
validateAddress("payments.mpp.recipient", mpp.recipient);
|
|
2068
|
+
validateAddress("payments.mpp.asset", mpp.asset);
|
|
2069
|
+
if (/[\r\n]/.test(mpp.realm)) {
|
|
2070
|
+
throw new CommerceError("CONFIG_INVALID", "payments.mpp.realm must be a single line", {
|
|
2071
|
+
details: { path: "payments.mpp.realm" }
|
|
2072
|
+
});
|
|
2073
|
+
}
|
|
2074
|
+
if (mpp.challengeSecret.length < MPP_MIN_CHALLENGE_SECRET_LENGTH) {
|
|
2075
|
+
throw new CommerceError(
|
|
2076
|
+
"CONFIG_INVALID",
|
|
2077
|
+
`payments.mpp.challengeSecret must have length at least ${MPP_MIN_CHALLENGE_SECRET_LENGTH}`,
|
|
2078
|
+
{ details: { path: "payments.mpp.challengeSecret" } }
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
resolveX402Deployment({
|
|
2082
|
+
network: mpp.network,
|
|
2083
|
+
payTo: mpp.recipient,
|
|
2084
|
+
asset: mpp.asset,
|
|
2085
|
+
assetName: mpp.assetName,
|
|
2086
|
+
assetVersion: mpp.assetVersion,
|
|
2087
|
+
facilitator: mpp.facilitator,
|
|
2088
|
+
...mpp.allowMainnet !== void 0 ? { allowMainnet: mpp.allowMainnet } : {},
|
|
2089
|
+
...mpp.allowUnauthenticatedFacilitator !== void 0 ? { allowUnauthenticatedFacilitator: mpp.allowUnauthenticatedFacilitator } : {},
|
|
2090
|
+
configPath: "payments.mpp",
|
|
2091
|
+
payToField: "recipient"
|
|
2092
|
+
});
|
|
2093
|
+
return mpp;
|
|
2094
|
+
}
|
|
2095
|
+
function normaliseResource(id, entry, protocols, x402, mpp, ap2) {
|
|
1994
2096
|
if (entry.input !== void 0) validateResourceSchemaKeywords(id, "input", entry.input);
|
|
1995
2097
|
const inputProperties = entry.input?.["properties"];
|
|
1996
2098
|
if (inputProperties && typeof inputProperties === "object") {
|
|
@@ -2066,7 +2168,7 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
|
2066
2168
|
);
|
|
2067
2169
|
}
|
|
2068
2170
|
if (entry.pricing.type === "fixed") {
|
|
2069
|
-
validatePricingAmount(id, entry.pricing.amount, x402);
|
|
2171
|
+
validatePricingAmount(id, entry.pricing.amount, x402?.assetDecimals);
|
|
2070
2172
|
}
|
|
2071
2173
|
const paymentMethods = entry.payments ?? [];
|
|
2072
2174
|
if (entry.pricing.type === "fixed") {
|
|
@@ -2081,17 +2183,28 @@ function normaliseResource(id, entry, protocols, x402, ap2) {
|
|
|
2081
2183
|
if (!SUPPORTED_PAYMENT_METHODS.has(method)) {
|
|
2082
2184
|
throw new CommerceError(
|
|
2083
2185
|
"CONFIG_INVALID",
|
|
2084
|
-
`Resource "${id}" names unsupported payment method "${method}". Supported:
|
|
2186
|
+
`Resource "${id}" names unsupported payment method "${method}". Supported: ${PAYMENT_METHOD_NAMES.join(", ")}.`,
|
|
2085
2187
|
{ details: { path: `resources.${id}.payments`, resourceId: id, method } }
|
|
2086
2188
|
);
|
|
2087
2189
|
}
|
|
2088
|
-
|
|
2190
|
+
}
|
|
2191
|
+
const enabledRails = [...x402?.enabled ? ["x402"] : [], ...mpp?.enabled ? ["mpp"] : []];
|
|
2192
|
+
if (!paymentMethods.some((method) => enabledRails.includes(method))) {
|
|
2193
|
+
throw new CommerceError(
|
|
2194
|
+
"CONFIG_INVALID",
|
|
2195
|
+
`Resource "${id}" names payment method(s) "${paymentMethods.join(", ")}", none of which is configured and enabled under "payments"`,
|
|
2196
|
+
{ details: { path: `resources.${id}.payments`, resourceId: id, paymentMethods } }
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
if (mpp?.enabled && paymentMethods.includes("mpp")) {
|
|
2200
|
+
if (entry.pricing.currency !== MPP_PROFILE.assetSymbol) {
|
|
2089
2201
|
throw new CommerceError(
|
|
2090
2202
|
"CONFIG_INVALID",
|
|
2091
|
-
`Resource "${id}"
|
|
2092
|
-
{ details: { path: `resources.${id}.
|
|
2203
|
+
`Resource "${id}" is priced in ${entry.pricing.currency}, but MPP charges ${MPP_PROFILE.assetSymbol}`,
|
|
2204
|
+
{ details: { path: `resources.${id}.pricing.currency`, resourceId: id } }
|
|
2093
2205
|
);
|
|
2094
2206
|
}
|
|
2207
|
+
validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
|
|
2095
2208
|
}
|
|
2096
2209
|
}
|
|
2097
2210
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
@@ -2233,7 +2346,7 @@ function validateResourceSchemaKeywords(id, path2, schema) {
|
|
|
2233
2346
|
}
|
|
2234
2347
|
var PRICING_AMOUNT_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
2235
2348
|
var ZERO_AMOUNT_PATTERN = /^0(?:\.0+)?$/;
|
|
2236
|
-
function validatePricingAmount(id, amount,
|
|
2349
|
+
function validatePricingAmount(id, amount, decimals) {
|
|
2237
2350
|
const path2 = `resources.${id}.pricing.amount`;
|
|
2238
2351
|
if (!PRICING_AMOUNT_PATTERN.test(amount)) {
|
|
2239
2352
|
throw new CommerceError(
|
|
@@ -2249,12 +2362,12 @@ function validatePricingAmount(id, amount, x402) {
|
|
|
2249
2362
|
{ details: { path: path2, resourceId: id } }
|
|
2250
2363
|
);
|
|
2251
2364
|
}
|
|
2252
|
-
if (
|
|
2365
|
+
if (decimals !== void 0) {
|
|
2253
2366
|
const fractionalDigits = amount.includes(".") ? amount.split(".")[1]?.length ?? 0 : 0;
|
|
2254
|
-
if (fractionalDigits >
|
|
2367
|
+
if (fractionalDigits > decimals) {
|
|
2255
2368
|
throw new CommerceError(
|
|
2256
2369
|
"CONFIG_INVALID",
|
|
2257
|
-
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${
|
|
2370
|
+
`Resource "${id}" has pricing.amount "${amount}" with more precision (${fractionalDigits} fractional digits) than the configured asset supports (${decimals} decimals)`,
|
|
2258
2371
|
{ details: { path: path2, resourceId: id } }
|
|
2259
2372
|
);
|
|
2260
2373
|
}
|
|
@@ -2513,6 +2626,7 @@ var SECRET_FIELD_NAMES = [
|
|
|
2513
2626
|
"seed",
|
|
2514
2627
|
"mnemonic",
|
|
2515
2628
|
"secret",
|
|
2629
|
+
"challengeSecret",
|
|
2516
2630
|
"apiKey",
|
|
2517
2631
|
"adminToken",
|
|
2518
2632
|
"token"
|
|
@@ -2578,7 +2692,7 @@ function wrapPino(instance) {
|
|
|
2578
2692
|
}
|
|
2579
2693
|
var CORS_METHODS = "GET,POST,DELETE,OPTIONS";
|
|
2580
2694
|
var CORS_HEADERS = "content-type,payment-signature,x-request-id,authorization";
|
|
2581
|
-
var CORS_EXPOSED_HEADERS = "payment-required,payment-response,x-request-id";
|
|
2695
|
+
var CORS_EXPOSED_HEADERS = "payment-required,payment-response,www-authenticate,payment-receipt,x-request-id";
|
|
2582
2696
|
function buildAccessControlHook(options) {
|
|
2583
2697
|
const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
|
|
2584
2698
|
const publicHostname = hostnameOf(options.publicBaseUrl);
|
|
@@ -2991,7 +3105,7 @@ async function buildWellKnownDocument(options) {
|
|
|
2991
3105
|
return { ...runtime.adapter.descriptor, health: publicHealth(health) };
|
|
2992
3106
|
})
|
|
2993
3107
|
);
|
|
2994
|
-
const x402 = options.config.payments
|
|
3108
|
+
const { x402, mpp } = options.config.payments;
|
|
2995
3109
|
return {
|
|
2996
3110
|
gateway: {
|
|
2997
3111
|
implementationVersion: PACKAGE_VERSION,
|
|
@@ -3027,6 +3141,22 @@ async function buildWellKnownDocument(options) {
|
|
|
3027
3141
|
x402.facilitator.mode
|
|
3028
3142
|
)
|
|
3029
3143
|
}
|
|
3144
|
+
} : {},
|
|
3145
|
+
...mpp !== void 0 ? {
|
|
3146
|
+
mpp: {
|
|
3147
|
+
enabled: mpp.enabled,
|
|
3148
|
+
network: mpp.network,
|
|
3149
|
+
asset: mpp.asset,
|
|
3150
|
+
assetName: mpp.assetName,
|
|
3151
|
+
assetVersion: mpp.assetVersion,
|
|
3152
|
+
assetDecimals: MPP_PROFILE.assetDecimals,
|
|
3153
|
+
recipient: mpp.recipient,
|
|
3154
|
+
facilitator: { mode: mpp.facilitator.mode },
|
|
3155
|
+
mode: resolveDeploymentMode(
|
|
3156
|
+
requireNetworkProfile(mpp.network, "payments.mpp.network"),
|
|
3157
|
+
mpp.facilitator.mode
|
|
3158
|
+
)
|
|
3159
|
+
}
|
|
3030
3160
|
} : {}
|
|
3031
3161
|
}
|
|
3032
3162
|
};
|
|
@@ -3148,9 +3278,8 @@ async function handleInvoke(request, reply, options) {
|
|
|
3148
3278
|
resourceId
|
|
3149
3279
|
});
|
|
3150
3280
|
}
|
|
3151
|
-
const paymentHeader = request.headers[PAYMENT_HEADER];
|
|
3152
|
-
const paymentValue = Array.isArray(paymentHeader) ? paymentHeader[0] : paymentHeader;
|
|
3153
3281
|
const paymentMethod = resource.paymentMethods[0];
|
|
3282
|
+
const paymentValue = paymentProof(request, paymentMethod);
|
|
3154
3283
|
const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
|
|
3155
3284
|
const authorization = parseAuthorizationHeader(
|
|
3156
3285
|
request.headers[AUTHORIZATION_HEADER],
|
|
@@ -3168,7 +3297,10 @@ async function handleInvoke(request, reply, options) {
|
|
|
3168
3297
|
const outcome = await options.pipeline.execute(canonicalRequest);
|
|
3169
3298
|
if (outcome.kind === "payment-required") {
|
|
3170
3299
|
const envelope = toPaymentRequiredEnvelope(outcome);
|
|
3171
|
-
if (envelope.payment.
|
|
3300
|
+
if (envelope.payment.provider === "mpp") {
|
|
3301
|
+
const challenge = envelope.payment.envelope?.["wwwAuthenticate"];
|
|
3302
|
+
if (typeof challenge === "string") reply.header("www-authenticate", challenge);
|
|
3303
|
+
} else if (envelope.payment.envelope !== void 0) {
|
|
3172
3304
|
reply.header(PAYMENT_REQUIRED_HEADER, encodeHeaderDocument(envelope.payment.envelope));
|
|
3173
3305
|
}
|
|
3174
3306
|
reply.header("cache-control", "no-store");
|
|
@@ -3177,6 +3309,10 @@ async function handleInvoke(request, reply, options) {
|
|
|
3177
3309
|
}
|
|
3178
3310
|
if (outcome.payment) {
|
|
3179
3311
|
reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(outcome.payment));
|
|
3312
|
+
const receipt = outcome.payment.metadata?.["receipt"];
|
|
3313
|
+
if (outcome.payment.provider === "mpp" && typeof receipt === "string") {
|
|
3314
|
+
reply.header("payment-receipt", receipt);
|
|
3315
|
+
}
|
|
3180
3316
|
}
|
|
3181
3317
|
reply.status(outcome.backendStatus).send(outcome.body);
|
|
3182
3318
|
} catch (error) {
|
|
@@ -3184,15 +3320,31 @@ async function handleInvoke(request, reply, options) {
|
|
|
3184
3320
|
const settledPayment = errorPaymentSummary(commerceError);
|
|
3185
3321
|
if (settledPayment !== void 0) {
|
|
3186
3322
|
reply.header(PAYMENT_RESPONSE_HEADER, encodePaymentSummary(settledPayment));
|
|
3323
|
+
if (settledPayment.provider === "mpp" && settledPayment.receipt !== void 0) {
|
|
3324
|
+
reply.header("payment-receipt", settledPayment.receipt);
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
const challenge = commerceError.details?.["challenge"];
|
|
3328
|
+
if (commerceError.code === "PAYMENT_INVALID" && typeof challenge?.["wwwAuthenticate"] === "string") {
|
|
3329
|
+
reply.header("www-authenticate", challenge["wwwAuthenticate"]);
|
|
3330
|
+
reply.header("cache-control", "no-store");
|
|
3187
3331
|
}
|
|
3188
3332
|
reply.status(commerceError.httpStatus).send(toErrorEnvelope(commerceError));
|
|
3189
3333
|
}
|
|
3190
3334
|
}
|
|
3335
|
+
function paymentProof(request, method) {
|
|
3336
|
+
if (method === "mpp") {
|
|
3337
|
+
const value2 = request.headers.authorization;
|
|
3338
|
+
return value2 !== void 0 && /^Payment\s+\S/i.test(value2) ? value2 : void 0;
|
|
3339
|
+
}
|
|
3340
|
+
const value = request.headers[PAYMENT_HEADER];
|
|
3341
|
+
return Array.isArray(value) ? value[0] : value;
|
|
3342
|
+
}
|
|
3191
3343
|
function errorPaymentSummary(error) {
|
|
3192
3344
|
const payment = error.details?.["payment"];
|
|
3193
3345
|
if (typeof payment !== "object" || payment === null) return void 0;
|
|
3194
3346
|
const rec = payment;
|
|
3195
|
-
const { status, provider, amount, currency, network, externalReference } = rec;
|
|
3347
|
+
const { status, provider, amount, currency, network, externalReference, receipt } = rec;
|
|
3196
3348
|
if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
|
|
3197
3349
|
return void 0;
|
|
3198
3350
|
}
|
|
@@ -3202,7 +3354,8 @@ function errorPaymentSummary(error) {
|
|
|
3202
3354
|
amount,
|
|
3203
3355
|
currency,
|
|
3204
3356
|
...typeof network === "string" ? { network } : {},
|
|
3205
|
-
...typeof externalReference === "string" ? { externalReference } : {}
|
|
3357
|
+
...typeof externalReference === "string" ? { externalReference } : {},
|
|
3358
|
+
...typeof receipt === "string" ? { receipt } : {}
|
|
3206
3359
|
};
|
|
3207
3360
|
}
|
|
3208
3361
|
function encodeHeaderDocument(document) {
|
|
@@ -3244,13 +3397,25 @@ function parseLimit(value) {
|
|
|
3244
3397
|
// src/gateway/server.ts
|
|
3245
3398
|
var REQUEST_ID_HEADER = "x-request-id";
|
|
3246
3399
|
var CLIENT_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
3400
|
+
function withProviderBackedMethodsFirst(resource, providers) {
|
|
3401
|
+
const backed = (method) => providers.some((provider) => provider.name === method);
|
|
3402
|
+
const ordered = [
|
|
3403
|
+
...resource.paymentMethods.filter(backed),
|
|
3404
|
+
...resource.paymentMethods.filter((method) => !backed(method))
|
|
3405
|
+
];
|
|
3406
|
+
return ordered.some((method, i) => method !== resource.paymentMethods[i]) ? { ...resource, paymentMethods: ordered } : resource;
|
|
3407
|
+
}
|
|
3247
3408
|
async function createGateway(options) {
|
|
3248
3409
|
const clock = options.clock ?? systemClock;
|
|
3249
3410
|
const ids = options.ids ?? createDefaultIdGenerator();
|
|
3250
3411
|
const logger = options.logger ?? createGatewayLogger({ name: options.config.merchant.id }).core;
|
|
3251
3412
|
const backend = options.backend ?? new HttpBackendExecutor({ logger });
|
|
3252
3413
|
const authorizationProviders = options.authorizationProviders ?? [];
|
|
3253
|
-
const resources = createResourceRegistry(
|
|
3414
|
+
const resources = createResourceRegistry(
|
|
3415
|
+
options.config.resources.map(
|
|
3416
|
+
(resource) => withProviderBackedMethodsFirst(resource, options.paymentProviders)
|
|
3417
|
+
)
|
|
3418
|
+
);
|
|
3254
3419
|
const eventBus = createEventBus({ store: options.store, logger });
|
|
3255
3420
|
const pipeline = createExecutionPipeline({
|
|
3256
3421
|
resources,
|
|
@@ -3334,13 +3499,14 @@ async function createGateway(options) {
|
|
|
3334
3499
|
|
|
3335
3500
|
// src/protocols/acp/checkout-mapping.ts
|
|
3336
3501
|
function toCanonicalRequest(options) {
|
|
3337
|
-
const { request, resourceId, requestId, receivedAt } = options;
|
|
3502
|
+
const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
|
|
3338
3503
|
return {
|
|
3339
3504
|
requestId,
|
|
3340
3505
|
resourceId,
|
|
3341
3506
|
input: canonicalInput(request),
|
|
3342
3507
|
protocol: "acp",
|
|
3343
|
-
receivedAt
|
|
3508
|
+
receivedAt,
|
|
3509
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
3344
3510
|
};
|
|
3345
3511
|
}
|
|
3346
3512
|
function canonicalInput(request) {
|
|
@@ -3448,8 +3614,8 @@ function writeAcpJson(res, status, body, headers = {}) {
|
|
|
3448
3614
|
function writeAcpFailure(res, failure, headers = {}) {
|
|
3449
3615
|
writeAcpJson(res, failure.status, failure.error, headers);
|
|
3450
3616
|
}
|
|
3451
|
-
function
|
|
3452
|
-
return sha256(`acp-
|
|
3617
|
+
function operationKey(scope) {
|
|
3618
|
+
return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
3453
3619
|
}
|
|
3454
3620
|
function requestFingerprint(body) {
|
|
3455
3621
|
return sha256(canonicalize(body));
|
|
@@ -3468,6 +3634,7 @@ function sha256(value) {
|
|
|
3468
3634
|
}
|
|
3469
3635
|
|
|
3470
3636
|
// src/protocols/acp/idempotency/store.ts
|
|
3637
|
+
var SCHEMA_VERSION = 2;
|
|
3471
3638
|
var ACP_MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
3472
3639
|
function createAcpIdempotencyStore(options) {
|
|
3473
3640
|
const logger = options.logger ?? NOOP_LOGGER;
|
|
@@ -3478,33 +3645,39 @@ function createAcpIdempotencyStore(options) {
|
|
|
3478
3645
|
label: "ACP idempotency database",
|
|
3479
3646
|
logger
|
|
3480
3647
|
});
|
|
3481
|
-
migrate(db);
|
|
3648
|
+
migrate(db, logger);
|
|
3482
3649
|
const selectStmt = db.prepare(
|
|
3483
3650
|
`SELECT state, fingerprint, status, body_json FROM acp_idempotency
|
|
3484
|
-
WHERE
|
|
3651
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3485
3652
|
);
|
|
3486
3653
|
const insertStmt = db.prepare(
|
|
3487
3654
|
`INSERT INTO acp_idempotency
|
|
3488
|
-
(
|
|
3489
|
-
VALUES (@
|
|
3655
|
+
(deployment, endpoint, idempotency_key, fingerprint, state, created_at, expires_at)
|
|
3656
|
+
VALUES (@deployment, @endpoint, @idempotency_key, @fingerprint, 'in_flight', @created_at, @expires_at)`
|
|
3490
3657
|
);
|
|
3491
3658
|
const completeStmt = db.prepare(
|
|
3492
3659
|
`UPDATE acp_idempotency SET state = 'completed', status = @status, body_json = @body_json
|
|
3493
|
-
WHERE
|
|
3660
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3494
3661
|
);
|
|
3495
3662
|
const deleteStmt = db.prepare(
|
|
3496
3663
|
`DELETE FROM acp_idempotency
|
|
3497
|
-
WHERE
|
|
3664
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3665
|
+
);
|
|
3666
|
+
const unresolvedStmt = db.prepare(
|
|
3667
|
+
`UPDATE acp_idempotency SET state = 'unresolved'
|
|
3668
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3669
|
+
);
|
|
3670
|
+
const expireStmt = db.prepare(
|
|
3671
|
+
"DELETE FROM acp_idempotency WHERE expires_at <= ? AND state = 'completed'"
|
|
3498
3672
|
);
|
|
3499
|
-
const expireStmt = db.prepare("DELETE FROM acp_idempotency WHERE expires_at <= ?");
|
|
3500
3673
|
const claimTx = db.transaction(
|
|
3501
3674
|
(scope, fingerprint) => {
|
|
3502
3675
|
const at = now();
|
|
3503
3676
|
expireStmt.run(new Date(at).toISOString());
|
|
3504
|
-
const existing = selectStmt.get(scope.
|
|
3677
|
+
const existing = selectStmt.get(scope.deployment, scope.endpoint, scope.key);
|
|
3505
3678
|
if (existing === void 0) {
|
|
3506
3679
|
insertStmt.run({
|
|
3507
|
-
|
|
3680
|
+
deployment: scope.deployment,
|
|
3508
3681
|
endpoint: scope.endpoint,
|
|
3509
3682
|
idempotency_key: scope.key,
|
|
3510
3683
|
fingerprint,
|
|
@@ -3514,6 +3687,7 @@ function createAcpIdempotencyStore(options) {
|
|
|
3514
3687
|
return { kind: "reserved" };
|
|
3515
3688
|
}
|
|
3516
3689
|
if (existing.fingerprint !== fingerprint) return { kind: "conflict" };
|
|
3690
|
+
if (existing.state === "unresolved") return { kind: "unresolved" };
|
|
3517
3691
|
if (existing.state !== "completed") return { kind: "in-flight" };
|
|
3518
3692
|
return {
|
|
3519
3693
|
kind: "replay",
|
|
@@ -3529,15 +3703,22 @@ function createAcpIdempotencyStore(options) {
|
|
|
3529
3703
|
},
|
|
3530
3704
|
complete(scope, response) {
|
|
3531
3705
|
completeStmt.run({
|
|
3532
|
-
|
|
3706
|
+
deployment: scope.deployment,
|
|
3533
3707
|
endpoint: scope.endpoint,
|
|
3534
3708
|
idempotency_key: scope.key,
|
|
3535
3709
|
status: response.status,
|
|
3536
3710
|
body_json: JSON.stringify(response.body ?? null)
|
|
3537
3711
|
});
|
|
3538
3712
|
},
|
|
3713
|
+
markUnresolved(scope) {
|
|
3714
|
+
unresolvedStmt.run({
|
|
3715
|
+
deployment: scope.deployment,
|
|
3716
|
+
endpoint: scope.endpoint,
|
|
3717
|
+
idempotency_key: scope.key
|
|
3718
|
+
});
|
|
3719
|
+
},
|
|
3539
3720
|
release(scope) {
|
|
3540
|
-
deleteStmt.run(scope.
|
|
3721
|
+
deleteStmt.run(scope.deployment, scope.endpoint, scope.key);
|
|
3541
3722
|
},
|
|
3542
3723
|
close() {
|
|
3543
3724
|
if (closed) return;
|
|
@@ -3546,13 +3727,23 @@ function createAcpIdempotencyStore(options) {
|
|
|
3546
3727
|
}
|
|
3547
3728
|
};
|
|
3548
3729
|
}
|
|
3549
|
-
function migrate(db) {
|
|
3730
|
+
function migrate(db, logger) {
|
|
3550
3731
|
const current = db.pragma("user_version", { simple: true });
|
|
3551
|
-
if (current >=
|
|
3732
|
+
if (current >= SCHEMA_VERSION) return;
|
|
3552
3733
|
const apply = db.transaction(() => {
|
|
3734
|
+
if (current === 1) {
|
|
3735
|
+
const carried = db.prepare("SELECT COUNT(*) AS count FROM acp_idempotency WHERE state != 'completed'").get();
|
|
3736
|
+
if (carried.count > 0) {
|
|
3737
|
+
logger.warn(
|
|
3738
|
+
{ unresolved: carried.count },
|
|
3739
|
+
"acp idempotency: schema v1 -> v2 discards claims keyed by the old bearer-token digest; reconcile these operations against the merchant before trusting a retry"
|
|
3740
|
+
);
|
|
3741
|
+
}
|
|
3742
|
+
db.exec("DROP TABLE acp_idempotency");
|
|
3743
|
+
}
|
|
3553
3744
|
db.exec(`
|
|
3554
3745
|
CREATE TABLE IF NOT EXISTS acp_idempotency (
|
|
3555
|
-
|
|
3746
|
+
deployment TEXT NOT NULL,
|
|
3556
3747
|
endpoint TEXT NOT NULL,
|
|
3557
3748
|
idempotency_key TEXT NOT NULL,
|
|
3558
3749
|
fingerprint TEXT NOT NULL,
|
|
@@ -3561,12 +3752,12 @@ function migrate(db) {
|
|
|
3561
3752
|
body_json TEXT,
|
|
3562
3753
|
created_at TEXT NOT NULL,
|
|
3563
3754
|
expires_at TEXT NOT NULL,
|
|
3564
|
-
PRIMARY KEY (
|
|
3755
|
+
PRIMARY KEY (deployment, endpoint, idempotency_key)
|
|
3565
3756
|
);
|
|
3566
3757
|
CREATE INDEX IF NOT EXISTS idx_acp_idempotency_expires_at
|
|
3567
3758
|
ON acp_idempotency(expires_at);
|
|
3568
3759
|
`);
|
|
3569
|
-
db.pragma(
|
|
3760
|
+
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
3570
3761
|
});
|
|
3571
3762
|
apply();
|
|
3572
3763
|
}
|
|
@@ -7974,6 +8165,28 @@ function asResponse(failure) {
|
|
|
7974
8165
|
|
|
7975
8166
|
// src/protocols/acp/adapter.ts
|
|
7976
8167
|
var DISCOVERY_CACHE_CONTROL = "public, max-age=3600";
|
|
8168
|
+
var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
8169
|
+
"CONFIG_INVALID",
|
|
8170
|
+
"RESOURCE_NOT_FOUND",
|
|
8171
|
+
"INPUT_INVALID",
|
|
8172
|
+
"PROTOCOL_UNSUPPORTED",
|
|
8173
|
+
"GATEWAY_BUSY",
|
|
8174
|
+
"PAYMENT_REQUIRED",
|
|
8175
|
+
"PAYMENT_INVALID",
|
|
8176
|
+
"PAYMENT_REPLAYED",
|
|
8177
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
8178
|
+
"PAYMENT_SETTLEMENT_FAILED",
|
|
8179
|
+
"AUTHORIZATION_REQUIRED",
|
|
8180
|
+
"AUTHORIZATION_INVALID",
|
|
8181
|
+
"AUTHORIZATION_REPLAYED",
|
|
8182
|
+
"AUTHORIZATION_PROVIDER_UNAVAILABLE"
|
|
8183
|
+
]);
|
|
8184
|
+
function reachedFor(code) {
|
|
8185
|
+
return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
|
|
8186
|
+
}
|
|
8187
|
+
function notReached(failure) {
|
|
8188
|
+
return { response: asResponse(failure), reached: "no" };
|
|
8189
|
+
}
|
|
7977
8190
|
var AcpProtocolAdapter = class {
|
|
7978
8191
|
name = "acp";
|
|
7979
8192
|
mountPath;
|
|
@@ -7982,8 +8195,14 @@ var AcpProtocolAdapter = class {
|
|
|
7982
8195
|
token;
|
|
7983
8196
|
operations;
|
|
7984
8197
|
idempotencyOptions;
|
|
7985
|
-
/**
|
|
7986
|
-
|
|
8198
|
+
/**
|
|
8199
|
+
* What an idempotency claim is scoped to, from `context.publicBaseUrl`.
|
|
8200
|
+
*
|
|
8201
|
+
* Not readonly because, unlike the token, it is known only once the adapter
|
|
8202
|
+
* starts. Worth that: scoping by the credential meant a rotation freed
|
|
8203
|
+
* every outstanding claim.
|
|
8204
|
+
*/
|
|
8205
|
+
deployment;
|
|
7987
8206
|
discoveryMetadata;
|
|
7988
8207
|
idempotency;
|
|
7989
8208
|
resources = /* @__PURE__ */ new Map();
|
|
@@ -7998,7 +8217,6 @@ var AcpProtocolAdapter = class {
|
|
|
7998
8217
|
this.token = options.token;
|
|
7999
8218
|
this.operations = options.operations;
|
|
8000
8219
|
this.idempotencyOptions = options.idempotency;
|
|
8001
|
-
this.identityHash = identityHash(options.token);
|
|
8002
8220
|
this.discoveryMetadata = options.discovery;
|
|
8003
8221
|
this.descriptor = buildDescriptor(PACKAGE_VERSION);
|
|
8004
8222
|
this.additionalHttpRoutes = [
|
|
@@ -8042,6 +8260,7 @@ var AcpProtocolAdapter = class {
|
|
|
8042
8260
|
resolved.set(operation, resource);
|
|
8043
8261
|
}
|
|
8044
8262
|
this.resources = resolved;
|
|
8263
|
+
this.deployment = context.publicBaseUrl;
|
|
8045
8264
|
this.idempotency = createAcpIdempotencyStore({
|
|
8046
8265
|
path: this.idempotencyOptions.path,
|
|
8047
8266
|
retentionHours: this.idempotencyOptions.retentionHours,
|
|
@@ -8130,22 +8349,33 @@ var AcpProtocolAdapter = class {
|
|
|
8130
8349
|
*/
|
|
8131
8350
|
async dispatch(request) {
|
|
8132
8351
|
const store = this.idempotency;
|
|
8352
|
+
const deployment = this.deployment;
|
|
8133
8353
|
const key = request.idempotencyKey;
|
|
8134
|
-
if (store === void 0 || key === void 0)
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
key
|
|
8139
|
-
};
|
|
8354
|
+
if (store === void 0 || deployment === void 0 || key === void 0) {
|
|
8355
|
+
return (await this.runCheckout(request)).response;
|
|
8356
|
+
}
|
|
8357
|
+
const scope = { deployment, endpoint: request.route.path, key };
|
|
8140
8358
|
const claim = store.claim(scope, requestFingerprint(request.body));
|
|
8141
8359
|
switch (claim.kind) {
|
|
8142
8360
|
case "in-flight":
|
|
8361
|
+
return {
|
|
8362
|
+
...asResponse(
|
|
8363
|
+
acpFailure(
|
|
8364
|
+
409,
|
|
8365
|
+
"invalid_request",
|
|
8366
|
+
"idempotency_in_flight",
|
|
8367
|
+
"A request with this Idempotency-Key is still being processed."
|
|
8368
|
+
)
|
|
8369
|
+
),
|
|
8370
|
+
retryAfterSeconds: ACP_IN_FLIGHT_RETRY_AFTER_SECONDS
|
|
8371
|
+
};
|
|
8372
|
+
case "unresolved":
|
|
8143
8373
|
return asResponse(
|
|
8144
8374
|
acpFailure(
|
|
8145
8375
|
409,
|
|
8146
|
-
"
|
|
8147
|
-
"
|
|
8148
|
-
"
|
|
8376
|
+
"processing_error",
|
|
8377
|
+
"idempotency_unresolved",
|
|
8378
|
+
"An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
|
|
8149
8379
|
)
|
|
8150
8380
|
);
|
|
8151
8381
|
case "conflict":
|
|
@@ -8160,16 +8390,21 @@ var AcpProtocolAdapter = class {
|
|
|
8160
8390
|
case "replay":
|
|
8161
8391
|
return { status: claim.status, body: claim.body, replayed: true };
|
|
8162
8392
|
}
|
|
8163
|
-
let
|
|
8393
|
+
let outcome;
|
|
8164
8394
|
try {
|
|
8165
|
-
|
|
8395
|
+
outcome = await this.runCheckout(request, operationKey(scope));
|
|
8166
8396
|
} catch (err) {
|
|
8167
|
-
store.
|
|
8397
|
+
store.markUnresolved(scope);
|
|
8168
8398
|
throw err;
|
|
8169
8399
|
}
|
|
8170
|
-
if (response.status
|
|
8171
|
-
|
|
8172
|
-
|
|
8400
|
+
if (outcome.response.status < 500) {
|
|
8401
|
+
store.complete(scope, outcome.response);
|
|
8402
|
+
} else if (outcome.reached === "no") {
|
|
8403
|
+
store.release(scope);
|
|
8404
|
+
} else {
|
|
8405
|
+
store.markUnresolved(scope);
|
|
8406
|
+
}
|
|
8407
|
+
return outcome.response;
|
|
8173
8408
|
}
|
|
8174
8409
|
/**
|
|
8175
8410
|
* One accepted request, one `pipeline.execute()`.
|
|
@@ -8178,11 +8413,11 @@ var AcpProtocolAdapter = class {
|
|
|
8178
8413
|
* merchant backend: the adapter builds a canonical request and reads back
|
|
8179
8414
|
* what the pipeline decided.
|
|
8180
8415
|
*/
|
|
8181
|
-
async runCheckout(request) {
|
|
8416
|
+
async runCheckout(request, idempotencyKey) {
|
|
8182
8417
|
const context = this.context;
|
|
8183
8418
|
const resource = this.resources.get(request.route.operation);
|
|
8184
8419
|
if (context === void 0 || resource === void 0) {
|
|
8185
|
-
return
|
|
8420
|
+
return notReached(
|
|
8186
8421
|
acpFailure(
|
|
8187
8422
|
503,
|
|
8188
8423
|
"service_unavailable",
|
|
@@ -8195,7 +8430,8 @@ var AcpProtocolAdapter = class {
|
|
|
8195
8430
|
request,
|
|
8196
8431
|
resourceId: resource.id,
|
|
8197
8432
|
requestId: context.ids.next("acp"),
|
|
8198
|
-
receivedAt: context.clock.nowIso()
|
|
8433
|
+
receivedAt: context.clock.nowIso(),
|
|
8434
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
8199
8435
|
});
|
|
8200
8436
|
try {
|
|
8201
8437
|
const outcome = await context.pipeline.execute(canonical);
|
|
@@ -8204,7 +8440,7 @@ var AcpProtocolAdapter = class {
|
|
|
8204
8440
|
{ resourceId: resource.id, requestId: canonical.requestId },
|
|
8205
8441
|
"acp adapter: mapped checkout resource returned payment-required - it must be priced free"
|
|
8206
8442
|
);
|
|
8207
|
-
return
|
|
8443
|
+
return notReached(
|
|
8208
8444
|
acpFailure(
|
|
8209
8445
|
500,
|
|
8210
8446
|
"processing_error",
|
|
@@ -8220,14 +8456,22 @@ var AcpProtocolAdapter = class {
|
|
|
8220
8456
|
"acp adapter: refusing to forward a non-conformant merchant response"
|
|
8221
8457
|
);
|
|
8222
8458
|
}
|
|
8223
|
-
return mapped.response;
|
|
8459
|
+
return { response: mapped.response, reached: "yes" };
|
|
8224
8460
|
} catch (err) {
|
|
8225
8461
|
const error = toCommerceError(err);
|
|
8226
8462
|
context.logger.warn(
|
|
8227
|
-
{
|
|
8463
|
+
{
|
|
8464
|
+
resourceId: resource.id,
|
|
8465
|
+
requestId: canonical.requestId,
|
|
8466
|
+
reached: reachedFor(error.code),
|
|
8467
|
+
err: error.toInfo()
|
|
8468
|
+
},
|
|
8228
8469
|
"acp adapter: checkout execution failed"
|
|
8229
8470
|
);
|
|
8230
|
-
return
|
|
8471
|
+
return {
|
|
8472
|
+
response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
|
|
8473
|
+
reached: reachedFor(error.code)
|
|
8474
|
+
};
|
|
8231
8475
|
}
|
|
8232
8476
|
}
|
|
8233
8477
|
/**
|
|
@@ -8240,7 +8484,7 @@ var AcpProtocolAdapter = class {
|
|
|
8240
8484
|
...request.requestId !== void 0 ? { [ACP_REQUEST_ID_HEADER]: request.requestId } : {},
|
|
8241
8485
|
...request.idempotencyKey !== void 0 ? { [ACP_IDEMPOTENCY_KEY_HEADER]: request.idempotencyKey } : {},
|
|
8242
8486
|
...replayed ? { [ACP_IDEMPOTENT_REPLAYED_HEADER]: "true" } : {},
|
|
8243
|
-
...response?.
|
|
8487
|
+
...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
|
|
8244
8488
|
};
|
|
8245
8489
|
}
|
|
8246
8490
|
fail(res, err, message) {
|
|
@@ -8267,6 +8511,7 @@ var AcpProtocolAdapter = class {
|
|
|
8267
8511
|
this.resources = /* @__PURE__ */ new Map();
|
|
8268
8512
|
this.idempotency?.close();
|
|
8269
8513
|
this.idempotency = void 0;
|
|
8514
|
+
this.deployment = void 0;
|
|
8270
8515
|
this.context = void 0;
|
|
8271
8516
|
}
|
|
8272
8517
|
};
|
|
@@ -8443,7 +8688,7 @@ var MIGRATIONS = [
|
|
|
8443
8688
|
}
|
|
8444
8689
|
}
|
|
8445
8690
|
];
|
|
8446
|
-
var
|
|
8691
|
+
var SCHEMA_VERSION2 = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
8447
8692
|
function migrate2(db) {
|
|
8448
8693
|
const current = db.pragma("user_version", { simple: true });
|
|
8449
8694
|
for (const migration of MIGRATIONS) {
|
|
@@ -8529,17 +8774,17 @@ function createSqliteReceiptStore(options) {
|
|
|
8529
8774
|
name: "sqlite-receipt-store",
|
|
8530
8775
|
kind: "storage",
|
|
8531
8776
|
implementationVersion: PACKAGE_VERSION,
|
|
8532
|
-
supportedSpec: `sqlite-schema-v${
|
|
8777
|
+
supportedSpec: `sqlite-schema-v${SCHEMA_VERSION2}`,
|
|
8533
8778
|
capabilities: ["receipts", "events", "payment-attempts"],
|
|
8534
8779
|
status: "stable"
|
|
8535
8780
|
};
|
|
8536
8781
|
const store = {
|
|
8537
8782
|
async init() {
|
|
8538
8783
|
const version = db.pragma("user_version", { simple: true });
|
|
8539
|
-
if (version !==
|
|
8784
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8540
8785
|
throw new CommerceError(
|
|
8541
8786
|
"STORAGE_ERROR",
|
|
8542
|
-
`Receipt store schema version mismatch (found ${version}, expected ${
|
|
8787
|
+
`Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION2})`
|
|
8543
8788
|
);
|
|
8544
8789
|
}
|
|
8545
8790
|
},
|
|
@@ -8673,9 +8918,9 @@ function createSqliteReceiptStore(options) {
|
|
|
8673
8918
|
}
|
|
8674
8919
|
try {
|
|
8675
8920
|
const version = db.pragma("user_version", { simple: true });
|
|
8676
|
-
if (version !==
|
|
8921
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8677
8922
|
logger.warn(
|
|
8678
|
-
{ found: version, expected:
|
|
8923
|
+
{ found: version, expected: SCHEMA_VERSION2 },
|
|
8679
8924
|
"receipt-store: health check failed (schema version mismatch)"
|
|
8680
8925
|
);
|
|
8681
8926
|
return {
|
|
@@ -8687,7 +8932,7 @@ function createSqliteReceiptStore(options) {
|
|
|
8687
8932
|
}
|
|
8688
8933
|
return {
|
|
8689
8934
|
status: "pass",
|
|
8690
|
-
detail: `sqlite schema v${
|
|
8935
|
+
detail: `sqlite schema v${SCHEMA_VERSION2} writable`,
|
|
8691
8936
|
checkedAt: clock.nowIso(),
|
|
8692
8937
|
durationMs: clock.monotonicMs() - startedAt
|
|
8693
8938
|
};
|