@devlab.group/agent-commerce 1.4.1 → 1.6.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 +168 -183
- package/dist/ap2.d.ts +21 -24
- package/dist/ap2.js +68 -82
- package/dist/ap2.js.map +1 -1
- package/dist/authorization-BpbWGGrf.d.ts +115 -0
- package/dist/{chunk-NWIQLJNU.js → chunk-74BSEXB5.js} +37 -34
- package/dist/chunk-74BSEXB5.js.map +1 -0
- package/dist/chunk-AFI2CEZS.js +10 -0
- package/dist/chunk-AFI2CEZS.js.map +1 -0
- package/dist/chunk-ATTWSFQP.js +680 -0
- package/dist/chunk-ATTWSFQP.js.map +1 -0
- package/dist/{chunk-ZEGFLRGV.js → chunk-IVJC5YLE.js} +63 -26
- package/dist/chunk-IVJC5YLE.js.map +1 -0
- package/dist/{chunk-GF6KHHKL.js → chunk-RAJGPGJG.js} +5 -8
- package/dist/chunk-RAJGPGJG.js.map +1 -0
- package/dist/chunk-V2V2E7HX.js +27 -0
- package/dist/chunk-V2V2E7HX.js.map +1 -0
- package/dist/cli/index.js +812 -573
- package/dist/cli/index.js.map +1 -1
- package/dist/constants-Bkq04FRZ.d.ts +48 -0
- package/dist/guardrails-71XwtXlT.d.ts +31 -0
- package/dist/index.d.ts +245 -265
- package/dist/index.js +1222 -626
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +6 -59
- package/dist/mcp.js +64 -53
- package/dist/mcp.js.map +1 -1
- package/dist/mpp.d.ts +50 -0
- package/dist/mpp.js +353 -0
- package/dist/mpp.js.map +1 -0
- package/dist/protocol-adapter-DTkrXTev.d.ts +238 -0
- package/dist/runtime-CiYbgGpi.d.ts +303 -0
- package/dist/{types-CJtwLu1c.d.ts → types-O6lg3Q5V.d.ts} +17 -23
- package/dist/x402.d.ts +43 -75
- package/dist/x402.js +13 -693
- package/dist/x402.js.map +1 -1
- package/package.json +13 -2
- package/dist/authorization-CPDcpJVa.d.ts +0 -123
- package/dist/chunk-GF6KHHKL.js.map +0 -1
- package/dist/chunk-NWIQLJNU.js.map +0 -1
- package/dist/chunk-ZEGFLRGV.js.map +0 -1
- package/dist/guardrails-Dmwi63KB.d.ts +0 -38
- package/dist/protocol-adapter-Bebmggf_.d.ts +0 -273
- package/dist/runtime-CKq3d_3K.d.ts +0 -340
package/dist/ap2.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { E as EnabledAp2Config } from './types-
|
|
2
|
-
export { A as AP2_CHECKOUT_MANDATE_VCT, a as AP2_CHECKOUT_PROFILE, b as AP2_DEFAULT_CLOCK_SKEW_SECONDS, c as AP2_MAX_CLOCK_SKEW_SECONDS, d as AP2_SIGNING_ALGORITHM, e as AP2_SPEC_VERSION, f as Ap2AuthorizationConfig, g as Ap2Mode, h as Ap2TrustedIssuer, i as Ap2TrustedKey } from './types-
|
|
3
|
-
import { A as AuthorizationProvider } from './authorization-
|
|
4
|
-
import { C as Clock, L as Logger } from './runtime-
|
|
1
|
+
import { E as EnabledAp2Config } from './types-O6lg3Q5V.js';
|
|
2
|
+
export { A as AP2_CHECKOUT_MANDATE_VCT, a as AP2_CHECKOUT_PROFILE, b as AP2_DEFAULT_CLOCK_SKEW_SECONDS, c as AP2_MAX_CLOCK_SKEW_SECONDS, d as AP2_SIGNING_ALGORITHM, e as AP2_SPEC_VERSION, f as Ap2AuthorizationConfig, g as Ap2Mode, h as Ap2TrustedIssuer, i as Ap2TrustedKey } from './types-O6lg3Q5V.js';
|
|
3
|
+
import { A as AuthorizationProvider } from './authorization-BpbWGGrf.js';
|
|
4
|
+
import { C as Clock, L as Logger } from './runtime-CiYbgGpi.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* A private ES256 key: either a private JWK (the pair of the public one in the
|
|
8
|
-
* gateway's `checkoutIssuers`) or a PKCS#8 PEM, as `openssl` emits it
|
|
8
|
+
* gateway's `checkoutIssuers`) or a PKCS#8 PEM, as `openssl` emits it
|
|
9
9
|
*/
|
|
10
10
|
type Ap2SigningKey = Readonly<Record<string, unknown>> | string;
|
|
11
11
|
interface CreateCheckoutJwtOptions {
|
|
@@ -19,12 +19,12 @@ interface CreateCheckoutJwtOptions {
|
|
|
19
19
|
readonly resourceId: string;
|
|
20
20
|
/**
|
|
21
21
|
* The resource input this purchase is for, exactly as the buyer will send
|
|
22
|
-
* it: no reserved fields, no request id, no transport metadata
|
|
22
|
+
* it: no reserved fields, no request id, no transport metadata
|
|
23
23
|
*/
|
|
24
24
|
readonly input: unknown;
|
|
25
25
|
/**
|
|
26
26
|
* Decimal string, never a number, and compared as a string: `0.10` and `0.1`
|
|
27
|
-
* are different mandates. Take it from your own
|
|
27
|
+
* are different mandates. Take it from your own catalog rather than from
|
|
28
28
|
* whatever the agent asked for.
|
|
29
29
|
*/
|
|
30
30
|
readonly amount: string;
|
|
@@ -39,7 +39,7 @@ interface CreateCheckoutJwtOptions {
|
|
|
39
39
|
readonly destination?: string;
|
|
40
40
|
readonly network?: string;
|
|
41
41
|
readonly asset?: string;
|
|
42
|
-
/** Defaults to a random UUID. Recorded on the receipt and used for replay
|
|
42
|
+
/** Defaults to a random UUID. Recorded on the receipt and used for replay defense */
|
|
43
43
|
readonly jwtId?: string;
|
|
44
44
|
/** Defaults to 900: a human approval sits inside this window */
|
|
45
45
|
readonly expiresInSeconds?: number;
|
|
@@ -48,7 +48,7 @@ interface CreateCheckoutJwtOptions {
|
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
50
|
* Returns the compact JWT to hand to the agent, which wraps it in the Checkout
|
|
51
|
-
* Mandate the buyer signs
|
|
51
|
+
* Mandate the buyer signs
|
|
52
52
|
*/
|
|
53
53
|
declare function createCheckoutJwt(options: CreateCheckoutJwtOptions): Promise<string>;
|
|
54
54
|
|
|
@@ -62,9 +62,9 @@ declare function createCheckoutJwt(options: CreateCheckoutJwtOptions): Promise<s
|
|
|
62
62
|
/** What this provider actually verifies */
|
|
63
63
|
declare const AP2_CAPABILITIES: readonly string[];
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
65
|
+
* Major AP2 features this provider does not support, published in its
|
|
66
|
+
* descriptor and printed by `doctor`, so no client assumes full protocol
|
|
67
|
+
* support
|
|
68
68
|
*/
|
|
69
69
|
declare const AP2_UNSUPPORTED: readonly string[];
|
|
70
70
|
|
|
@@ -74,7 +74,7 @@ declare const AP2_UNSUPPORTED: readonly string[];
|
|
|
74
74
|
* `CommerceError.message` reaches the client, and a mandate carries the
|
|
75
75
|
* buyer's purchase and often personal data, so messages are fixed phrases from
|
|
76
76
|
* the list below. The machine-readable code goes in `details.reason` and the
|
|
77
|
-
* original exception on `cause`, which is never
|
|
77
|
+
* original exception on `cause`, which is never serialized.
|
|
78
78
|
*
|
|
79
79
|
* Three codes: AUTHORIZATION_INVALID when the buyer's mandate is bad,
|
|
80
80
|
* AUTHORIZATION_REPLAYED when it is good but spent, and
|
|
@@ -84,9 +84,11 @@ declare const AP2_UNSUPPORTED: readonly string[];
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* Coarse on purpose: a client learns roughly where its mandate was refused,
|
|
87
|
-
* not which check failed. Finer detail is an oracle for probing trust policy
|
|
87
|
+
* not which check failed. Finer detail is an oracle for probing trust policy,
|
|
88
|
+
* so a mandate signed with a disallowed algorithm reports `invalid_signature`
|
|
89
|
+
* and one declaring a digest other than sha-256 `malformed_presentation`.
|
|
88
90
|
*/
|
|
89
|
-
declare const AP2_REJECTION_REASONS: readonly ["malformed_presentation", "untrusted_issuer", "unknown_key", "invalid_signature", "
|
|
91
|
+
declare const AP2_REJECTION_REASONS: readonly ["malformed_presentation", "untrusted_issuer", "unknown_key", "invalid_signature", "invalid_claims", "expired", "wrong_audience", "unsupported_mandate_type", "checkout_binding_failed", "purchase_mismatch"];
|
|
90
92
|
type Ap2RejectionReason = (typeof AP2_REJECTION_REASONS)[number];
|
|
91
93
|
|
|
92
94
|
/**
|
|
@@ -96,7 +98,7 @@ type Ap2RejectionReason = (typeof AP2_REJECTION_REASONS)[number];
|
|
|
96
98
|
*/
|
|
97
99
|
type Ap2AuthorizationState = 'reserved' | 'consumed' | 'released' | 'uncertain';
|
|
98
100
|
/**
|
|
99
|
-
* A digest and a few identifiers: everything replay
|
|
101
|
+
* A digest and a few identifiers: everything replay defense needs, and nothing
|
|
100
102
|
* a leaked database would hand an attacker
|
|
101
103
|
*/
|
|
102
104
|
interface Ap2ReservationRequest {
|
|
@@ -139,8 +141,8 @@ interface Ap2ReplayStore {
|
|
|
139
141
|
* The AP2 authorization provider: the seam between core's generic contract and
|
|
140
142
|
* the mandate machinery.
|
|
141
143
|
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
+
* It owns its replay store, so whoever creates it must call `close()`:
|
|
145
|
+
* `createGateway` does not close authorization providers.
|
|
144
146
|
*/
|
|
145
147
|
|
|
146
148
|
interface Ap2AuthorizationProviderOptions {
|
|
@@ -151,14 +153,9 @@ interface Ap2AuthorizationProviderOptions {
|
|
|
151
153
|
readonly replayStore?: Ap2ReplayStore;
|
|
152
154
|
}
|
|
153
155
|
interface Ap2AuthorizationProvider extends AuthorizationProvider {
|
|
154
|
-
/** Closes the replay
|
|
156
|
+
/** Closes the replay store */
|
|
155
157
|
close(): void;
|
|
156
158
|
}
|
|
157
|
-
/**
|
|
158
|
-
* A reservation is keyed by the mandate reference, so the handle and the
|
|
159
|
-
* receipt's identity are the same digest. Kept as one name rather than two
|
|
160
|
-
* fields that must never disagree.
|
|
161
|
-
*/
|
|
162
159
|
declare function createAp2AuthorizationProvider(options: Ap2AuthorizationProviderOptions): Ap2AuthorizationProvider;
|
|
163
160
|
|
|
164
161
|
export { AP2_CAPABILITIES, AP2_REJECTION_REASONS, AP2_UNSUPPORTED, type Ap2AuthorizationProvider, type Ap2AuthorizationProviderOptions, type Ap2RejectionReason, type Ap2SigningKey, type CreateCheckoutJwtOptions, EnabledAp2Config, createAp2AuthorizationProvider as ap2, createAp2AuthorizationProvider, createCheckoutJwt };
|
package/dist/ap2.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-
|
|
2
|
-
export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-
|
|
3
|
-
import { systemClock, NOOP_LOGGER, CommerceError, PACKAGE_VERSION } from './chunk-
|
|
4
|
-
import { SignJWT, importPKCS8, importJWK, jwtVerify } from 'jose';
|
|
1
|
+
import { AP2_CHECKOUT_MANDATE_VCT, AP2_SIGNING_ALGORITHM, AP2_DIGEST_ALGORITHM, AP2_CHECKOUT_PROFILE, AP2_SPEC_VERSION, AP2_KEY_TYPE, AP2_JWK_CURVE, openSqliteDatabase } from './chunk-RAJGPGJG.js';
|
|
2
|
+
export { AP2_CHECKOUT_MANDATE_VCT, AP2_CHECKOUT_PROFILE, AP2_DEFAULT_CLOCK_SKEW_SECONDS, AP2_MAX_CLOCK_SKEW_SECONDS, AP2_SIGNING_ALGORITHM, AP2_SPEC_VERSION } from './chunk-RAJGPGJG.js';
|
|
3
|
+
import { systemClock, NOOP_LOGGER, CommerceError, isRecord, PACKAGE_VERSION } from './chunk-IVJC5YLE.js';
|
|
4
|
+
import { SignJWT, importPKCS8, importJWK, jwtVerify, decodeProtectedHeader, decodeJwt } from 'jose';
|
|
5
5
|
import canonicalize from 'canonicalize';
|
|
6
|
+
import { createHash } from 'crypto';
|
|
6
7
|
import { decodeSdJwt, splitSdJwt, getClaims } from '@sd-jwt/core';
|
|
7
8
|
|
|
8
9
|
// src/authorization/ap2/errors.ts
|
|
@@ -11,7 +12,6 @@ var AP2_REJECTION_REASONS = [
|
|
|
11
12
|
"untrusted_issuer",
|
|
12
13
|
"unknown_key",
|
|
13
14
|
"invalid_signature",
|
|
14
|
-
"unsupported_algorithm",
|
|
15
15
|
"invalid_claims",
|
|
16
16
|
"expired",
|
|
17
17
|
"wrong_audience",
|
|
@@ -24,7 +24,6 @@ var MESSAGES = {
|
|
|
24
24
|
untrusted_issuer: "The mandate was issued by a party this merchant does not trust.",
|
|
25
25
|
unknown_key: "The mandate names a signing key this merchant does not trust.",
|
|
26
26
|
invalid_signature: "The mandate signature did not verify.",
|
|
27
|
-
unsupported_algorithm: "The mandate uses a signature or digest algorithm that is not accepted.",
|
|
28
27
|
invalid_claims: "The mandate is missing required claims or they are malformed.",
|
|
29
28
|
expired: "The mandate is expired or not yet valid.",
|
|
30
29
|
wrong_audience: "The mandate is addressed to a different audience.",
|
|
@@ -63,6 +62,9 @@ function ap2Unavailable(detail, context = {}) {
|
|
|
63
62
|
}
|
|
64
63
|
);
|
|
65
64
|
}
|
|
65
|
+
function sha256(data) {
|
|
66
|
+
return createHash("sha256").update(data).digest();
|
|
67
|
+
}
|
|
66
68
|
|
|
67
69
|
// src/authorization/ap2/profile.ts
|
|
68
70
|
var REQUIRED_PROFILE_CLAIMS = [
|
|
@@ -73,20 +75,25 @@ var REQUIRED_PROFILE_CLAIMS = [
|
|
|
73
75
|
"currency",
|
|
74
76
|
"payment_method"
|
|
75
77
|
];
|
|
76
|
-
|
|
78
|
+
function computeInputHash(input) {
|
|
77
79
|
const canonical = canonicalize(input ?? {});
|
|
78
80
|
if (canonical === void 0) {
|
|
79
|
-
throw new
|
|
81
|
+
throw new CommerceError("INTERNAL_ERROR", "resource input is not canonicalizable JSON");
|
|
80
82
|
}
|
|
81
|
-
|
|
82
|
-
return Buffer.from(digest).toString("base64url");
|
|
83
|
+
return sha256(canonical).toString("base64url");
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
var EVM_ADDRESS = /^0x[0-9a-f]{40}$/i;
|
|
86
|
+
function sameCoordinate(declared, expected) {
|
|
87
|
+
if (typeof declared === "string" && expected !== void 0) {
|
|
88
|
+
if (EVM_ADDRESS.test(declared) && EVM_ADDRESS.test(expected)) {
|
|
89
|
+
return declared.toLowerCase() === expected.toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return declared === expected;
|
|
86
93
|
}
|
|
87
94
|
async function bindMandateToPurchase(mandate, context, errorContext) {
|
|
88
|
-
const profile =
|
|
89
|
-
if (profile
|
|
95
|
+
const profile = mandate.checkoutClaims["agent_commerce"];
|
|
96
|
+
if (!isRecord(profile)) throw ap2Rejected("purchase_mismatch", errorContext);
|
|
90
97
|
for (const claim of REQUIRED_PROFILE_CLAIMS) {
|
|
91
98
|
const value = profile[claim];
|
|
92
99
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -94,7 +101,7 @@ async function bindMandateToPurchase(mandate, context, errorContext) {
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
const requirement = context.requirement;
|
|
97
|
-
const expectedInputHash =
|
|
104
|
+
const expectedInputHash = computeInputHash(context.input);
|
|
98
105
|
const mustMatch = [
|
|
99
106
|
["profile", AP2_CHECKOUT_PROFILE],
|
|
100
107
|
["resource_id", context.resourceId],
|
|
@@ -115,14 +122,8 @@ async function bindMandateToPurchase(mandate, context, errorContext) {
|
|
|
115
122
|
]) {
|
|
116
123
|
const declared = profile[claim];
|
|
117
124
|
if (declared === void 0 && expected === void 0) continue;
|
|
118
|
-
if (declared
|
|
125
|
+
if (!sameCoordinate(declared, expected)) throw ap2Rejected("purchase_mismatch", errorContext);
|
|
119
126
|
}
|
|
120
|
-
return {
|
|
121
|
-
resourceId: profile["resource_id"],
|
|
122
|
-
amount: profile["amount"],
|
|
123
|
-
currency: profile["currency"],
|
|
124
|
-
paymentMethod: profile["payment_method"]
|
|
125
|
-
};
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
// src/authorization/ap2/checkout-signer.ts
|
|
@@ -184,7 +185,7 @@ async function createCheckoutJwt(options) {
|
|
|
184
185
|
const agentCommerce = {
|
|
185
186
|
profile: AP2_CHECKOUT_PROFILE,
|
|
186
187
|
resource_id: resourceId,
|
|
187
|
-
input_hash:
|
|
188
|
+
input_hash: computeInputHash(options.input),
|
|
188
189
|
amount,
|
|
189
190
|
currency,
|
|
190
191
|
payment_method: paymentMethod,
|
|
@@ -209,8 +210,7 @@ var AP2_CAPABILITIES = [
|
|
|
209
210
|
"replay-defence"
|
|
210
211
|
];
|
|
211
212
|
var AP2_UNSUPPORTED = [
|
|
212
|
-
// Mandate kinds. Open mandates
|
|
213
|
-
// evaluates, so accepting one would tell a buyer their limits were checked.
|
|
213
|
+
// Mandate kinds. Open mandates are refused: see AP2_CHECKOUT_MANDATE_VCT
|
|
214
214
|
"autonomous mode",
|
|
215
215
|
"open checkout mandates (mandate.checkout.open.1)",
|
|
216
216
|
"intent mandates",
|
|
@@ -226,7 +226,6 @@ var AP2_UNSUPPORTED = [
|
|
|
226
226
|
"digest algorithms other than sha-256",
|
|
227
227
|
// Roles this gateway does not play
|
|
228
228
|
"mandate issuance",
|
|
229
|
-
"merchant checkout JWT issuance",
|
|
230
229
|
"AP2 over the ACP checkout adapter"
|
|
231
230
|
];
|
|
232
231
|
function buildAp2Descriptor(implementationVersion) {
|
|
@@ -277,17 +276,17 @@ function createAp2ReplayStore(options) {
|
|
|
277
276
|
const reserveTx = db.transaction((request) => {
|
|
278
277
|
const at = new Date(now()).toISOString();
|
|
279
278
|
const existing = selectByReference.get(request.reference);
|
|
280
|
-
if (existing !== void 0) {
|
|
281
|
-
|
|
282
|
-
return { kind: "replayed", state: existing.state };
|
|
283
|
-
}
|
|
284
|
-
reReserve.run({ reference: request.reference, request_id: request.requestId, at });
|
|
285
|
-
return { kind: "reserved" };
|
|
279
|
+
if (existing !== void 0 && existing.state !== "released") {
|
|
280
|
+
return { kind: "replayed", state: existing.state };
|
|
286
281
|
}
|
|
287
282
|
const sameCheckout = selectLiveByJti.get(request.checkoutJti, request.reference);
|
|
288
283
|
if (sameCheckout !== void 0) {
|
|
289
284
|
return { kind: "replayed", state: sameCheckout.state };
|
|
290
285
|
}
|
|
286
|
+
if (existing !== void 0) {
|
|
287
|
+
reReserve.run({ reference: request.reference, request_id: request.requestId, at });
|
|
288
|
+
return { kind: "reserved" };
|
|
289
|
+
}
|
|
291
290
|
insert.run({
|
|
292
291
|
reference: request.reference,
|
|
293
292
|
checkout_jti: request.checkoutJti,
|
|
@@ -350,16 +349,6 @@ function migrate(db) {
|
|
|
350
349
|
});
|
|
351
350
|
apply();
|
|
352
351
|
}
|
|
353
|
-
function decodeSegment(jwt, index) {
|
|
354
|
-
const segment = jwt.split(".")[index];
|
|
355
|
-
if (segment === void 0) return void 0;
|
|
356
|
-
try {
|
|
357
|
-
const parsed = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
358
|
-
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : void 0;
|
|
359
|
-
} catch {
|
|
360
|
-
return void 0;
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
352
|
async function verifyCheckoutJwt(mandateClaims, deps, context) {
|
|
364
353
|
const compact = mandateClaims["checkout_jwt"];
|
|
365
354
|
const expectedHash = mandateClaims["checkout_hash"];
|
|
@@ -369,17 +358,16 @@ async function verifyCheckoutJwt(mandateClaims, deps, context) {
|
|
|
369
358
|
if (typeof expectedHash !== "string" || expectedHash.length === 0) {
|
|
370
359
|
throw ap2Rejected("invalid_claims", context);
|
|
371
360
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
361
|
+
requireMatchingHash(compact, expectedHash, context);
|
|
362
|
+
let header;
|
|
363
|
+
let unverifiedPayload;
|
|
364
|
+
try {
|
|
365
|
+
header = decodeProtectedHeader(compact);
|
|
366
|
+
unverifiedPayload = decodeJwt(compact);
|
|
367
|
+
} catch (cause) {
|
|
368
|
+
throw ap2Rejected("checkout_binding_failed", { ...context, cause });
|
|
377
369
|
}
|
|
378
|
-
const { issuer, key } = await deps.trust.resolve(
|
|
379
|
-
unverifiedPayload["iss"],
|
|
380
|
-
header["kid"],
|
|
381
|
-
context
|
|
382
|
-
);
|
|
370
|
+
const { issuer, key } = await deps.trust.resolve(unverifiedPayload.iss, header.kid, context);
|
|
383
371
|
const options = {
|
|
384
372
|
algorithms: [AP2_SIGNING_ALGORITHM],
|
|
385
373
|
issuer: issuer.issuer,
|
|
@@ -407,20 +395,15 @@ async function verifyCheckoutJwt(mandateClaims, deps, context) {
|
|
|
407
395
|
if (iat > nowSeconds + deps.clockSkewSeconds) throw ap2Rejected("expired", context);
|
|
408
396
|
return { issuer: issuer.issuer, jwtId: jti, claims };
|
|
409
397
|
}
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
const actual = Buffer.from(digest).toString("base64url");
|
|
398
|
+
function requireMatchingHash(compact, expected, context) {
|
|
399
|
+
const actual = sha256(compact).toString("base64url");
|
|
413
400
|
if (actual !== expected) throw ap2Rejected("checkout_binding_failed", context);
|
|
414
401
|
}
|
|
415
|
-
|
|
402
|
+
function hasher(data, algorithm) {
|
|
416
403
|
if (algorithm.toLowerCase() !== AP2_DIGEST_ALGORITHM) {
|
|
417
|
-
throw
|
|
404
|
+
throw ap2Rejected("malformed_presentation");
|
|
418
405
|
}
|
|
419
|
-
|
|
420
|
-
return new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
|
|
421
|
-
}
|
|
422
|
-
function asRecord2(value) {
|
|
423
|
-
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
406
|
+
return sha256(typeof data === "string" ? data : new Uint8Array(data));
|
|
424
407
|
}
|
|
425
408
|
async function verifyMandate(presentation, deps, context) {
|
|
426
409
|
let decoded;
|
|
@@ -434,26 +417,25 @@ async function verifyMandate(presentation, deps, context) {
|
|
|
434
417
|
if (decoded.kbJwt !== void 0) {
|
|
435
418
|
throw ap2Rejected("unsupported_mandate_type", context);
|
|
436
419
|
}
|
|
437
|
-
const rawPayload =
|
|
438
|
-
const header =
|
|
439
|
-
if (rawPayload
|
|
420
|
+
const rawPayload = decoded.jwt.payload;
|
|
421
|
+
const header = decoded.jwt.header;
|
|
422
|
+
if (!isRecord(rawPayload) || !isRecord(header)) {
|
|
440
423
|
throw ap2Rejected("malformed_presentation", context);
|
|
441
424
|
}
|
|
442
425
|
const { issuer, key } = await deps.trust.resolve(rawPayload["iss"], header["kid"], context);
|
|
443
426
|
const options = {
|
|
444
|
-
// Redundant
|
|
445
|
-
// a forged HMAC
|
|
446
|
-
//
|
|
427
|
+
// Redundant with one resolved EC key, which already refuses `alg: none` and
|
|
428
|
+
// a forged HMAC. It stops the header picking the algorithm if a key set is
|
|
429
|
+
// ever resolved instead.
|
|
447
430
|
algorithms: [AP2_SIGNING_ALGORITHM],
|
|
448
|
-
// Also redundant,
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
// presenter's claim, checked against what the operator configured.
|
|
431
|
+
// Also redundant, since the key was resolved *from* `iss`, and kept as a
|
|
432
|
+
// backstop for a resolver that matches on something else. `audience` is
|
|
433
|
+
// not redundant: `aud` is the presenter's claim, checked against the
|
|
434
|
+
// configured value.
|
|
453
435
|
issuer: issuer.issuer,
|
|
454
436
|
audience: issuer.audience,
|
|
455
437
|
clockTolerance: deps.clockSkewSeconds,
|
|
456
|
-
// Injected clock, not jose's `Date.now()`, so expiry
|
|
438
|
+
// Injected clock, not jose's `Date.now()`, so expiry is testable
|
|
457
439
|
currentDate: deps.clock.now()
|
|
458
440
|
};
|
|
459
441
|
let verifiedPayload;
|
|
@@ -536,9 +518,8 @@ function createTrustStore(issuers) {
|
|
|
536
518
|
}
|
|
537
519
|
|
|
538
520
|
// src/authorization/ap2/verifier.ts
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
return `sha256:${Buffer.from(digest).toString("base64url")}`;
|
|
521
|
+
function mandateReference(signedToken) {
|
|
522
|
+
return `sha256:${sha256(signedToken).toString("base64url")}`;
|
|
542
523
|
}
|
|
543
524
|
function createAp2MandateVerifier(options) {
|
|
544
525
|
const mandateTrust = createTrustStore(options.config.trust.mandateIssuers);
|
|
@@ -556,12 +537,11 @@ function createAp2MandateVerifier(options) {
|
|
|
556
537
|
context
|
|
557
538
|
);
|
|
558
539
|
return {
|
|
559
|
-
reference:
|
|
540
|
+
reference: mandateReference(mandate.signedToken),
|
|
560
541
|
mandateIssuer: mandate.issuer,
|
|
561
542
|
checkoutIssuer: checkout.issuer,
|
|
562
543
|
checkoutJwtId: checkout.jwtId,
|
|
563
|
-
checkoutClaims: checkout.claims
|
|
564
|
-
mandateClaims: mandate.claims
|
|
544
|
+
checkoutClaims: checkout.claims
|
|
565
545
|
};
|
|
566
546
|
},
|
|
567
547
|
trustedIssuers() {
|
|
@@ -608,6 +588,8 @@ function createAp2AuthorizationProvider(options) {
|
|
|
608
588
|
status: "verified",
|
|
609
589
|
method: "ap2",
|
|
610
590
|
reference: mandate.reference,
|
|
591
|
+
// The reservation handle is the reference itself, so the two can never
|
|
592
|
+
// name different mandates
|
|
611
593
|
reservationId: mandate.reference,
|
|
612
594
|
// Opaque identifiers an operator can reconcile with. Never a claim from
|
|
613
595
|
// the mandate: those carry the buyer's purchase and their personal data.
|
|
@@ -643,14 +625,18 @@ function createAp2AuthorizationProvider(options) {
|
|
|
643
625
|
},
|
|
644
626
|
async health() {
|
|
645
627
|
const startedAt = clock.monotonicMs();
|
|
646
|
-
const trusted = verifier.trustedIssuers();
|
|
647
628
|
const checkedAt = clock.nowIso();
|
|
648
|
-
|
|
629
|
+
let available = true;
|
|
649
630
|
try {
|
|
650
631
|
replay.stateOf("health-probe");
|
|
651
632
|
} catch {
|
|
633
|
+
available = false;
|
|
634
|
+
}
|
|
635
|
+
const durationMs = Math.round(clock.monotonicMs() - startedAt);
|
|
636
|
+
if (!available) {
|
|
652
637
|
return { status: "fail", checkedAt, durationMs, detail: "replay-store-unavailable" };
|
|
653
638
|
}
|
|
639
|
+
const trusted = verifier.trustedIssuers();
|
|
654
640
|
return {
|
|
655
641
|
status: "pass",
|
|
656
642
|
checkedAt,
|