@devlab.group/agent-commerce 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -1
- package/dist/ap2.js +3 -3
- package/dist/{chunk-7NKWFGBY.js → chunk-GF6KHHKL.js} +3 -3
- package/dist/{chunk-7NKWFGBY.js.map → chunk-GF6KHHKL.js.map} +1 -1
- package/dist/{chunk-IUR3Q7DE.js → chunk-NWIQLJNU.js} +3 -3
- package/dist/{chunk-IUR3Q7DE.js.map → chunk-NWIQLJNU.js.map} +1 -1
- package/dist/{chunk-ZDVJ7EVL.js → chunk-ZEGFLRGV.js} +3 -3
- package/dist/{chunk-ZDVJ7EVL.js.map → chunk-ZEGFLRGV.js.map} +1 -1
- package/dist/cli/index.js +2 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +20 -4
- package/dist/index.js +173 -81
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.js +1 -1
- package/dist/{protocol-adapter-CaMZj6Wm.d.ts → protocol-adapter-Bebmggf_.d.ts} +23 -9
- package/dist/x402.js +17 -7
- package/dist/x402.js.map +1 -1
- package/package.json +11 -4
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { f as AuthorizationMethodName, g as BackendMethod, h as DEFAULT_BACKEND_
|
|
|
4
4
|
import { X as X402FacilitatorConfig } from './guardrails-Dmwi63KB.js';
|
|
5
5
|
import { a as AuthorizationRequirement, b as AuthorizationSubmission, A as AuthorizationProvider } from './authorization-CPDcpJVa.js';
|
|
6
6
|
export { c as AuthorizationFinalizeContext, d as AuthorizationRecord, e as AuthorizationVerification, f as AuthorizationVerificationContext } from './authorization-CPDcpJVa.js';
|
|
7
|
-
import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter, H as HttpProtocolAdapter, A as AdapterHttpRoute, d as ProtocolAdapterContext } from './protocol-adapter-
|
|
8
|
-
export { e as COMMERCE_EVENT_TYPES, f as CanonicalRequest, g as CommerceEventType, h as EventSink, i as ExecutionOutcome, j as isHttpProtocolAdapter } from './protocol-adapter-
|
|
7
|
+
import { D as DeliveredOutcome, P as PaymentRequiredOutcome, C as CommerceEvent, a as PaymentAttempt, b as CommerceReceipt, E as ExecutionPipeline, c as ProtocolAdapter, H as HttpProtocolAdapter, A as AdapterHttpRoute, d as ProtocolAdapterContext } from './protocol-adapter-Bebmggf_.js';
|
|
8
|
+
export { e as COMMERCE_EVENT_TYPES, f as CanonicalRequest, g as CommerceEventType, h as EventSink, i as ExecutionOutcome, j as isHttpProtocolAdapter } from './protocol-adapter-Bebmggf_.js';
|
|
9
9
|
import { FastifyInstance } from 'fastify';
|
|
10
10
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
11
|
|
|
@@ -303,6 +303,16 @@ interface BackendRequest {
|
|
|
303
303
|
readonly resourceId: string;
|
|
304
304
|
/** Validated resource input. */
|
|
305
305
|
readonly input: unknown;
|
|
306
|
+
/**
|
|
307
|
+
* Names the *operation*, so the merchant can recognise a repeat of it.
|
|
308
|
+
*
|
|
309
|
+
* Not `requestId`, which is fresh per call and would make every retry look
|
|
310
|
+
* like new work. An adapter sets this only when it can derive a value that
|
|
311
|
+
* survives a client retry, a reconnect and a gateway restart; the HTTP
|
|
312
|
+
* executor forwards it as the `Idempotency-Key` request header. Absent
|
|
313
|
+
* means the protocol has no such notion, and the merchant sees no header.
|
|
314
|
+
*/
|
|
315
|
+
readonly idempotencyKey?: string;
|
|
306
316
|
}
|
|
307
317
|
interface BackendResponse {
|
|
308
318
|
readonly status: number;
|
|
@@ -630,8 +640,14 @@ declare class AcpProtocolAdapter implements HttpProtocolAdapter {
|
|
|
630
640
|
private readonly token;
|
|
631
641
|
private readonly operations;
|
|
632
642
|
private readonly idempotencyOptions;
|
|
633
|
-
/**
|
|
634
|
-
|
|
643
|
+
/**
|
|
644
|
+
* What an idempotency claim is scoped to, from `context.publicBaseUrl`.
|
|
645
|
+
*
|
|
646
|
+
* Not readonly because, unlike the token, it is known only once the adapter
|
|
647
|
+
* starts. Worth that: scoping by the credential meant a rotation freed
|
|
648
|
+
* every outstanding claim.
|
|
649
|
+
*/
|
|
650
|
+
private deployment;
|
|
635
651
|
private readonly discoveryMetadata;
|
|
636
652
|
private idempotency;
|
|
637
653
|
private resources;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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 { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-
|
|
3
|
-
import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-
|
|
4
|
-
export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-
|
|
1
|
+
import { AP2_SPEC_VERSION, AP2_MODES, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_JWK_MEMBERS, AP2_KEY_TYPE, AP2_SIGNING_ALGORITHM, AP2_JWK_CURVE, AP2_JWK_COORDINATE_BYTES, openSqliteDatabase, AP2_MAX_CLOCK_SKEW_SECONDS } from './chunk-GF6KHHKL.js';
|
|
2
|
+
import { resolveX402Deployment, resolveDeploymentMode, requireNetworkProfile } from './chunk-NWIQLJNU.js';
|
|
3
|
+
import { RESERVED_INPUT_FIELDS, PROTOCOL_NAMES, PAYMENT_HEADER, AUTHORIZATION_HEADER, CommerceError, systemClock, NOOP_LOGGER, PACKAGE_VERSION, toCommerceError, DEFAULT_BACKEND_TIMEOUT_MS, toErrorEnvelope, isHttpProtocolAdapter, isCommerceError, parseAuthorizationHeader, toPaymentRequiredEnvelope, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER } from './chunk-ZEGFLRGV.js';
|
|
4
|
+
export { AUTHORIZATION_HEADER, AUTHORIZATION_INPUT_FIELD, COMMERCE_ERROR_CODES, COMMERCE_ERROR_HTTP_STATUS, COMMERCE_EVENT_TYPES, CommerceError, DEFAULT_BACKEND_TIMEOUT_MS, DELIVERY_SUMMARY_META_KEY, MAX_AUTHORIZATION_HEADER_BYTES, NOOP_LOGGER, PAYMENT_HEADER, PAYMENT_INPUT_FIELD, PAYMENT_REQUIRED_HEADER, PAYMENT_RESPONSE_HEADER, PROTOCOL_NAMES, RESERVED_INPUT_FIELDS, RETRYABLE_ERROR_CODES, extractReservedInputFields, isCommerceError, isHttpProtocolAdapter, isPaymentRequiredEnvelope, parseAuthorizationHeader, parseAuthorizationSubmission, systemClock, toCommerceError, toDeliverySummary, toErrorEnvelope, toPaymentRequiredEnvelope } from './chunk-ZEGFLRGV.js';
|
|
5
5
|
import { promises, accessSync, constants } from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
7
|
import { parse, YAMLError } from 'yaml';
|
|
@@ -85,6 +85,7 @@ var DEFAULT_CONFIG_FILENAME = "config.yaml";
|
|
|
85
85
|
|
|
86
86
|
// src/core/execution/backend-http.ts
|
|
87
87
|
var MAX_BODY_SNIPPET_LENGTH = 512;
|
|
88
|
+
var IDEMPOTENCY_KEY_HEADER = "idempotency-key";
|
|
88
89
|
var PATH_PARAM_PATTERN = /\{([a-zA-Z0-9_.-]+)\}/g;
|
|
89
90
|
var MAX_RESPONSE_BODY_BYTES = 1024 * 1024;
|
|
90
91
|
var HttpBackendExecutor = class {
|
|
@@ -126,6 +127,10 @@ var HttpBackendExecutor = class {
|
|
|
126
127
|
if (error instanceof CommerceError) throw error;
|
|
127
128
|
}
|
|
128
129
|
const headers = { ...handler.headers ?? {} };
|
|
130
|
+
if (request.idempotencyKey !== void 0) {
|
|
131
|
+
deleteHeader(headers, IDEMPOTENCY_KEY_HEADER);
|
|
132
|
+
headers[IDEMPOTENCY_KEY_HEADER] = request.idempotencyKey;
|
|
133
|
+
}
|
|
129
134
|
let body;
|
|
130
135
|
checkQueryCollision(target, parts.query, context);
|
|
131
136
|
for (const [key, value] of Object.entries(parts.query)) {
|
|
@@ -334,6 +339,12 @@ function hasHeader(headers, name) {
|
|
|
334
339
|
const target = name.toLowerCase();
|
|
335
340
|
return Object.keys(headers).some((key) => key.toLowerCase() === target);
|
|
336
341
|
}
|
|
342
|
+
function deleteHeader(headers, name) {
|
|
343
|
+
const target = name.toLowerCase();
|
|
344
|
+
for (const key of Object.keys(headers)) {
|
|
345
|
+
if (key.toLowerCase() === target) delete headers[key];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
337
348
|
function headersToRecord(headers) {
|
|
338
349
|
const record = {};
|
|
339
350
|
headers.forEach((value, key) => {
|
|
@@ -924,17 +935,16 @@ function createExecutionPipeline(options) {
|
|
|
924
935
|
verification
|
|
925
936
|
});
|
|
926
937
|
} catch (error) {
|
|
927
|
-
const
|
|
928
|
-
await hold?.finalize(
|
|
938
|
+
const txHash = settlementTxHash(error);
|
|
939
|
+
await hold?.finalize("markUncertain");
|
|
929
940
|
await safePersist(
|
|
930
|
-
() => options.store.updatePaymentAttempt(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
),
|
|
937
|
-
uncertainTxHash !== void 0 ? "updatePaymentAttempt(settlement-uncertain)" : "updatePaymentAttempt(failed)",
|
|
941
|
+
() => options.store.updatePaymentAttempt({
|
|
942
|
+
replayKey,
|
|
943
|
+
status: "settlement-uncertain",
|
|
944
|
+
...txHash !== void 0 ? { externalReference: txHash } : {},
|
|
945
|
+
...error instanceof Error ? { rejectionReason: error.message } : {}
|
|
946
|
+
}),
|
|
947
|
+
"updatePaymentAttempt(settlement-uncertain)",
|
|
938
948
|
request.requestId
|
|
939
949
|
);
|
|
940
950
|
await safeEmit(
|
|
@@ -945,19 +955,21 @@ function createExecutionPipeline(options) {
|
|
|
945
955
|
adapter: request.protocol,
|
|
946
956
|
paymentProvider: provider.name,
|
|
947
957
|
status: "error",
|
|
948
|
-
data:
|
|
958
|
+
data: {
|
|
959
|
+
reason: "settlement-uncertain",
|
|
960
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
961
|
+
}
|
|
949
962
|
})
|
|
950
963
|
);
|
|
951
|
-
throw new CommerceError(
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
...uncertainTxHash !== void 0 ? { details: { settlementUncertain: true, transactionHash: uncertainTxHash } } : {}
|
|
964
|
+
throw new CommerceError("PAYMENT_SETTLEMENT_FAILED", "Settlement could not be confirmed", {
|
|
965
|
+
requestId: request.requestId,
|
|
966
|
+
resourceId: resource.id,
|
|
967
|
+
cause: error,
|
|
968
|
+
details: {
|
|
969
|
+
settlementUncertain: true,
|
|
970
|
+
...txHash !== void 0 ? { transactionHash: txHash } : {}
|
|
959
971
|
}
|
|
960
|
-
);
|
|
972
|
+
});
|
|
961
973
|
}
|
|
962
974
|
if (settlement.status !== "settled") {
|
|
963
975
|
await hold?.finalize("release");
|
|
@@ -1018,7 +1030,8 @@ function createExecutionPipeline(options) {
|
|
|
1018
1030
|
backendResponse = await options.backend.call(resource.handler, {
|
|
1019
1031
|
requestId: request.requestId,
|
|
1020
1032
|
resourceId: resource.id,
|
|
1021
|
-
input: validInput
|
|
1033
|
+
input: validInput,
|
|
1034
|
+
...request.idempotencyKey !== void 0 ? { idempotencyKey: request.idempotencyKey } : {}
|
|
1022
1035
|
});
|
|
1023
1036
|
} catch (error) {
|
|
1024
1037
|
const commerceError = toCommerceError(error, "BACKEND_ERROR", "Backend call failed");
|
|
@@ -1124,9 +1137,8 @@ function backendErrorStatus(error) {
|
|
|
1124
1137
|
const status = error.details?.["status"];
|
|
1125
1138
|
return typeof status === "number" ? status : 0;
|
|
1126
1139
|
}
|
|
1127
|
-
function
|
|
1128
|
-
|
|
1129
|
-
const hash = error.details?.["transactionHash"];
|
|
1140
|
+
function settlementTxHash(error) {
|
|
1141
|
+
const hash = isCommerceError(error) ? error.details?.["transactionHash"] : void 0;
|
|
1130
1142
|
return typeof hash === "string" ? hash : void 0;
|
|
1131
1143
|
}
|
|
1132
1144
|
function resolveAuthorizationProviders(providers, resource, requestId) {
|
|
@@ -3334,13 +3346,14 @@ async function createGateway(options) {
|
|
|
3334
3346
|
|
|
3335
3347
|
// src/protocols/acp/checkout-mapping.ts
|
|
3336
3348
|
function toCanonicalRequest(options) {
|
|
3337
|
-
const { request, resourceId, requestId, receivedAt } = options;
|
|
3349
|
+
const { request, resourceId, requestId, receivedAt, idempotencyKey } = options;
|
|
3338
3350
|
return {
|
|
3339
3351
|
requestId,
|
|
3340
3352
|
resourceId,
|
|
3341
3353
|
input: canonicalInput(request),
|
|
3342
3354
|
protocol: "acp",
|
|
3343
|
-
receivedAt
|
|
3355
|
+
receivedAt,
|
|
3356
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
3344
3357
|
};
|
|
3345
3358
|
}
|
|
3346
3359
|
function canonicalInput(request) {
|
|
@@ -3448,8 +3461,8 @@ function writeAcpJson(res, status, body, headers = {}) {
|
|
|
3448
3461
|
function writeAcpFailure(res, failure, headers = {}) {
|
|
3449
3462
|
writeAcpJson(res, failure.status, failure.error, headers);
|
|
3450
3463
|
}
|
|
3451
|
-
function
|
|
3452
|
-
return sha256(`acp-
|
|
3464
|
+
function operationKey(scope) {
|
|
3465
|
+
return sha256(`acp-operation:${scope.deployment}:${scope.endpoint}:${scope.key}`);
|
|
3453
3466
|
}
|
|
3454
3467
|
function requestFingerprint(body) {
|
|
3455
3468
|
return sha256(canonicalize(body));
|
|
@@ -3468,6 +3481,7 @@ function sha256(value) {
|
|
|
3468
3481
|
}
|
|
3469
3482
|
|
|
3470
3483
|
// src/protocols/acp/idempotency/store.ts
|
|
3484
|
+
var SCHEMA_VERSION = 2;
|
|
3471
3485
|
var ACP_MAX_IDEMPOTENCY_KEY_LENGTH = 255;
|
|
3472
3486
|
function createAcpIdempotencyStore(options) {
|
|
3473
3487
|
const logger = options.logger ?? NOOP_LOGGER;
|
|
@@ -3478,33 +3492,39 @@ function createAcpIdempotencyStore(options) {
|
|
|
3478
3492
|
label: "ACP idempotency database",
|
|
3479
3493
|
logger
|
|
3480
3494
|
});
|
|
3481
|
-
migrate(db);
|
|
3495
|
+
migrate(db, logger);
|
|
3482
3496
|
const selectStmt = db.prepare(
|
|
3483
3497
|
`SELECT state, fingerprint, status, body_json FROM acp_idempotency
|
|
3484
|
-
WHERE
|
|
3498
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3485
3499
|
);
|
|
3486
3500
|
const insertStmt = db.prepare(
|
|
3487
3501
|
`INSERT INTO acp_idempotency
|
|
3488
|
-
(
|
|
3489
|
-
VALUES (@
|
|
3502
|
+
(deployment, endpoint, idempotency_key, fingerprint, state, created_at, expires_at)
|
|
3503
|
+
VALUES (@deployment, @endpoint, @idempotency_key, @fingerprint, 'in_flight', @created_at, @expires_at)`
|
|
3490
3504
|
);
|
|
3491
3505
|
const completeStmt = db.prepare(
|
|
3492
3506
|
`UPDATE acp_idempotency SET state = 'completed', status = @status, body_json = @body_json
|
|
3493
|
-
WHERE
|
|
3507
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3494
3508
|
);
|
|
3495
3509
|
const deleteStmt = db.prepare(
|
|
3496
3510
|
`DELETE FROM acp_idempotency
|
|
3497
|
-
WHERE
|
|
3511
|
+
WHERE deployment = ? AND endpoint = ? AND idempotency_key = ?`
|
|
3512
|
+
);
|
|
3513
|
+
const unresolvedStmt = db.prepare(
|
|
3514
|
+
`UPDATE acp_idempotency SET state = 'unresolved'
|
|
3515
|
+
WHERE deployment = @deployment AND endpoint = @endpoint AND idempotency_key = @idempotency_key`
|
|
3516
|
+
);
|
|
3517
|
+
const expireStmt = db.prepare(
|
|
3518
|
+
"DELETE FROM acp_idempotency WHERE expires_at <= ? AND state = 'completed'"
|
|
3498
3519
|
);
|
|
3499
|
-
const expireStmt = db.prepare("DELETE FROM acp_idempotency WHERE expires_at <= ?");
|
|
3500
3520
|
const claimTx = db.transaction(
|
|
3501
3521
|
(scope, fingerprint) => {
|
|
3502
3522
|
const at = now();
|
|
3503
3523
|
expireStmt.run(new Date(at).toISOString());
|
|
3504
|
-
const existing = selectStmt.get(scope.
|
|
3524
|
+
const existing = selectStmt.get(scope.deployment, scope.endpoint, scope.key);
|
|
3505
3525
|
if (existing === void 0) {
|
|
3506
3526
|
insertStmt.run({
|
|
3507
|
-
|
|
3527
|
+
deployment: scope.deployment,
|
|
3508
3528
|
endpoint: scope.endpoint,
|
|
3509
3529
|
idempotency_key: scope.key,
|
|
3510
3530
|
fingerprint,
|
|
@@ -3514,6 +3534,7 @@ function createAcpIdempotencyStore(options) {
|
|
|
3514
3534
|
return { kind: "reserved" };
|
|
3515
3535
|
}
|
|
3516
3536
|
if (existing.fingerprint !== fingerprint) return { kind: "conflict" };
|
|
3537
|
+
if (existing.state === "unresolved") return { kind: "unresolved" };
|
|
3517
3538
|
if (existing.state !== "completed") return { kind: "in-flight" };
|
|
3518
3539
|
return {
|
|
3519
3540
|
kind: "replay",
|
|
@@ -3529,15 +3550,22 @@ function createAcpIdempotencyStore(options) {
|
|
|
3529
3550
|
},
|
|
3530
3551
|
complete(scope, response) {
|
|
3531
3552
|
completeStmt.run({
|
|
3532
|
-
|
|
3553
|
+
deployment: scope.deployment,
|
|
3533
3554
|
endpoint: scope.endpoint,
|
|
3534
3555
|
idempotency_key: scope.key,
|
|
3535
3556
|
status: response.status,
|
|
3536
3557
|
body_json: JSON.stringify(response.body ?? null)
|
|
3537
3558
|
});
|
|
3538
3559
|
},
|
|
3560
|
+
markUnresolved(scope) {
|
|
3561
|
+
unresolvedStmt.run({
|
|
3562
|
+
deployment: scope.deployment,
|
|
3563
|
+
endpoint: scope.endpoint,
|
|
3564
|
+
idempotency_key: scope.key
|
|
3565
|
+
});
|
|
3566
|
+
},
|
|
3539
3567
|
release(scope) {
|
|
3540
|
-
deleteStmt.run(scope.
|
|
3568
|
+
deleteStmt.run(scope.deployment, scope.endpoint, scope.key);
|
|
3541
3569
|
},
|
|
3542
3570
|
close() {
|
|
3543
3571
|
if (closed) return;
|
|
@@ -3546,13 +3574,23 @@ function createAcpIdempotencyStore(options) {
|
|
|
3546
3574
|
}
|
|
3547
3575
|
};
|
|
3548
3576
|
}
|
|
3549
|
-
function migrate(db) {
|
|
3577
|
+
function migrate(db, logger) {
|
|
3550
3578
|
const current = db.pragma("user_version", { simple: true });
|
|
3551
|
-
if (current >=
|
|
3579
|
+
if (current >= SCHEMA_VERSION) return;
|
|
3552
3580
|
const apply = db.transaction(() => {
|
|
3581
|
+
if (current === 1) {
|
|
3582
|
+
const carried = db.prepare("SELECT COUNT(*) AS count FROM acp_idempotency WHERE state != 'completed'").get();
|
|
3583
|
+
if (carried.count > 0) {
|
|
3584
|
+
logger.warn(
|
|
3585
|
+
{ unresolved: carried.count },
|
|
3586
|
+
"acp idempotency: schema v1 -> v2 discards claims keyed by the old bearer-token digest; reconcile these operations against the merchant before trusting a retry"
|
|
3587
|
+
);
|
|
3588
|
+
}
|
|
3589
|
+
db.exec("DROP TABLE acp_idempotency");
|
|
3590
|
+
}
|
|
3553
3591
|
db.exec(`
|
|
3554
3592
|
CREATE TABLE IF NOT EXISTS acp_idempotency (
|
|
3555
|
-
|
|
3593
|
+
deployment TEXT NOT NULL,
|
|
3556
3594
|
endpoint TEXT NOT NULL,
|
|
3557
3595
|
idempotency_key TEXT NOT NULL,
|
|
3558
3596
|
fingerprint TEXT NOT NULL,
|
|
@@ -3561,12 +3599,12 @@ function migrate(db) {
|
|
|
3561
3599
|
body_json TEXT,
|
|
3562
3600
|
created_at TEXT NOT NULL,
|
|
3563
3601
|
expires_at TEXT NOT NULL,
|
|
3564
|
-
PRIMARY KEY (
|
|
3602
|
+
PRIMARY KEY (deployment, endpoint, idempotency_key)
|
|
3565
3603
|
);
|
|
3566
3604
|
CREATE INDEX IF NOT EXISTS idx_acp_idempotency_expires_at
|
|
3567
3605
|
ON acp_idempotency(expires_at);
|
|
3568
3606
|
`);
|
|
3569
|
-
db.pragma(
|
|
3607
|
+
db.pragma(`user_version = ${SCHEMA_VERSION}`);
|
|
3570
3608
|
});
|
|
3571
3609
|
apply();
|
|
3572
3610
|
}
|
|
@@ -7974,6 +8012,28 @@ function asResponse(failure) {
|
|
|
7974
8012
|
|
|
7975
8013
|
// src/protocols/acp/adapter.ts
|
|
7976
8014
|
var DISCOVERY_CACHE_CONTROL = "public, max-age=3600";
|
|
8015
|
+
var PRE_BACKEND_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
8016
|
+
"CONFIG_INVALID",
|
|
8017
|
+
"RESOURCE_NOT_FOUND",
|
|
8018
|
+
"INPUT_INVALID",
|
|
8019
|
+
"PROTOCOL_UNSUPPORTED",
|
|
8020
|
+
"GATEWAY_BUSY",
|
|
8021
|
+
"PAYMENT_REQUIRED",
|
|
8022
|
+
"PAYMENT_INVALID",
|
|
8023
|
+
"PAYMENT_REPLAYED",
|
|
8024
|
+
"PAYMENT_PROVIDER_UNAVAILABLE",
|
|
8025
|
+
"PAYMENT_SETTLEMENT_FAILED",
|
|
8026
|
+
"AUTHORIZATION_REQUIRED",
|
|
8027
|
+
"AUTHORIZATION_INVALID",
|
|
8028
|
+
"AUTHORIZATION_REPLAYED",
|
|
8029
|
+
"AUTHORIZATION_PROVIDER_UNAVAILABLE"
|
|
8030
|
+
]);
|
|
8031
|
+
function reachedFor(code) {
|
|
8032
|
+
return PRE_BACKEND_ERROR_CODES.has(code) ? "no" : "unknown";
|
|
8033
|
+
}
|
|
8034
|
+
function notReached(failure) {
|
|
8035
|
+
return { response: asResponse(failure), reached: "no" };
|
|
8036
|
+
}
|
|
7977
8037
|
var AcpProtocolAdapter = class {
|
|
7978
8038
|
name = "acp";
|
|
7979
8039
|
mountPath;
|
|
@@ -7982,8 +8042,14 @@ var AcpProtocolAdapter = class {
|
|
|
7982
8042
|
token;
|
|
7983
8043
|
operations;
|
|
7984
8044
|
idempotencyOptions;
|
|
7985
|
-
/**
|
|
7986
|
-
|
|
8045
|
+
/**
|
|
8046
|
+
* What an idempotency claim is scoped to, from `context.publicBaseUrl`.
|
|
8047
|
+
*
|
|
8048
|
+
* Not readonly because, unlike the token, it is known only once the adapter
|
|
8049
|
+
* starts. Worth that: scoping by the credential meant a rotation freed
|
|
8050
|
+
* every outstanding claim.
|
|
8051
|
+
*/
|
|
8052
|
+
deployment;
|
|
7987
8053
|
discoveryMetadata;
|
|
7988
8054
|
idempotency;
|
|
7989
8055
|
resources = /* @__PURE__ */ new Map();
|
|
@@ -7998,7 +8064,6 @@ var AcpProtocolAdapter = class {
|
|
|
7998
8064
|
this.token = options.token;
|
|
7999
8065
|
this.operations = options.operations;
|
|
8000
8066
|
this.idempotencyOptions = options.idempotency;
|
|
8001
|
-
this.identityHash = identityHash(options.token);
|
|
8002
8067
|
this.discoveryMetadata = options.discovery;
|
|
8003
8068
|
this.descriptor = buildDescriptor(PACKAGE_VERSION);
|
|
8004
8069
|
this.additionalHttpRoutes = [
|
|
@@ -8042,6 +8107,7 @@ var AcpProtocolAdapter = class {
|
|
|
8042
8107
|
resolved.set(operation, resource);
|
|
8043
8108
|
}
|
|
8044
8109
|
this.resources = resolved;
|
|
8110
|
+
this.deployment = context.publicBaseUrl;
|
|
8045
8111
|
this.idempotency = createAcpIdempotencyStore({
|
|
8046
8112
|
path: this.idempotencyOptions.path,
|
|
8047
8113
|
retentionHours: this.idempotencyOptions.retentionHours,
|
|
@@ -8130,22 +8196,33 @@ var AcpProtocolAdapter = class {
|
|
|
8130
8196
|
*/
|
|
8131
8197
|
async dispatch(request) {
|
|
8132
8198
|
const store = this.idempotency;
|
|
8199
|
+
const deployment = this.deployment;
|
|
8133
8200
|
const key = request.idempotencyKey;
|
|
8134
|
-
if (store === void 0 || key === void 0)
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
|
|
8138
|
-
key
|
|
8139
|
-
};
|
|
8201
|
+
if (store === void 0 || deployment === void 0 || key === void 0) {
|
|
8202
|
+
return (await this.runCheckout(request)).response;
|
|
8203
|
+
}
|
|
8204
|
+
const scope = { deployment, endpoint: request.route.path, key };
|
|
8140
8205
|
const claim = store.claim(scope, requestFingerprint(request.body));
|
|
8141
8206
|
switch (claim.kind) {
|
|
8142
8207
|
case "in-flight":
|
|
8208
|
+
return {
|
|
8209
|
+
...asResponse(
|
|
8210
|
+
acpFailure(
|
|
8211
|
+
409,
|
|
8212
|
+
"invalid_request",
|
|
8213
|
+
"idempotency_in_flight",
|
|
8214
|
+
"A request with this Idempotency-Key is still being processed."
|
|
8215
|
+
)
|
|
8216
|
+
),
|
|
8217
|
+
retryAfterSeconds: ACP_IN_FLIGHT_RETRY_AFTER_SECONDS
|
|
8218
|
+
};
|
|
8219
|
+
case "unresolved":
|
|
8143
8220
|
return asResponse(
|
|
8144
8221
|
acpFailure(
|
|
8145
8222
|
409,
|
|
8146
|
-
"
|
|
8147
|
-
"
|
|
8148
|
-
"
|
|
8223
|
+
"processing_error",
|
|
8224
|
+
"idempotency_unresolved",
|
|
8225
|
+
"An earlier request with this Idempotency-Key reached the merchant and its outcome is unknown. Check the operation with the merchant before retrying."
|
|
8149
8226
|
)
|
|
8150
8227
|
);
|
|
8151
8228
|
case "conflict":
|
|
@@ -8160,16 +8237,21 @@ var AcpProtocolAdapter = class {
|
|
|
8160
8237
|
case "replay":
|
|
8161
8238
|
return { status: claim.status, body: claim.body, replayed: true };
|
|
8162
8239
|
}
|
|
8163
|
-
let
|
|
8240
|
+
let outcome;
|
|
8164
8241
|
try {
|
|
8165
|
-
|
|
8242
|
+
outcome = await this.runCheckout(request, operationKey(scope));
|
|
8166
8243
|
} catch (err) {
|
|
8167
|
-
store.
|
|
8244
|
+
store.markUnresolved(scope);
|
|
8168
8245
|
throw err;
|
|
8169
8246
|
}
|
|
8170
|
-
if (response.status
|
|
8171
|
-
|
|
8172
|
-
|
|
8247
|
+
if (outcome.response.status < 500) {
|
|
8248
|
+
store.complete(scope, outcome.response);
|
|
8249
|
+
} else if (outcome.reached === "no") {
|
|
8250
|
+
store.release(scope);
|
|
8251
|
+
} else {
|
|
8252
|
+
store.markUnresolved(scope);
|
|
8253
|
+
}
|
|
8254
|
+
return outcome.response;
|
|
8173
8255
|
}
|
|
8174
8256
|
/**
|
|
8175
8257
|
* One accepted request, one `pipeline.execute()`.
|
|
@@ -8178,11 +8260,11 @@ var AcpProtocolAdapter = class {
|
|
|
8178
8260
|
* merchant backend: the adapter builds a canonical request and reads back
|
|
8179
8261
|
* what the pipeline decided.
|
|
8180
8262
|
*/
|
|
8181
|
-
async runCheckout(request) {
|
|
8263
|
+
async runCheckout(request, idempotencyKey) {
|
|
8182
8264
|
const context = this.context;
|
|
8183
8265
|
const resource = this.resources.get(request.route.operation);
|
|
8184
8266
|
if (context === void 0 || resource === void 0) {
|
|
8185
|
-
return
|
|
8267
|
+
return notReached(
|
|
8186
8268
|
acpFailure(
|
|
8187
8269
|
503,
|
|
8188
8270
|
"service_unavailable",
|
|
@@ -8195,7 +8277,8 @@ var AcpProtocolAdapter = class {
|
|
|
8195
8277
|
request,
|
|
8196
8278
|
resourceId: resource.id,
|
|
8197
8279
|
requestId: context.ids.next("acp"),
|
|
8198
|
-
receivedAt: context.clock.nowIso()
|
|
8280
|
+
receivedAt: context.clock.nowIso(),
|
|
8281
|
+
...idempotencyKey !== void 0 ? { idempotencyKey } : {}
|
|
8199
8282
|
});
|
|
8200
8283
|
try {
|
|
8201
8284
|
const outcome = await context.pipeline.execute(canonical);
|
|
@@ -8204,7 +8287,7 @@ var AcpProtocolAdapter = class {
|
|
|
8204
8287
|
{ resourceId: resource.id, requestId: canonical.requestId },
|
|
8205
8288
|
"acp adapter: mapped checkout resource returned payment-required - it must be priced free"
|
|
8206
8289
|
);
|
|
8207
|
-
return
|
|
8290
|
+
return notReached(
|
|
8208
8291
|
acpFailure(
|
|
8209
8292
|
500,
|
|
8210
8293
|
"processing_error",
|
|
@@ -8220,14 +8303,22 @@ var AcpProtocolAdapter = class {
|
|
|
8220
8303
|
"acp adapter: refusing to forward a non-conformant merchant response"
|
|
8221
8304
|
);
|
|
8222
8305
|
}
|
|
8223
|
-
return mapped.response;
|
|
8306
|
+
return { response: mapped.response, reached: "yes" };
|
|
8224
8307
|
} catch (err) {
|
|
8225
8308
|
const error = toCommerceError(err);
|
|
8226
8309
|
context.logger.warn(
|
|
8227
|
-
{
|
|
8310
|
+
{
|
|
8311
|
+
resourceId: resource.id,
|
|
8312
|
+
requestId: canonical.requestId,
|
|
8313
|
+
reached: reachedFor(error.code),
|
|
8314
|
+
err: error.toInfo()
|
|
8315
|
+
},
|
|
8228
8316
|
"acp adapter: checkout execution failed"
|
|
8229
8317
|
);
|
|
8230
|
-
return
|
|
8318
|
+
return {
|
|
8319
|
+
response: asResponse(mapCommerceErrorToAcp(error, request.route.operation)),
|
|
8320
|
+
reached: reachedFor(error.code)
|
|
8321
|
+
};
|
|
8231
8322
|
}
|
|
8232
8323
|
}
|
|
8233
8324
|
/**
|
|
@@ -8240,7 +8331,7 @@ var AcpProtocolAdapter = class {
|
|
|
8240
8331
|
...request.requestId !== void 0 ? { [ACP_REQUEST_ID_HEADER]: request.requestId } : {},
|
|
8241
8332
|
...request.idempotencyKey !== void 0 ? { [ACP_IDEMPOTENCY_KEY_HEADER]: request.idempotencyKey } : {},
|
|
8242
8333
|
...replayed ? { [ACP_IDEMPOTENT_REPLAYED_HEADER]: "true" } : {},
|
|
8243
|
-
...response?.
|
|
8334
|
+
...response?.retryAfterSeconds !== void 0 ? { "retry-after": String(response.retryAfterSeconds) } : {}
|
|
8244
8335
|
};
|
|
8245
8336
|
}
|
|
8246
8337
|
fail(res, err, message) {
|
|
@@ -8267,6 +8358,7 @@ var AcpProtocolAdapter = class {
|
|
|
8267
8358
|
this.resources = /* @__PURE__ */ new Map();
|
|
8268
8359
|
this.idempotency?.close();
|
|
8269
8360
|
this.idempotency = void 0;
|
|
8361
|
+
this.deployment = void 0;
|
|
8270
8362
|
this.context = void 0;
|
|
8271
8363
|
}
|
|
8272
8364
|
};
|
|
@@ -8443,7 +8535,7 @@ var MIGRATIONS = [
|
|
|
8443
8535
|
}
|
|
8444
8536
|
}
|
|
8445
8537
|
];
|
|
8446
|
-
var
|
|
8538
|
+
var SCHEMA_VERSION2 = MIGRATIONS[MIGRATIONS.length - 1]?.version ?? 0;
|
|
8447
8539
|
function migrate2(db) {
|
|
8448
8540
|
const current = db.pragma("user_version", { simple: true });
|
|
8449
8541
|
for (const migration of MIGRATIONS) {
|
|
@@ -8529,17 +8621,17 @@ function createSqliteReceiptStore(options) {
|
|
|
8529
8621
|
name: "sqlite-receipt-store",
|
|
8530
8622
|
kind: "storage",
|
|
8531
8623
|
implementationVersion: PACKAGE_VERSION,
|
|
8532
|
-
supportedSpec: `sqlite-schema-v${
|
|
8624
|
+
supportedSpec: `sqlite-schema-v${SCHEMA_VERSION2}`,
|
|
8533
8625
|
capabilities: ["receipts", "events", "payment-attempts"],
|
|
8534
8626
|
status: "stable"
|
|
8535
8627
|
};
|
|
8536
8628
|
const store = {
|
|
8537
8629
|
async init() {
|
|
8538
8630
|
const version = db.pragma("user_version", { simple: true });
|
|
8539
|
-
if (version !==
|
|
8631
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8540
8632
|
throw new CommerceError(
|
|
8541
8633
|
"STORAGE_ERROR",
|
|
8542
|
-
`Receipt store schema version mismatch (found ${version}, expected ${
|
|
8634
|
+
`Receipt store schema version mismatch (found ${version}, expected ${SCHEMA_VERSION2})`
|
|
8543
8635
|
);
|
|
8544
8636
|
}
|
|
8545
8637
|
},
|
|
@@ -8673,9 +8765,9 @@ function createSqliteReceiptStore(options) {
|
|
|
8673
8765
|
}
|
|
8674
8766
|
try {
|
|
8675
8767
|
const version = db.pragma("user_version", { simple: true });
|
|
8676
|
-
if (version !==
|
|
8768
|
+
if (version !== SCHEMA_VERSION2) {
|
|
8677
8769
|
logger.warn(
|
|
8678
|
-
{ found: version, expected:
|
|
8770
|
+
{ found: version, expected: SCHEMA_VERSION2 },
|
|
8679
8771
|
"receipt-store: health check failed (schema version mismatch)"
|
|
8680
8772
|
);
|
|
8681
8773
|
return {
|
|
@@ -8687,7 +8779,7 @@ function createSqliteReceiptStore(options) {
|
|
|
8687
8779
|
}
|
|
8688
8780
|
return {
|
|
8689
8781
|
status: "pass",
|
|
8690
|
-
detail: `sqlite schema v${
|
|
8782
|
+
detail: `sqlite schema v${SCHEMA_VERSION2} writable`,
|
|
8691
8783
|
checkedAt: clock.nowIso(),
|
|
8692
8784
|
durationMs: clock.monotonicMs() - startedAt
|
|
8693
8785
|
};
|