@goodmeta/agent-verifier 0.2.0 → 0.5.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 +67 -26
- package/dist/ap2/chain.d.ts +43 -0
- package/dist/ap2/chain.d.ts.map +1 -0
- package/dist/ap2/chain.js +100 -0
- package/dist/ap2/chain.js.map +1 -0
- package/dist/ap2/chains.d.ts +68 -0
- package/dist/ap2/chains.d.ts.map +1 -0
- package/dist/ap2/chains.js +119 -0
- package/dist/ap2/chains.js.map +1 -0
- package/dist/ap2/constraints.d.ts +32 -0
- package/dist/ap2/constraints.d.ts.map +1 -0
- package/dist/ap2/constraints.js +222 -0
- package/dist/ap2/constraints.js.map +1 -0
- package/dist/ap2/hash.d.ts +23 -0
- package/dist/ap2/hash.d.ts.map +1 -0
- package/dist/ap2/hash.js +54 -0
- package/dist/ap2/hash.js.map +1 -0
- package/dist/ap2/index.d.ts +28 -0
- package/dist/ap2/index.d.ts.map +1 -0
- package/dist/ap2/index.js +35 -0
- package/dist/ap2/index.js.map +1 -0
- package/dist/ap2/jwk.d.ts +24 -0
- package/dist/ap2/jwk.d.ts.map +1 -0
- package/dist/ap2/jwk.js +29 -0
- package/dist/ap2/jwk.js.map +1 -0
- package/dist/ap2/kb-sd-jwt.d.ts +44 -0
- package/dist/ap2/kb-sd-jwt.d.ts.map +1 -0
- package/dist/ap2/kb-sd-jwt.js +97 -0
- package/dist/ap2/kb-sd-jwt.js.map +1 -0
- package/dist/ap2/keys.d.ts +41 -0
- package/dist/ap2/keys.d.ts.map +1 -0
- package/dist/ap2/keys.js +114 -0
- package/dist/ap2/keys.js.map +1 -0
- package/dist/ap2/max-flow.d.ts +26 -0
- package/dist/ap2/max-flow.d.ts.map +1 -0
- package/dist/ap2/max-flow.js +173 -0
- package/dist/ap2/max-flow.js.map +1 -0
- package/dist/ap2/parse.d.ts +35 -0
- package/dist/ap2/parse.d.ts.map +1 -0
- package/dist/ap2/parse.js +78 -0
- package/dist/ap2/parse.js.map +1 -0
- package/dist/ap2/sd-jwt.d.ts +83 -0
- package/dist/ap2/sd-jwt.d.ts.map +1 -0
- package/dist/ap2/sd-jwt.js +306 -0
- package/dist/ap2/sd-jwt.js.map +1 -0
- package/dist/ap2/types.d.ts +270 -0
- package/dist/ap2/types.d.ts.map +1 -0
- package/dist/ap2/types.js +141 -0
- package/dist/ap2/types.js.map +1 -0
- package/dist/client.d.ts +152 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +171 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/policy.d.ts +45 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +88 -0
- package/dist/policy.js.map +1 -0
- package/dist/receipt-jwt.d.ts +30 -0
- package/dist/receipt-jwt.d.ts.map +1 -0
- package/dist/receipt-jwt.js +39 -0
- package/dist/receipt-jwt.js.map +1 -0
- package/dist/schema.d.ts +41 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +50 -0
- package/dist/schema.js.map +1 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +24 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +69 -0
- package/dist/verify.js.map +1 -0
- package/package.json +14 -7
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-token trust anchoring: x5c (certificate chain) or kid lookup.
|
|
3
|
+
*
|
|
4
|
+
* Wire format follows AP2's `chain.py::X5cOrKidPublicKeyProvider` (H7): the x5c
|
|
5
|
+
* header is a list of **base64url**-DER certs, leaf first, `cert[i]` signed by
|
|
6
|
+
* `cert[i+1]`, and the chain is anchored when the LAST cert is signed by a
|
|
7
|
+
* trusted root that is itself NOT in the chain.
|
|
8
|
+
*
|
|
9
|
+
* Trust is STRICTER than AP2 (H2 — AP2's x5c path FAILS OPEN: with no configured
|
|
10
|
+
* roots it accepts a self-signed leaf, and it checks no validity / CA / curve /
|
|
11
|
+
* name chaining). We fail closed:
|
|
12
|
+
* - `trustedRoots` is MANDATORY when an x5c is present (else refuse, no fail-open).
|
|
13
|
+
* - every cert must be inside its validity window;
|
|
14
|
+
* - each issuer cert must assert `basicConstraints CA:TRUE`;
|
|
15
|
+
* - issuer/subject names must chain (`checkIssued`) AND signatures verify;
|
|
16
|
+
* - the leaf must be an EC P-256 key;
|
|
17
|
+
* - the chain must anchor to a trusted root (AP2-compatible: root not in chain).
|
|
18
|
+
*
|
|
19
|
+
* KNOWN LIMITATION: node's `X509Certificate` does not expose the basic
|
|
20
|
+
* KeyUsage / pathLenConstraint extensions (`.keyUsage` returns only EXTENDED key
|
|
21
|
+
* usage), so we cannot enforce `keyCertSign` / `digitalSignature` / pathLen
|
|
22
|
+
* without hand-parsing DER. We rely on `CA:TRUE` for the CA constraint and leave
|
|
23
|
+
* basic-keyUsage enforcement as a documented refinement (PLAN §5 H2).
|
|
24
|
+
*/
|
|
25
|
+
import { X509Certificate } from "node:crypto";
|
|
26
|
+
import type { RootKeyProvider } from "./chain.js";
|
|
27
|
+
import type { VerificationKey } from "./sd-jwt.js";
|
|
28
|
+
export interface X5cKidProviderOptions {
|
|
29
|
+
/** REQUIRED whenever a root token presents an x5c header (fail-closed, H2). */
|
|
30
|
+
trustedRoots?: X509Certificate[];
|
|
31
|
+
/** Resolve a verification key by `kid` when no x5c is present. */
|
|
32
|
+
kidLookup?: (kid: string) => VerificationKey | null;
|
|
33
|
+
/** Validity-window reference time; defaults to the current time. */
|
|
34
|
+
currentTime?: Date;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a root-key provider that resolves an x5c chain (fail-closed) or, absent
|
|
38
|
+
* x5c, a `kid` via `kidLookup`. Plug into `verifyChain(tokens, provider, …)`.
|
|
39
|
+
*/
|
|
40
|
+
export declare function x5cOrKidProvider(opts?: X5cKidProviderOptions): RootKeyProvider;
|
|
41
|
+
//# sourceMappingURL=keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../../src/ap2/keys.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAInD,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,YAAY,CAAC,EAAE,eAAe,EAAE,CAAC;IACjC,kEAAkE;IAClE,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,eAAe,GAAG,IAAI,CAAC;IACpD,oEAAoE;IACpE,WAAW,CAAC,EAAE,IAAI,CAAC;CACpB;AA+DD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,eAAe,CAmBlF"}
|
package/dist/ap2/keys.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-token trust anchoring: x5c (certificate chain) or kid lookup.
|
|
3
|
+
*
|
|
4
|
+
* Wire format follows AP2's `chain.py::X5cOrKidPublicKeyProvider` (H7): the x5c
|
|
5
|
+
* header is a list of **base64url**-DER certs, leaf first, `cert[i]` signed by
|
|
6
|
+
* `cert[i+1]`, and the chain is anchored when the LAST cert is signed by a
|
|
7
|
+
* trusted root that is itself NOT in the chain.
|
|
8
|
+
*
|
|
9
|
+
* Trust is STRICTER than AP2 (H2 — AP2's x5c path FAILS OPEN: with no configured
|
|
10
|
+
* roots it accepts a self-signed leaf, and it checks no validity / CA / curve /
|
|
11
|
+
* name chaining). We fail closed:
|
|
12
|
+
* - `trustedRoots` is MANDATORY when an x5c is present (else refuse, no fail-open).
|
|
13
|
+
* - every cert must be inside its validity window;
|
|
14
|
+
* - each issuer cert must assert `basicConstraints CA:TRUE`;
|
|
15
|
+
* - issuer/subject names must chain (`checkIssued`) AND signatures verify;
|
|
16
|
+
* - the leaf must be an EC P-256 key;
|
|
17
|
+
* - the chain must anchor to a trusted root (AP2-compatible: root not in chain).
|
|
18
|
+
*
|
|
19
|
+
* KNOWN LIMITATION: node's `X509Certificate` does not expose the basic
|
|
20
|
+
* KeyUsage / pathLenConstraint extensions (`.keyUsage` returns only EXTENDED key
|
|
21
|
+
* usage), so we cannot enforce `keyCertSign` / `digitalSignature` / pathLen
|
|
22
|
+
* without hand-parsing DER. We rely on `CA:TRUE` for the CA constraint and leave
|
|
23
|
+
* basic-keyUsage enforcement as a documented refinement (PLAN §5 H2).
|
|
24
|
+
*/
|
|
25
|
+
import { Buffer } from "node:buffer";
|
|
26
|
+
import { X509Certificate } from "node:crypto";
|
|
27
|
+
const MAX_X5C_CHAIN = 8;
|
|
28
|
+
function isP256(cert) {
|
|
29
|
+
const key = cert.publicKey;
|
|
30
|
+
return key.asymmetricKeyType === "ec" && key.asymmetricKeyDetails?.namedCurve === "prime256v1";
|
|
31
|
+
}
|
|
32
|
+
/** `validFromDate`/`validToDate` exist at runtime (Node 18.13+) but aren't in the
|
|
33
|
+
* pinned `@types/node`; fall back to parsing the string form. */
|
|
34
|
+
function certDate(cert, which) {
|
|
35
|
+
const runtime = cert[which === "from" ? "validFromDate" : "validToDate"];
|
|
36
|
+
return runtime instanceof Date ? runtime : new Date(which === "from" ? cert.validFrom : cert.validTo);
|
|
37
|
+
}
|
|
38
|
+
function withinValidity(cert, now) {
|
|
39
|
+
return now >= certDate(cert, "from") && now <= certDate(cert, "to");
|
|
40
|
+
}
|
|
41
|
+
function resolveX5cKey(x5c, trustedRoots, now) {
|
|
42
|
+
if (!Array.isArray(x5c) || x5c.length === 0)
|
|
43
|
+
throw new Error("x5c header must be a non-empty array");
|
|
44
|
+
if (x5c.length > MAX_X5C_CHAIN)
|
|
45
|
+
throw new Error(`x5c chain too long (> ${MAX_X5C_CHAIN})`);
|
|
46
|
+
const certs = x5c.map((b64, i) => {
|
|
47
|
+
if (typeof b64 !== "string")
|
|
48
|
+
throw new Error(`x5c[${i}] must be a base64url string`);
|
|
49
|
+
return new X509Certificate(Buffer.from(b64, "base64url")); // base64url to match AP2's decoder
|
|
50
|
+
});
|
|
51
|
+
// Every cert must be inside its validity window.
|
|
52
|
+
certs.forEach((c, i) => {
|
|
53
|
+
if (!withinValidity(c, now))
|
|
54
|
+
throw new Error(`x5c cert ${i} is outside its validity window`);
|
|
55
|
+
});
|
|
56
|
+
// Chain links: cert[i] is signed by, and named-chained to, cert[i+1]; the
|
|
57
|
+
// issuer must be a CA.
|
|
58
|
+
for (let i = 0; i < certs.length - 1; i++) {
|
|
59
|
+
const sub = certs[i];
|
|
60
|
+
const iss = certs[i + 1];
|
|
61
|
+
if (!sub.verify(iss.publicKey))
|
|
62
|
+
throw new Error(`x5c cert ${i} signature is not from cert ${i + 1}`);
|
|
63
|
+
// CA check first: node's checkIssued also rejects a non-CA issuer, so the
|
|
64
|
+
// explicit constraint must run before it to surface the precise reason.
|
|
65
|
+
if (!iss.ca)
|
|
66
|
+
throw new Error(`x5c cert ${i + 1} is not a CA (basicConstraints CA:TRUE required)`);
|
|
67
|
+
if (!sub.checkIssued(iss))
|
|
68
|
+
throw new Error(`x5c cert ${i} does not chain (issuer name) to cert ${i + 1}`);
|
|
69
|
+
}
|
|
70
|
+
// The leaf (signing) cert must be an EC P-256 key.
|
|
71
|
+
if (!isP256(certs[0]))
|
|
72
|
+
throw new Error("x5c leaf is not an EC P-256 key");
|
|
73
|
+
// Trust anchor (H7, AP2-compatible): the last cert must be signed by some
|
|
74
|
+
// trusted root that is NOT part of the chain.
|
|
75
|
+
const last = certs[certs.length - 1];
|
|
76
|
+
const anchored = trustedRoots.some((root) => {
|
|
77
|
+
try {
|
|
78
|
+
return last.verify(root.publicKey) && last.checkIssued(root);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (!anchored)
|
|
85
|
+
throw new Error("x5c chain does not chain to a trusted root");
|
|
86
|
+
return certs[0].publicKey; // leaf public key verifies the root SD-JWT signature
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a root-key provider that resolves an x5c chain (fail-closed) or, absent
|
|
90
|
+
* x5c, a `kid` via `kidLookup`. Plug into `verifyChain(tokens, provider, …)`.
|
|
91
|
+
*/
|
|
92
|
+
export function x5cOrKidProvider(opts = {}) {
|
|
93
|
+
const now = opts.currentTime ?? new Date();
|
|
94
|
+
return (root) => {
|
|
95
|
+
const header = root.header;
|
|
96
|
+
if ("x5c" in header) {
|
|
97
|
+
if (!opts.trustedRoots || opts.trustedRoots.length === 0) {
|
|
98
|
+
throw new Error("x5c present but no trustedRoots configured — refusing to fail open");
|
|
99
|
+
}
|
|
100
|
+
return resolveX5cKey(header["x5c"], opts.trustedRoots, now);
|
|
101
|
+
}
|
|
102
|
+
const kid = header["kid"];
|
|
103
|
+
if (typeof kid !== "string" || kid === "") {
|
|
104
|
+
throw new Error("Root token header has neither 'x5c' nor a usable 'kid'");
|
|
105
|
+
}
|
|
106
|
+
if (!opts.kidLookup)
|
|
107
|
+
throw new Error("Root token uses 'kid' but no kidLookup is configured");
|
|
108
|
+
const key = opts.kidLookup(kid);
|
|
109
|
+
if (!key)
|
|
110
|
+
throw new Error(`No key registered for kid: ${kid}`);
|
|
111
|
+
return key;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keys.js","sourceRoot":"","sources":["../../src/ap2/keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-items constraint via bipartite max-flow — line-for-line port of AP2's
|
|
3
|
+
* `sdk/max_flow_helper.py` (commit e1ea56d), Dinic variant (AP2's default).
|
|
4
|
+
*
|
|
5
|
+
* A checkout satisfies a `checkout.line_items` constraint when every cart item
|
|
6
|
+
* can be assigned to a requirement slot that accepts it (an item matches if its
|
|
7
|
+
* id is in the requirement's `acceptable_items`, or the requirement is a
|
|
8
|
+
* wildcard — empty `acceptable_items`), each requirement filled up to its
|
|
9
|
+
* `quantity`, and no cart unit assigned twice. A fast greedy pass resolves
|
|
10
|
+
* degree-1 items; the remaining ambiguous ("complex") items go through max-flow.
|
|
11
|
+
* Violation strings match AP2 byte-for-byte.
|
|
12
|
+
*/
|
|
13
|
+
export interface CartLineItem {
|
|
14
|
+
item: {
|
|
15
|
+
id: string;
|
|
16
|
+
};
|
|
17
|
+
quantity: number;
|
|
18
|
+
}
|
|
19
|
+
export interface LineItemRequirement {
|
|
20
|
+
acceptable_items: {
|
|
21
|
+
id: string;
|
|
22
|
+
}[];
|
|
23
|
+
quantity: number;
|
|
24
|
+
}
|
|
25
|
+
export declare function evaluateLineItemsMaxFlow(checkoutItems: CartLineItem[], requirements: LineItemRequirement[]): string[];
|
|
26
|
+
//# sourceMappingURL=max-flow.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-flow.d.ts","sourceRoot":"","sources":["../../src/ap2/max-flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;CAClB;AACD,MAAM,WAAW,mBAAmB;IAClC,gBAAgB,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,YAAY,EAAE,EAAE,YAAY,EAAE,mBAAmB,EAAE,GAAG,MAAM,EAAE,CA0ErH"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Line-items constraint via bipartite max-flow — line-for-line port of AP2's
|
|
3
|
+
* `sdk/max_flow_helper.py` (commit e1ea56d), Dinic variant (AP2's default).
|
|
4
|
+
*
|
|
5
|
+
* A checkout satisfies a `checkout.line_items` constraint when every cart item
|
|
6
|
+
* can be assigned to a requirement slot that accepts it (an item matches if its
|
|
7
|
+
* id is in the requirement's `acceptable_items`, or the requirement is a
|
|
8
|
+
* wildcard — empty `acceptable_items`), each requirement filled up to its
|
|
9
|
+
* `quantity`, and no cart unit assigned twice. A fast greedy pass resolves
|
|
10
|
+
* degree-1 items; the remaining ambiguous ("complex") items go through max-flow.
|
|
11
|
+
* Violation strings match AP2 byte-for-byte.
|
|
12
|
+
*/
|
|
13
|
+
const INF = 1e15;
|
|
14
|
+
export function evaluateLineItemsMaxFlow(checkoutItems, requirements) {
|
|
15
|
+
const cartQty = new Map();
|
|
16
|
+
for (const li of checkoutItems) {
|
|
17
|
+
const sku = li.item.id;
|
|
18
|
+
cartQty.set(sku, (cartQty.get(sku) ?? 0) + li.quantity);
|
|
19
|
+
}
|
|
20
|
+
const skuList = [...cartQty.keys()];
|
|
21
|
+
const reqAcceptable = requirements.map((req) => req.acceptable_items.length ? new Set(req.acceptable_items.map((ai) => ai.id)) : new Set());
|
|
22
|
+
const reqIsWildcard = requirements.map((req) => req.acceptable_items.length === 0);
|
|
23
|
+
const violations = [];
|
|
24
|
+
const hasWildcard = reqIsWildcard.some(Boolean);
|
|
25
|
+
const allAcceptable = new Set();
|
|
26
|
+
if (!hasWildcard)
|
|
27
|
+
for (const acc of reqAcceptable)
|
|
28
|
+
for (const x of acc)
|
|
29
|
+
allAcceptable.add(x);
|
|
30
|
+
for (const [sku, qty] of cartQty) {
|
|
31
|
+
if (qty <= 0)
|
|
32
|
+
continue;
|
|
33
|
+
if (!hasWildcard && !allAcceptable.has(sku)) {
|
|
34
|
+
violations.push(`Item ${sku} not in any requirement's acceptable items`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (violations.length)
|
|
38
|
+
return violations;
|
|
39
|
+
// Greedy elimination of degree-1 matches.
|
|
40
|
+
const reqRemaining = requirements.map((req) => req.quantity);
|
|
41
|
+
const complexSkus = [];
|
|
42
|
+
const unassigned = [];
|
|
43
|
+
for (const sku of skuList) {
|
|
44
|
+
const qty = cartQty.get(sku);
|
|
45
|
+
if (qty <= 0)
|
|
46
|
+
continue;
|
|
47
|
+
let matchIdx = -1;
|
|
48
|
+
let isComplex = false;
|
|
49
|
+
for (let j = 0; j < requirements.length; j++) {
|
|
50
|
+
if (reqIsWildcard[j] || reqAcceptable[j].has(sku)) {
|
|
51
|
+
if (matchIdx === -1)
|
|
52
|
+
matchIdx = j;
|
|
53
|
+
else {
|
|
54
|
+
isComplex = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (matchIdx !== -1 && !isComplex) {
|
|
60
|
+
const assigned = Math.min(qty, reqRemaining[matchIdx]);
|
|
61
|
+
reqRemaining[matchIdx] -= assigned;
|
|
62
|
+
const leftover = qty - assigned;
|
|
63
|
+
if (leftover > 0)
|
|
64
|
+
unassigned.push(`${sku} (${leftover})`);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
complexSkus.push(sku);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (complexSkus.length) {
|
|
71
|
+
const { maxFlow, residual } = lineItemsMaxFlow(complexSkus, cartQty, requirements, reqAcceptable, reqIsWildcard, reqRemaining);
|
|
72
|
+
const totalComplexCart = complexSkus.reduce((s, sku) => s + cartQty.get(sku), 0);
|
|
73
|
+
if (maxFlow < totalComplexCart) {
|
|
74
|
+
const source = 0;
|
|
75
|
+
const skuOffset = 1;
|
|
76
|
+
complexSkus.forEach((sku, i) => {
|
|
77
|
+
const remaining = residual[source].get(skuOffset + i) ?? 0;
|
|
78
|
+
if (remaining > 0)
|
|
79
|
+
unassigned.push(`${sku} (${remaining})`);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (unassigned.length) {
|
|
84
|
+
violations.push(`Cannot satisfy line item constraints: ${unassigned.join(", ")} could not be assigned to any requirement slot`);
|
|
85
|
+
}
|
|
86
|
+
return violations;
|
|
87
|
+
}
|
|
88
|
+
function lineItemsMaxFlow(skuList, cartQty, requirements, reqAcceptable, reqIsWildcard, reqRemaining) {
|
|
89
|
+
const sCount = skuList.length;
|
|
90
|
+
const rCount = requirements.length;
|
|
91
|
+
const n = 1 + sCount + rCount + 1;
|
|
92
|
+
const source = 0;
|
|
93
|
+
const sink = n - 1;
|
|
94
|
+
const skuOffset = 1;
|
|
95
|
+
const reqOffset = skuOffset + sCount;
|
|
96
|
+
const graph = Array.from({ length: n }, () => new Map());
|
|
97
|
+
// 1. source -> SKUs
|
|
98
|
+
skuList.forEach((sku, i) => {
|
|
99
|
+
graph[source].set(skuOffset + i, cartQty.get(sku));
|
|
100
|
+
graph[skuOffset + i].set(source, 0);
|
|
101
|
+
});
|
|
102
|
+
// 2. SKUs -> requirements
|
|
103
|
+
skuList.forEach((sku, i) => {
|
|
104
|
+
for (let j = 0; j < requirements.length; j++) {
|
|
105
|
+
if (reqIsWildcard[j] || reqAcceptable[j].has(sku)) {
|
|
106
|
+
graph[skuOffset + i].set(reqOffset + j, INF);
|
|
107
|
+
graph[reqOffset + j].set(skuOffset + i, 0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
// 3. requirements -> sink
|
|
112
|
+
for (let j = 0; j < requirements.length; j++) {
|
|
113
|
+
graph[reqOffset + j].set(sink, reqRemaining[j]);
|
|
114
|
+
graph[sink].set(reqOffset + j, 0);
|
|
115
|
+
}
|
|
116
|
+
return { maxFlow: dinic(graph, source, sink, n), residual: graph };
|
|
117
|
+
}
|
|
118
|
+
function dinic(graph, source, sink, n) {
|
|
119
|
+
const adjNodes = graph.map((m) => [...m.keys()]);
|
|
120
|
+
function bfsLevel() {
|
|
121
|
+
const level = new Array(n).fill(-1);
|
|
122
|
+
level[source] = 0;
|
|
123
|
+
const q = [source];
|
|
124
|
+
let head = 0;
|
|
125
|
+
while (head < q.length) {
|
|
126
|
+
const u = q[head++];
|
|
127
|
+
for (const v of adjNodes[u]) {
|
|
128
|
+
if (level[v] === -1 && (graph[u].get(v) ?? 0) > 0) {
|
|
129
|
+
level[v] = level[u] + 1;
|
|
130
|
+
q.push(v);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return level[sink] !== -1 ? level : null;
|
|
135
|
+
}
|
|
136
|
+
function dfsBlock(u, pushed, level, it) {
|
|
137
|
+
if (u === sink || pushed === 0)
|
|
138
|
+
return pushed;
|
|
139
|
+
let totalPushed = 0;
|
|
140
|
+
while (it[u] < adjNodes[u].length) {
|
|
141
|
+
const v = adjNodes[u][it[u]];
|
|
142
|
+
const cap = graph[u].get(v) ?? 0;
|
|
143
|
+
if (level[v] === level[u] + 1 && cap > 0) {
|
|
144
|
+
const d = dfsBlock(v, Math.min(pushed, cap), level, it);
|
|
145
|
+
if (d > 0) {
|
|
146
|
+
graph[u].set(v, (graph[u].get(v) ?? 0) - d);
|
|
147
|
+
graph[v].set(u, (graph[v].get(u) ?? 0) + d);
|
|
148
|
+
totalPushed += d;
|
|
149
|
+
pushed -= d;
|
|
150
|
+
if (pushed === 0)
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
it[u]++;
|
|
155
|
+
}
|
|
156
|
+
return totalPushed;
|
|
157
|
+
}
|
|
158
|
+
let total = 0;
|
|
159
|
+
for (;;) {
|
|
160
|
+
const level = bfsLevel();
|
|
161
|
+
if (level === null)
|
|
162
|
+
break;
|
|
163
|
+
const it = new Array(n).fill(0);
|
|
164
|
+
for (;;) {
|
|
165
|
+
const f = dfsBlock(source, INF, level, it);
|
|
166
|
+
if (f === 0)
|
|
167
|
+
break;
|
|
168
|
+
total += f;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return total;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=max-flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"max-flow.js","sourceRoot":"","sources":["../../src/ap2/max-flow.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dSD-JWT parsing + canonical serialization.
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact port of AP2's `sdjwt/common.py` (`ParsedToken`, `parse_token`) and
|
|
5
|
+
* `mandate.py` (`_canonical_chain_segment`). The canonical strings produced here
|
|
6
|
+
* feed the binding hashes (hash.ts), so a 1-byte divergence breaks every
|
|
7
|
+
* `sd_hash`/`issuer_jwt_hash` comparison — these are tested byte-exact against
|
|
8
|
+
* vectors minted by AP2's own SDK (test/fixtures/ap2-hash-pairs.json).
|
|
9
|
+
*/
|
|
10
|
+
export interface ParsedToken {
|
|
11
|
+
issuerJwt: string;
|
|
12
|
+
disclosures: string[];
|
|
13
|
+
kbJwt: string | null;
|
|
14
|
+
header: Record<string, unknown>;
|
|
15
|
+
payload: Record<string, unknown>;
|
|
16
|
+
/** `_sd_alg` from the payload, if a string. */
|
|
17
|
+
sdAlg?: string;
|
|
18
|
+
/** `issuer_jwt + '~' + disclosures.join('~') + '~'` (no KB segment). */
|
|
19
|
+
sdJwt: string;
|
|
20
|
+
/** `sdJwt + kbJwt` when a trailing KB-JWT is present, else `sdJwt`. */
|
|
21
|
+
canonical: string;
|
|
22
|
+
/** `typ` from the protected header, if a string. */
|
|
23
|
+
typ?: string;
|
|
24
|
+
}
|
|
25
|
+
/** Parse + canonicalize a compact SD-JWT token (one chain segment). */
|
|
26
|
+
export declare function parseToken(token: string): ParsedToken;
|
|
27
|
+
/**
|
|
28
|
+
* Restore the trailing `~` that the `~~` join consumed from a non-final segment.
|
|
29
|
+
* Port of `_canonical_chain_segment`: leave it alone if it's the final segment,
|
|
30
|
+
* already ends with `~`, or ends in a 3-part KB-JWT; otherwise append `~`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function canonicalChainSegment(segment: string, index: number, total: number): string;
|
|
33
|
+
/** Split a compact dSD-JWT chain on `~~`, restore each segment, and parse. */
|
|
34
|
+
export declare function splitChain(compact: string): ParsedToken[];
|
|
35
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../src/ap2/parse.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,+CAA+C;IAC/C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAeD,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAmCrD;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAM3F;AAED,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAGzD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dSD-JWT parsing + canonical serialization.
|
|
3
|
+
*
|
|
4
|
+
* Byte-exact port of AP2's `sdjwt/common.py` (`ParsedToken`, `parse_token`) and
|
|
5
|
+
* `mandate.py` (`_canonical_chain_segment`). The canonical strings produced here
|
|
6
|
+
* feed the binding hashes (hash.ts), so a 1-byte divergence breaks every
|
|
7
|
+
* `sd_hash`/`issuer_jwt_hash` comparison — these are tested byte-exact against
|
|
8
|
+
* vectors minted by AP2's own SDK (test/fixtures/ap2-hash-pairs.json).
|
|
9
|
+
*/
|
|
10
|
+
const JWT_PARTS = 3;
|
|
11
|
+
function decodeSegment(segment, part) {
|
|
12
|
+
let decoded;
|
|
13
|
+
try {
|
|
14
|
+
decoded = JSON.parse(Buffer.from(segment, "base64url").toString("utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`Cannot parse JWT ${part}: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded)) {
|
|
20
|
+
throw new Error(`JWT ${part} must decode to a JSON object`);
|
|
21
|
+
}
|
|
22
|
+
return decoded;
|
|
23
|
+
}
|
|
24
|
+
/** Parse + canonicalize a compact SD-JWT token (one chain segment). */
|
|
25
|
+
export function parseToken(token) {
|
|
26
|
+
if (token.startsWith("~"))
|
|
27
|
+
throw new Error("Malformed SD-JWT: empty issuer JWT");
|
|
28
|
+
if (!token.includes("~"))
|
|
29
|
+
throw new Error("Malformed SD-JWT: missing disclosure separator");
|
|
30
|
+
const parts = token.split("~");
|
|
31
|
+
const issuerJwt = parts[0];
|
|
32
|
+
const disclosureParts = parts.slice(1, -1);
|
|
33
|
+
if (disclosureParts.some((d) => d === "")) {
|
|
34
|
+
throw new Error("Malformed SD-JWT: empty disclosure segment");
|
|
35
|
+
}
|
|
36
|
+
let kbJwt;
|
|
37
|
+
if (token.endsWith("~")) {
|
|
38
|
+
kbJwt = null;
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
kbJwt = parts[parts.length - 1];
|
|
42
|
+
if (kbJwt.split(".").length !== JWT_PARTS) {
|
|
43
|
+
throw new Error("Malformed KB-JWT: expected header.payload.signature");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const disclosures = disclosureParts;
|
|
47
|
+
const jwtParts = issuerJwt.split(".");
|
|
48
|
+
if (jwtParts.length !== JWT_PARTS) {
|
|
49
|
+
throw new Error("Malformed SD-JWT: issuer JWT must have header.payload.signature");
|
|
50
|
+
}
|
|
51
|
+
const header = decodeSegment(jwtParts[0], "header");
|
|
52
|
+
const payload = decodeSegment(jwtParts[1], "payload");
|
|
53
|
+
const sdAlg = typeof payload["_sd_alg"] === "string" ? payload["_sd_alg"] : undefined;
|
|
54
|
+
const sdJwt = disclosures.length ? `${issuerJwt}~${disclosures.join("~")}~` : `${issuerJwt}~`;
|
|
55
|
+
const canonical = kbJwt ? sdJwt + kbJwt : sdJwt;
|
|
56
|
+
const typ = typeof header["typ"] === "string" ? header["typ"] : undefined;
|
|
57
|
+
return { issuerJwt, disclosures, kbJwt, header, payload, sdAlg, sdJwt, canonical, typ };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Restore the trailing `~` that the `~~` join consumed from a non-final segment.
|
|
61
|
+
* Port of `_canonical_chain_segment`: leave it alone if it's the final segment,
|
|
62
|
+
* already ends with `~`, or ends in a 3-part KB-JWT; otherwise append `~`.
|
|
63
|
+
*/
|
|
64
|
+
export function canonicalChainSegment(segment, index, total) {
|
|
65
|
+
if (index === total - 1 || segment.endsWith("~"))
|
|
66
|
+
return segment;
|
|
67
|
+
const idx = segment.lastIndexOf("~");
|
|
68
|
+
const last = idx === -1 ? segment : segment.slice(idx + 1);
|
|
69
|
+
if (last.split(".").length === JWT_PARTS)
|
|
70
|
+
return segment;
|
|
71
|
+
return segment + "~";
|
|
72
|
+
}
|
|
73
|
+
/** Split a compact dSD-JWT chain on `~~`, restore each segment, and parse. */
|
|
74
|
+
export function splitChain(compact) {
|
|
75
|
+
const segs = compact.split("~~");
|
|
76
|
+
return segs.map((s, i) => parseToken(canonicalChainSegment(s, i, segs.length)));
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../../src/ap2/parse.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root SD-JWT verification + disclosure resolution.
|
|
3
|
+
*
|
|
4
|
+
* Signatures: verified by jose, ES256-pinned, key supplied out-of-band — never
|
|
5
|
+
* by a disclosure library, never resolving a header `jwk`/`jku`/`x5u`/`kid`.
|
|
6
|
+
*
|
|
7
|
+
* Disclosure resolution mirrors AP2's actual reference behavior, which is a
|
|
8
|
+
* hybrid of THREE layers (AP2's golden vectors are minted by the Python
|
|
9
|
+
* `sd_jwt` lib + AP2's own post-passes, so we mirror that exact stack, not the
|
|
10
|
+
* unrelated TS `@sd-jwt` lib):
|
|
11
|
+
*
|
|
12
|
+
* A. Standard RFC 9901 unpack — port of `sd_jwt` lib
|
|
13
|
+
* `SDJWTVerifier._unpack_disclosed_claims`: resolve `{"...": digest}` array
|
|
14
|
+
* elements ([salt, value]) and `_sd: [digest]` object members
|
|
15
|
+
* ([salt, name, value]) against the token's disclosures; strip `_sd` /
|
|
16
|
+
* `_sd_alg`; reject duplicate disclosure hashes, duplicate `_sd` digests,
|
|
17
|
+
* and duplicate disclosed keys. Only sha-256 (the lib rejects others).
|
|
18
|
+
* B. AP2 delegate-item resolution — port of `chain.py::_resolve_delegate_items`
|
|
19
|
+
* + `_inline_sd_claims`: walk a resolved `delegate_payload` array; for dict
|
|
20
|
+
* items, inline any remaining object-property `_sd` digests; decode any
|
|
21
|
+
* bare-string items into their dict value.
|
|
22
|
+
* C. CMWallet quirk — port of `kb_sd_jwt.py::_resolve_delegate_payload` +
|
|
23
|
+
* `_try_resolve_digest`: `delegate_payload` items that are bare digest
|
|
24
|
+
* STRINGS get replaced in place by the dict value of the matching
|
|
25
|
+
* disclosure (used by KB hops in P3).
|
|
26
|
+
*
|
|
27
|
+
* Validated byte-exact against `test/fixtures/ap2-vectors.json`
|
|
28
|
+
* (`expectedPayloads`), minted from AP2's own SDK.
|
|
29
|
+
*/
|
|
30
|
+
import { type CryptoKey, type JWK as JoseJwk, type KeyObject } from "jose";
|
|
31
|
+
import type { ParsedToken } from "./parse.js";
|
|
32
|
+
import { type Jwk } from "./jwk.js";
|
|
33
|
+
/** A jose-importable verification key, or a bare JWK jose imports for us. */
|
|
34
|
+
export type VerificationKey = CryptoKey | KeyObject | JoseJwk | Uint8Array;
|
|
35
|
+
type Json = Record<string, unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve all disclosures in an issuer-verified payload (Layer A). Mirrors
|
|
38
|
+
* `_extract_sd_claims`: rejects a non-sha-256 `_sd_alg` (the lib supports only
|
|
39
|
+
* sha-256), then unpacks.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveDisclosures(payload: Json, disclosures: string[], sdAlg: string | undefined): Json;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve bare digest-string items inside `delegate_payload` in place. Port of
|
|
44
|
+
* `kb_sd_jwt.py::_resolve_delegate_payload`: dict items are left unchanged;
|
|
45
|
+
* string items are replaced by the matching disclosure's dict value (or kept
|
|
46
|
+
* as-is when no disclosure matches).
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveDelegatePayload(payload: Json, token: ParsedToken): void;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the effective delegate items from a verified payload's
|
|
51
|
+
* `delegate_payload`. Port of `chain.py::_resolve_delegate_items`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveDelegateItems(delegatePayload: unknown, token: ParsedToken): Json[];
|
|
54
|
+
export interface ResolvedToken {
|
|
55
|
+
/** The parsed token this resolution belongs to. */
|
|
56
|
+
token: ParsedToken;
|
|
57
|
+
/** Full issuer-verified payload after Layer A disclosure resolution. */
|
|
58
|
+
verifiedPayload: Json;
|
|
59
|
+
/** Effective delegate items (Layer B) — `[OpenMandate]` for a payment root. */
|
|
60
|
+
delegateItems: Json[];
|
|
61
|
+
}
|
|
62
|
+
/** Verify one ES256 compact JWS signature; throws on any failure. Key is supplied
|
|
63
|
+
* out-of-band — a header `jwk`/`jku`/`x5u`/`kid` is NEVER trusted to pick it. */
|
|
64
|
+
export declare function verifyEs256(issuerJwt: string, key: VerificationKey): Promise<void>;
|
|
65
|
+
/**
|
|
66
|
+
* Verify the root issuer JWT signature (ES256, pinned) and resolve its
|
|
67
|
+
* disclosures. `key` is the issuer's public key, supplied out-of-band (x5c/kid
|
|
68
|
+
* trust is resolved by the caller in P4).
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyRootSdJwt(token: ParsedToken, key: VerificationKey): Promise<ResolvedToken>;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve the `cnf.jwk` that the NEXT hop must be signed under. Port of AP2's
|
|
73
|
+
* `common.py::_find_cnf` (3-tier: delegate_items[*].cnf → verified
|
|
74
|
+
* delegate_payload[*].cnf → top-level cnf), returned as a strict EC-P256 JWK.
|
|
75
|
+
*
|
|
76
|
+
* Hardening H3 (stricter than AP2's first-match): reject if MORE THAN ONE
|
|
77
|
+
* delegate item carries a `cnf` — a disclosure-injected second `cnf` must not be
|
|
78
|
+
* able to silently win. (The signed-`_sd` binding refinement is pending a
|
|
79
|
+
* crafted ambiguous-cnf vector.)
|
|
80
|
+
*/
|
|
81
|
+
export declare function cnfJwk(resolved: ResolvedToken): Jwk;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=sd-jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sd-jwt.d.ts","sourceRoot":"","sources":["../../src/ap2/sd-jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAGH,OAAO,EAAiB,KAAK,SAAS,EAAE,KAAK,GAAG,IAAI,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,MAAM,CAAC;AAC1F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,OAAO,EAAY,KAAK,GAAG,EAAE,MAAM,UAAU,CAAC;AAE9C,6EAA6E;AAC7E,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AAU3E,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA8GpC;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAMxG;AAsBD;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,CAW9E;AAmCD;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,eAAe,EAAE,OAAO,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI,EAAE,CAazF;AAID,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,KAAK,EAAE,WAAW,CAAC;IACnB,wEAAwE;IACxE,eAAe,EAAE,IAAI,CAAC;IACtB,+EAA+E;IAC/E,aAAa,EAAE,IAAI,EAAE,CAAC;CACvB;AAED;iFACiF;AACjF,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAExF;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CAKtG;AAmBD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,aAAa,GAAG,GAAG,CAanD"}
|