@devlab.group/agent-commerce 2.1.0 → 2.2.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 +35 -5
- package/dist/ap2.js +3 -3
- package/dist/{chunk-SY6GUYSQ.js → chunk-7ZMMXZ4G.js} +3 -3
- package/dist/{chunk-SY6GUYSQ.js.map → chunk-7ZMMXZ4G.js.map} +1 -1
- package/dist/{chunk-UHMYRV3J.js → chunk-HNRDZLSN.js} +7 -5
- package/dist/chunk-HNRDZLSN.js.map +1 -0
- package/dist/{chunk-LZS5BL3W.js → chunk-IEW7NSP2.js} +23 -5
- package/dist/chunk-IEW7NSP2.js.map +1 -0
- package/dist/{chunk-IFGRCXME.js → chunk-Y4RE3XYF.js} +3 -3
- package/dist/{chunk-IFGRCXME.js.map → chunk-Y4RE3XYF.js.map} +1 -1
- package/dist/cli/ap2-MBMQHFX4.js +678 -0
- package/dist/cli/ap2-MBMQHFX4.js.map +1 -0
- package/dist/cli/chunk-2ZZOGI5I.js +1352 -0
- package/dist/cli/chunk-2ZZOGI5I.js.map +1 -0
- package/dist/cli/chunk-3EQBCFZY.js +53 -0
- package/dist/cli/chunk-3EQBCFZY.js.map +1 -0
- package/dist/cli/chunk-3M6U7ILB.js +8 -0
- package/dist/cli/chunk-3M6U7ILB.js.map +1 -0
- package/dist/cli/chunk-3MGAJ5FX.js +70 -0
- package/dist/cli/chunk-3MGAJ5FX.js.map +1 -0
- package/dist/cli/chunk-5K4XEAZV.js +113 -0
- package/dist/cli/chunk-5K4XEAZV.js.map +1 -0
- package/dist/cli/chunk-7JNYFUT3.js +256 -0
- package/dist/cli/chunk-7JNYFUT3.js.map +1 -0
- package/dist/cli/chunk-AA3FIEAR.js +27 -0
- package/dist/cli/chunk-AA3FIEAR.js.map +1 -0
- package/dist/cli/chunk-FHCFR2GS.js +17 -0
- package/dist/cli/chunk-FHCFR2GS.js.map +1 -0
- package/dist/cli/chunk-FRMLWGSS.js +8 -0
- package/dist/cli/chunk-FRMLWGSS.js.map +1 -0
- package/dist/cli/chunk-KLX2YZDG.js +1400 -0
- package/dist/cli/chunk-KLX2YZDG.js.map +1 -0
- package/dist/cli/chunk-NSPPEF5M.js +334 -0
- package/dist/cli/chunk-NSPPEF5M.js.map +1 -0
- package/dist/cli/chunk-OSTCU6ZK.js +44 -0
- package/dist/cli/chunk-OSTCU6ZK.js.map +1 -0
- package/dist/cli/chunk-QQ3WY5L3.js +271 -0
- package/dist/cli/chunk-QQ3WY5L3.js.map +1 -0
- package/dist/cli/chunk-RWVPYAKC.js +1742 -0
- package/dist/cli/chunk-RWVPYAKC.js.map +1 -0
- package/dist/cli/chunk-SPAYKVUP.js +118 -0
- package/dist/cli/chunk-SPAYKVUP.js.map +1 -0
- package/dist/cli/chunk-SS3QFF5N.js +614 -0
- package/dist/cli/chunk-SS3QFF5N.js.map +1 -0
- package/dist/cli/chunk-TPC4EJLT.js +11 -0
- package/dist/cli/chunk-TPC4EJLT.js.map +1 -0
- package/dist/cli/config-4IBGVFSP.js +11 -0
- package/dist/cli/config-4IBGVFSP.js.map +1 -0
- package/dist/cli/index.js +150 -3627
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-CNWQ2MOE.js +559 -0
- package/dist/cli/mcp-CNWQ2MOE.js.map +1 -0
- package/dist/cli/near-5AY3HWYF.js +58 -0
- package/dist/cli/near-5AY3HWYF.js.map +1 -0
- package/dist/cli/provider-4BQDH56Y.js +396 -0
- package/dist/cli/provider-4BQDH56Y.js.map +1 -0
- package/dist/cli/run-5MB2D45X.js +7286 -0
- package/dist/cli/run-5MB2D45X.js.map +1 -0
- package/dist/cli/svm-3MMYFPC7.js +33 -0
- package/dist/cli/svm-3MMYFPC7.js.map +1 -0
- package/dist/cli/x402-NKR3WQDQ.js +96 -0
- package/dist/cli/x402-NKR3WQDQ.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +48 -5
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +6 -16
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.js +3 -3
- package/dist/x402.d.ts +5 -0
- package/dist/x402.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-LZS5BL3W.js.map +0 -1
- package/dist/chunk-UHMYRV3J.js.map +0 -1
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { buildAp2Descriptor } from './chunk-3EQBCFZY.js';
|
|
2
|
+
export { AP2_CAPABILITIES, AP2_UNSUPPORTED, buildAp2Descriptor } from './chunk-3EQBCFZY.js';
|
|
3
|
+
import { openSqliteDatabase } from './chunk-3MGAJ5FX.js';
|
|
4
|
+
import { AP2_CHECKOUT_PROFILE, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_MANDATE_VCT } from './chunk-FHCFR2GS.js';
|
|
5
|
+
export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_DIGEST_ALGORITHM, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_MODES, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-FHCFR2GS.js';
|
|
6
|
+
import { PACKAGE_VERSION } from './chunk-FRMLWGSS.js';
|
|
7
|
+
import { NOOP_LOGGER, systemClock, CommerceError } from './chunk-QQ3WY5L3.js';
|
|
8
|
+
import { isRecord } from './chunk-3M6U7ILB.js';
|
|
9
|
+
import { SignJWT, importPKCS8, importJWK, jwtVerify, decodeProtectedHeader, decodeJwt } from 'jose';
|
|
10
|
+
import canonicalize from 'canonicalize';
|
|
11
|
+
import { createHash } from 'crypto';
|
|
12
|
+
import { decodeSdJwt, splitSdJwt, getClaims } from '@sd-jwt/core';
|
|
13
|
+
|
|
14
|
+
// src/authorization/ap2/errors.ts
|
|
15
|
+
var AP2_REJECTION_REASONS = [
|
|
16
|
+
"malformed_presentation",
|
|
17
|
+
"untrusted_issuer",
|
|
18
|
+
"unknown_key",
|
|
19
|
+
"invalid_signature",
|
|
20
|
+
"invalid_claims",
|
|
21
|
+
"expired",
|
|
22
|
+
"wrong_audience",
|
|
23
|
+
"unsupported_mandate_type",
|
|
24
|
+
"checkout_binding_failed",
|
|
25
|
+
"purchase_mismatch"
|
|
26
|
+
];
|
|
27
|
+
var MESSAGES = {
|
|
28
|
+
malformed_presentation: "The authorization is not a well-formed SD-JWT presentation.",
|
|
29
|
+
untrusted_issuer: "The mandate was issued by a party this merchant does not trust.",
|
|
30
|
+
unknown_key: "The mandate names a signing key this merchant does not trust.",
|
|
31
|
+
invalid_signature: "The mandate signature did not verify.",
|
|
32
|
+
invalid_claims: "The mandate is missing required claims or they are malformed.",
|
|
33
|
+
expired: "The mandate is expired or not yet valid.",
|
|
34
|
+
wrong_audience: "The mandate is addressed to a different audience.",
|
|
35
|
+
unsupported_mandate_type: "The mandate is not a closed Direct Checkout Mandate.",
|
|
36
|
+
checkout_binding_failed: "The merchant checkout document bound to the mandate did not verify.",
|
|
37
|
+
purchase_mismatch: "The mandate does not authorize this purchase."
|
|
38
|
+
};
|
|
39
|
+
function ap2Rejected(reason, context = {}) {
|
|
40
|
+
return new CommerceError("AUTHORIZATION_INVALID", MESSAGES[reason], {
|
|
41
|
+
details: { method: "ap2", reason },
|
|
42
|
+
...context.requestId !== void 0 ? { requestId: context.requestId } : {},
|
|
43
|
+
...context.resourceId !== void 0 ? { resourceId: context.resourceId } : {},
|
|
44
|
+
...context.cause !== void 0 ? { cause: context.cause } : {}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function ap2Replayed(state, context = {}) {
|
|
48
|
+
return new CommerceError(
|
|
49
|
+
"AUTHORIZATION_REPLAYED",
|
|
50
|
+
"This mandate has already been presented and cannot authorize another purchase.",
|
|
51
|
+
{
|
|
52
|
+
details: { method: "ap2", reason: "replayed", state },
|
|
53
|
+
...context.requestId !== void 0 ? { requestId: context.requestId } : {},
|
|
54
|
+
...context.resourceId !== void 0 ? { resourceId: context.resourceId } : {}
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function ap2Unavailable(detail, context = {}) {
|
|
59
|
+
return new CommerceError(
|
|
60
|
+
"AUTHORIZATION_PROVIDER_UNAVAILABLE",
|
|
61
|
+
"Authorization could not be verified right now. This is a merchant-side fault, not a problem with the presented mandate.",
|
|
62
|
+
{
|
|
63
|
+
details: { method: "ap2", reason: "verifier_unavailable", detail },
|
|
64
|
+
...context.requestId !== void 0 ? { requestId: context.requestId } : {},
|
|
65
|
+
...context.resourceId !== void 0 ? { resourceId: context.resourceId } : {},
|
|
66
|
+
...context.cause !== void 0 ? { cause: context.cause } : {}
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
function sha256(data) {
|
|
71
|
+
return createHash("sha256").update(data).digest();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/authorization/ap2/profile.ts
|
|
75
|
+
var REQUIRED_PROFILE_CLAIMS = [
|
|
76
|
+
"profile",
|
|
77
|
+
"resource_id",
|
|
78
|
+
"input_hash",
|
|
79
|
+
"amount",
|
|
80
|
+
"currency",
|
|
81
|
+
"payment_method"
|
|
82
|
+
];
|
|
83
|
+
function computeInputHash(input) {
|
|
84
|
+
const canonical = canonicalize(input ?? {});
|
|
85
|
+
if (canonical === void 0) {
|
|
86
|
+
throw new CommerceError("INTERNAL_ERROR", "resource input is not canonicalizable JSON");
|
|
87
|
+
}
|
|
88
|
+
return sha256(canonical).toString("base64url");
|
|
89
|
+
}
|
|
90
|
+
var EVM_ADDRESS = /^0x[0-9a-f]{40}$/i;
|
|
91
|
+
function sameCoordinate(declared, expected) {
|
|
92
|
+
if (typeof declared === "string" && expected !== void 0) {
|
|
93
|
+
if (EVM_ADDRESS.test(declared) && EVM_ADDRESS.test(expected)) {
|
|
94
|
+
return declared.toLowerCase() === expected.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return declared === expected;
|
|
98
|
+
}
|
|
99
|
+
async function bindMandateToPurchase(mandate, context, errorContext) {
|
|
100
|
+
const profile = mandate.checkoutClaims["agent_commerce"];
|
|
101
|
+
if (!isRecord(profile)) throw ap2Rejected("purchase_mismatch", errorContext);
|
|
102
|
+
for (const claim of REQUIRED_PROFILE_CLAIMS) {
|
|
103
|
+
const value = profile[claim];
|
|
104
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
105
|
+
throw ap2Rejected("purchase_mismatch", errorContext);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const requirement = context.requirement;
|
|
109
|
+
const expectedInputHash = computeInputHash(context.input);
|
|
110
|
+
const mustMatch = [
|
|
111
|
+
["profile", AP2_CHECKOUT_PROFILE],
|
|
112
|
+
["resource_id", context.resourceId],
|
|
113
|
+
["input_hash", expectedInputHash],
|
|
114
|
+
// Decimal strings on both sides. Comparing numerically would make "0.10"
|
|
115
|
+
// and "0.1" equal, and a mandate says what it says.
|
|
116
|
+
["amount", requirement.amount],
|
|
117
|
+
["currency", requirement.currency],
|
|
118
|
+
["payment_method", requirement.provider]
|
|
119
|
+
];
|
|
120
|
+
for (const [claim, expected] of mustMatch) {
|
|
121
|
+
if (profile[claim] !== expected) throw ap2Rejected("purchase_mismatch", errorContext);
|
|
122
|
+
}
|
|
123
|
+
for (const [claim, expected] of [
|
|
124
|
+
["destination", requirement.destination],
|
|
125
|
+
["network", requirement.network],
|
|
126
|
+
["asset", requirement.asset]
|
|
127
|
+
]) {
|
|
128
|
+
const declared = profile[claim];
|
|
129
|
+
if (declared === void 0 && expected === void 0) continue;
|
|
130
|
+
if (!sameCoordinate(declared, expected)) throw ap2Rejected("purchase_mismatch", errorContext);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/authorization/ap2/checkout-signer.ts
|
|
135
|
+
var DEFAULT_EXPIRES_IN_SECONDS = 900;
|
|
136
|
+
function requireText(value, field) {
|
|
137
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
138
|
+
throw new TypeError(
|
|
139
|
+
`createCheckoutJwt: ${field} must be a non-empty string, received ${describe(value)}`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
}
|
|
144
|
+
function describe(value) {
|
|
145
|
+
return value === null ? "null" : typeof value;
|
|
146
|
+
}
|
|
147
|
+
async function resolveKey(key) {
|
|
148
|
+
if (typeof key === "string") {
|
|
149
|
+
if (!key.includes("BEGIN PRIVATE KEY")) {
|
|
150
|
+
throw new TypeError(
|
|
151
|
+
'createCheckoutJwt: a string privateKey must be a PKCS#8 PEM beginning "-----BEGIN PRIVATE KEY-----"'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return importPKCS8(key, AP2_SIGNING_ALGORITHM);
|
|
155
|
+
}
|
|
156
|
+
if (typeof key !== "object" || key === null) {
|
|
157
|
+
throw new TypeError("createCheckoutJwt: privateKey must be a private JWK or a PKCS#8 PEM");
|
|
158
|
+
}
|
|
159
|
+
if (key["d"] === void 0) {
|
|
160
|
+
throw new TypeError(
|
|
161
|
+
'createCheckoutJwt: privateKey is a public JWK (no "d"). Use the private half of the pair whose public key is configured under checkoutIssuers'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (key["kty"] !== AP2_KEY_TYPE || key["crv"] !== AP2_JWK_CURVE) {
|
|
165
|
+
throw new TypeError(
|
|
166
|
+
`createCheckoutJwt: privateKey must be ${AP2_KEY_TYPE}/${AP2_JWK_CURVE}, the pair ${AP2_SIGNING_ALGORITHM} implies`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return key;
|
|
170
|
+
}
|
|
171
|
+
async function createCheckoutJwt(options) {
|
|
172
|
+
const kid = requireText(options.kid, "kid");
|
|
173
|
+
const issuer = requireText(options.issuer, "issuer");
|
|
174
|
+
const audience = requireText(options.audience, "audience");
|
|
175
|
+
const resourceId = requireText(options.resourceId, "resourceId");
|
|
176
|
+
const currency = requireText(options.currency, "currency");
|
|
177
|
+
const paymentMethod = requireText(options.paymentMethod, "paymentMethod");
|
|
178
|
+
if (typeof options.amount === "number") {
|
|
179
|
+
throw new TypeError(
|
|
180
|
+
'createCheckoutJwt: amount must be a decimal string, not a number. It is compared as a string, so 0.1 and "0.10" are different mandates'
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const amount = requireText(options.amount, "amount");
|
|
184
|
+
const key = await resolveKey(options.privateKey);
|
|
185
|
+
const issuedAt = Math.floor((options.now ?? /* @__PURE__ */ new Date()).getTime() / 1e3);
|
|
186
|
+
const expiresIn = options.expiresInSeconds ?? DEFAULT_EXPIRES_IN_SECONDS;
|
|
187
|
+
if (!Number.isInteger(expiresIn) || expiresIn <= 0) {
|
|
188
|
+
throw new TypeError("createCheckoutJwt: expiresInSeconds must be a positive whole number");
|
|
189
|
+
}
|
|
190
|
+
const agentCommerce = {
|
|
191
|
+
profile: AP2_CHECKOUT_PROFILE,
|
|
192
|
+
resource_id: resourceId,
|
|
193
|
+
input_hash: computeInputHash(options.input),
|
|
194
|
+
amount,
|
|
195
|
+
currency,
|
|
196
|
+
payment_method: paymentMethod,
|
|
197
|
+
...options.destination !== void 0 ? { destination: options.destination } : {},
|
|
198
|
+
...options.network !== void 0 ? { network: options.network } : {},
|
|
199
|
+
...options.asset !== void 0 ? { asset: options.asset } : {}
|
|
200
|
+
};
|
|
201
|
+
return new SignJWT({ agent_commerce: agentCommerce }).setProtectedHeader({ alg: AP2_SIGNING_ALGORITHM, kid, typ: "JWT" }).setIssuer(issuer).setAudience(audience).setIssuedAt(issuedAt).setExpirationTime(issuedAt + expiresIn).setJti(options.jwtId ?? crypto.randomUUID()).sign(key);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/authorization/ap2/replay-store.ts
|
|
205
|
+
function createAp2ReplayStore(options) {
|
|
206
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
207
|
+
const now = options.now ?? (() => Date.now());
|
|
208
|
+
const db = openSqliteDatabase({
|
|
209
|
+
path: options.path,
|
|
210
|
+
label: "AP2 authorization database",
|
|
211
|
+
logger
|
|
212
|
+
});
|
|
213
|
+
migrate(db);
|
|
214
|
+
const selectByReference = db.prepare(
|
|
215
|
+
"SELECT state FROM ap2_authorizations WHERE reference = ?"
|
|
216
|
+
);
|
|
217
|
+
const selectLiveByJti = db.prepare(
|
|
218
|
+
`SELECT state FROM ap2_authorizations
|
|
219
|
+
WHERE checkout_jti = ? AND reference != ? AND state != 'released'`
|
|
220
|
+
);
|
|
221
|
+
const insert = db.prepare(
|
|
222
|
+
`INSERT INTO ap2_authorizations
|
|
223
|
+
(reference, checkout_jti, mandate_issuer, checkout_issuer, resource_id, request_id,
|
|
224
|
+
state, created_at, updated_at)
|
|
225
|
+
VALUES (@reference, @checkout_jti, @mandate_issuer, @checkout_issuer, @resource_id,
|
|
226
|
+
@request_id, 'reserved', @at, @at)`
|
|
227
|
+
);
|
|
228
|
+
const reReserve = db.prepare(
|
|
229
|
+
`UPDATE ap2_authorizations
|
|
230
|
+
SET state = 'reserved', request_id = @request_id, updated_at = @at
|
|
231
|
+
WHERE reference = @reference AND state = 'released'`
|
|
232
|
+
);
|
|
233
|
+
const transition = db.prepare(
|
|
234
|
+
`UPDATE ap2_authorizations SET state = @state, updated_at = @at
|
|
235
|
+
WHERE reference = @reference AND state = 'reserved'`
|
|
236
|
+
);
|
|
237
|
+
const reserveTx = db.transaction((request) => {
|
|
238
|
+
const at = new Date(now()).toISOString();
|
|
239
|
+
const existing = selectByReference.get(request.reference);
|
|
240
|
+
if (existing !== void 0 && existing.state !== "released") {
|
|
241
|
+
return { kind: "replayed", state: existing.state };
|
|
242
|
+
}
|
|
243
|
+
const sameCheckout = selectLiveByJti.get(request.checkoutJti, request.reference);
|
|
244
|
+
if (sameCheckout !== void 0) {
|
|
245
|
+
return { kind: "replayed", state: sameCheckout.state };
|
|
246
|
+
}
|
|
247
|
+
if (existing !== void 0) {
|
|
248
|
+
reReserve.run({ reference: request.reference, request_id: request.requestId, at });
|
|
249
|
+
return { kind: "reserved" };
|
|
250
|
+
}
|
|
251
|
+
insert.run({
|
|
252
|
+
reference: request.reference,
|
|
253
|
+
checkout_jti: request.checkoutJti,
|
|
254
|
+
mandate_issuer: request.mandateIssuer,
|
|
255
|
+
checkout_issuer: request.checkoutIssuer,
|
|
256
|
+
resource_id: request.resourceId,
|
|
257
|
+
request_id: request.requestId,
|
|
258
|
+
at
|
|
259
|
+
});
|
|
260
|
+
return { kind: "reserved" };
|
|
261
|
+
});
|
|
262
|
+
const move = (reference, state) => {
|
|
263
|
+
transition.run({ reference, state, at: new Date(now()).toISOString() });
|
|
264
|
+
};
|
|
265
|
+
let closed = false;
|
|
266
|
+
return {
|
|
267
|
+
reserve(request) {
|
|
268
|
+
return reserveTx(request);
|
|
269
|
+
},
|
|
270
|
+
consume(reference) {
|
|
271
|
+
move(reference, "consumed");
|
|
272
|
+
},
|
|
273
|
+
release(reference) {
|
|
274
|
+
move(reference, "released");
|
|
275
|
+
},
|
|
276
|
+
markUncertain(reference) {
|
|
277
|
+
move(reference, "uncertain");
|
|
278
|
+
},
|
|
279
|
+
stateOf(reference) {
|
|
280
|
+
return selectByReference.get(reference)?.state;
|
|
281
|
+
},
|
|
282
|
+
close() {
|
|
283
|
+
if (closed) return;
|
|
284
|
+
closed = true;
|
|
285
|
+
db.close();
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function migrate(db) {
|
|
290
|
+
const current = db.pragma("user_version", { simple: true });
|
|
291
|
+
if (current >= 1) return;
|
|
292
|
+
const apply = db.transaction(() => {
|
|
293
|
+
db.exec(`
|
|
294
|
+
CREATE TABLE IF NOT EXISTS ap2_authorizations (
|
|
295
|
+
reference TEXT PRIMARY KEY,
|
|
296
|
+
checkout_jti TEXT NOT NULL,
|
|
297
|
+
mandate_issuer TEXT NOT NULL,
|
|
298
|
+
checkout_issuer TEXT NOT NULL,
|
|
299
|
+
resource_id TEXT NOT NULL,
|
|
300
|
+
request_id TEXT NOT NULL,
|
|
301
|
+
state TEXT NOT NULL
|
|
302
|
+
CHECK (state IN ('reserved', 'consumed', 'released', 'uncertain')),
|
|
303
|
+
created_at TEXT NOT NULL,
|
|
304
|
+
updated_at TEXT NOT NULL
|
|
305
|
+
);
|
|
306
|
+
CREATE INDEX IF NOT EXISTS idx_ap2_authorizations_checkout_jti
|
|
307
|
+
ON ap2_authorizations(checkout_jti);
|
|
308
|
+
`);
|
|
309
|
+
db.pragma("user_version = 1");
|
|
310
|
+
});
|
|
311
|
+
apply();
|
|
312
|
+
}
|
|
313
|
+
async function verifyCheckoutJwt(mandateClaims, deps, context) {
|
|
314
|
+
const compact = mandateClaims["checkout_jwt"];
|
|
315
|
+
const expectedHash = mandateClaims["checkout_hash"];
|
|
316
|
+
if (typeof compact !== "string" || compact.length === 0) {
|
|
317
|
+
throw ap2Rejected("invalid_claims", context);
|
|
318
|
+
}
|
|
319
|
+
if (typeof expectedHash !== "string" || expectedHash.length === 0) {
|
|
320
|
+
throw ap2Rejected("invalid_claims", context);
|
|
321
|
+
}
|
|
322
|
+
requireMatchingHash(compact, expectedHash, context);
|
|
323
|
+
let header;
|
|
324
|
+
let unverifiedPayload;
|
|
325
|
+
try {
|
|
326
|
+
header = decodeProtectedHeader(compact);
|
|
327
|
+
unverifiedPayload = decodeJwt(compact);
|
|
328
|
+
} catch (cause) {
|
|
329
|
+
throw ap2Rejected("checkout_binding_failed", { ...context, cause });
|
|
330
|
+
}
|
|
331
|
+
const { issuer, key } = await deps.trust.resolve(unverifiedPayload.iss, header.kid, context);
|
|
332
|
+
const options = {
|
|
333
|
+
algorithms: [AP2_SIGNING_ALGORITHM],
|
|
334
|
+
issuer: issuer.issuer,
|
|
335
|
+
audience: issuer.audience,
|
|
336
|
+
clockTolerance: deps.clockSkewSeconds,
|
|
337
|
+
currentDate: deps.clock.now()
|
|
338
|
+
};
|
|
339
|
+
let claims;
|
|
340
|
+
try {
|
|
341
|
+
const result = await jwtVerify(compact, key, options);
|
|
342
|
+
claims = result.payload;
|
|
343
|
+
} catch (cause) {
|
|
344
|
+
throw ap2Rejected("checkout_binding_failed", { ...context, cause });
|
|
345
|
+
}
|
|
346
|
+
const exp = claims["exp"];
|
|
347
|
+
const iat = claims["iat"];
|
|
348
|
+
const jti = claims["jti"];
|
|
349
|
+
if (typeof exp !== "number" || typeof iat !== "number") {
|
|
350
|
+
throw ap2Rejected("invalid_claims", context);
|
|
351
|
+
}
|
|
352
|
+
if (typeof jti !== "string" || jti.length === 0) {
|
|
353
|
+
throw ap2Rejected("invalid_claims", context);
|
|
354
|
+
}
|
|
355
|
+
const nowSeconds = Math.floor(deps.clock.now().getTime() / 1e3);
|
|
356
|
+
if (iat > nowSeconds + deps.clockSkewSeconds) throw ap2Rejected("expired", context);
|
|
357
|
+
return { issuer: issuer.issuer, jwtId: jti, claims };
|
|
358
|
+
}
|
|
359
|
+
function requireMatchingHash(compact, expected, context) {
|
|
360
|
+
const actual = sha256(compact).toString("base64url");
|
|
361
|
+
if (actual !== expected) throw ap2Rejected("checkout_binding_failed", context);
|
|
362
|
+
}
|
|
363
|
+
function hasher(data, algorithm) {
|
|
364
|
+
if (algorithm.toLowerCase() !== AP2_DIGEST_ALGORITHM) {
|
|
365
|
+
throw ap2Rejected("malformed_presentation");
|
|
366
|
+
}
|
|
367
|
+
return sha256(typeof data === "string" ? data : new Uint8Array(data));
|
|
368
|
+
}
|
|
369
|
+
var KEY_BOUND_TYPES = /* @__PURE__ */ new Set([
|
|
370
|
+
"kb+sd-jwt",
|
|
371
|
+
"kb-sd-jwt",
|
|
372
|
+
"kb+sd-jwt+kb",
|
|
373
|
+
"kb-sd-jwt+kb"
|
|
374
|
+
]);
|
|
375
|
+
async function verifyMandate(presentation, deps, context) {
|
|
376
|
+
if (presentation.includes("~~")) {
|
|
377
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
378
|
+
}
|
|
379
|
+
let decoded;
|
|
380
|
+
let encodedJws;
|
|
381
|
+
try {
|
|
382
|
+
decoded = await decodeSdJwt(presentation, hasher);
|
|
383
|
+
encodedJws = splitSdJwt(presentation).jwt;
|
|
384
|
+
} catch (cause) {
|
|
385
|
+
throw ap2Rejected("malformed_presentation", { ...context, cause });
|
|
386
|
+
}
|
|
387
|
+
if (decoded.kbJwt !== void 0) {
|
|
388
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
389
|
+
}
|
|
390
|
+
const rawPayload = decoded.jwt.payload;
|
|
391
|
+
const header = decoded.jwt.header;
|
|
392
|
+
if (!isRecord(rawPayload) || !isRecord(header)) {
|
|
393
|
+
throw ap2Rejected("malformed_presentation", context);
|
|
394
|
+
}
|
|
395
|
+
if (KEY_BOUND_TYPES.has(String(header["typ"])) || rawPayload["sd_hash"] !== void 0 || rawPayload["issuer_jwt_hash"] !== void 0) {
|
|
396
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
397
|
+
}
|
|
398
|
+
const { issuer, key } = await deps.trust.resolve(rawPayload["iss"], header["kid"], context);
|
|
399
|
+
const hasAudience = rawPayload["aud"] !== void 0;
|
|
400
|
+
if (!hasAudience && deps.requireAudience === true) {
|
|
401
|
+
throw ap2Rejected("invalid_claims", context);
|
|
402
|
+
}
|
|
403
|
+
const options = {
|
|
404
|
+
// Redundant with one resolved EC key, which already refuses `alg: none` and
|
|
405
|
+
// a forged HMAC. It stops the header picking the algorithm if a key set is
|
|
406
|
+
// ever resolved instead.
|
|
407
|
+
algorithms: [AP2_SIGNING_ALGORITHM],
|
|
408
|
+
// Also redundant, since the key was resolved *from* `iss`, and kept as a
|
|
409
|
+
// backstop for a resolver that matches on something else
|
|
410
|
+
...rawPayload["iss"] !== void 0 ? { issuer: issuer.issuer } : {},
|
|
411
|
+
// Not redundant: `aud` is the presenter's claim, checked against the
|
|
412
|
+
// configured value
|
|
413
|
+
...hasAudience ? { audience: issuer.audience } : {},
|
|
414
|
+
clockTolerance: deps.clockSkewSeconds,
|
|
415
|
+
// Injected clock, not jose's `Date.now()`, so expiry is testable
|
|
416
|
+
currentDate: deps.clock.now()
|
|
417
|
+
};
|
|
418
|
+
let verifiedPayload;
|
|
419
|
+
try {
|
|
420
|
+
const result = await jwtVerify(encodedJws, key, options);
|
|
421
|
+
verifiedPayload = result.payload;
|
|
422
|
+
} catch (cause) {
|
|
423
|
+
throw ap2Rejected(classifyJoseFailure(cause), { ...context, cause });
|
|
424
|
+
}
|
|
425
|
+
let claims;
|
|
426
|
+
try {
|
|
427
|
+
claims = await getClaims(verifiedPayload, decoded.disclosures, hasher);
|
|
428
|
+
} catch (cause) {
|
|
429
|
+
throw ap2Rejected("malformed_presentation", { ...context, cause });
|
|
430
|
+
}
|
|
431
|
+
const mandate = delegatedMandate(claims, context);
|
|
432
|
+
if (claims["cnf"] !== void 0 || mandate["cnf"] !== void 0) {
|
|
433
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
434
|
+
}
|
|
435
|
+
requireClosedCheckoutMandate(mandate, context);
|
|
436
|
+
if (mandate["constraints"] !== void 0) {
|
|
437
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
438
|
+
}
|
|
439
|
+
requireFreshness([verifiedPayload, mandate], deps, context);
|
|
440
|
+
return { issuer: issuer.issuer, claims: mandate, signedToken: encodedJws };
|
|
441
|
+
}
|
|
442
|
+
function delegatedMandate(claims, context) {
|
|
443
|
+
const delegated = claims["delegate_payload"];
|
|
444
|
+
if (!Array.isArray(delegated)) throw ap2Rejected("unsupported_mandate_type", context);
|
|
445
|
+
const disclosed = delegated.filter(isRecord);
|
|
446
|
+
const [only] = disclosed;
|
|
447
|
+
if (disclosed.length !== 1 || only === void 0) throw ap2Rejected("invalid_claims", context);
|
|
448
|
+
return only;
|
|
449
|
+
}
|
|
450
|
+
function classifyJoseFailure(cause) {
|
|
451
|
+
const code = cause?.code;
|
|
452
|
+
if (code === "ERR_JWT_EXPIRED") return "expired";
|
|
453
|
+
if (code === "ERR_JWT_CLAIM_VALIDATION_FAILED") {
|
|
454
|
+
const { claim, reason } = cause;
|
|
455
|
+
if (claim === "aud") return "wrong_audience";
|
|
456
|
+
if (claim === "nbf" || claim === "exp")
|
|
457
|
+
return reason === "invalid" ? "invalid_claims" : "expired";
|
|
458
|
+
}
|
|
459
|
+
return "invalid_signature";
|
|
460
|
+
}
|
|
461
|
+
function requireFreshness(sources, deps, context) {
|
|
462
|
+
const nowSeconds = Math.floor(deps.clock.now().getTime() / 1e3);
|
|
463
|
+
let expires = false;
|
|
464
|
+
for (const claims of sources) {
|
|
465
|
+
const exp = claims["exp"];
|
|
466
|
+
const iat = claims["iat"];
|
|
467
|
+
if (exp !== void 0) {
|
|
468
|
+
if (typeof exp !== "number") throw ap2Rejected("invalid_claims", context);
|
|
469
|
+
if (exp <= nowSeconds - deps.clockSkewSeconds) throw ap2Rejected("expired", context);
|
|
470
|
+
expires = true;
|
|
471
|
+
}
|
|
472
|
+
const nbf = claims["nbf"];
|
|
473
|
+
if (nbf !== void 0) {
|
|
474
|
+
if (typeof nbf !== "number") throw ap2Rejected("invalid_claims", context);
|
|
475
|
+
if (nbf > nowSeconds + deps.clockSkewSeconds) throw ap2Rejected("expired", context);
|
|
476
|
+
}
|
|
477
|
+
if (iat !== void 0) {
|
|
478
|
+
if (typeof iat !== "number") throw ap2Rejected("invalid_claims", context);
|
|
479
|
+
if (iat > nowSeconds + deps.clockSkewSeconds) throw ap2Rejected("expired", context);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (!expires && deps.requireExpiry === true) throw ap2Rejected("invalid_claims", context);
|
|
483
|
+
}
|
|
484
|
+
function requireClosedCheckoutMandate(claims, context) {
|
|
485
|
+
if (claims["vct"] !== AP2_CHECKOUT_MANDATE_VCT) {
|
|
486
|
+
throw ap2Rejected("unsupported_mandate_type", context);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function createTrustStore(issuers) {
|
|
490
|
+
const byIssuer = new Map(issuers.map((entry) => [entry.issuer, entry]));
|
|
491
|
+
const imported = /* @__PURE__ */ new Map();
|
|
492
|
+
function issuerByName(iss, context) {
|
|
493
|
+
if (typeof iss !== "string" || iss.length === 0) {
|
|
494
|
+
throw ap2Rejected("invalid_claims", context);
|
|
495
|
+
}
|
|
496
|
+
const issuer = byIssuer.get(iss);
|
|
497
|
+
if (issuer === void 0) throw ap2Rejected("untrusted_issuer", context);
|
|
498
|
+
return issuer;
|
|
499
|
+
}
|
|
500
|
+
function issuerByKid(kid, context) {
|
|
501
|
+
const matches = issuers.filter((entry) => entry.keys.some((key) => key.kid === kid));
|
|
502
|
+
const [only] = matches;
|
|
503
|
+
if (matches.length !== 1 || only === void 0) throw ap2Rejected("unknown_key", context);
|
|
504
|
+
return only;
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
/** Every trusted issuer id, for diagnostics. Never the keys themselves */
|
|
508
|
+
issuerIds() {
|
|
509
|
+
return [...byIssuer.keys()];
|
|
510
|
+
},
|
|
511
|
+
async resolve(iss, kid, context) {
|
|
512
|
+
if (typeof kid !== "string" || kid.length === 0) {
|
|
513
|
+
throw ap2Rejected("unknown_key", context);
|
|
514
|
+
}
|
|
515
|
+
const issuer = iss === void 0 ? issuerByKid(kid, context) : issuerByName(iss, context);
|
|
516
|
+
const trusted = issuer.keys.find((candidate) => candidate.kid === kid);
|
|
517
|
+
if (trusted === void 0) throw ap2Rejected("unknown_key", context);
|
|
518
|
+
const cacheKey = JSON.stringify([iss, kid]);
|
|
519
|
+
let pending = imported.get(cacheKey);
|
|
520
|
+
if (pending === void 0) {
|
|
521
|
+
pending = importJWK(trusted.jwk, AP2_SIGNING_ALGORITHM);
|
|
522
|
+
imported.set(cacheKey, pending);
|
|
523
|
+
}
|
|
524
|
+
try {
|
|
525
|
+
return { issuer, key: await pending };
|
|
526
|
+
} catch (cause) {
|
|
527
|
+
imported.delete(cacheKey);
|
|
528
|
+
throw ap2Unavailable("configured verification key could not be imported", {
|
|
529
|
+
...context,
|
|
530
|
+
cause
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/authorization/ap2/verifier.ts
|
|
538
|
+
function mandateReference(signedToken) {
|
|
539
|
+
return `sha256:${sha256(signedToken).toString("base64url")}`;
|
|
540
|
+
}
|
|
541
|
+
function createAp2MandateVerifier(options) {
|
|
542
|
+
const mandateTrust = createTrustStore(options.config.trust.mandateIssuers);
|
|
543
|
+
const checkoutTrust = createTrustStore(options.config.trust.checkoutIssuers);
|
|
544
|
+
const deps = { clock: options.clock, clockSkewSeconds: options.config.clockSkewSeconds };
|
|
545
|
+
const mandatePolicy = {
|
|
546
|
+
...deps,
|
|
547
|
+
trust: mandateTrust,
|
|
548
|
+
requireAudience: options.config.requireMandateAudience === true,
|
|
549
|
+
requireExpiry: options.config.requireMandateExpiry === true
|
|
550
|
+
};
|
|
551
|
+
return {
|
|
552
|
+
async verify(presentation, context = {}) {
|
|
553
|
+
if (typeof presentation !== "string" || presentation.length === 0) {
|
|
554
|
+
throw ap2Rejected("malformed_presentation", context);
|
|
555
|
+
}
|
|
556
|
+
const mandate = await verifyMandate(presentation, mandatePolicy, context);
|
|
557
|
+
const checkout = await verifyCheckoutJwt(
|
|
558
|
+
mandate.claims,
|
|
559
|
+
{ ...deps, trust: checkoutTrust },
|
|
560
|
+
context
|
|
561
|
+
);
|
|
562
|
+
return {
|
|
563
|
+
reference: mandateReference(mandate.signedToken),
|
|
564
|
+
mandateIssuer: mandate.issuer,
|
|
565
|
+
checkoutIssuer: checkout.issuer,
|
|
566
|
+
checkoutJwtId: checkout.jwtId,
|
|
567
|
+
checkoutClaims: checkout.claims
|
|
568
|
+
};
|
|
569
|
+
},
|
|
570
|
+
trustedIssuers() {
|
|
571
|
+
return { mandate: mandateTrust.issuerIds(), checkout: checkoutTrust.issuerIds() };
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/authorization/ap2/provider.ts
|
|
577
|
+
function createAp2AuthorizationProvider(options) {
|
|
578
|
+
const clock = options.clock ?? systemClock;
|
|
579
|
+
const logger = options.logger ?? NOOP_LOGGER;
|
|
580
|
+
const verifier = createAp2MandateVerifier({ config: options.config, clock });
|
|
581
|
+
const replay = options.replayStore ?? createAp2ReplayStore({ path: options.config.replay.path, logger });
|
|
582
|
+
const requirement = {
|
|
583
|
+
method: "ap2",
|
|
584
|
+
version: AP2_SPEC_VERSION,
|
|
585
|
+
profile: AP2_CHECKOUT_PROFILE
|
|
586
|
+
};
|
|
587
|
+
async function verifyAndReserve(context) {
|
|
588
|
+
const errorContext = {
|
|
589
|
+
requestId: context.requestId,
|
|
590
|
+
resourceId: context.resourceId
|
|
591
|
+
};
|
|
592
|
+
const mandate = await verifier.verify(context.submission.payload, errorContext);
|
|
593
|
+
await bindMandateToPurchase(mandate, context, errorContext);
|
|
594
|
+
let reservation;
|
|
595
|
+
try {
|
|
596
|
+
reservation = replay.reserve({
|
|
597
|
+
reference: mandate.reference,
|
|
598
|
+
checkoutJti: mandate.checkoutJwtId,
|
|
599
|
+
mandateIssuer: mandate.mandateIssuer,
|
|
600
|
+
checkoutIssuer: mandate.checkoutIssuer,
|
|
601
|
+
resourceId: context.resourceId,
|
|
602
|
+
requestId: context.requestId
|
|
603
|
+
});
|
|
604
|
+
} catch (cause) {
|
|
605
|
+
throw ap2Unavailable("replay store could not be written", { ...errorContext, cause });
|
|
606
|
+
}
|
|
607
|
+
if (reservation.kind === "replayed") {
|
|
608
|
+
throw ap2Replayed(reservation.state, errorContext);
|
|
609
|
+
}
|
|
610
|
+
return {
|
|
611
|
+
status: "verified",
|
|
612
|
+
method: "ap2",
|
|
613
|
+
reference: mandate.reference,
|
|
614
|
+
// The reservation handle is the reference itself, so the two can never
|
|
615
|
+
// name different mandates
|
|
616
|
+
reservationId: mandate.reference,
|
|
617
|
+
// Opaque identifiers an operator can reconcile with. Never a claim from
|
|
618
|
+
// the mandate: those carry the buyer's purchase and their personal data.
|
|
619
|
+
// `checkoutId`, not `checkoutJwtId` - the receipt store redacts any key
|
|
620
|
+
// that reads as secret-shaped, and "jwt" is on that list.
|
|
621
|
+
metadata: {
|
|
622
|
+
mandateIssuer: mandate.mandateIssuer,
|
|
623
|
+
checkoutIssuer: mandate.checkoutIssuer,
|
|
624
|
+
checkoutId: mandate.checkoutJwtId
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const finalize = (action, reservationId, context) => {
|
|
629
|
+
try {
|
|
630
|
+
replay[action](reservationId);
|
|
631
|
+
} catch (cause) {
|
|
632
|
+
throw ap2Unavailable(`replay store could not record ${action}`, { ...context, cause });
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
return {
|
|
636
|
+
name: "ap2",
|
|
637
|
+
descriptor: buildAp2Descriptor(PACKAGE_VERSION),
|
|
638
|
+
requirement,
|
|
639
|
+
verifyAndReserve,
|
|
640
|
+
async consume(reservationId, context) {
|
|
641
|
+
finalize("consume", reservationId, context);
|
|
642
|
+
},
|
|
643
|
+
async release(reservationId, context) {
|
|
644
|
+
finalize("release", reservationId, context);
|
|
645
|
+
},
|
|
646
|
+
async markUncertain(reservationId, context) {
|
|
647
|
+
finalize("markUncertain", reservationId, context);
|
|
648
|
+
},
|
|
649
|
+
async health() {
|
|
650
|
+
const startedAt = clock.monotonicMs();
|
|
651
|
+
const checkedAt = clock.nowIso();
|
|
652
|
+
let available = true;
|
|
653
|
+
try {
|
|
654
|
+
replay.stateOf("health-probe");
|
|
655
|
+
} catch {
|
|
656
|
+
available = false;
|
|
657
|
+
}
|
|
658
|
+
const durationMs = Math.round(clock.monotonicMs() - startedAt);
|
|
659
|
+
if (!available) {
|
|
660
|
+
return { status: "fail", checkedAt, durationMs, detail: "replay-store-unavailable" };
|
|
661
|
+
}
|
|
662
|
+
const trusted = verifier.trustedIssuers();
|
|
663
|
+
return {
|
|
664
|
+
status: "pass",
|
|
665
|
+
checkedAt,
|
|
666
|
+
durationMs,
|
|
667
|
+
detail: `mandate-issuers=${trusted.mandate.length} checkout-issuers=${trusted.checkout.length}`
|
|
668
|
+
};
|
|
669
|
+
},
|
|
670
|
+
close() {
|
|
671
|
+
replay.close();
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export { AP2_REJECTION_REASONS, createAp2AuthorizationProvider, createAp2ReplayStore, createCheckoutJwt };
|
|
677
|
+
//# sourceMappingURL=ap2-MBMQHFX4.js.map
|
|
678
|
+
//# sourceMappingURL=ap2-MBMQHFX4.js.map
|