@devlab.group/agent-commerce 1.6.0 → 2.0.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/LICENSE +187 -2
- package/README.md +6 -6
- package/dist/ap2.d.ts +2 -2
- package/dist/ap2.js +94 -28
- package/dist/ap2.js.map +1 -1
- package/dist/{chunk-V2V2E7HX.js → chunk-3LUJHVKX.js} +4 -4
- package/dist/{chunk-V2V2E7HX.js.map → chunk-3LUJHVKX.js.map} +1 -1
- package/dist/{chunk-IVJC5YLE.js → chunk-53WXXIPR.js} +3 -3
- package/dist/{chunk-IVJC5YLE.js.map → chunk-53WXXIPR.js.map} +1 -1
- package/dist/chunk-CKJRITWY.js +113 -0
- package/dist/chunk-CKJRITWY.js.map +1 -0
- package/dist/chunk-HOZA3IWP.js +115 -0
- package/dist/chunk-HOZA3IWP.js.map +1 -0
- package/dist/{chunk-ATTWSFQP.js → chunk-RKBGCPCK.js} +78 -25
- package/dist/chunk-RKBGCPCK.js.map +1 -0
- package/dist/{chunk-RAJGPGJG.js → chunk-SYEGCNJZ.js} +3 -3
- package/dist/{chunk-RAJGPGJG.js.map → chunk-SYEGCNJZ.js.map} +1 -1
- package/dist/{chunk-74BSEXB5.js → chunk-TWRUNWDR.js} +11 -4
- package/dist/chunk-TWRUNWDR.js.map +1 -0
- package/dist/cli/index.js +143 -34
- package/dist/cli/index.js.map +1 -1
- package/dist/{constants-Bkq04FRZ.d.ts → constants-BSBRlKRR.d.ts} +2 -2
- package/dist/index.d.ts +42 -22
- package/dist/index.js +1254 -377
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +247 -94
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +6 -2
- package/dist/mpp.js +65 -28
- package/dist/mpp.js.map +1 -1
- package/dist/{guardrails-71XwtXlT.d.ts → payment-flow-DCS882tR.d.ts} +9 -1
- package/dist/{protocol-adapter-DTkrXTev.d.ts → protocol-adapter-DonxfAwa.d.ts} +2 -0
- package/dist/{types-O6lg3Q5V.d.ts → types-CVw6Sf46.d.ts} +11 -0
- package/dist/x402.d.ts +11 -3
- package/dist/x402.js +4 -4
- package/package.json +22 -17
- package/dist/chunk-74BSEXB5.js.map +0 -1
- package/dist/chunk-AFI2CEZS.js +0 -10
- package/dist/chunk-AFI2CEZS.js.map +0 -1
- package/dist/chunk-ATTWSFQP.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
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 { MCP_TOOL_NAME_PATTERN } from './chunk-
|
|
3
|
-
import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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-SYEGCNJZ.js';
|
|
2
|
+
import { MCP_TOOL_NAME_PATTERN, readCappedBody, createBearerCheck, settlementResponse, joinUrl, settlementResponseFromDetails, x402ErrorCode, settlementFailure } from './chunk-HOZA3IWP.js';
|
|
3
|
+
import { MPP_DEFAULT_NETWORK, isMppNetwork, MPP_NETWORKS, MPP_MIN_CHALLENGE_SECRET_LENGTH, MPP_PROFILE } from './chunk-3LUJHVKX.js';
|
|
4
|
+
import { contentDigest, CONTENT_DIGEST_METADATA_KEY, mppProblem, mppProblemDetail } from './chunk-CKJRITWY.js';
|
|
5
|
+
import { X402_PAYMENT_FLOWS, X402_DEFAULT_PAYMENT_FLOW, resolveX402Deployment, SETTLE_AFTER_BACKEND_METADATA_KEY, resolveDeploymentMode, requireNetworkProfile } from './chunk-TWRUNWDR.js';
|
|
6
|
+
import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_METHOD_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, isRecord, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, isCommerceError, describeError, isHttpProtocolAdapter, toLogInfo, extractReservedInputFields, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, toErrorEnvelope, DELIVERY_SUMMARY_META_KEY, toDeliverySummary, AUTHORIZATION_INPUT_FIELD, parseAuthorizationSubmission, PAYMENT_INPUT_FIELD } from './chunk-53WXXIPR.js';
|
|
7
|
+
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-53WXXIPR.js';
|
|
7
8
|
import { promises, accessSync, constants } from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import { parse, YAMLError } from 'yaml';
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
import { isDeepStrictEqual } from 'util';
|
|
12
13
|
import Fastify from 'fastify';
|
|
13
|
-
import { randomUUID,
|
|
14
|
+
import { randomUUID, createHash } from 'crypto';
|
|
14
15
|
import { createRequire } from 'module';
|
|
15
16
|
import pino from 'pino';
|
|
17
|
+
import { Transform } from 'stream';
|
|
16
18
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
17
19
|
import addFormats from 'ajv-formats';
|
|
18
20
|
|
|
@@ -85,8 +87,19 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
|
85
87
|
// src/core/execution/backend-http.ts
|
|
86
88
|
var MAX_BODY_SNIPPET_LENGTH = 512;
|
|
87
89
|
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
90
|
+
var NEVER_FORWARDED_HEADER = /^(authorization|cookie|set-cookie|host|content-length|content-type|connection|keep-alive|te|trailer|transfer-encoding|upgrade|idempotency-key|proxy-.*)$/;
|
|
88
91
|
var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
89
92
|
var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
|
|
93
|
+
var BackendErrorResponse = class {
|
|
94
|
+
constructor(status, body, headers = {}) {
|
|
95
|
+
this.status = status;
|
|
96
|
+
this.body = body;
|
|
97
|
+
this.headers = headers;
|
|
98
|
+
}
|
|
99
|
+
status;
|
|
100
|
+
body;
|
|
101
|
+
headers;
|
|
102
|
+
};
|
|
90
103
|
var HttpBackendExecutor = class {
|
|
91
104
|
fetchImpl;
|
|
92
105
|
logger;
|
|
@@ -117,7 +130,7 @@ var HttpBackendExecutor = class {
|
|
|
117
130
|
context
|
|
118
131
|
);
|
|
119
132
|
}
|
|
120
|
-
const headers = buildHeaders(handler, context);
|
|
133
|
+
const headers = buildHeaders(handler, context, request.backendHeaders);
|
|
121
134
|
if (request.idempotencyKey !== void 0) {
|
|
122
135
|
setHeader(headers, IDEMPOTENCY_KEY_HEADER, request.idempotencyKey, context);
|
|
123
136
|
}
|
|
@@ -205,7 +218,8 @@ var HttpBackendExecutor = class {
|
|
|
205
218
|
throw new CommerceError("BACKEND_ERROR", `Backend responded with status ${response.status}`, {
|
|
206
219
|
requestId: request.requestId,
|
|
207
220
|
resourceId: request.resourceId,
|
|
208
|
-
details: { status: response.status }
|
|
221
|
+
details: { status: response.status },
|
|
222
|
+
cause: new BackendErrorResponse(response.status, parsedBody, responseHeaders)
|
|
209
223
|
});
|
|
210
224
|
}
|
|
211
225
|
return {
|
|
@@ -226,10 +240,10 @@ function findUnparsedBraceToken(url) {
|
|
|
226
240
|
const token = /\{[^{}]*\}?|\}/.exec(residue.slice(index));
|
|
227
241
|
return token?.[0] ?? residue[index];
|
|
228
242
|
}
|
|
229
|
-
function validateBackendRequestShape(handler, input, context) {
|
|
243
|
+
function validateBackendRequestShape(handler, input, context, backendHeaders) {
|
|
230
244
|
const inputRecord = isRecord(input) ? input : {};
|
|
231
245
|
const parts = buildBackendRequestParts(handler, inputRecord, context);
|
|
232
|
-
buildHeaders(handler, context);
|
|
246
|
+
buildHeaders(handler, context, backendHeaders);
|
|
233
247
|
let target;
|
|
234
248
|
try {
|
|
235
249
|
target = new URL(parts.url);
|
|
@@ -321,12 +335,25 @@ function stringifyPrimitive(value) {
|
|
|
321
335
|
return String(value);
|
|
322
336
|
}
|
|
323
337
|
}
|
|
324
|
-
function buildHeaders(handler, context) {
|
|
338
|
+
function buildHeaders(handler, context, forwarded = {}) {
|
|
339
|
+
let headers;
|
|
325
340
|
try {
|
|
326
|
-
|
|
341
|
+
headers = new Headers(handler.headers);
|
|
327
342
|
} catch {
|
|
328
343
|
throw invalidHeaderError(context);
|
|
329
344
|
}
|
|
345
|
+
for (const [name, value] of Object.entries(forwarded)) {
|
|
346
|
+
if (NEVER_FORWARDED_HEADER.test(name.toLowerCase())) continue;
|
|
347
|
+
try {
|
|
348
|
+
if (!headers.has(name)) headers.set(name, value);
|
|
349
|
+
} catch {
|
|
350
|
+
throw new CommerceError("INPUT_INVALID", "A forwarded request header is invalid", {
|
|
351
|
+
...context,
|
|
352
|
+
details: { reason: "invalid-forwarded-header" }
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return headers;
|
|
330
357
|
}
|
|
331
358
|
function setHeader(headers, name, value, context) {
|
|
332
359
|
try {
|
|
@@ -645,6 +672,138 @@ function createExecutionPipeline(options) {
|
|
|
645
672
|
}
|
|
646
673
|
};
|
|
647
674
|
}
|
|
675
|
+
async function settle(args) {
|
|
676
|
+
const { request, resource, provider, requirement, payment, verification, replayKey, hold } = args;
|
|
677
|
+
let settlement;
|
|
678
|
+
try {
|
|
679
|
+
settlement = await provider.settle({
|
|
680
|
+
requestId: request.requestId,
|
|
681
|
+
resource,
|
|
682
|
+
requirement,
|
|
683
|
+
submission: payment,
|
|
684
|
+
verification
|
|
685
|
+
});
|
|
686
|
+
} catch (error) {
|
|
687
|
+
const txHash = settlementTxHash(error);
|
|
688
|
+
await hold?.finalize("markUncertain");
|
|
689
|
+
await safePersist(
|
|
690
|
+
() => options.store.updatePaymentAttempt({
|
|
691
|
+
replayKey,
|
|
692
|
+
status: "settlement-uncertain",
|
|
693
|
+
...txHash !== void 0 ? { externalReference: txHash } : {},
|
|
694
|
+
...error instanceof Error ? { rejectionReason: error.message } : {}
|
|
695
|
+
}),
|
|
696
|
+
"updatePaymentAttempt(settlement-uncertain)",
|
|
697
|
+
request.requestId
|
|
698
|
+
);
|
|
699
|
+
await safeEmit(
|
|
700
|
+
buildEvent({
|
|
701
|
+
type: "payment.rejected",
|
|
702
|
+
requestId: request.requestId,
|
|
703
|
+
resourceId: resource.id,
|
|
704
|
+
adapter: request.protocol,
|
|
705
|
+
paymentProvider: provider.name,
|
|
706
|
+
status: "error",
|
|
707
|
+
data: {
|
|
708
|
+
reason: "settlement-uncertain",
|
|
709
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
710
|
+
}
|
|
711
|
+
})
|
|
712
|
+
);
|
|
713
|
+
throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
|
|
714
|
+
requestId: request.requestId,
|
|
715
|
+
resourceId: resource.id,
|
|
716
|
+
cause: error,
|
|
717
|
+
details: {
|
|
718
|
+
settlementUncertain: true,
|
|
719
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {},
|
|
720
|
+
...settlementParties(requirement, verification)
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
if (settlement.status !== "settled") {
|
|
725
|
+
await hold?.finalize("release");
|
|
726
|
+
await safePersist(
|
|
727
|
+
() => options.store.updatePaymentAttempt({
|
|
728
|
+
replayKey,
|
|
729
|
+
status: "rejected",
|
|
730
|
+
...settlement.rejectionReason !== void 0 ? { rejectionReason: settlement.rejectionReason } : {}
|
|
731
|
+
}),
|
|
732
|
+
"updatePaymentAttempt(rejected)",
|
|
733
|
+
request.requestId
|
|
734
|
+
);
|
|
735
|
+
await safeEmit(
|
|
736
|
+
buildEvent({
|
|
737
|
+
type: "payment.rejected",
|
|
738
|
+
requestId: request.requestId,
|
|
739
|
+
resourceId: resource.id,
|
|
740
|
+
adapter: request.protocol,
|
|
741
|
+
paymentProvider: provider.name,
|
|
742
|
+
status: "error",
|
|
743
|
+
data: settlement.rejectionReason ? { reason: settlement.rejectionReason } : {}
|
|
744
|
+
})
|
|
745
|
+
);
|
|
746
|
+
const rejectionDetails = retryChallenge(requirement, settlement.rejectionReason).details;
|
|
747
|
+
throw new CommerceError(
|
|
748
|
+
"PAYMENT_SETTLEMENT_FAILED",
|
|
749
|
+
settlement.rejectionReason ?? "Settlement was rejected",
|
|
750
|
+
{
|
|
751
|
+
requestId: request.requestId,
|
|
752
|
+
resourceId: resource.id,
|
|
753
|
+
httpStatus: 402,
|
|
754
|
+
details: {
|
|
755
|
+
...rejectionDetails,
|
|
756
|
+
...settlementParties(requirement, settlement, verification)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
await hold?.finalize("consume");
|
|
762
|
+
await safePersist(
|
|
763
|
+
() => options.store.updatePaymentAttempt({
|
|
764
|
+
replayKey,
|
|
765
|
+
status: "settled",
|
|
766
|
+
...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
|
|
767
|
+
}),
|
|
768
|
+
"updatePaymentAttempt(settled)",
|
|
769
|
+
request.requestId
|
|
770
|
+
);
|
|
771
|
+
await safeEmit(
|
|
772
|
+
buildEvent({
|
|
773
|
+
type: "payment.settled",
|
|
774
|
+
requestId: request.requestId,
|
|
775
|
+
resourceId: resource.id,
|
|
776
|
+
adapter: request.protocol,
|
|
777
|
+
paymentProvider: provider.name,
|
|
778
|
+
status: "ok"
|
|
779
|
+
})
|
|
780
|
+
);
|
|
781
|
+
return settlement;
|
|
782
|
+
}
|
|
783
|
+
async function abandonSettlement(args, backendErrorCode) {
|
|
784
|
+
const { request, resource, provider, replayKey, hold } = args;
|
|
785
|
+
await hold?.finalize("release");
|
|
786
|
+
await safePersist(
|
|
787
|
+
() => options.store.updatePaymentAttempt({
|
|
788
|
+
replayKey,
|
|
789
|
+
status: "rejected",
|
|
790
|
+
rejectionReason: "backend_failed"
|
|
791
|
+
}),
|
|
792
|
+
"updatePaymentAttempt(backend-failed)",
|
|
793
|
+
request.requestId
|
|
794
|
+
);
|
|
795
|
+
await safeEmit(
|
|
796
|
+
buildEvent({
|
|
797
|
+
type: "payment.rejected",
|
|
798
|
+
requestId: request.requestId,
|
|
799
|
+
resourceId: resource.id,
|
|
800
|
+
adapter: request.protocol,
|
|
801
|
+
paymentProvider: provider.name,
|
|
802
|
+
status: "error",
|
|
803
|
+
data: { reason: "backend-failed", backendErrorCode }
|
|
804
|
+
})
|
|
805
|
+
);
|
|
806
|
+
}
|
|
648
807
|
async function execute(request) {
|
|
649
808
|
const pipelineStart = options.clock.monotonicMs();
|
|
650
809
|
const resource = options.resources.get(request.resourceId);
|
|
@@ -680,10 +839,12 @@ function createExecutionPipeline(options) {
|
|
|
680
839
|
);
|
|
681
840
|
}
|
|
682
841
|
const validInput = validation.value;
|
|
683
|
-
validateBackendRequestShape(
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
842
|
+
validateBackendRequestShape(
|
|
843
|
+
resource.handler,
|
|
844
|
+
validInput,
|
|
845
|
+
{ requestId: request.requestId, resourceId: resource.id },
|
|
846
|
+
request.backendHeaders
|
|
847
|
+
);
|
|
687
848
|
if (resource.pricing.type === "dynamic") {
|
|
688
849
|
throw new CommerceError(
|
|
689
850
|
"CONFIG_INVALID",
|
|
@@ -706,6 +867,7 @@ function createExecutionPipeline(options) {
|
|
|
706
867
|
}
|
|
707
868
|
let paymentResult;
|
|
708
869
|
let authorization;
|
|
870
|
+
let deferredSettlement;
|
|
709
871
|
if (resource.pricing.type === "fixed") {
|
|
710
872
|
const pricing = resource.pricing;
|
|
711
873
|
const provider = pickProvider(options.paymentProviders, resource.paymentMethods);
|
|
@@ -732,7 +894,9 @@ function createExecutionPipeline(options) {
|
|
|
732
894
|
resource,
|
|
733
895
|
amount: pricing.amount,
|
|
734
896
|
currency: pricing.currency,
|
|
735
|
-
requestedAt: options.clock.nowIso()
|
|
897
|
+
requestedAt: options.clock.nowIso(),
|
|
898
|
+
// Transport facts a rail may bind into its challenge, such as a body digest
|
|
899
|
+
...request.metadata !== void 0 ? { metadata: request.metadata } : {}
|
|
736
900
|
});
|
|
737
901
|
} catch (error) {
|
|
738
902
|
throw isCommerceError(error) ? error : new CommerceError("PAYMENT_PROVIDER_UNAVAILABLE", "Payment provider is unavailable", {
|
|
@@ -806,7 +970,7 @@ function createExecutionPipeline(options) {
|
|
|
806
970
|
{
|
|
807
971
|
requestId: request.requestId,
|
|
808
972
|
resourceId: resource.id,
|
|
809
|
-
...retryChallenge(requirement)
|
|
973
|
+
...retryChallenge(requirement, verification.rejectionReason)
|
|
810
974
|
}
|
|
811
975
|
);
|
|
812
976
|
}
|
|
@@ -865,7 +1029,7 @@ function createExecutionPipeline(options) {
|
|
|
865
1029
|
data: { reason: mapped.code }
|
|
866
1030
|
})
|
|
867
1031
|
);
|
|
868
|
-
throw mapped;
|
|
1032
|
+
throw finishedReplay(mapped, requirement);
|
|
869
1033
|
}
|
|
870
1034
|
await safeEmit(
|
|
871
1035
|
buildEvent({
|
|
@@ -877,104 +1041,21 @@ function createExecutionPipeline(options) {
|
|
|
877
1041
|
status: "ok"
|
|
878
1042
|
})
|
|
879
1043
|
);
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
replayKey,
|
|
895
|
-
status: "settlement-uncertain",
|
|
896
|
-
...txHash !== void 0 ? { externalReference: txHash } : {},
|
|
897
|
-
...error instanceof Error ? { rejectionReason: error.message } : {}
|
|
898
|
-
}),
|
|
899
|
-
"updatePaymentAttempt(settlement-uncertain)",
|
|
900
|
-
request.requestId
|
|
901
|
-
);
|
|
902
|
-
await safeEmit(
|
|
903
|
-
buildEvent({
|
|
904
|
-
type: "payment.rejected",
|
|
905
|
-
requestId: request.requestId,
|
|
906
|
-
resourceId: resource.id,
|
|
907
|
-
adapter: request.protocol,
|
|
908
|
-
paymentProvider: provider.name,
|
|
909
|
-
status: "error",
|
|
910
|
-
data: {
|
|
911
|
-
reason: "settlement-uncertain",
|
|
912
|
-
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
913
|
-
}
|
|
914
|
-
})
|
|
915
|
-
);
|
|
916
|
-
throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
|
|
917
|
-
requestId: request.requestId,
|
|
918
|
-
resourceId: resource.id,
|
|
919
|
-
cause: error,
|
|
920
|
-
details: {
|
|
921
|
-
settlementUncertain: true,
|
|
922
|
-
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
923
|
-
}
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
if (settlement.status !== "settled") {
|
|
927
|
-
await hold?.finalize("release");
|
|
928
|
-
await safePersist(
|
|
929
|
-
() => options.store.updatePaymentAttempt({
|
|
930
|
-
replayKey,
|
|
931
|
-
status: "rejected",
|
|
932
|
-
...settlement.rejectionReason !== void 0 ? { rejectionReason: settlement.rejectionReason } : {}
|
|
933
|
-
}),
|
|
934
|
-
"updatePaymentAttempt(rejected)",
|
|
935
|
-
request.requestId
|
|
936
|
-
);
|
|
937
|
-
await safeEmit(
|
|
938
|
-
buildEvent({
|
|
939
|
-
type: "payment.rejected",
|
|
940
|
-
requestId: request.requestId,
|
|
941
|
-
resourceId: resource.id,
|
|
942
|
-
adapter: request.protocol,
|
|
943
|
-
paymentProvider: provider.name,
|
|
944
|
-
status: "error",
|
|
945
|
-
data: settlement.rejectionReason ? { reason: settlement.rejectionReason } : {}
|
|
946
|
-
})
|
|
947
|
-
);
|
|
948
|
-
throw new CommerceError(
|
|
949
|
-
"PAYMENT_SETTLEMENT_FAILED",
|
|
950
|
-
settlement.rejectionReason ?? "Settlement was rejected",
|
|
951
|
-
{
|
|
952
|
-
requestId: request.requestId,
|
|
953
|
-
resourceId: resource.id
|
|
954
|
-
}
|
|
955
|
-
);
|
|
1044
|
+
const settleArgs = {
|
|
1045
|
+
request,
|
|
1046
|
+
resource,
|
|
1047
|
+
provider,
|
|
1048
|
+
requirement,
|
|
1049
|
+
payment,
|
|
1050
|
+
verification,
|
|
1051
|
+
replayKey,
|
|
1052
|
+
hold
|
|
1053
|
+
};
|
|
1054
|
+
if (requirement.metadata?.[SETTLE_AFTER_BACKEND_METADATA_KEY] === true) {
|
|
1055
|
+
deferredSettlement = settleArgs;
|
|
1056
|
+
} else {
|
|
1057
|
+
paymentResult = await settle(settleArgs);
|
|
956
1058
|
}
|
|
957
|
-
await hold?.finalize("consume");
|
|
958
|
-
await safePersist(
|
|
959
|
-
() => options.store.updatePaymentAttempt({
|
|
960
|
-
replayKey,
|
|
961
|
-
status: "settled",
|
|
962
|
-
...settlement.externalReference !== void 0 ? { externalReference: settlement.externalReference } : {}
|
|
963
|
-
}),
|
|
964
|
-
"updatePaymentAttempt(settled)",
|
|
965
|
-
request.requestId
|
|
966
|
-
);
|
|
967
|
-
await safeEmit(
|
|
968
|
-
buildEvent({
|
|
969
|
-
type: "payment.settled",
|
|
970
|
-
requestId: request.requestId,
|
|
971
|
-
resourceId: resource.id,
|
|
972
|
-
adapter: request.protocol,
|
|
973
|
-
paymentProvider: provider.name,
|
|
974
|
-
status: "ok"
|
|
975
|
-
})
|
|
976
|
-
);
|
|
977
|
-
paymentResult = settlement;
|
|
978
1059
|
}
|
|
979
1060
|
const backendStart = options.clock.monotonicMs();
|
|
980
1061
|
let backendResponse;
|
|
@@ -983,7 +1064,8 @@ function createExecutionPipeline(options) {
|
|
|
983
1064
|
requestId: request.requestId,
|
|
984
1065
|
resourceId: resource.id,
|
|
985
1066
|
input: validInput,
|
|
986
|
-
...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
|
|
1067
|
+
...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {},
|
|
1068
|
+
...request.backendHeaders !== void 0 ? { backendHeaders: request.backendHeaders } : {}
|
|
987
1069
|
});
|
|
988
1070
|
} catch (error) {
|
|
989
1071
|
const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
|
|
@@ -998,6 +1080,10 @@ function createExecutionPipeline(options) {
|
|
|
998
1080
|
data: { code: commerceError.code }
|
|
999
1081
|
})
|
|
1000
1082
|
);
|
|
1083
|
+
if (deferredSettlement !== void 0) {
|
|
1084
|
+
await abandonSettlement(deferredSettlement, commerceError.code);
|
|
1085
|
+
throw commerceError;
|
|
1086
|
+
}
|
|
1001
1087
|
if (paymentResult !== void 0) {
|
|
1002
1088
|
const failedReceipt = {
|
|
1003
1089
|
id: options.ids.next("receipt"),
|
|
@@ -1030,6 +1116,8 @@ function createExecutionPipeline(options) {
|
|
|
1030
1116
|
currency: paymentResult.currency,
|
|
1031
1117
|
...paymentResult.network !== void 0 ? { network: paymentResult.network } : {},
|
|
1032
1118
|
...paymentResult.externalReference !== void 0 ? { externalReference: paymentResult.externalReference } : {},
|
|
1119
|
+
...paymentResult.payer !== void 0 ? { payer: paymentResult.payer } : {},
|
|
1120
|
+
...typeof paymentResult.metadata?.["amountBaseUnits"] === "string" ? { amountBaseUnits: paymentResult.metadata["amountBaseUnits"] } : {},
|
|
1033
1121
|
...typeof paymentResult.metadata?.["receipt"] === "string" ? { receipt: paymentResult.metadata["receipt"] } : {}
|
|
1034
1122
|
}
|
|
1035
1123
|
}
|
|
@@ -1049,6 +1137,9 @@ function createExecutionPipeline(options) {
|
|
|
1049
1137
|
data: { status: backendResponse.status }
|
|
1050
1138
|
})
|
|
1051
1139
|
);
|
|
1140
|
+
if (deferredSettlement !== void 0) {
|
|
1141
|
+
paymentResult = await settle(deferredSettlement);
|
|
1142
|
+
}
|
|
1052
1143
|
const receipt = {
|
|
1053
1144
|
id: options.ids.next("receipt"),
|
|
1054
1145
|
requestId: request.requestId,
|
|
@@ -1108,9 +1199,35 @@ function resolveAuthorizationProviders(providers, resource, requestId) {
|
|
|
1108
1199
|
return found;
|
|
1109
1200
|
});
|
|
1110
1201
|
}
|
|
1111
|
-
function retryChallenge(requirement) {
|
|
1202
|
+
function retryChallenge(requirement, reason) {
|
|
1112
1203
|
const envelope = requirement.challenge.envelope;
|
|
1113
|
-
|
|
1204
|
+
if (envelope === void 0 && reason === void 0) return {};
|
|
1205
|
+
return {
|
|
1206
|
+
details: {
|
|
1207
|
+
...envelope !== void 0 ? { challenge: envelope } : {},
|
|
1208
|
+
...reason !== void 0 ? { reason } : {}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
var FINISHED_ATTEMPT_STATUSES = /* @__PURE__ */ new Set(["settled", "rejected"]);
|
|
1213
|
+
function finishedReplay(error, requirement) {
|
|
1214
|
+
const status = error.details?.["attemptStatus"];
|
|
1215
|
+
if (error.code !== "PAYMENT_REPLAYED" || typeof status !== "string") return error;
|
|
1216
|
+
if (!FINISHED_ATTEMPT_STATUSES.has(status)) return error;
|
|
1217
|
+
return new CommerceError("PAYMENT_REPLAYED", error.message, {
|
|
1218
|
+
...error.requestId !== void 0 ? { requestId: error.requestId } : {},
|
|
1219
|
+
...error.resourceId !== void 0 ? { resourceId: error.resourceId } : {},
|
|
1220
|
+
httpStatus: 402,
|
|
1221
|
+
details: { ...error.details, ...retryChallenge(requirement).details }
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
function settlementParties(requirement, ...results) {
|
|
1225
|
+
const network = results.find((r) => r.network !== void 0)?.network ?? requirement.network;
|
|
1226
|
+
const payer = results.find((r) => r.payer !== void 0)?.payer;
|
|
1227
|
+
return {
|
|
1228
|
+
...network !== void 0 ? { network } : {},
|
|
1229
|
+
...payer !== void 0 ? { payer } : {}
|
|
1230
|
+
};
|
|
1114
1231
|
}
|
|
1115
1232
|
function pickProvider(providers, methods) {
|
|
1116
1233
|
for (const method of methods) {
|
|
@@ -1199,6 +1316,7 @@ var ACP_OPERATION_INPUT_KEYS = {
|
|
|
1199
1316
|
var ACP_OPERATION_OPTIONAL_INPUT_KEYS = {
|
|
1200
1317
|
cancelCheckoutSession: ["body"]
|
|
1201
1318
|
};
|
|
1319
|
+
var ACP_SESSION_ID_INPUT_KEY = "checkout_session_id";
|
|
1202
1320
|
var ACP_JSON_MEDIA_TYPE = "application/json";
|
|
1203
1321
|
var ACP_API_VERSION_HEADER = "api-version";
|
|
1204
1322
|
var ACP_REQUEST_ID_HEADER = "request-id";
|
|
@@ -1206,6 +1324,17 @@ var ACP_IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
|
1206
1324
|
var ACP_IDEMPOTENT_REPLAYED_HEADER = "idempotent-replayed";
|
|
1207
1325
|
var ACP_IN_FLIGHT_RETRY_AFTER_SECONDS = 1;
|
|
1208
1326
|
var ACP_MAX_REQUEST_ID_LENGTH = 128;
|
|
1327
|
+
var ACP_FORWARDED_HEADERS = [
|
|
1328
|
+
"accept-language",
|
|
1329
|
+
"user-agent",
|
|
1330
|
+
ACP_REQUEST_ID_HEADER,
|
|
1331
|
+
ACP_API_VERSION_HEADER,
|
|
1332
|
+
"signature",
|
|
1333
|
+
"timestamp"
|
|
1334
|
+
];
|
|
1335
|
+
var ACP_MAX_FORWARDED_HEADER_LENGTH = 2048;
|
|
1336
|
+
var ACP_MERCHANT_RETRY_AFTER_DEFAULT_SECONDS = 5;
|
|
1337
|
+
var ACP_MERCHANT_RETRY_AFTER_MAX_SECONDS = 300;
|
|
1209
1338
|
|
|
1210
1339
|
// src/config/schema.ts
|
|
1211
1340
|
var SUPPORTED_CONFIG_VERSION = 1;
|
|
@@ -1272,7 +1401,11 @@ var MountPathSchema = z.string().min(1).refine((value) => value.startsWith("/"),
|
|
|
1272
1401
|
}
|
|
1273
1402
|
);
|
|
1274
1403
|
var AcpAuthSchema = z.object({ type: z.literal("bearer"), token: z.string().min(1) }).strict();
|
|
1275
|
-
var AcpIdempotencySchema = z.object({
|
|
1404
|
+
var AcpIdempotencySchema = z.object({
|
|
1405
|
+
path: z.string().min(1),
|
|
1406
|
+
retentionHours: NumberOrString.optional(),
|
|
1407
|
+
merchantIdempotent: z.boolean().optional()
|
|
1408
|
+
}).strict();
|
|
1276
1409
|
var AcpCheckoutSchema = z.object({ operations: z.record(z.string().min(1)) }).strict();
|
|
1277
1410
|
var AcpDiscoverySchema = z.object({
|
|
1278
1411
|
documentationUrl: z.string().min(1).optional(),
|
|
@@ -1342,6 +1475,7 @@ var ResourceEntrySchema = z.object({
|
|
|
1342
1475
|
pricing: PricingSchema,
|
|
1343
1476
|
expose: z.array(z.string().min(1)).min(1),
|
|
1344
1477
|
payments: z.array(z.string().min(1)).optional(),
|
|
1478
|
+
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
1345
1479
|
authorization: z.object({ required: z.array(z.string().min(1)).min(1) }).strict().optional()
|
|
1346
1480
|
}).strict();
|
|
1347
1481
|
var ResourcesMapSchema = z.record(z.string().min(1), ResourceEntrySchema);
|
|
@@ -1379,6 +1513,8 @@ var X402Schema = z.object({
|
|
|
1379
1513
|
assetDecimals: NumberOrString,
|
|
1380
1514
|
payTo: z.string().min(1),
|
|
1381
1515
|
maxTimeoutSeconds: NumberOrString,
|
|
1516
|
+
// `authorization` (the x402 default) settles after the backend call
|
|
1517
|
+
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
1382
1518
|
facilitator: FacilitatorSchema,
|
|
1383
1519
|
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1384
1520
|
allowMainnet: BooleanOrString.optional(),
|
|
@@ -1396,6 +1532,8 @@ var MppSchema = z.object({
|
|
|
1396
1532
|
realm: z.string().min(1),
|
|
1397
1533
|
challengeSecret: z.string().min(1),
|
|
1398
1534
|
challengeTtlSeconds: NumberOrString.optional(),
|
|
1535
|
+
// Same values and default as `payments.x402.paymentFlow`
|
|
1536
|
+
paymentFlow: z.enum(X402_PAYMENT_FLOWS).optional(),
|
|
1399
1537
|
facilitator: FacilitatorSchema,
|
|
1400
1538
|
// Real funds. Never defaulted; see src/payments/x402/guardrails.ts
|
|
1401
1539
|
allowMainnet: BooleanOrString.optional(),
|
|
@@ -1426,6 +1564,8 @@ var Ap2Schema = z.object({
|
|
|
1426
1564
|
checkoutIssuers: z.array(Ap2IssuerSchema).optional()
|
|
1427
1565
|
}).strict().optional(),
|
|
1428
1566
|
clockSkewSeconds: NumberOrString.optional(),
|
|
1567
|
+
requireMandateAudience: BooleanOrString.optional(),
|
|
1568
|
+
requireMandateExpiry: BooleanOrString.optional(),
|
|
1429
1569
|
replay: z.object({ path: z.string().min(1) }).strict().optional()
|
|
1430
1570
|
}).strict();
|
|
1431
1571
|
var AuthorizationSchema = z.object({ ap2: Ap2Schema.optional() }).strict();
|
|
@@ -1573,6 +1713,7 @@ function normalize(raw) {
|
|
|
1573
1713
|
min: 1
|
|
1574
1714
|
}
|
|
1575
1715
|
),
|
|
1716
|
+
paymentFlow: x402Raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
|
|
1576
1717
|
facilitator: normalizeFacilitator(x402Raw.facilitator),
|
|
1577
1718
|
...x402Raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(x402Raw.allowMainnet, "payments.x402.allowMainnet") } : {},
|
|
1578
1719
|
...x402Raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
@@ -1604,7 +1745,22 @@ function normalize(raw) {
|
|
|
1604
1745
|
const resources = Object.entries(raw.resources).map(
|
|
1605
1746
|
([id, entry]) => normalizeResource(id, entry, protocols, x402, mpp, ap2)
|
|
1606
1747
|
);
|
|
1748
|
+
const resourcePaymentFlows = Object.fromEntries(
|
|
1749
|
+
Object.entries(raw.resources).flatMap(
|
|
1750
|
+
([id, entry]) => entry.paymentFlow !== void 0 ? [[id, entry.paymentFlow]] : []
|
|
1751
|
+
)
|
|
1752
|
+
);
|
|
1753
|
+
const hasFlowOverrides = Object.keys(resourcePaymentFlows).length > 0;
|
|
1754
|
+
const x402WithFlows = x402 !== void 0 && hasFlowOverrides ? { ...x402, resourcePaymentFlows } : x402;
|
|
1755
|
+
const mppWithFlows = mpp !== void 0 && hasFlowOverrides ? { ...mpp, resourcePaymentFlows } : mpp;
|
|
1607
1756
|
if (protocols.acp.enabled) validateAcpCheckoutMapping(protocols.acp, resources);
|
|
1757
|
+
if (protocols.a2a.enabled && !resources.some((r) => r.exposedVia.includes("a2a"))) {
|
|
1758
|
+
throw new CommerceError(
|
|
1759
|
+
"CONFIG_INVALID",
|
|
1760
|
+
'protocols.a2a.enabled is true but no resource lists "a2a" in its expose',
|
|
1761
|
+
{ details: { path: "protocols.a2a.enabled" } }
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1608
1764
|
return {
|
|
1609
1765
|
version: SUPPORTED_CONFIG_VERSION,
|
|
1610
1766
|
merchant: {
|
|
@@ -1623,8 +1779,8 @@ function normalize(raw) {
|
|
|
1623
1779
|
protocols,
|
|
1624
1780
|
resources,
|
|
1625
1781
|
payments: {
|
|
1626
|
-
...
|
|
1627
|
-
...
|
|
1782
|
+
...x402WithFlows !== void 0 ? { x402: x402WithFlows } : {},
|
|
1783
|
+
...mppWithFlows !== void 0 ? { mpp: mppWithFlows } : {}
|
|
1628
1784
|
},
|
|
1629
1785
|
...ap2 !== void 0 ? { authorization: { ap2 } } : {}
|
|
1630
1786
|
};
|
|
@@ -1693,6 +1849,14 @@ function normalizeAp2(raw) {
|
|
|
1693
1849
|
"authorization.ap2.clockSkewSeconds",
|
|
1694
1850
|
{ min: 0, max: AP2_MAX_CLOCK_SKEW_SECONDS }
|
|
1695
1851
|
),
|
|
1852
|
+
requireMandateAudience: toBoolean(
|
|
1853
|
+
raw.requireMandateAudience ?? false,
|
|
1854
|
+
"authorization.ap2.requireMandateAudience"
|
|
1855
|
+
),
|
|
1856
|
+
requireMandateExpiry: toBoolean(
|
|
1857
|
+
raw.requireMandateExpiry ?? false,
|
|
1858
|
+
"authorization.ap2.requireMandateExpiry"
|
|
1859
|
+
),
|
|
1696
1860
|
replay: { path: raw.replay.path }
|
|
1697
1861
|
};
|
|
1698
1862
|
}
|
|
@@ -1868,7 +2032,8 @@ function normalizeAcp(raw) {
|
|
|
1868
2032
|
raw.idempotency.retentionHours ?? ACP_RETENTION_HOURS,
|
|
1869
2033
|
"protocols.acp.idempotency.retentionHours",
|
|
1870
2034
|
{ min: ACP_RETENTION_HOURS }
|
|
1871
|
-
)
|
|
2035
|
+
),
|
|
2036
|
+
merchantIdempotent: raw.idempotency.merchantIdempotent ?? false
|
|
1872
2037
|
},
|
|
1873
2038
|
checkout: { operations: normalizeAcpOperations(raw.checkout) },
|
|
1874
2039
|
// Built key by key so absent metadata stays absent rather than `undefined`
|
|
@@ -1951,10 +2116,11 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1951
2116
|
if (schema === void 0) return;
|
|
1952
2117
|
const keys = ACP_OPERATION_INPUT_KEYS[operation];
|
|
1953
2118
|
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[operation] ?? [];
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2119
|
+
const sent = [...keys, ...optionalKeys];
|
|
2120
|
+
const topLevel = closedObjectProperties(schema);
|
|
2121
|
+
if (topLevel !== void 0) {
|
|
2122
|
+
for (const key of sent) {
|
|
2123
|
+
if (!Object.hasOwn(topLevel, key)) {
|
|
1958
2124
|
const when = keys.includes(key) ? "always sends" : "sends when the caller supplies it";
|
|
1959
2125
|
throw acpInvalid(
|
|
1960
2126
|
path2,
|
|
@@ -1964,6 +2130,23 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1964
2130
|
}
|
|
1965
2131
|
}
|
|
1966
2132
|
}
|
|
2133
|
+
const declared = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
2134
|
+
const pathKeys = closedObjectProperties(declared["path"]);
|
|
2135
|
+
if (sent.includes("path") && pathKeys !== void 0 && !Object.hasOwn(pathKeys, ACP_SESSION_ID_INPUT_KEY)) {
|
|
2136
|
+
throw acpInvalid(
|
|
2137
|
+
path2,
|
|
2138
|
+
`Resource "${resource.id}" implements ACP operation "${operation}" but its "path" schema sets additionalProperties: false without declaring "${ACP_SESSION_ID_INPUT_KEY}", which the adapter always sends inside "path"`,
|
|
2139
|
+
{ resourceId: resource.id }
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
const bodyKeys = closedObjectProperties(declared["body"]);
|
|
2143
|
+
if (sent.includes("body") && bodyKeys !== void 0 && Object.keys(bodyKeys).length === 0) {
|
|
2144
|
+
throw acpInvalid(
|
|
2145
|
+
path2,
|
|
2146
|
+
`Resource "${resource.id}" implements ACP operation "${operation}" but its "body" schema accepts no keys, so it refuses every ACP document. Config load closes an object schema that omits additionalProperties: declare body as { type: object, additionalProperties: true }`,
|
|
2147
|
+
{ resourceId: resource.id }
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
1967
2150
|
const required = schema["required"];
|
|
1968
2151
|
if (Array.isArray(required)) {
|
|
1969
2152
|
for (const entry of required) {
|
|
@@ -1977,6 +2160,10 @@ function validateAcpOperationInput(path2, operation, resource) {
|
|
|
1977
2160
|
}
|
|
1978
2161
|
}
|
|
1979
2162
|
}
|
|
2163
|
+
function closedObjectProperties(schema) {
|
|
2164
|
+
if (!isRecord(schema) || schema["additionalProperties"] !== false) return void 0;
|
|
2165
|
+
return isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
2166
|
+
}
|
|
1980
2167
|
function normalizeFacilitator(raw) {
|
|
1981
2168
|
return raw.mode === "local" ? { mode: "local", signerPrivateKey: raw.signerPrivateKey } : { mode: "remote", url: raw.url, auth: raw.auth ?? { type: "none" } };
|
|
1982
2169
|
}
|
|
@@ -2007,6 +2194,7 @@ function normalizeMpp(raw) {
|
|
|
2007
2194
|
{ min: 1 }
|
|
2008
2195
|
)
|
|
2009
2196
|
} : {},
|
|
2197
|
+
paymentFlow: raw.paymentFlow ?? X402_DEFAULT_PAYMENT_FLOW,
|
|
2010
2198
|
facilitator: normalizeFacilitator(raw.facilitator),
|
|
2011
2199
|
...raw.allowMainnet !== void 0 ? { allowMainnet: toBoolean(raw.allowMainnet, "payments.mpp.allowMainnet") } : {},
|
|
2012
2200
|
...raw.allowUnauthenticatedFacilitator !== void 0 ? {
|
|
@@ -2160,6 +2348,13 @@ function normalizeResource(id, entry, protocols, x402, mpp, ap2) {
|
|
|
2160
2348
|
validatePricingAmount(id, entry.pricing.amount, MPP_PROFILE.assetDecimals);
|
|
2161
2349
|
}
|
|
2162
2350
|
}
|
|
2351
|
+
if (entry.paymentFlow !== void 0 && (entry.pricing.type !== "fixed" || !paymentMethods.some((method) => method === "x402" || method === "mpp"))) {
|
|
2352
|
+
throw new CommerceError(
|
|
2353
|
+
"CONFIG_INVALID",
|
|
2354
|
+
`Resource "${id}" sets paymentFlow, which requires fixed pricing and "x402" or "mpp" in payments`,
|
|
2355
|
+
{ details: { path: `resources.${id}.paymentFlow`, resourceId: id } }
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2163
2358
|
const pricing = entry.pricing.type === "free" ? { type: "free" } : { type: "fixed", amount: entry.pricing.amount, currency: entry.pricing.currency };
|
|
2164
2359
|
const authorization = normalizeResourceAuthorization(id, entry, pricing, ap2);
|
|
2165
2360
|
return {
|
|
@@ -2566,36 +2761,6 @@ function resolveConfigPath(explicitPath, env, cwd) {
|
|
|
2566
2761
|
if (fromEnv) return path.resolve(cwd, fromEnv);
|
|
2567
2762
|
return path.resolve(cwd, DEFAULT_CONFIG_FILENAME);
|
|
2568
2763
|
}
|
|
2569
|
-
var BEARER_PREFIX = /^Bearer +/i;
|
|
2570
|
-
function createBearerCheck(expectedToken) {
|
|
2571
|
-
const expected = sha256(expectedToken);
|
|
2572
|
-
return (header2) => {
|
|
2573
|
-
if (header2 === void 0 || !BEARER_PREFIX.test(header2)) return false;
|
|
2574
|
-
const presented = header2.replace(BEARER_PREFIX, "");
|
|
2575
|
-
return presented.length > 0 && timingSafeEqual(sha256(presented), expected);
|
|
2576
|
-
};
|
|
2577
|
-
}
|
|
2578
|
-
function sha256(value) {
|
|
2579
|
-
return createHash("sha256").update(value, "utf8").digest();
|
|
2580
|
-
}
|
|
2581
|
-
async function readCappedBody(req, maxBytes) {
|
|
2582
|
-
const chunks = [];
|
|
2583
|
-
let total = 0;
|
|
2584
|
-
try {
|
|
2585
|
-
for await (const chunk of req) {
|
|
2586
|
-
const buffer = chunk;
|
|
2587
|
-
total += buffer.length;
|
|
2588
|
-
if (total > maxBytes) return { kind: "too-large" };
|
|
2589
|
-
chunks.push(buffer);
|
|
2590
|
-
}
|
|
2591
|
-
} catch {
|
|
2592
|
-
return { kind: "unreadable" };
|
|
2593
|
-
}
|
|
2594
|
-
return { kind: "ok", text: Buffer.concat(chunks).toString("utf8") };
|
|
2595
|
-
}
|
|
2596
|
-
function joinUrl(baseUrl, mountPath) {
|
|
2597
|
-
return `${baseUrl.replace(/\/+$/, "")}/${mountPath.replace(/^\/+|\/+$/g, "")}`;
|
|
2598
|
-
}
|
|
2599
2764
|
|
|
2600
2765
|
// src/gateway/access-control.ts
|
|
2601
2766
|
var CORS_METHODS = "GET,POST,OPTIONS";
|
|
@@ -2604,9 +2769,29 @@ var CORS_HEADERS = [
|
|
|
2604
2769
|
PAYMENT_HEADER,
|
|
2605
2770
|
AUTHORIZATION_HEADER,
|
|
2606
2771
|
"x-request-id",
|
|
2607
|
-
"authorization"
|
|
2772
|
+
"authorization",
|
|
2773
|
+
"mcp-protocol-version",
|
|
2774
|
+
"mcp-method",
|
|
2775
|
+
"mcp-name",
|
|
2776
|
+
"a2a-version",
|
|
2777
|
+
"a2a-extensions",
|
|
2778
|
+
"api-version",
|
|
2779
|
+
"idempotency-key",
|
|
2780
|
+
"request-id",
|
|
2781
|
+
"signature",
|
|
2782
|
+
"timestamp",
|
|
2783
|
+
"access-control-expose-headers"
|
|
2784
|
+
].join(",");
|
|
2785
|
+
var CORS_EXPOSED_HEADERS = [
|
|
2786
|
+
"payment-required",
|
|
2787
|
+
"payment-response",
|
|
2788
|
+
"www-authenticate",
|
|
2789
|
+
"payment-receipt",
|
|
2790
|
+
"idempotency-key",
|
|
2791
|
+
"request-id",
|
|
2792
|
+
"idempotent-replayed",
|
|
2793
|
+
"retry-after"
|
|
2608
2794
|
].join(",");
|
|
2609
|
-
var CORS_EXPOSED_HEADERS = "payment-required,payment-response,www-authenticate,payment-receipt";
|
|
2610
2795
|
function buildAccessControlHook(options) {
|
|
2611
2796
|
const allowedOrigins = new Set(options.allowedOrigins.map((origin) => origin.toLowerCase()));
|
|
2612
2797
|
const publicHostname = hostnameOf(options.publicBaseUrl);
|
|
@@ -3146,6 +3331,7 @@ function buildWellKnownDocument(options) {
|
|
|
3146
3331
|
}
|
|
3147
3332
|
|
|
3148
3333
|
// src/gateway/routes.ts
|
|
3334
|
+
var X402_V1_PAYMENT_HEADER = "x-payment";
|
|
3149
3335
|
function registerRoutes(options) {
|
|
3150
3336
|
const { server } = options;
|
|
3151
3337
|
const readinessProbe = createReadinessProbe({
|
|
@@ -3172,12 +3358,20 @@ function registerRoutes(options) {
|
|
|
3172
3358
|
adapters: await readinessProbe.adapterHealth()
|
|
3173
3359
|
})
|
|
3174
3360
|
);
|
|
3361
|
+
const backed = (method) => options.paymentProviders.some((provider) => provider.name === method);
|
|
3175
3362
|
server.get("/api/resources", async () => ({
|
|
3176
|
-
resources: options.resources.list().map(
|
|
3363
|
+
resources: options.resources.list().map((resource) => {
|
|
3364
|
+
const listed = toPublicResource(resource);
|
|
3365
|
+
return { ...listed, paymentMethods: listed.paymentMethods.filter(backed) };
|
|
3366
|
+
})
|
|
3177
3367
|
}));
|
|
3178
|
-
server.post(
|
|
3179
|
-
|
|
3180
|
-
|
|
3368
|
+
server.post(
|
|
3369
|
+
"/api/resources/:id/invoke",
|
|
3370
|
+
{ preParsing: async (request, _reply, payload) => payload.pipe(digestingStream(request)) },
|
|
3371
|
+
async (request, reply) => {
|
|
3372
|
+
await handleInvoke(request, reply, options);
|
|
3373
|
+
}
|
|
3374
|
+
);
|
|
3181
3375
|
const tokenHook = buildOperatorTokenHook(options.config.server.adminToken);
|
|
3182
3376
|
server.get(
|
|
3183
3377
|
"/api/receipts",
|
|
@@ -3190,6 +3384,20 @@ function registerRoutes(options) {
|
|
|
3190
3384
|
ledgerHandler("events", (list) => options.store.listEvents(list))
|
|
3191
3385
|
);
|
|
3192
3386
|
}
|
|
3387
|
+
var bodyDigests = /* @__PURE__ */ new WeakMap();
|
|
3388
|
+
function digestingStream(request) {
|
|
3389
|
+
const chunks = [];
|
|
3390
|
+
return new Transform({
|
|
3391
|
+
transform(chunk, _encoding, callback) {
|
|
3392
|
+
chunks.push(chunk);
|
|
3393
|
+
callback(null, chunk);
|
|
3394
|
+
},
|
|
3395
|
+
flush(callback) {
|
|
3396
|
+
if (chunks.length > 0) bodyDigests.set(request, contentDigest(Buffer.concat(chunks)));
|
|
3397
|
+
callback();
|
|
3398
|
+
}
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3193
3401
|
function ledgerHandler(key, list) {
|
|
3194
3402
|
return async (request, reply) => {
|
|
3195
3403
|
try {
|
|
@@ -3204,6 +3412,7 @@ function ledgerHandler(key, list) {
|
|
|
3204
3412
|
}
|
|
3205
3413
|
async function handleInvoke(request, reply, options) {
|
|
3206
3414
|
const resourceId = request.params.id;
|
|
3415
|
+
let paymentMethod;
|
|
3207
3416
|
try {
|
|
3208
3417
|
const resource = options.resources.get(resourceId);
|
|
3209
3418
|
if (!resource?.exposedVia.includes("http")) {
|
|
@@ -3212,13 +3421,14 @@ async function handleInvoke(request, reply, options) {
|
|
|
3212
3421
|
resourceId
|
|
3213
3422
|
});
|
|
3214
3423
|
}
|
|
3215
|
-
|
|
3424
|
+
paymentMethod = resource.paymentMethods[0];
|
|
3216
3425
|
const paymentValue = paymentProof(request, paymentMethod);
|
|
3217
3426
|
const payment = paymentValue !== void 0 && paymentMethod !== void 0 ? { method: paymentMethod, payload: paymentValue } : void 0;
|
|
3218
3427
|
const authorization = parseAuthorizationHeader(
|
|
3219
3428
|
request.headers[AUTHORIZATION_HEADER],
|
|
3220
3429
|
request.id
|
|
3221
3430
|
);
|
|
3431
|
+
const digest = bodyDigests.get(request);
|
|
3222
3432
|
const canonicalRequest = {
|
|
3223
3433
|
requestId: request.id,
|
|
3224
3434
|
resourceId,
|
|
@@ -3226,7 +3436,8 @@ async function handleInvoke(request, reply, options) {
|
|
|
3226
3436
|
protocol: "http",
|
|
3227
3437
|
receivedAt: options.clock.nowIso(),
|
|
3228
3438
|
...payment !== void 0 ? { payment } : {},
|
|
3229
|
-
...authorization !== void 0 ? { authorization } : {}
|
|
3439
|
+
...authorization !== void 0 ? { authorization } : {},
|
|
3440
|
+
...digest !== void 0 ? { metadata: { [CONTENT_DIGEST_METADATA_KEY]: digest } } : {}
|
|
3230
3441
|
};
|
|
3231
3442
|
const outcome = await options.pipeline.execute(canonicalRequest);
|
|
3232
3443
|
if (outcome.kind === "payment-required") {
|
|
@@ -3235,36 +3446,88 @@ async function handleInvoke(request, reply, options) {
|
|
|
3235
3446
|
const challenge = envelope.payment.envelope?.["wwwAuthenticate"];
|
|
3236
3447
|
if (typeof challenge === "string") reply.header("www-authenticate", challenge);
|
|
3237
3448
|
} else if (envelope.payment.envelope !== void 0) {
|
|
3238
|
-
|
|
3449
|
+
const v1Only = request.headers[X402_V1_PAYMENT_HEADER] !== void 0 && request.headers[PAYMENT_HEADER] === void 0;
|
|
3450
|
+
reply.header(
|
|
3451
|
+
PAYMENT_REQUIRED_HEADER,
|
|
3452
|
+
encodeHeaderDocument(
|
|
3453
|
+
v1Only ? { ...envelope.payment.envelope, error: "invalid_x402_version" } : envelope.payment.envelope
|
|
3454
|
+
)
|
|
3455
|
+
);
|
|
3239
3456
|
}
|
|
3240
3457
|
reply.header("cache-control", "no-store");
|
|
3458
|
+
if (envelope.payment.provider === "mpp") {
|
|
3459
|
+
const { status: _envelopeStatus, ...members } = envelope;
|
|
3460
|
+
reply.header("content-type", "application/problem+json").status(402).send({
|
|
3461
|
+
...mppProblem(envelope.code, 402),
|
|
3462
|
+
status: 402,
|
|
3463
|
+
detail: envelope.message,
|
|
3464
|
+
...members
|
|
3465
|
+
});
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3241
3468
|
reply.status(402).send(envelope);
|
|
3242
3469
|
return;
|
|
3243
3470
|
}
|
|
3244
3471
|
if (outcome.payment) {
|
|
3245
|
-
reply.header(
|
|
3472
|
+
reply.header(
|
|
3473
|
+
PAYMENT_RESPONSE_HEADER,
|
|
3474
|
+
encodeHeaderDocument(settlementResponse(outcome.payment))
|
|
3475
|
+
);
|
|
3246
3476
|
const receipt = outcome.payment.metadata?.["receipt"];
|
|
3247
3477
|
if (outcome.payment.provider === "mpp" && typeof receipt === "string") {
|
|
3248
3478
|
reply.header("payment-receipt", receipt);
|
|
3249
3479
|
}
|
|
3480
|
+
reply.header("cache-control", "private");
|
|
3250
3481
|
}
|
|
3251
3482
|
reply.status(outcome.backendStatus).send(outcome.body);
|
|
3252
3483
|
} catch (error) {
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3484
|
+
sendInvokeError(reply, toCommerceError(error), paymentMethod);
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
var MPP_PROBLEM_CODES = /* @__PURE__ */ new Set([
|
|
3488
|
+
"PAYMENT_INVALID",
|
|
3489
|
+
"PAYMENT_REPLAYED",
|
|
3490
|
+
"PAYMENT_SETTLEMENT_FAILED",
|
|
3491
|
+
"PAYMENT_PROVIDER_UNAVAILABLE"
|
|
3492
|
+
]);
|
|
3493
|
+
function sendInvokeError(reply, error, paymentMethod) {
|
|
3494
|
+
const details = error.details;
|
|
3495
|
+
const settled = settlementResponseFromDetails(details);
|
|
3496
|
+
if (settled !== void 0) {
|
|
3497
|
+
reply.header(PAYMENT_RESPONSE_HEADER, encodeHeaderDocument(settled));
|
|
3498
|
+
}
|
|
3499
|
+
if (error.code === "PAYMENT_SETTLEMENT_FAILED") {
|
|
3500
|
+
reply.header(PAYMENT_RESPONSE_HEADER, encodeHeaderDocument(settlementFailure(details)));
|
|
3501
|
+
}
|
|
3502
|
+
const challenge = details?.["challenge"];
|
|
3503
|
+
if (error.httpStatus === 402 && isRecord(challenge)) {
|
|
3504
|
+
if (paymentMethod === "mpp" && typeof challenge["wwwAuthenticate"] === "string") {
|
|
3263
3505
|
reply.header("www-authenticate", challenge["wwwAuthenticate"]);
|
|
3264
|
-
|
|
3506
|
+
} else if (paymentMethod === "x402" && error.code !== "PAYMENT_SETTLEMENT_FAILED") {
|
|
3507
|
+
reply.header(
|
|
3508
|
+
PAYMENT_REQUIRED_HEADER,
|
|
3509
|
+
encodeHeaderDocument({
|
|
3510
|
+
...challenge,
|
|
3511
|
+
error: x402ErrorCode(error.code, details?.["reason"])
|
|
3512
|
+
})
|
|
3513
|
+
);
|
|
3265
3514
|
}
|
|
3266
|
-
reply.
|
|
3515
|
+
reply.header("cache-control", "no-store");
|
|
3516
|
+
}
|
|
3517
|
+
const envelope = toErrorEnvelope(error);
|
|
3518
|
+
if (paymentMethod === "mpp" && MPP_PROBLEM_CODES.has(error.code)) {
|
|
3519
|
+
const { status: _envelopeStatus, ...members } = envelope;
|
|
3520
|
+
const reason = typeof details?.["reason"] === "string" ? details["reason"] : void 0;
|
|
3521
|
+
reply.header("content-type", "application/problem+json").status(error.httpStatus).send({
|
|
3522
|
+
...mppProblem(error.code, error.httpStatus, reason),
|
|
3523
|
+
status: error.httpStatus,
|
|
3524
|
+
// The reason token stays in `message` and `details.reason`
|
|
3525
|
+
detail: mppProblemDetail(reason, error.message),
|
|
3526
|
+
...members
|
|
3527
|
+
});
|
|
3528
|
+
return;
|
|
3267
3529
|
}
|
|
3530
|
+
reply.status(error.httpStatus).send(envelope);
|
|
3268
3531
|
}
|
|
3269
3532
|
function paymentProof(request, method) {
|
|
3270
3533
|
if (method === "mpp") {
|
|
@@ -3274,38 +3537,9 @@ function paymentProof(request, method) {
|
|
|
3274
3537
|
const value = request.headers[PAYMENT_HEADER];
|
|
3275
3538
|
return Array.isArray(value) ? value[0] : value;
|
|
3276
3539
|
}
|
|
3277
|
-
function errorPaymentSummary(error) {
|
|
3278
|
-
const payment = error.details?.["payment"];
|
|
3279
|
-
if (!isRecord(payment)) return void 0;
|
|
3280
|
-
const { status, provider, amount, currency, network, externalReference, receipt } = payment;
|
|
3281
|
-
if (typeof status !== "string" || typeof provider !== "string" || typeof amount !== "string" || typeof currency !== "string") {
|
|
3282
|
-
return void 0;
|
|
3283
|
-
}
|
|
3284
|
-
return {
|
|
3285
|
-
status,
|
|
3286
|
-
provider,
|
|
3287
|
-
amount,
|
|
3288
|
-
currency,
|
|
3289
|
-
...typeof network === "string" ? { network } : {},
|
|
3290
|
-
...typeof externalReference === "string" ? { externalReference } : {},
|
|
3291
|
-
...typeof receipt === "string" ? { receipt } : {}
|
|
3292
|
-
};
|
|
3293
|
-
}
|
|
3294
3540
|
function encodeHeaderDocument(document) {
|
|
3295
3541
|
return Buffer.from(JSON.stringify(document), "utf8").toString("base64");
|
|
3296
3542
|
}
|
|
3297
|
-
function encodePaymentSummary(payment) {
|
|
3298
|
-
return encodeHeaderDocument({
|
|
3299
|
-
success: payment.status === "settled",
|
|
3300
|
-
transaction: payment.externalReference ?? "",
|
|
3301
|
-
network: payment.network ?? "",
|
|
3302
|
-
status: payment.status,
|
|
3303
|
-
provider: payment.provider,
|
|
3304
|
-
amount: payment.amount,
|
|
3305
|
-
currency: payment.currency,
|
|
3306
|
-
...payment.externalReference !== void 0 ? { externalReference: payment.externalReference } : {}
|
|
3307
|
-
});
|
|
3308
|
-
}
|
|
3309
3543
|
function parseLimit(value) {
|
|
3310
3544
|
if (Array.isArray(value)) return parseLimit(value[0]);
|
|
3311
3545
|
let n;
|
|
@@ -3437,7 +3671,7 @@ async function createGateway(options) {
|
|
|
3437
3671
|
}
|
|
3438
3672
|
|
|
3439
3673
|
// src/protocols/a2a/constants.ts
|
|
3440
|
-
var A2A_SPEC_VERSION = "1.0.
|
|
3674
|
+
var A2A_SPEC_VERSION = "1.0.1";
|
|
3441
3675
|
var A2A_PROTOCOL_VERSION = "1.0";
|
|
3442
3676
|
var A2A_PROTOCOL_BINDING = "JSONRPC";
|
|
3443
3677
|
var A2A_AGENT_CARD_PATH = "/.well-known/agent-card.json";
|
|
@@ -3446,93 +3680,26 @@ var A2A_JSON_MEDIA_TYPE = "application/json";
|
|
|
3446
3680
|
var A2A_DEFAULT_AGENT_NAME = "agent-commerce";
|
|
3447
3681
|
var A2A_VERSION_HEADER = "a2a-version";
|
|
3448
3682
|
var A2A_METHOD_SEND_MESSAGE = "SendMessage";
|
|
3683
|
+
var A2A_PUSH_CONFIG_METHODS = [
|
|
3684
|
+
"CreateTaskPushNotificationConfig",
|
|
3685
|
+
"GetTaskPushNotificationConfig",
|
|
3686
|
+
"ListTaskPushNotificationConfigs",
|
|
3687
|
+
"DeleteTaskPushNotificationConfig"
|
|
3688
|
+
];
|
|
3449
3689
|
var A2A_UNSUPPORTED_METHODS = [
|
|
3450
3690
|
"SendStreamingMessage",
|
|
3451
3691
|
"GetTask",
|
|
3452
3692
|
"ListTasks",
|
|
3453
3693
|
"CancelTask",
|
|
3454
3694
|
"SubscribeToTask",
|
|
3455
|
-
|
|
3456
|
-
"GetTaskPushNotificationConfig",
|
|
3457
|
-
"ListTaskPushNotificationConfigs",
|
|
3458
|
-
"DeleteTaskPushNotificationConfig",
|
|
3695
|
+
...A2A_PUSH_CONFIG_METHODS,
|
|
3459
3696
|
"GetExtendedAgentCard"
|
|
3460
3697
|
];
|
|
3698
|
+
var A2A_TASK_LOOKUP_METHODS = ["GetTask", "CancelTask"];
|
|
3461
3699
|
var A2A_TASK_STATE_COMPLETED = "TASK_STATE_COMPLETED";
|
|
3462
3700
|
var A2A_TASK_STATE_FAILED = "TASK_STATE_FAILED";
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
function skillTags(resource) {
|
|
3466
|
-
return ["agent-commerce", resource.pricing.type === "free" ? "free" : "paid"];
|
|
3467
|
-
}
|
|
3468
|
-
function skillDescription(resource) {
|
|
3469
|
-
const base = resource.description ?? resource.name;
|
|
3470
|
-
if (resource.pricing.type === "fixed") {
|
|
3471
|
-
return `${base} Costs ${resource.pricing.amount} ${resource.pricing.currency} per call.`;
|
|
3472
|
-
}
|
|
3473
|
-
if (resource.pricing.type === "dynamic") {
|
|
3474
|
-
return `${base} Requires payment (amount determined at request time).`;
|
|
3475
|
-
}
|
|
3476
|
-
return base;
|
|
3477
|
-
}
|
|
3478
|
-
function buildAgentSkill(resource) {
|
|
3479
|
-
return {
|
|
3480
|
-
id: resource.id,
|
|
3481
|
-
name: resource.name,
|
|
3482
|
-
description: skillDescription(resource),
|
|
3483
|
-
tags: skillTags(resource),
|
|
3484
|
-
inputModes: [A2A_JSON_MEDIA_TYPE],
|
|
3485
|
-
outputModes: [A2A_JSON_MEDIA_TYPE]
|
|
3486
|
-
};
|
|
3487
|
-
}
|
|
3488
|
-
function buildAgentCard(options) {
|
|
3489
|
-
return {
|
|
3490
|
-
name: options.name,
|
|
3491
|
-
description: options.description,
|
|
3492
|
-
version: options.version,
|
|
3493
|
-
supportedInterfaces: [
|
|
3494
|
-
{
|
|
3495
|
-
url: joinUrl(options.publicBaseUrl, options.mountPath),
|
|
3496
|
-
protocolBinding: A2A_PROTOCOL_BINDING,
|
|
3497
|
-
protocolVersion: A2A_PROTOCOL_VERSION
|
|
3498
|
-
}
|
|
3499
|
-
],
|
|
3500
|
-
capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
|
|
3501
|
-
defaultInputModes: [A2A_JSON_MEDIA_TYPE],
|
|
3502
|
-
defaultOutputModes: [A2A_JSON_MEDIA_TYPE],
|
|
3503
|
-
skills: options.resources.map(buildAgentSkill)
|
|
3504
|
-
};
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
// src/protocols/a2a/descriptor.ts
|
|
3508
|
-
var A2A_CAPABILITIES = ["agent-card", "jsonrpc", "SendMessage"];
|
|
3509
|
-
var A2A_UNSUPPORTED = [
|
|
3510
|
-
// Methods, as the protocol names them; the transport refuses the same list
|
|
3511
|
-
...A2A_UNSUPPORTED_METHODS,
|
|
3512
|
-
// Transports other than the one binding served
|
|
3513
|
-
"HTTP+JSON/REST binding",
|
|
3514
|
-
"gRPC binding",
|
|
3515
|
-
// Behaviors
|
|
3516
|
-
"SSE",
|
|
3517
|
-
"long-running task persistence",
|
|
3518
|
-
"task resumption",
|
|
3519
|
-
"push notifications",
|
|
3520
|
-
"multi-turn conversational continuation",
|
|
3521
|
-
"authenticated extended agent cards",
|
|
3522
|
-
"A2A authentication schemes",
|
|
3523
|
-
"artifact types beyond Agent Commerce outcome data"
|
|
3524
|
-
];
|
|
3525
|
-
function buildDescriptor(implementationVersion) {
|
|
3526
|
-
return {
|
|
3527
|
-
name: "a2a",
|
|
3528
|
-
kind: "protocol",
|
|
3529
|
-
implementationVersion,
|
|
3530
|
-
supportedSpec: A2A_SPEC_VERSION,
|
|
3531
|
-
capabilities: A2A_CAPABILITIES,
|
|
3532
|
-
status: "experimental",
|
|
3533
|
-
unsupported: A2A_UNSUPPORTED
|
|
3534
|
-
};
|
|
3535
|
-
}
|
|
3701
|
+
var A2A_TASK_STATE_INPUT_REQUIRED = "TASK_STATE_INPUT_REQUIRED";
|
|
3702
|
+
var A2A_EXTENSIONS_HEADER = "a2a-extensions";
|
|
3536
3703
|
|
|
3537
3704
|
// src/protocols/a2a/jsonrpc.ts
|
|
3538
3705
|
var JSONRPC_PARSE_ERROR = -32700;
|
|
@@ -3540,7 +3707,18 @@ var JSONRPC_INVALID_REQUEST = -32600;
|
|
|
3540
3707
|
var JSONRPC_METHOD_NOT_FOUND = -32601;
|
|
3541
3708
|
var JSONRPC_INVALID_PARAMS = -32602;
|
|
3542
3709
|
var JSONRPC_INTERNAL_ERROR = -32603;
|
|
3710
|
+
var A2A_ERROR_TASK_NOT_FOUND = -32001;
|
|
3711
|
+
var A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED = -32003;
|
|
3543
3712
|
var A2A_ERROR_UNSUPPORTED_OPERATION = -32004;
|
|
3713
|
+
var A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED = -32005;
|
|
3714
|
+
var A2A_ERROR_VERSION_NOT_SUPPORTED = -32009;
|
|
3715
|
+
var A2A_ERROR_REASONS = /* @__PURE__ */ new Map([
|
|
3716
|
+
[A2A_ERROR_TASK_NOT_FOUND, "TASK_NOT_FOUND"],
|
|
3717
|
+
[A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED, "PUSH_NOTIFICATION_NOT_SUPPORTED"],
|
|
3718
|
+
[A2A_ERROR_UNSUPPORTED_OPERATION, "UNSUPPORTED_OPERATION"],
|
|
3719
|
+
[A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED, "CONTENT_TYPE_NOT_SUPPORTED"],
|
|
3720
|
+
[A2A_ERROR_VERSION_NOT_SUPPORTED, "VERSION_NOT_SUPPORTED"]
|
|
3721
|
+
]);
|
|
3544
3722
|
function readId(value) {
|
|
3545
3723
|
if (typeof value === "string" || typeof value === "number") return value;
|
|
3546
3724
|
return null;
|
|
@@ -3549,7 +3727,21 @@ function jsonRpcResult(id, result) {
|
|
|
3549
3727
|
return { jsonrpc: "2.0", id, result };
|
|
3550
3728
|
}
|
|
3551
3729
|
function jsonRpcError(id, code, message) {
|
|
3552
|
-
|
|
3730
|
+
const reason = A2A_ERROR_REASONS.get(code);
|
|
3731
|
+
const error = {
|
|
3732
|
+
code,
|
|
3733
|
+
message,
|
|
3734
|
+
...reason !== void 0 ? {
|
|
3735
|
+
data: [
|
|
3736
|
+
{
|
|
3737
|
+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
3738
|
+
reason,
|
|
3739
|
+
domain: "a2a-protocol.org"
|
|
3740
|
+
}
|
|
3741
|
+
]
|
|
3742
|
+
} : {}
|
|
3743
|
+
};
|
|
3744
|
+
return { jsonrpc: "2.0", id, error };
|
|
3553
3745
|
}
|
|
3554
3746
|
function parseJsonRpcRequest(rawBody) {
|
|
3555
3747
|
let payload;
|
|
@@ -3576,6 +3768,16 @@ function parseJsonRpcRequest(rawBody) {
|
|
|
3576
3768
|
};
|
|
3577
3769
|
}
|
|
3578
3770
|
const id = readId(payload["id"]);
|
|
3771
|
+
if (payload["id"] === void 0) {
|
|
3772
|
+
return {
|
|
3773
|
+
ok: false,
|
|
3774
|
+
id,
|
|
3775
|
+
error: {
|
|
3776
|
+
code: JSONRPC_INVALID_REQUEST,
|
|
3777
|
+
message: 'Request must include an "id"; notifications are unsupported.'
|
|
3778
|
+
}
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3579
3781
|
if (payload["jsonrpc"] !== "2.0") {
|
|
3580
3782
|
return {
|
|
3581
3783
|
ok: false,
|
|
@@ -3604,7 +3806,13 @@ function parseJsonRpcRequest(rawBody) {
|
|
|
3604
3806
|
}
|
|
3605
3807
|
return { ok: true, request: { id, method, params: params ?? {} } };
|
|
3606
3808
|
}
|
|
3809
|
+
|
|
3810
|
+
// src/protocols/a2a/message-mapping.ts
|
|
3607
3811
|
var A2A_USER_ROLE = "ROLE_USER";
|
|
3812
|
+
var A2A_CALL_SHAPE = 'one data part {"resource": "<skill id>", "input": {...}}';
|
|
3813
|
+
function userRoleProblem(role) {
|
|
3814
|
+
return role === A2A_USER_ROLE ? void 0 : `Unsupported message role "${String(role)}": only ${A2A_USER_ROLE} is accepted.`;
|
|
3815
|
+
}
|
|
3608
3816
|
var UnknownRecord = z.record(z.string(), z.unknown());
|
|
3609
3817
|
var MessageSchema = z.object({
|
|
3610
3818
|
role: z.string(),
|
|
@@ -3622,13 +3830,20 @@ var ParamsSchema = z.object({
|
|
|
3622
3830
|
function invalid(message) {
|
|
3623
3831
|
return new CommerceError("INPUT_INVALID", message);
|
|
3624
3832
|
}
|
|
3625
|
-
function unsupported(message) {
|
|
3626
|
-
return new CommerceError(
|
|
3833
|
+
function unsupported(message, a2aErrorCode) {
|
|
3834
|
+
return new CommerceError(
|
|
3835
|
+
"PROTOCOL_UNSUPPORTED",
|
|
3836
|
+
message,
|
|
3837
|
+
a2aErrorCode !== void 0 ? { details: { a2aErrorCode } } : {}
|
|
3838
|
+
);
|
|
3627
3839
|
}
|
|
3628
3840
|
function assertNoContinuation(params) {
|
|
3629
3841
|
const message = params.message;
|
|
3630
3842
|
if (params.taskId !== void 0 || message.taskId !== void 0) {
|
|
3631
|
-
throw unsupported(
|
|
3843
|
+
throw unsupported(
|
|
3844
|
+
"Task not found: tasks are not persisted, so send a request with no taskId.",
|
|
3845
|
+
A2A_ERROR_TASK_NOT_FOUND
|
|
3846
|
+
);
|
|
3632
3847
|
}
|
|
3633
3848
|
if (params.contextId !== void 0 || message.contextId !== void 0) {
|
|
3634
3849
|
throw unsupported(
|
|
@@ -3641,10 +3856,16 @@ function assertNoContinuation(params) {
|
|
|
3641
3856
|
}
|
|
3642
3857
|
function assertSupportedPart(part) {
|
|
3643
3858
|
if ("file" in part || "raw" in part || "url" in part) {
|
|
3644
|
-
throw unsupported(
|
|
3859
|
+
throw unsupported(
|
|
3860
|
+
`File and URL parts are not supported: send ${A2A_CALL_SHAPE}.`,
|
|
3861
|
+
A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
|
|
3862
|
+
);
|
|
3645
3863
|
}
|
|
3646
3864
|
if ("text" in part) {
|
|
3647
|
-
throw unsupported(
|
|
3865
|
+
throw unsupported(
|
|
3866
|
+
`Text parts are not supported: send ${A2A_CALL_SHAPE}.`,
|
|
3867
|
+
A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
|
|
3868
|
+
);
|
|
3648
3869
|
}
|
|
3649
3870
|
if (!("data" in part)) {
|
|
3650
3871
|
throw invalid('Message part carries no "data": send a structured data part.');
|
|
@@ -3652,7 +3873,8 @@ function assertSupportedPart(part) {
|
|
|
3652
3873
|
const mediaType = part["mediaType"];
|
|
3653
3874
|
if (mediaType !== void 0 && mediaType !== A2A_JSON_MEDIA_TYPE) {
|
|
3654
3875
|
throw unsupported(
|
|
3655
|
-
`Media type "${String(mediaType)}" is not supported: parts must be ${A2A_JSON_MEDIA_TYPE}
|
|
3876
|
+
`Media type "${String(mediaType)}" is not supported: parts must be ${A2A_JSON_MEDIA_TYPE}.`,
|
|
3877
|
+
A2A_ERROR_CONTENT_TYPE_NOT_SUPPORTED
|
|
3656
3878
|
);
|
|
3657
3879
|
}
|
|
3658
3880
|
}
|
|
@@ -3665,10 +3887,13 @@ function parseInvocation(rawParams) {
|
|
|
3665
3887
|
}
|
|
3666
3888
|
const params = parsed.data;
|
|
3667
3889
|
const message = params.message;
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
throw invalid(`Unsupported message role "${message.role}": only ${A2A_USER_ROLE} is accepted.`);
|
|
3890
|
+
if (message.messageId === void 0 || message.messageId.length === 0) {
|
|
3891
|
+
throw invalid('Message must carry a non-empty "messageId".');
|
|
3671
3892
|
}
|
|
3893
|
+
const messageId = message.messageId;
|
|
3894
|
+
assertNoContinuation(params);
|
|
3895
|
+
const roleProblem = userRoleProblem(message.role);
|
|
3896
|
+
if (roleProblem !== void 0) throw invalid(roleProblem);
|
|
3672
3897
|
if (message.parts.length === 0) {
|
|
3673
3898
|
throw invalid("Message carries no parts: send exactly one structured data part.");
|
|
3674
3899
|
}
|
|
@@ -3677,28 +3902,287 @@ function parseInvocation(rawParams) {
|
|
|
3677
3902
|
`Multi-part messages are not supported: send exactly one structured data part (received ${message.parts.length}).`
|
|
3678
3903
|
);
|
|
3679
3904
|
}
|
|
3680
|
-
const part = message.parts[0];
|
|
3681
|
-
if (part === void 0) throw invalid("Message carries no parts.");
|
|
3682
|
-
assertSupportedPart(part);
|
|
3683
|
-
const data = part["data"];
|
|
3684
|
-
if (!isRecord(data)) {
|
|
3685
|
-
throw invalid('Message part "data" must be a JSON object.');
|
|
3905
|
+
const part = message.parts[0];
|
|
3906
|
+
if (part === void 0) throw invalid("Message carries no parts.");
|
|
3907
|
+
assertSupportedPart(part);
|
|
3908
|
+
const data = part["data"];
|
|
3909
|
+
if (!isRecord(data)) {
|
|
3910
|
+
throw invalid('Message part "data" must be a JSON object.');
|
|
3911
|
+
}
|
|
3912
|
+
const resourceId = data["resource"];
|
|
3913
|
+
if (typeof resourceId !== "string") {
|
|
3914
|
+
throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
|
|
3915
|
+
}
|
|
3916
|
+
if (resourceId.length === 0) {
|
|
3917
|
+
throw invalid('Message part data "resource" must not be empty.');
|
|
3918
|
+
}
|
|
3919
|
+
const rawInput = data["input"];
|
|
3920
|
+
if (rawInput !== void 0 && !isRecord(rawInput)) {
|
|
3921
|
+
throw invalid('Message part data "input" must be a JSON object.');
|
|
3922
|
+
}
|
|
3923
|
+
return { resourceId, input: rawInput ?? {}, messageId };
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// src/protocols/a2a/x402-extension.ts
|
|
3927
|
+
var A2A_X402_EXTENSION_URI = "https://github.com/google-a2a/a2a-x402/v0.1";
|
|
3928
|
+
var A2A_X402_PROTOCOL_VERSION = 2;
|
|
3929
|
+
var X402_PAYMENT_STATUS_KEY = "x402.payment.status";
|
|
3930
|
+
var X402_PAYMENT_REQUIRED_KEY = "x402.payment.required";
|
|
3931
|
+
var X402_PAYMENT_PAYLOAD_KEY = "x402.payment.payload";
|
|
3932
|
+
var X402_PAYMENT_RECEIPTS_KEY = "x402.payment.receipts";
|
|
3933
|
+
var X402_PAYMENT_ERROR_KEY = "x402.payment.error";
|
|
3934
|
+
var DEFAULT_PENDING_TTL_MS = 10 * 60 * 1e3;
|
|
3935
|
+
var MAX_PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
3936
|
+
var MAX_PENDING_PAYMENTS = 256;
|
|
3937
|
+
function requestsX402Extension(header2) {
|
|
3938
|
+
const values = typeof header2 === "string" ? [header2] : header2 ?? [];
|
|
3939
|
+
return values.some(
|
|
3940
|
+
(value) => value.split(",").some((uri) => uri.trim() === A2A_X402_EXTENSION_URI)
|
|
3941
|
+
);
|
|
3942
|
+
}
|
|
3943
|
+
function createPendingPayments(clock, maxEntries = MAX_PENDING_PAYMENTS) {
|
|
3944
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
3945
|
+
const now = () => clock.now().getTime();
|
|
3946
|
+
function purgeExpired() {
|
|
3947
|
+
const at = now();
|
|
3948
|
+
for (const [taskId, pending] of tasks) {
|
|
3949
|
+
if (pending.expiresAtMs <= at) tasks.delete(taskId);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
return {
|
|
3953
|
+
/** Holds a purchase until `expiresAt`, evicting the oldest one when full */
|
|
3954
|
+
put(taskId, pending, expiresAt) {
|
|
3955
|
+
purgeExpired();
|
|
3956
|
+
if (tasks.size >= maxEntries) {
|
|
3957
|
+
const [oldest] = tasks.keys();
|
|
3958
|
+
if (oldest !== void 0) tasks.delete(oldest);
|
|
3959
|
+
}
|
|
3960
|
+
const requested = expiresAt !== void 0 ? Date.parse(expiresAt) : Number.NaN;
|
|
3961
|
+
const ttl = Number.isFinite(requested) ? requested - now() : DEFAULT_PENDING_TTL_MS;
|
|
3962
|
+
const expiresAtMs = now() + Math.min(Math.max(ttl, 0), MAX_PENDING_TTL_MS);
|
|
3963
|
+
tasks.set(taskId, { ...pending, expiresAtMs });
|
|
3964
|
+
},
|
|
3965
|
+
get(taskId) {
|
|
3966
|
+
const pending = tasks.get(taskId);
|
|
3967
|
+
if (pending === void 0) return void 0;
|
|
3968
|
+
if (pending.expiresAtMs <= now()) {
|
|
3969
|
+
tasks.delete(taskId);
|
|
3970
|
+
return void 0;
|
|
3971
|
+
}
|
|
3972
|
+
return pending;
|
|
3973
|
+
},
|
|
3974
|
+
delete(taskId) {
|
|
3975
|
+
tasks.delete(taskId);
|
|
3976
|
+
},
|
|
3977
|
+
size() {
|
|
3978
|
+
return tasks.size;
|
|
3979
|
+
},
|
|
3980
|
+
clear() {
|
|
3981
|
+
tasks.clear();
|
|
3982
|
+
}
|
|
3983
|
+
};
|
|
3984
|
+
}
|
|
3985
|
+
function readPaymentSubmission(message) {
|
|
3986
|
+
const metadata = isRecord(message) ? message["metadata"] : void 0;
|
|
3987
|
+
if (!isRecord(metadata)) return void 0;
|
|
3988
|
+
const status = metadata[X402_PAYMENT_STATUS_KEY];
|
|
3989
|
+
if (status === "payment-rejected") return { kind: "rejected" };
|
|
3990
|
+
const payload = metadata[X402_PAYMENT_PAYLOAD_KEY];
|
|
3991
|
+
if (status === "payment-submitted" && isRecord(payload)) return { kind: "submitted", payload };
|
|
3992
|
+
return void 0;
|
|
3993
|
+
}
|
|
3994
|
+
function readFollowUpAuthorization(message) {
|
|
3995
|
+
const parts = isRecord(message) ? message["parts"] : void 0;
|
|
3996
|
+
if (!Array.isArray(parts)) return void 0;
|
|
3997
|
+
for (const part of parts) {
|
|
3998
|
+
const data = isRecord(part) ? part["data"] : void 0;
|
|
3999
|
+
const input = isRecord(data) ? data["input"] : void 0;
|
|
4000
|
+
if (isRecord(input) && input[AUTHORIZATION_INPUT_FIELD] !== void 0) {
|
|
4001
|
+
return parseAuthorizationSubmission(input[AUTHORIZATION_INPUT_FIELD]);
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
return void 0;
|
|
4005
|
+
}
|
|
4006
|
+
function paymentRequiredMetadata(envelope) {
|
|
4007
|
+
return {
|
|
4008
|
+
[X402_PAYMENT_STATUS_KEY]: "payment-required",
|
|
4009
|
+
[X402_PAYMENT_REQUIRED_KEY]: envelope
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
function paymentCompletedMetadata(payment) {
|
|
4013
|
+
return {
|
|
4014
|
+
[X402_PAYMENT_STATUS_KEY]: "payment-completed",
|
|
4015
|
+
[X402_PAYMENT_RECEIPTS_KEY]: [settlementResponse(payment)]
|
|
4016
|
+
};
|
|
4017
|
+
}
|
|
4018
|
+
function paymentFailureMetadata(error, network) {
|
|
4019
|
+
const settled = settlementResponseFromDetails(error.details);
|
|
4020
|
+
if (settled !== void 0 && settled["success"] === true) {
|
|
4021
|
+
return {
|
|
4022
|
+
[X402_PAYMENT_STATUS_KEY]: "payment-completed",
|
|
4023
|
+
[X402_PAYMENT_RECEIPTS_KEY]: [settled]
|
|
4024
|
+
};
|
|
4025
|
+
}
|
|
4026
|
+
const code = error.code.startsWith("PAYMENT_") ? x402ErrorCode(error.code, error.details?.["reason"]) : error.code.toLowerCase();
|
|
4027
|
+
const failure = settlementFailure(error.details);
|
|
4028
|
+
const receipt = failure["network"] === "" && network !== void 0 ? { ...failure, network } : failure;
|
|
4029
|
+
return {
|
|
4030
|
+
[X402_PAYMENT_STATUS_KEY]: "payment-failed",
|
|
4031
|
+
[X402_PAYMENT_ERROR_KEY]: code,
|
|
4032
|
+
// Keep the failure reason, including `settlement_pending` when a hash is known
|
|
4033
|
+
[X402_PAYMENT_RECEIPTS_KEY]: [
|
|
4034
|
+
error.details?.["settlementUncertain"] === true ? receipt : { ...receipt, errorReason: code }
|
|
4035
|
+
]
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
function paymentRejectedMetadata() {
|
|
4039
|
+
return { [X402_PAYMENT_STATUS_KEY]: "payment-rejected" };
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
// src/protocols/a2a/agent-card.ts
|
|
4043
|
+
var RESOURCES_PATH = "/api/resources";
|
|
4044
|
+
function skillTags(resource) {
|
|
4045
|
+
return ["agent-commerce", resource.pricing.type === "free" ? "free" : "paid"];
|
|
4046
|
+
}
|
|
4047
|
+
function skillDescription(resource) {
|
|
4048
|
+
const base = resource.description ?? resource.name;
|
|
4049
|
+
if (resource.pricing.type === "fixed") {
|
|
4050
|
+
return `${base} Costs ${resource.pricing.amount} ${resource.pricing.currency} per call.`;
|
|
3686
4051
|
}
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
throw invalid('Message part data must carry a "resource" string naming a canonical resource.');
|
|
4052
|
+
if (resource.pricing.type === "dynamic") {
|
|
4053
|
+
return `${base} Requires payment (amount determined at request time).`;
|
|
3690
4054
|
}
|
|
3691
|
-
|
|
3692
|
-
|
|
4055
|
+
return base;
|
|
4056
|
+
}
|
|
4057
|
+
function exampleValue(schema, name) {
|
|
4058
|
+
if (!isRecord(schema)) return `<${name}>`;
|
|
4059
|
+
const examples = schema["examples"];
|
|
4060
|
+
if (Array.isArray(examples) && examples.length > 0) return examples[0];
|
|
4061
|
+
if (schema["default"] !== void 0) return schema["default"];
|
|
4062
|
+
const allowed = schema["enum"];
|
|
4063
|
+
if (Array.isArray(allowed) && allowed.length > 0) return allowed[0];
|
|
4064
|
+
const declared = schema["type"];
|
|
4065
|
+
const type = Array.isArray(declared) ? declared[0] : declared;
|
|
4066
|
+
switch (type) {
|
|
4067
|
+
case "object":
|
|
4068
|
+
return exampleObject(schema);
|
|
4069
|
+
case "array":
|
|
4070
|
+
return schema["items"] !== void 0 ? [exampleValue(schema["items"], name)] : [];
|
|
4071
|
+
case "number":
|
|
4072
|
+
case "integer":
|
|
4073
|
+
return 0;
|
|
4074
|
+
case "boolean":
|
|
4075
|
+
return false;
|
|
4076
|
+
case "null":
|
|
4077
|
+
return null;
|
|
4078
|
+
default:
|
|
4079
|
+
return `<${name}>`;
|
|
3693
4080
|
}
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
4081
|
+
}
|
|
4082
|
+
function exampleObject(schema) {
|
|
4083
|
+
const properties = isRecord(schema["properties"]) ? schema["properties"] : {};
|
|
4084
|
+
const required = Array.isArray(schema["required"]) ? schema["required"] : [];
|
|
4085
|
+
const example = {};
|
|
4086
|
+
for (const key of required) {
|
|
4087
|
+
if (typeof key !== "string" || key === PAYMENT_INPUT_FIELD || key === AUTHORIZATION_INPUT_FIELD)
|
|
4088
|
+
continue;
|
|
4089
|
+
example[key] = exampleValue(properties[key], key);
|
|
3697
4090
|
}
|
|
4091
|
+
return example;
|
|
4092
|
+
}
|
|
4093
|
+
function skillExample(resource) {
|
|
4094
|
+
return JSON.stringify({
|
|
4095
|
+
resource: resource.id,
|
|
4096
|
+
input: exampleObject(resource.inputSchema ?? {})
|
|
4097
|
+
});
|
|
4098
|
+
}
|
|
4099
|
+
function buildAgentSkill(resource) {
|
|
3698
4100
|
return {
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
4101
|
+
id: resource.id,
|
|
4102
|
+
name: resource.name,
|
|
4103
|
+
description: skillDescription(resource),
|
|
4104
|
+
tags: skillTags(resource),
|
|
4105
|
+
examples: [skillExample(resource)],
|
|
4106
|
+
inputModes: [A2A_JSON_MEDIA_TYPE],
|
|
4107
|
+
outputModes: [A2A_JSON_MEDIA_TYPE]
|
|
4108
|
+
};
|
|
4109
|
+
}
|
|
4110
|
+
function agentDescription(options) {
|
|
4111
|
+
const call = `Call a skill by sending ${A2A_CALL_SHAPE} with media type ${A2A_JSON_MEDIA_TYPE}. Each skill has an example; input schemas are at ${joinUrl(options.publicBaseUrl, RESOURCES_PATH)}.`;
|
|
4112
|
+
const pay = options.resources.some((resource) => resource.pricing.type !== "free") ? ` For paid skills, retry with a payment proof in "input.${PAYMENT_INPUT_FIELD}".` : "";
|
|
4113
|
+
return `${options.description} ${call}${pay}`;
|
|
4114
|
+
}
|
|
4115
|
+
function buildAgentCard(options) {
|
|
4116
|
+
return {
|
|
4117
|
+
name: options.name,
|
|
4118
|
+
description: agentDescription(options),
|
|
4119
|
+
version: options.version,
|
|
4120
|
+
supportedInterfaces: [
|
|
4121
|
+
{
|
|
4122
|
+
url: joinUrl(options.publicBaseUrl, options.mountPath),
|
|
4123
|
+
protocolBinding: A2A_PROTOCOL_BINDING,
|
|
4124
|
+
protocolVersion: A2A_PROTOCOL_VERSION
|
|
4125
|
+
}
|
|
4126
|
+
],
|
|
4127
|
+
capabilities: {
|
|
4128
|
+
streaming: false,
|
|
4129
|
+
pushNotifications: false,
|
|
4130
|
+
extendedAgentCard: false,
|
|
4131
|
+
// Only a skill whose rail is x402 can be paid through the extension.
|
|
4132
|
+
// Not required: `_payment` keeps working for clients without it.
|
|
4133
|
+
...options.resources.some((resource) => resource.paymentMethods[0] === "x402") ? {
|
|
4134
|
+
extensions: [
|
|
4135
|
+
{
|
|
4136
|
+
uri: A2A_X402_EXTENSION_URI,
|
|
4137
|
+
description: "x402 payments: input-required tasks carrying x402 v2 documents.",
|
|
4138
|
+
required: false,
|
|
4139
|
+
// Lets a client holding an x402 v1 library tell before it pays
|
|
4140
|
+
params: { x402Version: A2A_X402_PROTOCOL_VERSION }
|
|
4141
|
+
}
|
|
4142
|
+
]
|
|
4143
|
+
} : {}
|
|
4144
|
+
},
|
|
4145
|
+
defaultInputModes: [A2A_JSON_MEDIA_TYPE],
|
|
4146
|
+
defaultOutputModes: [A2A_JSON_MEDIA_TYPE],
|
|
4147
|
+
skills: options.resources.map(buildAgentSkill)
|
|
4148
|
+
};
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
// src/protocols/a2a/descriptor.ts
|
|
4152
|
+
var A2A_CAPABILITIES = [
|
|
4153
|
+
"agent-card",
|
|
4154
|
+
"jsonrpc",
|
|
4155
|
+
"SendMessage",
|
|
4156
|
+
// The x402 A2A transport, opt-in per request
|
|
4157
|
+
"x402-extension"
|
|
4158
|
+
];
|
|
4159
|
+
var A2A_UNSUPPORTED = [
|
|
4160
|
+
// Methods, as the protocol names them; the transport refuses the same list
|
|
4161
|
+
...A2A_UNSUPPORTED_METHODS,
|
|
4162
|
+
// Transports other than the one binding served
|
|
4163
|
+
"HTTP+JSON/REST binding",
|
|
4164
|
+
"gRPC binding",
|
|
4165
|
+
// Behaviors
|
|
4166
|
+
"SSE",
|
|
4167
|
+
"long-running task persistence",
|
|
4168
|
+
"task resumption other than an x402 extension payment",
|
|
4169
|
+
"push notifications",
|
|
4170
|
+
"multi-turn conversational continuation",
|
|
4171
|
+
"authenticated extended agent cards",
|
|
4172
|
+
"A2A authentication schemes",
|
|
4173
|
+
"artifact types beyond Agent Commerce outcome data",
|
|
4174
|
+
// Pending x402 payments live in process memory
|
|
4175
|
+
"x402 extension payments across gateway restarts or instances"
|
|
4176
|
+
];
|
|
4177
|
+
function buildDescriptor(implementationVersion) {
|
|
4178
|
+
return {
|
|
4179
|
+
name: "a2a",
|
|
4180
|
+
kind: "protocol",
|
|
4181
|
+
implementationVersion,
|
|
4182
|
+
supportedSpec: A2A_SPEC_VERSION,
|
|
4183
|
+
capabilities: A2A_CAPABILITIES,
|
|
4184
|
+
status: "experimental",
|
|
4185
|
+
unsupported: A2A_UNSUPPORTED
|
|
3702
4186
|
};
|
|
3703
4187
|
}
|
|
3704
4188
|
|
|
@@ -3724,19 +4208,56 @@ function completedTask(outcome, identity) {
|
|
|
3724
4208
|
});
|
|
3725
4209
|
}
|
|
3726
4210
|
function paymentRequiredTask(outcome, identity) {
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
4211
|
+
const envelope = toPaymentRequiredEnvelope(outcome);
|
|
4212
|
+
return withStatusMessage(
|
|
4213
|
+
task(identity, A2A_TASK_STATE_FAILED, {
|
|
4214
|
+
name: outcome.resourceId,
|
|
4215
|
+
parts: [{ data: { ...envelope }, mediaType: A2A_JSON_MEDIA_TYPE }]
|
|
4216
|
+
}),
|
|
4217
|
+
envelope.message,
|
|
4218
|
+
identity.statusMessageId
|
|
4219
|
+
);
|
|
4220
|
+
}
|
|
4221
|
+
function inputRequired(base, metadata, messageId) {
|
|
4222
|
+
return withStatusMessage(
|
|
4223
|
+
{ ...base, status: { ...base.status, state: A2A_TASK_STATE_INPUT_REQUIRED } },
|
|
4224
|
+
"Payment is required. Send the x402 payment payload on this task.",
|
|
4225
|
+
messageId,
|
|
4226
|
+
metadata
|
|
4227
|
+
);
|
|
4228
|
+
}
|
|
4229
|
+
function withStatusMessage(base, text, messageId, metadata) {
|
|
4230
|
+
return {
|
|
4231
|
+
...base,
|
|
4232
|
+
status: {
|
|
4233
|
+
...base.status,
|
|
4234
|
+
message: {
|
|
4235
|
+
role: "ROLE_AGENT",
|
|
4236
|
+
messageId,
|
|
4237
|
+
contextId: base.contextId,
|
|
4238
|
+
taskId: base.id,
|
|
4239
|
+
parts: [{ text }],
|
|
4240
|
+
...metadata !== void 0 ? { metadata, extensions: [A2A_X402_EXTENSION_URI] } : {}
|
|
4241
|
+
}
|
|
4242
|
+
}
|
|
4243
|
+
};
|
|
3731
4244
|
}
|
|
3732
4245
|
function failedTask(error, identity) {
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
4246
|
+
const envelope = toErrorEnvelope(error);
|
|
4247
|
+
return withStatusMessage(
|
|
4248
|
+
task(identity, A2A_TASK_STATE_FAILED, {
|
|
4249
|
+
parts: [{ data: { ...envelope }, mediaType: A2A_JSON_MEDIA_TYPE }]
|
|
4250
|
+
}),
|
|
4251
|
+
envelope.message,
|
|
4252
|
+
identity.statusMessageId
|
|
4253
|
+
);
|
|
3736
4254
|
}
|
|
3737
4255
|
|
|
3738
4256
|
// src/protocols/a2a/adapter.ts
|
|
3739
4257
|
var MAX_REQUEST_BODY_BYTES = 256 * 1024;
|
|
4258
|
+
var VERSION_PATTERN = /^(\d+\.\d+)(?:\.\d+)?$/;
|
|
4259
|
+
var A2A_VERSION_PARAMETER = "A2A-Version";
|
|
4260
|
+
var AGENT_CARD_CACHE_CONTROL = "public, max-age=300";
|
|
3740
4261
|
var DEFAULT_AGENT_DESCRIPTION = "Agent Commerce Gateway: canonical commerce resources exposed as A2A skills.";
|
|
3741
4262
|
var A2aProtocolAdapter = class {
|
|
3742
4263
|
name = "a2a";
|
|
@@ -3754,6 +4275,10 @@ var A2aProtocolAdapter = class {
|
|
|
3754
4275
|
// Built once at start: resources are fixed at config load, and a per-request
|
|
3755
4276
|
// build would be work an unauthenticated GET could trigger at will
|
|
3756
4277
|
card;
|
|
4278
|
+
cardBody = "";
|
|
4279
|
+
cardEtag = "";
|
|
4280
|
+
// Purchases waiting for an x402 extension payment
|
|
4281
|
+
pendingPayments;
|
|
3757
4282
|
constructor(options = {}) {
|
|
3758
4283
|
this.mountPath = options.mountPath ?? A2A_DEFAULT_MOUNT_PATH;
|
|
3759
4284
|
this.agentName = options.agentName ?? A2A_DEFAULT_AGENT_NAME;
|
|
@@ -3790,6 +4315,9 @@ var A2aProtocolAdapter = class {
|
|
|
3790
4315
|
mountPath: this.mountPath,
|
|
3791
4316
|
resources
|
|
3792
4317
|
});
|
|
4318
|
+
this.pendingPayments = createPendingPayments(context.clock);
|
|
4319
|
+
this.cardBody = JSON.stringify(this.card);
|
|
4320
|
+
this.cardEtag = `"${createHash("sha256").update(this.cardBody).digest("base64url")}"`;
|
|
3793
4321
|
this.started = true;
|
|
3794
4322
|
context.logger.info(
|
|
3795
4323
|
{ mountPath: this.mountPath, cardPath: A2A_AGENT_CARD_PATH, skillCount: resources.length },
|
|
@@ -3807,7 +4335,14 @@ var A2aProtocolAdapter = class {
|
|
|
3807
4335
|
this.writeJson(res, 405, { error: "Method not allowed. The Agent Card is read-only." });
|
|
3808
4336
|
return;
|
|
3809
4337
|
}
|
|
3810
|
-
|
|
4338
|
+
const cacheHeaders = { "cache-control": AGENT_CARD_CACHE_CONTROL, etag: this.cardEtag };
|
|
4339
|
+
if (matchesEtag(req.headers["if-none-match"], this.cardEtag)) {
|
|
4340
|
+
res.writeHead(304, cacheHeaders);
|
|
4341
|
+
res.end();
|
|
4342
|
+
return;
|
|
4343
|
+
}
|
|
4344
|
+
res.writeHead(200, { "content-type": A2A_JSON_MEDIA_TYPE, ...cacheHeaders });
|
|
4345
|
+
res.end(this.cardBody);
|
|
3811
4346
|
} catch (err) {
|
|
3812
4347
|
this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: agent card request failed");
|
|
3813
4348
|
this.writeJson(res, 500, { error: "Internal server error." });
|
|
@@ -3836,21 +4371,19 @@ var A2aProtocolAdapter = class {
|
|
|
3836
4371
|
);
|
|
3837
4372
|
return;
|
|
3838
4373
|
}
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
if (declared !== A2A_PROTOCOL_VERSION) {
|
|
4374
|
+
const read = await readCappedBody(req, MAX_REQUEST_BODY_BYTES);
|
|
4375
|
+
if (read.kind === "too-large") {
|
|
3842
4376
|
this.writeJson(
|
|
3843
4377
|
res,
|
|
3844
|
-
|
|
4378
|
+
413,
|
|
3845
4379
|
jsonRpcError(
|
|
3846
4380
|
null,
|
|
3847
|
-
|
|
3848
|
-
`
|
|
4381
|
+
JSONRPC_INVALID_REQUEST,
|
|
4382
|
+
`Request body exceeds the ${MAX_REQUEST_BODY_BYTES}-byte limit.`
|
|
3849
4383
|
)
|
|
3850
4384
|
);
|
|
3851
4385
|
return;
|
|
3852
4386
|
}
|
|
3853
|
-
const read = await readCappedBody(req, MAX_REQUEST_BODY_BYTES);
|
|
3854
4387
|
if (read.kind !== "ok") {
|
|
3855
4388
|
this.writeJson(
|
|
3856
4389
|
res,
|
|
@@ -3864,10 +4397,31 @@ var A2aProtocolAdapter = class {
|
|
|
3864
4397
|
this.writeJson(res, 200, jsonRpcError(parsed.id, parsed.error.code, parsed.error.message));
|
|
3865
4398
|
return;
|
|
3866
4399
|
}
|
|
4400
|
+
const version = req.headers[A2A_VERSION_HEADER];
|
|
4401
|
+
const declared = (Array.isArray(version) ? version[0] : version) ?? new URL(req.url ?? "/", "http://localhost").searchParams.get(A2A_VERSION_PARAMETER);
|
|
4402
|
+
if (declared?.match(VERSION_PATTERN)?.[1] !== A2A_PROTOCOL_VERSION) {
|
|
4403
|
+
this.writeJson(
|
|
4404
|
+
res,
|
|
4405
|
+
200,
|
|
4406
|
+
jsonRpcError(
|
|
4407
|
+
parsed.request.id,
|
|
4408
|
+
A2A_ERROR_VERSION_NOT_SUPPORTED,
|
|
4409
|
+
`Unsupported A2A protocol version. Send the ${A2A_VERSION_PARAMETER} header with "${A2A_PROTOCOL_VERSION}".`
|
|
4410
|
+
)
|
|
4411
|
+
);
|
|
4412
|
+
return;
|
|
4413
|
+
}
|
|
4414
|
+
const response = await this.dispatch(
|
|
4415
|
+
parsed.request.id,
|
|
4416
|
+
parsed.request.method,
|
|
4417
|
+
parsed.request.params,
|
|
4418
|
+
requestsX402Extension(req.headers[A2A_EXTENSIONS_HEADER])
|
|
4419
|
+
);
|
|
3867
4420
|
this.writeJson(
|
|
3868
4421
|
res,
|
|
3869
4422
|
200,
|
|
3870
|
-
|
|
4423
|
+
response,
|
|
4424
|
+
usesX402Extension(response) ? { [A2A_EXTENSIONS_HEADER]: A2A_X402_EXTENSION_URI } : {}
|
|
3871
4425
|
);
|
|
3872
4426
|
} catch (err) {
|
|
3873
4427
|
this.context?.logger.error({ err: toLogInfo(err) }, "a2a adapter: request handling failed");
|
|
@@ -3878,7 +4432,21 @@ var A2aProtocolAdapter = class {
|
|
|
3878
4432
|
);
|
|
3879
4433
|
}
|
|
3880
4434
|
}
|
|
3881
|
-
async dispatch(id, method, params) {
|
|
4435
|
+
async dispatch(id, method, params, x402Extension) {
|
|
4436
|
+
if (A2A_PUSH_CONFIG_METHODS.includes(method)) {
|
|
4437
|
+
return jsonRpcError(
|
|
4438
|
+
id,
|
|
4439
|
+
A2A_ERROR_PUSH_NOTIFICATION_NOT_SUPPORTED,
|
|
4440
|
+
`A2A method "${method}" is not supported: this agent does not support push notifications.`
|
|
4441
|
+
);
|
|
4442
|
+
}
|
|
4443
|
+
if (A2A_TASK_LOOKUP_METHODS.includes(method)) {
|
|
4444
|
+
return jsonRpcError(
|
|
4445
|
+
id,
|
|
4446
|
+
A2A_ERROR_TASK_NOT_FOUND,
|
|
4447
|
+
"Task not found. Finished tasks are not retained; continue a pending payment with SendMessage."
|
|
4448
|
+
);
|
|
4449
|
+
}
|
|
3882
4450
|
if (A2A_UNSUPPORTED_METHODS.includes(method)) {
|
|
3883
4451
|
return jsonRpcError(
|
|
3884
4452
|
id,
|
|
@@ -3889,18 +4457,21 @@ var A2aProtocolAdapter = class {
|
|
|
3889
4457
|
if (method !== A2A_METHOD_SEND_MESSAGE) {
|
|
3890
4458
|
return jsonRpcError(id, JSONRPC_METHOD_NOT_FOUND, `Unknown method "${method}".`);
|
|
3891
4459
|
}
|
|
4460
|
+
const resumed = await this.resumePayment(id, params);
|
|
4461
|
+
if (resumed !== void 0) return resumed;
|
|
3892
4462
|
let invocation;
|
|
3893
4463
|
try {
|
|
3894
4464
|
invocation = parseInvocation(params);
|
|
3895
4465
|
} catch (err) {
|
|
3896
4466
|
const error = toCommerceError(err);
|
|
3897
|
-
const
|
|
4467
|
+
const a2aErrorCode = error.details?.["a2aErrorCode"];
|
|
4468
|
+
const code = typeof a2aErrorCode === "number" ? a2aErrorCode : error.code === "PROTOCOL_UNSUPPORTED" ? A2A_ERROR_UNSUPPORTED_OPERATION : JSONRPC_INVALID_PARAMS;
|
|
3898
4469
|
return jsonRpcError(id, code, error.message);
|
|
3899
4470
|
}
|
|
3900
|
-
return this.execute(id, invocation);
|
|
4471
|
+
return this.execute(id, invocation, x402Extension);
|
|
3901
4472
|
}
|
|
3902
4473
|
// One accepted invocation, one `pipeline.execute()`
|
|
3903
|
-
async execute(id, invocation) {
|
|
4474
|
+
async execute(id, invocation, x402Extension) {
|
|
3904
4475
|
const context = this.context;
|
|
3905
4476
|
if (context === void 0) {
|
|
3906
4477
|
return jsonRpcError(id, JSONRPC_INTERNAL_ERROR, "A2A adapter is not running.");
|
|
@@ -3936,9 +4507,29 @@ var A2aProtocolAdapter = class {
|
|
|
3936
4507
|
...authorization !== void 0 ? { authorization } : {}
|
|
3937
4508
|
};
|
|
3938
4509
|
const outcome = await context.pipeline.execute(request);
|
|
4510
|
+
if (outcome.kind !== "payment-required") {
|
|
4511
|
+
return this.taskResult(id, completedTask(outcome, identity));
|
|
4512
|
+
}
|
|
4513
|
+
const task2 = paymentRequiredTask(outcome, identity);
|
|
4514
|
+
const envelope = outcome.requirement.challenge.envelope;
|
|
4515
|
+
if (!x402Extension || outcome.requirement.provider !== "x402" || !isRecord(envelope)) {
|
|
4516
|
+
return this.taskResult(id, task2);
|
|
4517
|
+
}
|
|
4518
|
+
const network = outcome.requirement.network;
|
|
4519
|
+
this.pendingPayments?.put(
|
|
4520
|
+
identity.taskId,
|
|
4521
|
+
{
|
|
4522
|
+
contextId: identity.contextId,
|
|
4523
|
+
resourceId: invocation.resourceId,
|
|
4524
|
+
input,
|
|
4525
|
+
...authorization !== void 0 ? { authorization } : {},
|
|
4526
|
+
...network !== void 0 ? { network } : {}
|
|
4527
|
+
},
|
|
4528
|
+
outcome.requirement.expiresAt
|
|
4529
|
+
);
|
|
3939
4530
|
return this.taskResult(
|
|
3940
4531
|
id,
|
|
3941
|
-
|
|
4532
|
+
inputRequired(task2, paymentRequiredMetadata(envelope), identity.statusMessageId)
|
|
3942
4533
|
);
|
|
3943
4534
|
} catch (err) {
|
|
3944
4535
|
const error = toCommerceError(err);
|
|
@@ -3949,11 +4540,139 @@ var A2aProtocolAdapter = class {
|
|
|
3949
4540
|
return this.taskResult(id, failedTask(error, identity));
|
|
3950
4541
|
}
|
|
3951
4542
|
}
|
|
4543
|
+
/**
|
|
4544
|
+
* A message on a task waiting for an x402 extension payment, recognized by
|
|
4545
|
+
* its `taskId` whether or not the header activated the extension again. Any
|
|
4546
|
+
* other task id falls through to `parseInvocation`, which answers
|
|
4547
|
+
* TaskNotFoundError.
|
|
4548
|
+
*/
|
|
4549
|
+
async resumePayment(id, params) {
|
|
4550
|
+
const context = this.context;
|
|
4551
|
+
const message = isRecord(params) ? params["message"] : void 0;
|
|
4552
|
+
const taskId = isRecord(message) ? message["taskId"] : void 0;
|
|
4553
|
+
if (context === void 0 || typeof taskId !== "string") return void 0;
|
|
4554
|
+
const pending = this.pendingPayments?.get(taskId);
|
|
4555
|
+
if (pending === void 0) return void 0;
|
|
4556
|
+
const messageId = isRecord(message) ? message["messageId"] : void 0;
|
|
4557
|
+
if (typeof messageId !== "string" || messageId.length === 0) {
|
|
4558
|
+
return jsonRpcError(
|
|
4559
|
+
id,
|
|
4560
|
+
JSONRPC_INVALID_PARAMS,
|
|
4561
|
+
'Message must carry a non-empty "messageId".'
|
|
4562
|
+
);
|
|
4563
|
+
}
|
|
4564
|
+
const roleProblem = userRoleProblem(isRecord(message) ? message["role"] : void 0);
|
|
4565
|
+
if (roleProblem !== void 0) return jsonRpcError(id, JSONRPC_INVALID_PARAMS, roleProblem);
|
|
4566
|
+
const contextId = isRecord(message) ? message["contextId"] : void 0;
|
|
4567
|
+
if (contextId !== void 0 && contextId !== pending.contextId) {
|
|
4568
|
+
return jsonRpcError(
|
|
4569
|
+
id,
|
|
4570
|
+
JSONRPC_INVALID_PARAMS,
|
|
4571
|
+
'Message "contextId" does not match the task.'
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
4574
|
+
const submission = readPaymentSubmission(message);
|
|
4575
|
+
if (submission === void 0) {
|
|
4576
|
+
return jsonRpcError(
|
|
4577
|
+
id,
|
|
4578
|
+
JSONRPC_INVALID_PARAMS,
|
|
4579
|
+
`For a pending task, set "${X402_PAYMENT_STATUS_KEY}" to "payment-submitted" and provide a "${X402_PAYMENT_PAYLOAD_KEY}" object, or set it to "payment-rejected".`
|
|
4580
|
+
);
|
|
4581
|
+
}
|
|
4582
|
+
let authorization = pending.authorization;
|
|
4583
|
+
if (authorization === void 0 && submission.kind === "submitted") {
|
|
4584
|
+
try {
|
|
4585
|
+
authorization = readFollowUpAuthorization(message);
|
|
4586
|
+
} catch (err) {
|
|
4587
|
+
return jsonRpcError(id, JSONRPC_INVALID_PARAMS, toCommerceError(err).message);
|
|
4588
|
+
}
|
|
4589
|
+
}
|
|
4590
|
+
this.pendingPayments?.delete(taskId);
|
|
4591
|
+
const statusMessageId = context.ids.next("a2a-msg");
|
|
4592
|
+
const identity = {
|
|
4593
|
+
taskId,
|
|
4594
|
+
contextId: pending.contextId,
|
|
4595
|
+
artifactId: context.ids.next("a2a-artifact"),
|
|
4596
|
+
statusMessageId,
|
|
4597
|
+
timestamp: context.clock.nowIso()
|
|
4598
|
+
};
|
|
4599
|
+
if (submission.kind === "rejected") {
|
|
4600
|
+
const declined = {
|
|
4601
|
+
id: taskId,
|
|
4602
|
+
contextId: pending.contextId,
|
|
4603
|
+
status: { state: A2A_TASK_STATE_FAILED, timestamp: identity.timestamp },
|
|
4604
|
+
artifacts: []
|
|
4605
|
+
};
|
|
4606
|
+
return this.taskResult(
|
|
4607
|
+
id,
|
|
4608
|
+
withStatusMessage(
|
|
4609
|
+
declined,
|
|
4610
|
+
"Payment was declined.",
|
|
4611
|
+
statusMessageId,
|
|
4612
|
+
paymentRejectedMetadata()
|
|
4613
|
+
)
|
|
4614
|
+
);
|
|
4615
|
+
}
|
|
4616
|
+
return this.taskResult(
|
|
4617
|
+
id,
|
|
4618
|
+
await this.payPending(
|
|
4619
|
+
context,
|
|
4620
|
+
{ ...pending, ...authorization !== void 0 ? { authorization } : {} },
|
|
4621
|
+
submission.payload,
|
|
4622
|
+
identity
|
|
4623
|
+
)
|
|
4624
|
+
);
|
|
4625
|
+
}
|
|
4626
|
+
// The pending purchase, now with the x402 `PaymentPayload` as its proof
|
|
4627
|
+
async payPending(context, pending, payload, identity) {
|
|
4628
|
+
const requestId = context.ids.next("a2a");
|
|
4629
|
+
const request = {
|
|
4630
|
+
requestId,
|
|
4631
|
+
resourceId: pending.resourceId,
|
|
4632
|
+
input: pending.input,
|
|
4633
|
+
protocol: "a2a",
|
|
4634
|
+
receivedAt: context.clock.nowIso(),
|
|
4635
|
+
// The same encoding as the HTTP header's base64 PaymentPayload JSON
|
|
4636
|
+
payment: {
|
|
4637
|
+
method: "x402",
|
|
4638
|
+
payload: Buffer.from(JSON.stringify(payload), "utf8").toString("base64")
|
|
4639
|
+
},
|
|
4640
|
+
...pending.authorization !== void 0 ? { authorization: pending.authorization } : {}
|
|
4641
|
+
};
|
|
4642
|
+
let error;
|
|
4643
|
+
try {
|
|
4644
|
+
const outcome = await context.pipeline.execute(request);
|
|
4645
|
+
if (outcome.kind === "delivered" && outcome.payment !== void 0) {
|
|
4646
|
+
return withStatusMessage(
|
|
4647
|
+
completedTask(outcome, identity),
|
|
4648
|
+
"Payment settled.",
|
|
4649
|
+
identity.statusMessageId,
|
|
4650
|
+
paymentCompletedMetadata(outcome.payment)
|
|
4651
|
+
);
|
|
4652
|
+
}
|
|
4653
|
+
error = new CommerceError("PAYMENT_INVALID", "The payment was not accepted.", { requestId });
|
|
4654
|
+
} catch (err) {
|
|
4655
|
+
error = toCommerceError(err);
|
|
4656
|
+
context.logger.warn(
|
|
4657
|
+
{ resourceId: pending.resourceId, requestId, err: toLogInfo(error) },
|
|
4658
|
+
"a2a adapter: x402 extension payment failed"
|
|
4659
|
+
);
|
|
4660
|
+
}
|
|
4661
|
+
const metadata = paymentFailureMetadata(error, pending.network);
|
|
4662
|
+
let text = "Payment failed.";
|
|
4663
|
+
if (metadata[X402_PAYMENT_STATUS_KEY] === "payment-completed") {
|
|
4664
|
+
text = "Payment settled, but the resource could not be delivered.";
|
|
4665
|
+
} else if (!error.code.startsWith("PAYMENT_")) {
|
|
4666
|
+
text = "The resource could not be delivered.";
|
|
4667
|
+
}
|
|
4668
|
+
return withStatusMessage(failedTask(error, identity), text, identity.statusMessageId, metadata);
|
|
4669
|
+
}
|
|
3952
4670
|
taskIdentity(context, requestId) {
|
|
3953
4671
|
return {
|
|
3954
4672
|
taskId: requestId,
|
|
3955
4673
|
contextId: context.ids.next("a2a-ctx"),
|
|
3956
4674
|
artifactId: context.ids.next("a2a-artifact"),
|
|
4675
|
+
statusMessageId: context.ids.next("a2a-msg"),
|
|
3957
4676
|
timestamp: context.clock.nowIso()
|
|
3958
4677
|
};
|
|
3959
4678
|
}
|
|
@@ -3971,15 +4690,27 @@ var A2aProtocolAdapter = class {
|
|
|
3971
4690
|
async stop() {
|
|
3972
4691
|
this.started = false;
|
|
3973
4692
|
this.card = void 0;
|
|
4693
|
+
this.cardBody = "";
|
|
4694
|
+
this.cardEtag = "";
|
|
4695
|
+
this.pendingPayments?.clear();
|
|
4696
|
+
this.pendingPayments = void 0;
|
|
3974
4697
|
this.skillsById = /* @__PURE__ */ new Map();
|
|
3975
4698
|
this.context = void 0;
|
|
3976
4699
|
}
|
|
3977
|
-
writeJson(res, status, body) {
|
|
4700
|
+
writeJson(res, status, body, headers = {}) {
|
|
3978
4701
|
if (res.headersSent) return;
|
|
3979
|
-
res.writeHead(status, { "content-type": A2A_JSON_MEDIA_TYPE });
|
|
4702
|
+
res.writeHead(status, { "content-type": A2A_JSON_MEDIA_TYPE, ...headers });
|
|
3980
4703
|
res.end(JSON.stringify(body));
|
|
3981
4704
|
}
|
|
3982
4705
|
};
|
|
4706
|
+
function usesX402Extension(response) {
|
|
4707
|
+
const result = response["result"];
|
|
4708
|
+
return result?.task?.status.message?.extensions?.includes(A2A_X402_EXTENSION_URI) === true;
|
|
4709
|
+
}
|
|
4710
|
+
function matchesEtag(header2, etag) {
|
|
4711
|
+
if (header2 === void 0) return false;
|
|
4712
|
+
return header2.split(",").map((tag) => tag.trim().replace(/^W\//, "")).some((tag) => tag === "*" || tag === etag);
|
|
4713
|
+
}
|
|
3983
4714
|
function createA2aAdapter(options = {}) {
|
|
3984
4715
|
return new A2aProtocolAdapter(options);
|
|
3985
4716
|
}
|
|
@@ -3993,7 +4724,8 @@ function toCanonicalRequest(options) {
|
|
|
3993
4724
|
input: canonicalInput(request),
|
|
3994
4725
|
protocol: "acp",
|
|
3995
4726
|
receivedAt,
|
|
3996
|
-
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
4727
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {},
|
|
4728
|
+
...request.forwardedHeaders !== void 0 && Object.keys(request.forwardedHeaders).length > 0 ? { backendHeaders: request.forwardedHeaders } : {}
|
|
3997
4729
|
};
|
|
3998
4730
|
}
|
|
3999
4731
|
function canonicalInput(request) {
|
|
@@ -4001,7 +4733,7 @@ function canonicalInput(request) {
|
|
|
4001
4733
|
const optionalKeys = ACP_OPERATION_OPTIONAL_INPUT_KEYS[request.route.operation] ?? [];
|
|
4002
4734
|
const input = {};
|
|
4003
4735
|
if (keys.includes("path") && request.route.sessionId !== void 0) {
|
|
4004
|
-
input["path"] = {
|
|
4736
|
+
input["path"] = { [ACP_SESSION_ID_INPUT_KEY]: request.route.sessionId };
|
|
4005
4737
|
}
|
|
4006
4738
|
if (keys.includes("body")) {
|
|
4007
4739
|
input["body"] = request.body;
|
|
@@ -4016,7 +4748,9 @@ var ACP_CAPABILITIES = [
|
|
|
4016
4748
|
"rest transport",
|
|
4017
4749
|
"well-known discovery",
|
|
4018
4750
|
"bearer authentication",
|
|
4019
|
-
"API-Version
|
|
4751
|
+
"API-Version enforcement",
|
|
4752
|
+
// Forwarded as received: the gateway verifies neither signature header
|
|
4753
|
+
"request header forwarding to the merchant",
|
|
4020
4754
|
"checkout service",
|
|
4021
4755
|
...ACP_CHECKOUT_OPERATIONS
|
|
4022
4756
|
];
|
|
@@ -4099,10 +4833,10 @@ function writeAcpFailure(res, failure, headers = {}) {
|
|
|
4099
4833
|
writeAcpJson(res, failure.status, failure.error, headers);
|
|
4100
4834
|
}
|
|
4101
4835
|
function operationKey(scope) {
|
|
4102
|
-
return
|
|
4836
|
+
return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
4103
4837
|
}
|
|
4104
4838
|
function requestFingerprint(body) {
|
|
4105
|
-
return
|
|
4839
|
+
return sha256(canonicalize(body));
|
|
4106
4840
|
}
|
|
4107
4841
|
function canonicalize(value) {
|
|
4108
4842
|
if (value === null) return "null";
|
|
@@ -4113,7 +4847,7 @@ function canonicalize(value) {
|
|
|
4113
4847
|
}
|
|
4114
4848
|
return JSON.stringify(value) ?? "null";
|
|
4115
4849
|
}
|
|
4116
|
-
function
|
|
4850
|
+
function sha256(value) {
|
|
4117
4851
|
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
4118
4852
|
}
|
|
4119
4853
|
|
|
@@ -4130,7 +4864,7 @@ function createAcpIdempotencyStore(options) {
|
|
|
4130
4864
|
logger
|
|
4131
4865
|
});
|
|
4132
4866
|
migrate(db, logger);
|
|
4133
|
-
resolveOrphanedClaims(db, logger);
|
|
4867
|
+
resolveOrphanedClaims(db, logger, options.releaseOrphans === true);
|
|
4134
4868
|
const selectStmt = db.prepare(
|
|
4135
4869
|
`SELECT state, fingerprint, status, body_json FROM acp_idempotency
|
|
4136
4870
|
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
@@ -4212,7 +4946,17 @@ function createAcpIdempotencyStore(options) {
|
|
|
4212
4946
|
}
|
|
4213
4947
|
};
|
|
4214
4948
|
}
|
|
4215
|
-
function resolveOrphanedClaims(db, logger) {
|
|
4949
|
+
function resolveOrphanedClaims(db, logger, release) {
|
|
4950
|
+
if (release) {
|
|
4951
|
+
const { changes: changes2 } = db.prepare("DELETE FROM acp_idempotency WHERE state = 'in_flight'").run();
|
|
4952
|
+
if (changes2 > 0) {
|
|
4953
|
+
logger.warn(
|
|
4954
|
+
{ released: changes2 },
|
|
4955
|
+
"acp idempotency: freed claims left in flight by an earlier process; merchant deduplication is enabled"
|
|
4956
|
+
);
|
|
4957
|
+
}
|
|
4958
|
+
return;
|
|
4959
|
+
}
|
|
4216
4960
|
const { changes } = db.prepare("UPDATE acp_idempotency SET state = 'unresolved' WHERE state = 'in_flight'").run();
|
|
4217
4961
|
if (changes > 0) {
|
|
4218
4962
|
logger.warn(
|
|
@@ -4257,8 +5001,7 @@ function migrate(db, logger) {
|
|
|
4257
5001
|
}
|
|
4258
5002
|
|
|
4259
5003
|
// src/protocols/acp/router.ts
|
|
4260
|
-
var
|
|
4261
|
-
var SESSION_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
|
5004
|
+
var MAX_SEGMENT_LENGTH = 512;
|
|
4262
5005
|
var COLLECTION = "checkout_sessions";
|
|
4263
5006
|
function matchAcpRoute(method, url, mountPath) {
|
|
4264
5007
|
const segments = routeSegments(url, mountPath);
|
|
@@ -4276,7 +5019,7 @@ function matchAcpRoute(method, url, mountPath) {
|
|
|
4276
5019
|
};
|
|
4277
5020
|
}
|
|
4278
5021
|
const sessionId = segments[1];
|
|
4279
|
-
if (sessionId === void 0 || !
|
|
5022
|
+
if (sessionId === void 0 || !isSessionId(sessionId)) return { kind: "not-found" };
|
|
4280
5023
|
if (segments.length === 2) {
|
|
4281
5024
|
if (verb === "GET") {
|
|
4282
5025
|
return {
|
|
@@ -4313,8 +5056,16 @@ function matchAcpRoute(method, url, mountPath) {
|
|
|
4313
5056
|
}
|
|
4314
5057
|
return { kind: "not-found" };
|
|
4315
5058
|
}
|
|
5059
|
+
function isSessionId(value) {
|
|
5060
|
+
if (value.length === 0) return false;
|
|
5061
|
+
for (const char of value) {
|
|
5062
|
+
const code = char.charCodeAt(0);
|
|
5063
|
+
if (code < 32 || code === 127) return false;
|
|
5064
|
+
}
|
|
5065
|
+
return true;
|
|
5066
|
+
}
|
|
4316
5067
|
function endpointPath(mountPath, segments) {
|
|
4317
|
-
return `${mountPath.replace(/\/+$/, "")}/${segments.join("/")}`;
|
|
5068
|
+
return `${mountPath.replace(/\/+$/, "")}/${segments.map(encodeURIComponent).join("/")}`;
|
|
4318
5069
|
}
|
|
4319
5070
|
function routeSegments(url, mountPath) {
|
|
4320
5071
|
if (url === void 0) return void 0;
|
|
@@ -4325,7 +5076,7 @@ function routeSegments(url, mountPath) {
|
|
|
4325
5076
|
const segments = [];
|
|
4326
5077
|
for (const raw of rest.split("/")) {
|
|
4327
5078
|
if (raw.length === 0) continue;
|
|
4328
|
-
if (raw.length >
|
|
5079
|
+
if (raw.length > MAX_SEGMENT_LENGTH) return void 0;
|
|
4329
5080
|
try {
|
|
4330
5081
|
segments.push(decodeURIComponent(raw));
|
|
4331
5082
|
} catch {
|
|
@@ -8269,6 +9020,7 @@ function acpValidators() {
|
|
|
8269
9020
|
});
|
|
8270
9021
|
addFormats(ajv);
|
|
8271
9022
|
const document = relaxExtensibleEnums(structuredClone(schema_agentic_checkout_default));
|
|
9023
|
+
openCapabilityValues(document);
|
|
8272
9024
|
const schemaId = typeof document["$id"] === "string" ? document["$id"] : "";
|
|
8273
9025
|
ajv.addSchema(document);
|
|
8274
9026
|
const compiled = /* @__PURE__ */ new Map();
|
|
@@ -8300,6 +9052,14 @@ function relaxExtensibleEnums(node) {
|
|
|
8300
9052
|
for (const value of Object.values(record)) relaxExtensibleEnums(value);
|
|
8301
9053
|
return node;
|
|
8302
9054
|
}
|
|
9055
|
+
function openCapabilityValues(document) {
|
|
9056
|
+
const defs = document["$defs"];
|
|
9057
|
+
const properties = defs["InterventionCapabilities"]?.properties ?? {};
|
|
9058
|
+
for (const list of ["supported", "required"]) {
|
|
9059
|
+
const items = properties[list]?.items;
|
|
9060
|
+
if (items !== void 0) delete items["enum"];
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
8303
9063
|
function validateAcpDocument(definition, value) {
|
|
8304
9064
|
const validate = acpValidators().get(definition);
|
|
8305
9065
|
if (validate === void 0) return { code: "unknown_definition" };
|
|
@@ -8330,7 +9090,23 @@ var REQUEST_DEFINITIONS = {
|
|
|
8330
9090
|
cancelCheckoutSession: "cancelRequest",
|
|
8331
9091
|
getCheckoutSession: void 0
|
|
8332
9092
|
};
|
|
9093
|
+
function acpEchoHeaders(req) {
|
|
9094
|
+
const echo = {};
|
|
9095
|
+
const presentedRequestId = normalizeRequestId(header(req, ACP_REQUEST_ID_HEADER));
|
|
9096
|
+
if (presentedRequestId !== void 0) echo[ACP_REQUEST_ID_HEADER] = presentedRequestId;
|
|
9097
|
+
const presentedKey = header(req, ACP_IDEMPOTENCY_KEY_HEADER)?.trim();
|
|
9098
|
+
if (req.method === "POST" && presentedKey !== void 0 && isUsableIdempotencyKey(presentedKey)) {
|
|
9099
|
+
echo[ACP_IDEMPOTENCY_KEY_HEADER] = presentedKey;
|
|
9100
|
+
}
|
|
9101
|
+
return echo;
|
|
9102
|
+
}
|
|
8333
9103
|
async function guardAcpRequest(req, options) {
|
|
9104
|
+
const echo = acpEchoHeaders(req);
|
|
9105
|
+
const failed = (failure2, extra = {}) => ({
|
|
9106
|
+
ok: false,
|
|
9107
|
+
...failure2,
|
|
9108
|
+
headers: { ...echo, ...extra }
|
|
9109
|
+
});
|
|
8334
9110
|
const matched = matchAcpRoute(req.method, req.url, options.mountPath);
|
|
8335
9111
|
if (matched.kind === "not-found") {
|
|
8336
9112
|
return failed(acpFailure(404, "invalid_request", "not_found", "Unknown ACP route."));
|
|
@@ -8342,7 +9118,8 @@ async function guardAcpRequest(req, options) {
|
|
|
8342
9118
|
"invalid_request",
|
|
8343
9119
|
"method_not_allowed",
|
|
8344
9120
|
`This ACP route accepts ${matched.allow.join(", ")}.`
|
|
8345
|
-
)
|
|
9121
|
+
),
|
|
9122
|
+
{ allow: matched.allow.join(", ") }
|
|
8346
9123
|
);
|
|
8347
9124
|
}
|
|
8348
9125
|
const route = matched.route;
|
|
@@ -8353,7 +9130,9 @@ async function guardAcpRequest(req, options) {
|
|
|
8353
9130
|
"invalid_request",
|
|
8354
9131
|
"unauthorized",
|
|
8355
9132
|
'A valid "Authorization: Bearer <token>" header is required.'
|
|
8356
|
-
)
|
|
9133
|
+
),
|
|
9134
|
+
// RFC 9110 requires a challenge on every 401
|
|
9135
|
+
{ "www-authenticate": "Bearer" }
|
|
8357
9136
|
);
|
|
8358
9137
|
}
|
|
8359
9138
|
const version = header(req, ACP_API_VERSION_HEADER);
|
|
@@ -8381,8 +9160,8 @@ async function guardAcpRequest(req, options) {
|
|
|
8381
9160
|
}
|
|
8382
9161
|
let idempotencyKey;
|
|
8383
9162
|
if (route.acceptsBody) {
|
|
8384
|
-
const
|
|
8385
|
-
if (
|
|
9163
|
+
const presentedKey = header(req, ACP_IDEMPOTENCY_KEY_HEADER)?.trim();
|
|
9164
|
+
if (presentedKey === void 0 || presentedKey.length === 0) {
|
|
8386
9165
|
return failed(
|
|
8387
9166
|
acpFailure(
|
|
8388
9167
|
400,
|
|
@@ -8392,7 +9171,7 @@ async function guardAcpRequest(req, options) {
|
|
|
8392
9171
|
)
|
|
8393
9172
|
);
|
|
8394
9173
|
}
|
|
8395
|
-
if (
|
|
9174
|
+
if (!isUsableIdempotencyKey(presentedKey)) {
|
|
8396
9175
|
return failed(
|
|
8397
9176
|
acpFailure(
|
|
8398
9177
|
400,
|
|
@@ -8402,7 +9181,7 @@ async function guardAcpRequest(req, options) {
|
|
|
8402
9181
|
)
|
|
8403
9182
|
);
|
|
8404
9183
|
}
|
|
8405
|
-
idempotencyKey =
|
|
9184
|
+
idempotencyKey = presentedKey;
|
|
8406
9185
|
}
|
|
8407
9186
|
const read = await readCappedBody(req, options.maxBodyBytes ?? ACP_MAX_REQUEST_BODY_BYTES);
|
|
8408
9187
|
if (read.kind === "too-large") {
|
|
@@ -8481,12 +9260,23 @@ function ok(route, body, req, idempotencyKey) {
|
|
|
8481
9260
|
route,
|
|
8482
9261
|
body,
|
|
8483
9262
|
...requestId !== void 0 ? { requestId } : {},
|
|
8484
|
-
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
9263
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {},
|
|
9264
|
+
forwardedHeaders: forwardedHeaders(req, requestId)
|
|
8485
9265
|
}
|
|
8486
9266
|
};
|
|
8487
9267
|
}
|
|
8488
|
-
function
|
|
8489
|
-
|
|
9268
|
+
function forwardedHeaders(req, requestId) {
|
|
9269
|
+
const forwarded = {};
|
|
9270
|
+
for (const name of ACP_FORWARDED_HEADERS) {
|
|
9271
|
+
const value = name === ACP_REQUEST_ID_HEADER ? requestId : header(req, name)?.trim();
|
|
9272
|
+
if (value !== void 0 && value.length > 0 && value.length <= ACP_MAX_FORWARDED_HEADER_LENGTH && isVisibleAscii(value)) {
|
|
9273
|
+
forwarded[name] = value;
|
|
9274
|
+
}
|
|
9275
|
+
}
|
|
9276
|
+
return forwarded;
|
|
9277
|
+
}
|
|
9278
|
+
function isUsableIdempotencyKey(value) {
|
|
9279
|
+
return value.length > 0 && value.length <= ACP_MAX_IDEMPOTENCY_KEY_LENGTH && isVisibleAscii(value);
|
|
8490
9280
|
}
|
|
8491
9281
|
function normalizeRequestId(value) {
|
|
8492
9282
|
if (value === void 0) return void 0;
|
|
@@ -8544,8 +9334,36 @@ function toAcpResponse(operation, outcome) {
|
|
|
8544
9334
|
}
|
|
8545
9335
|
};
|
|
8546
9336
|
}
|
|
9337
|
+
const htmlPath = rawHtmlPath(outcome.body, "$");
|
|
9338
|
+
if (htmlPath !== void 0) {
|
|
9339
|
+
return {
|
|
9340
|
+
response: asResponse(PROCESSING_ERROR),
|
|
9341
|
+
logDetail: { reason: "backend-markdown-has-raw-html", operation, path: htmlPath }
|
|
9342
|
+
};
|
|
9343
|
+
}
|
|
8547
9344
|
return { response: { status: expected, body: outcome.body } };
|
|
8548
9345
|
}
|
|
9346
|
+
var RAW_HTML = /<\/?[A-Za-z][A-Za-z0-9-]*(?=[\s/>]|$)|<!--|<\?|<![A-Za-z]|<!\[CDATA\[/;
|
|
9347
|
+
var CODE_SPAN = /(`+)[\s\S]*?\1/g;
|
|
9348
|
+
function rawHtmlPath(node, path2) {
|
|
9349
|
+
if (Array.isArray(node)) {
|
|
9350
|
+
for (const [index, item] of node.entries()) {
|
|
9351
|
+
const found = rawHtmlPath(item, `${path2}[${index}]`);
|
|
9352
|
+
if (found !== void 0) return found;
|
|
9353
|
+
}
|
|
9354
|
+
return void 0;
|
|
9355
|
+
}
|
|
9356
|
+
if (!isRecord(node)) return void 0;
|
|
9357
|
+
const content = node["content"];
|
|
9358
|
+
if (node["content_type"] === "markdown" && typeof content === "string" && RAW_HTML.test(content.replace(CODE_SPAN, ""))) {
|
|
9359
|
+
return `${path2}.content`;
|
|
9360
|
+
}
|
|
9361
|
+
for (const [key, value] of Object.entries(node)) {
|
|
9362
|
+
const found = rawHtmlPath(value, `${path2}.${key}`);
|
|
9363
|
+
if (found !== void 0) return found;
|
|
9364
|
+
}
|
|
9365
|
+
return void 0;
|
|
9366
|
+
}
|
|
8549
9367
|
function responseDefinition(operation, body) {
|
|
8550
9368
|
if (operation !== "completeCheckoutSession") return "checkoutSession";
|
|
8551
9369
|
const status = body?.status;
|
|
@@ -8581,8 +9399,39 @@ function mapCommerceErrorToAcp(error, operation) {
|
|
|
8581
9399
|
}
|
|
8582
9400
|
}
|
|
8583
9401
|
function fromBackendStatus(error, operation) {
|
|
8584
|
-
const
|
|
8585
|
-
|
|
9402
|
+
const failure = statusFailure(error, operation);
|
|
9403
|
+
return failure.status < 500 ? withMerchantError(failure, error) : failure;
|
|
9404
|
+
}
|
|
9405
|
+
function isMerchantRateLimit(error) {
|
|
9406
|
+
return error.code === "BACKEND_ERROR" && error.details?.["status"] === 429;
|
|
9407
|
+
}
|
|
9408
|
+
function merchantRetryAfterSeconds(error) {
|
|
9409
|
+
const value = error.cause instanceof BackendErrorResponse ? error.cause.headers["retry-after"] : void 0;
|
|
9410
|
+
if (value === void 0 || !/^\d{1,9}$/.test(value.trim())) {
|
|
9411
|
+
return ACP_MERCHANT_RETRY_AFTER_DEFAULT_SECONDS;
|
|
9412
|
+
}
|
|
9413
|
+
return Math.min(Math.max(Number(value), 1), ACP_MERCHANT_RETRY_AFTER_MAX_SECONDS);
|
|
9414
|
+
}
|
|
9415
|
+
var MERCHANT_CODE = /^(?!idempotency_)[a-z][a-z0-9_]{0,63}$/;
|
|
9416
|
+
var MERCHANT_PARAM = /^\$[\x20-\x7e]{0,255}$/;
|
|
9417
|
+
function withMerchantError(failure, error) {
|
|
9418
|
+
const body = error.cause instanceof BackendErrorResponse ? error.cause.body : void 0;
|
|
9419
|
+
if (!isRecord(body) || validateAcpDocument("error", body) !== void 0) return failure;
|
|
9420
|
+
const { type, code, param } = body;
|
|
9421
|
+
if (typeof code !== "string" || !MERCHANT_CODE.test(code)) return failure;
|
|
9422
|
+
const relayParam = typeof param === "string" && MERCHANT_PARAM.test(param) ? param : void 0;
|
|
9423
|
+
return acpFailure(
|
|
9424
|
+
failure.status,
|
|
9425
|
+
type,
|
|
9426
|
+
code,
|
|
9427
|
+
failure.error.message,
|
|
9428
|
+
relayParam !== void 0 ? { param: relayParam } : {}
|
|
9429
|
+
);
|
|
9430
|
+
}
|
|
9431
|
+
function statusFailure(error, operation) {
|
|
9432
|
+
const merchantStatus = error.details?.["status"];
|
|
9433
|
+
const status = typeof merchantStatus === "number" ? merchantStatus : 0;
|
|
9434
|
+
switch (status) {
|
|
8586
9435
|
case 404:
|
|
8587
9436
|
return acpFailure(
|
|
8588
9437
|
404,
|
|
@@ -8605,7 +9454,7 @@ function fromBackendStatus(error, operation) {
|
|
|
8605
9454
|
case 400:
|
|
8606
9455
|
case 422:
|
|
8607
9456
|
return acpFailure(
|
|
8608
|
-
|
|
9457
|
+
status,
|
|
8609
9458
|
"invalid_request",
|
|
8610
9459
|
"invalid_request_body",
|
|
8611
9460
|
"The merchant rejected this checkout request."
|
|
@@ -8617,6 +9466,17 @@ function fromBackendStatus(error, operation) {
|
|
|
8617
9466
|
"checkout_session_conflict",
|
|
8618
9467
|
"This checkout session is in a state that does not allow this operation."
|
|
8619
9468
|
);
|
|
9469
|
+
case 429:
|
|
9470
|
+
case 503:
|
|
9471
|
+
return {
|
|
9472
|
+
...acpFailure(
|
|
9473
|
+
503,
|
|
9474
|
+
"service_unavailable",
|
|
9475
|
+
"service_unavailable",
|
|
9476
|
+
"The merchant is temporarily unavailable."
|
|
9477
|
+
),
|
|
9478
|
+
retryAfterSeconds: merchantRetryAfterSeconds(error)
|
|
9479
|
+
};
|
|
8620
9480
|
default:
|
|
8621
9481
|
return acpFailure(
|
|
8622
9482
|
502,
|
|
@@ -8627,7 +9487,11 @@ function fromBackendStatus(error, operation) {
|
|
|
8627
9487
|
}
|
|
8628
9488
|
}
|
|
8629
9489
|
function asResponse(failure) {
|
|
8630
|
-
return {
|
|
9490
|
+
return {
|
|
9491
|
+
status: failure.status,
|
|
9492
|
+
body: failure.error,
|
|
9493
|
+
...failure.retryAfterSeconds !== void 0 ? { retryAfterSeconds: failure.retryAfterSeconds } : {}
|
|
9494
|
+
};
|
|
8631
9495
|
}
|
|
8632
9496
|
|
|
8633
9497
|
// src/protocols/acp/adapter.ts
|
|
@@ -8648,8 +9512,8 @@ var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
|
8648
9512
|
"AUTHORIZATION_REPLAYED",
|
|
8649
9513
|
"AUTHORIZATION_PROVIDER_UNAVAILABLE"
|
|
8650
9514
|
]);
|
|
8651
|
-
function reachedFor(
|
|
8652
|
-
return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
|
|
9515
|
+
function reachedFor(error) {
|
|
9516
|
+
return PRE_BACKEND_ERROR_CODES.has(error.code) || isMerchantRateLimit(error) ? "no" : "unknown";
|
|
8653
9517
|
}
|
|
8654
9518
|
function notReached(failure) {
|
|
8655
9519
|
return { response: asResponse(failure), reached: "no" };
|
|
@@ -8727,6 +9591,7 @@ var AcpProtocolAdapter = class {
|
|
|
8727
9591
|
this.idempotency = createAcpIdempotencyStore({
|
|
8728
9592
|
path: this.idempotencyOptions.path,
|
|
8729
9593
|
retentionHours: this.idempotencyOptions.retentionHours,
|
|
9594
|
+
releaseOrphans: this.idempotencyOptions.merchantIdempotent === true,
|
|
8730
9595
|
logger: context.logger
|
|
8731
9596
|
});
|
|
8732
9597
|
this.discovery = document;
|
|
@@ -8763,7 +9628,8 @@ var AcpProtocolAdapter = class {
|
|
|
8763
9628
|
"invalid_request",
|
|
8764
9629
|
"method_not_allowed",
|
|
8765
9630
|
"The ACP discovery document is read-only."
|
|
8766
|
-
)
|
|
9631
|
+
),
|
|
9632
|
+
{ allow: "GET" }
|
|
8767
9633
|
);
|
|
8768
9634
|
return;
|
|
8769
9635
|
}
|
|
@@ -8783,7 +9649,8 @@ var AcpProtocolAdapter = class {
|
|
|
8783
9649
|
"service_unavailable",
|
|
8784
9650
|
"service_unavailable",
|
|
8785
9651
|
"The ACP adapter is not running."
|
|
8786
|
-
)
|
|
9652
|
+
),
|
|
9653
|
+
acpEchoHeaders(req)
|
|
8787
9654
|
);
|
|
8788
9655
|
return;
|
|
8789
9656
|
}
|
|
@@ -8792,14 +9659,14 @@ var AcpProtocolAdapter = class {
|
|
|
8792
9659
|
isAuthorized: this.isAuthorized
|
|
8793
9660
|
});
|
|
8794
9661
|
if (!guarded.ok) {
|
|
8795
|
-
writeAcpFailure(res, guarded);
|
|
9662
|
+
writeAcpFailure(res, guarded, guarded.headers);
|
|
8796
9663
|
return;
|
|
8797
9664
|
}
|
|
8798
9665
|
const request = guarded.value;
|
|
8799
9666
|
const response = await this.dispatch(request);
|
|
8800
9667
|
writeAcpJson(res, response.status, response.body, this.responseHeaders(request, response));
|
|
8801
9668
|
} catch (err) {
|
|
8802
|
-
this.fail(res, err, "acp adapter: request handling failed");
|
|
9669
|
+
this.fail(res, err, "acp adapter: request handling failed", acpEchoHeaders(req));
|
|
8803
9670
|
}
|
|
8804
9671
|
}
|
|
8805
9672
|
/**
|
|
@@ -8833,7 +9700,7 @@ var AcpProtocolAdapter = class {
|
|
|
8833
9700
|
return asResponse(
|
|
8834
9701
|
acpFailure(
|
|
8835
9702
|
409,
|
|
8836
|
-
"
|
|
9703
|
+
"invalid_request",
|
|
8837
9704
|
"idempotency_unresolved",
|
|
8838
9705
|
"An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
|
|
8839
9706
|
)
|
|
@@ -8859,7 +9726,7 @@ var AcpProtocolAdapter = class {
|
|
|
8859
9726
|
}
|
|
8860
9727
|
if (outcome.response.status < 500) {
|
|
8861
9728
|
store.complete(scope, outcome.response);
|
|
8862
|
-
} else if (outcome.reached === "no") {
|
|
9729
|
+
} else if (outcome.reached === "no" || this.idempotencyOptions.merchantIdempotent === true) {
|
|
8863
9730
|
store.release(scope);
|
|
8864
9731
|
} else {
|
|
8865
9732
|
store.markUnresolved(scope);
|
|
@@ -8917,18 +9784,19 @@ var AcpProtocolAdapter = class {
|
|
|
8917
9784
|
{
|
|
8918
9785
|
resourceId: resource.id,
|
|
8919
9786
|
requestId: canonical.requestId,
|
|
8920
|
-
reached: reachedFor(error
|
|
9787
|
+
reached: reachedFor(error),
|
|
8921
9788
|
err: toLogInfo(error)
|
|
8922
9789
|
},
|
|
8923
9790
|
"acp adapter: checkout execution failed"
|
|
8924
9791
|
);
|
|
8925
9792
|
return {
|
|
8926
9793
|
response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
|
|
8927
|
-
reached: reachedFor(error
|
|
9794
|
+
reached: reachedFor(error)
|
|
8928
9795
|
};
|
|
8929
9796
|
}
|
|
8930
9797
|
}
|
|
8931
|
-
// Protocol-owned headers only
|
|
9798
|
+
// Protocol-owned headers only. No merchant header is proxied: `Retry-After`
|
|
9799
|
+
// is written from seconds the mapping parsed and capped.
|
|
8932
9800
|
responseHeaders(request, response) {
|
|
8933
9801
|
const replayed = response?.replayed === true;
|
|
8934
9802
|
return {
|
|
@@ -8938,11 +9806,12 @@ var AcpProtocolAdapter = class {
|
|
|
8938
9806
|
...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
|
|
8939
9807
|
};
|
|
8940
9808
|
}
|
|
8941
|
-
fail(res, err, message) {
|
|
9809
|
+
fail(res, err, message, headers = {}) {
|
|
8942
9810
|
this.context?.logger.error({ err: toLogInfo(err) }, message);
|
|
8943
9811
|
writeAcpFailure(
|
|
8944
9812
|
res,
|
|
8945
|
-
acpFailure(500, "processing_error", "internal_error", "Internal server error.")
|
|
9813
|
+
acpFailure(500, "processing_error", "internal_error", "Internal server error."),
|
|
9814
|
+
headers
|
|
8946
9815
|
);
|
|
8947
9816
|
}
|
|
8948
9817
|
async health() {
|
|
@@ -9218,6 +10087,9 @@ function createSqliteReceiptStore(options) {
|
|
|
9218
10087
|
const listPaymentAttemptsByRequestStmt = db.prepare(
|
|
9219
10088
|
"SELECT * FROM payment_attempts WHERE request_id = ? ORDER BY created_at DESC, seq DESC LIMIT ?"
|
|
9220
10089
|
);
|
|
10090
|
+
const paymentAttemptStatusStmt = db.prepare(
|
|
10091
|
+
"SELECT status FROM payment_attempts WHERE replay_key = ?"
|
|
10092
|
+
);
|
|
9221
10093
|
let closed = false;
|
|
9222
10094
|
const descriptor = {
|
|
9223
10095
|
name: "sqlite-receipt-store",
|
|
@@ -9274,10 +10146,15 @@ function createSqliteReceiptStore(options) {
|
|
|
9274
10146
|
});
|
|
9275
10147
|
} catch (err) {
|
|
9276
10148
|
if (isUniqueConstraintOn(err, "payment_attempts.replay_key")) {
|
|
10149
|
+
const existing = paymentAttemptStatusStmt.get(reservation.replayKey);
|
|
9277
10150
|
throw new CommerceError(
|
|
9278
10151
|
"PAYMENT_REPLAYED",
|
|
9279
10152
|
`Payment authorization has already been used (replayKey=${reservation.replayKey})`,
|
|
9280
|
-
{
|
|
10153
|
+
{
|
|
10154
|
+
requestId: reservation.requestId,
|
|
10155
|
+
resourceId: reservation.resourceId,
|
|
10156
|
+
...existing !== void 0 ? { details: { attemptStatus: existing.status } } : {}
|
|
10157
|
+
}
|
|
9281
10158
|
);
|
|
9282
10159
|
}
|
|
9283
10160
|
throw toCommerceError(err, "STORAGE_ERROR", "Failed to reserve payment attempt");
|