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