@proof-holdings/delegation-verifier 0.1.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/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/carriers.d.ts +73 -0
- package/dist/carriers.d.ts.map +1 -0
- package/dist/carriers.js +265 -0
- package/dist/carriers.js.map +1 -0
- package/dist/delegate.d.ts +34 -0
- package/dist/delegate.d.ts.map +1 -0
- package/dist/delegate.js +156 -0
- package/dist/delegate.js.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/jwks.d.ts +47 -0
- package/dist/jwks.d.ts.map +1 -0
- package/dist/jwks.js +185 -0
- package/dist/jwks.js.map +1 -0
- package/dist/jwt.d.ts +20 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +63 -0
- package/dist/jwt.js.map +1 -0
- package/dist/publication.d.ts +19 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +39 -0
- package/dist/publication.js.map +1 -0
- package/dist/status.d.ts +86 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +321 -0
- package/dist/status.js.map +1 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +35 -0
- package/dist/types.js.map +1 -0
- package/dist/verify.d.ts +11 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +240 -0
- package/dist/verify.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying the delegation a card publishes.
|
|
3
|
+
*
|
|
4
|
+
* The carrier layout itself lives in `carriers.ts` (kept crypto-free so it can be read without
|
|
5
|
+
* pulling in the verification machinery); this module is the part that decides.
|
|
6
|
+
*/
|
|
7
|
+
import type { VerificationResult, VerifyOptions } from './types.js';
|
|
8
|
+
export { DELEGATION_META_KEY, DELEGATION_EXTENSION_URI, REGISTRY_PUBLISHER_PROVIDED_KEY, extractDelegationPublications, parseDelegationPointer, pointerRecordName, POINTER_VERSION, POINTER_RECORD_PREFIX, } from './carriers.js';
|
|
9
|
+
export type { FoundPublication, PointerParseResult, PointerRejection } from './carriers.js';
|
|
10
|
+
/**
|
|
11
|
+
* Verify the delegation an MCP server card or A2A agent card publishes.
|
|
12
|
+
*
|
|
13
|
+
* `options.delegate` still means what it means everywhere else: the artifact identity YOU
|
|
14
|
+
* resolved — the npm package you are about to install, the endpoint you are about to call —
|
|
15
|
+
* NOT a field read out of this card. A card that describes itself is not evidence about
|
|
16
|
+
* itself.
|
|
17
|
+
*/
|
|
18
|
+
export declare function verifyPublishedDelegation(card: unknown, options: VerifyOptions): Promise<VerificationResult>;
|
|
19
|
+
//# sourceMappingURL=publication.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publication.d.ts","sourceRoot":"","sources":["../src/publication.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGpE,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,+BAA+B,EAC/B,6BAA6B,EAC7B,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,GACtB,MAAM,eAAe,CAAC;AACvB,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE5F;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,IAAI,EAAE,OAAO,EACb,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAmB7B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Verifying the delegation a card publishes.
|
|
3
|
+
*
|
|
4
|
+
* The carrier layout itself lives in `carriers.ts` (kept crypto-free so it can be read without
|
|
5
|
+
* pulling in the verification machinery); this module is the part that decides.
|
|
6
|
+
*/
|
|
7
|
+
import { collectPublishedTokens } from './carriers.js';
|
|
8
|
+
import { classifyFailureReason } from './types.js';
|
|
9
|
+
import { verifyDelegation } from './verify.js';
|
|
10
|
+
export { DELEGATION_META_KEY, DELEGATION_EXTENSION_URI, REGISTRY_PUBLISHER_PROVIDED_KEY, extractDelegationPublications, parseDelegationPointer, pointerRecordName, POINTER_VERSION, POINTER_RECORD_PREFIX, } from './carriers.js';
|
|
11
|
+
/**
|
|
12
|
+
* Verify the delegation an MCP server card or A2A agent card publishes.
|
|
13
|
+
*
|
|
14
|
+
* `options.delegate` still means what it means everywhere else: the artifact identity YOU
|
|
15
|
+
* resolved — the npm package you are about to install, the endpoint you are about to call —
|
|
16
|
+
* NOT a field read out of this card. A card that describes itself is not evidence about
|
|
17
|
+
* itself.
|
|
18
|
+
*/
|
|
19
|
+
export async function verifyPublishedDelegation(card, options) {
|
|
20
|
+
const tokens = collectPublishedTokens(card);
|
|
21
|
+
if (tokens.length === 0) {
|
|
22
|
+
return {
|
|
23
|
+
valid: false,
|
|
24
|
+
reason: 'no_publication',
|
|
25
|
+
outcome: classifyFailureReason('no_publication'),
|
|
26
|
+
message: 'card carries no delegation publication in any sanctioned location',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (tokens.length > 1) {
|
|
30
|
+
return {
|
|
31
|
+
valid: false,
|
|
32
|
+
reason: 'multiple_publications',
|
|
33
|
+
outcome: classifyFailureReason('multiple_publications'),
|
|
34
|
+
message: `card publishes ${tokens.length} different delegation tokens — refusing to guess which one it means`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return verifyDelegation(tokens[0], options);
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=publication.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publication.js","sourceRoot":"","sources":["../src/publication.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,wBAAwB,EACxB,+BAA+B,EAC/B,6BAA6B,EAC7B,sBAAsB,EACtB,iBAAiB,EACjB,eAAe,EACf,qBAAqB,GACtB,MAAM,eAAe,CAAC;AAGvB;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,IAAa,EACb,OAAsB;IAEtB,MAAM,MAAM,GAAG,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,gBAAgB;YACxB,OAAO,EAAE,qBAAqB,CAAC,gBAAgB,CAAC;YAChD,OAAO,EAAE,mEAAmE;SAC7E,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACtB,OAAO;YACL,KAAK,EAAE,KAAK;YACZ,MAAM,EAAE,uBAAuB;YAC/B,OAAO,EAAE,qBAAqB,CAAC,uBAAuB,CAAC;YACvD,OAAO,EAAE,kBAAkB,MAAM,CAAC,MAAM,qEAAqE;SAC9G,CAAC;IACJ,CAAC;IACD,OAAO,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC"}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revocation status of a delegation, over two channels that answer different questions.
|
|
3
|
+
*
|
|
4
|
+
* 1. **Token Status List** (IETF draft-ietf-oauth-status-list). The token carries its own
|
|
5
|
+
* slot — `status.status_list.{idx, uri}` — and the signed list at that uri says whether
|
|
6
|
+
* the slot is valid (0), revoked (1) or suspended (2). This is the standard, cacheable,
|
|
7
|
+
* herd-private channel, and — since `h-status-list-cascade-fold` — it is AUTHORITATIVE
|
|
8
|
+
* for the delegation, including the domain control proof cascade underneath it: a
|
|
9
|
+
* revoked or suspended control folds into this same bit, so `checkDelegationStatus`
|
|
10
|
+
* trusts a `valid`/`revoked`/`suspended` list read and does not also ask the endpoint
|
|
11
|
+
* (`h-delegation-verifier-status-fallthrough-gate`).
|
|
12
|
+
* 2. **The issuer's status endpoint.** Stays the only channel for three things the bit
|
|
13
|
+
* cannot express: `unknown_delegation` ("no record of this handle" is distinguishable
|
|
14
|
+
* from "we have a record and it is dead" only by a live call), a token carrying no
|
|
15
|
+
* status-list claim at all, and cascade-derived EXPIRY — the control's own `expires_at`
|
|
16
|
+
* is never packed into the bit (only revoked/suspended are), so a delegation whose
|
|
17
|
+
* effective lifetime is capped by an expired control, while its own list bit and its own
|
|
18
|
+
* `exp` claim both still read fine, is only visible through this endpoint.
|
|
19
|
+
*
|
|
20
|
+
* `h-fix-control-proof-reissue-shorter` closes the write side of that expiry gap going
|
|
21
|
+
* forward — a delegation can no longer be minted longer than its control, and reissuing a
|
|
22
|
+
* control shorter than an outstanding delegation now caps the reissue instead of leaving
|
|
23
|
+
* the delegation to outlive it — but two bounded residuals remain, both deliberately
|
|
24
|
+
* accepted there rather than fixed here: a control shortened before that task shipped
|
|
25
|
+
* (measured zero on staging, unmeasured in production), and that task's own fail-open
|
|
26
|
+
* when its registry-budget guard cannot run. A verifier package cannot special-case any
|
|
27
|
+
* one issuer's guarantees, so this residual is disclosed, not silently assumed away — see
|
|
28
|
+
* the package README's Failure reasons section.
|
|
29
|
+
*
|
|
30
|
+
* Both channels are authenticated: the status list is a signed JWT checked against the same
|
|
31
|
+
* key set as the delegation, and the uri inside the token is refused unless it belongs to the
|
|
32
|
+
* trusted issuer — it arrives INSIDE the artifact being verified, so treating it as an address
|
|
33
|
+
* to fetch would hand an attacker both the status authority and an SSRF primitive.
|
|
34
|
+
*/
|
|
35
|
+
import type { FetchLike, TrustedIssuer } from './types.js';
|
|
36
|
+
export type StatusOutcome = {
|
|
37
|
+
state: 'valid';
|
|
38
|
+
} | {
|
|
39
|
+
state: 'revoked';
|
|
40
|
+
message: string;
|
|
41
|
+
} | {
|
|
42
|
+
state: 'suspended';
|
|
43
|
+
message: string;
|
|
44
|
+
}
|
|
45
|
+
/** The issuer's clock says this delegation is over — distinct from withdrawn. */
|
|
46
|
+
| {
|
|
47
|
+
state: 'expired';
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
/** The issuer has no record of the delegation at all. */
|
|
51
|
+
| {
|
|
52
|
+
state: 'unknown';
|
|
53
|
+
message: string;
|
|
54
|
+
} | {
|
|
55
|
+
state: 'untrusted_uri';
|
|
56
|
+
message: string;
|
|
57
|
+
} | {
|
|
58
|
+
state: 'unavailable';
|
|
59
|
+
message: string;
|
|
60
|
+
};
|
|
61
|
+
export declare function allowedStatusOrigins(issuer: TrustedIssuer): string[];
|
|
62
|
+
/**
|
|
63
|
+
* Read one 2-bit slot out of a decoded status list. Lowest index in the least significant
|
|
64
|
+
* bits, `bits` statuses per byte — the draft's byte order.
|
|
65
|
+
*/
|
|
66
|
+
export declare function readStatusSlot(bytes: Uint8Array, index: number, bits: number): number | null;
|
|
67
|
+
interface StatusListClaim {
|
|
68
|
+
idx: number;
|
|
69
|
+
uri: string;
|
|
70
|
+
}
|
|
71
|
+
export declare function readStatusListClaim(claims: Record<string, unknown>): StatusListClaim | null;
|
|
72
|
+
/**
|
|
73
|
+
* Combined status. A negative answer from EITHER channel is decisive; `unavailable` from
|
|
74
|
+
* either one is reported so the caller can fail closed.
|
|
75
|
+
*/
|
|
76
|
+
export declare function checkDelegationStatus(params: {
|
|
77
|
+
token: string;
|
|
78
|
+
claims: Record<string, unknown>;
|
|
79
|
+
issuer: TrustedIssuer;
|
|
80
|
+
fetch: FetchLike;
|
|
81
|
+
timeoutMs: number;
|
|
82
|
+
now: Date;
|
|
83
|
+
clockToleranceSeconds: number;
|
|
84
|
+
}): Promise<StatusOutcome>;
|
|
85
|
+
export {};
|
|
86
|
+
//# sourceMappingURL=status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3D,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAClB;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACzC,iFAAiF;GAC/E;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AACvC,yDAAyD;GACvD;IAAE,KAAK,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,KAAK,EAAE,eAAe,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3C;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAM9C,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,EAAE,CAWpE;AASD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAS5F;AAED,UAAU,eAAe;IACvB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,eAAe,GAAG,IAAI,CAS3F;AA+ND;;;GAGG;AACH,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,aAAa,CAAC;IACtB,KAAK,EAAE,SAAS,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,IAAI,CAAC;IACV,qBAAqB,EAAE,MAAM,CAAC;CAC/B,GAAG,OAAO,CAAC,aAAa,CAAC,CA0BzB"}
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revocation status of a delegation, over two channels that answer different questions.
|
|
3
|
+
*
|
|
4
|
+
* 1. **Token Status List** (IETF draft-ietf-oauth-status-list). The token carries its own
|
|
5
|
+
* slot — `status.status_list.{idx, uri}` — and the signed list at that uri says whether
|
|
6
|
+
* the slot is valid (0), revoked (1) or suspended (2). This is the standard, cacheable,
|
|
7
|
+
* herd-private channel, and — since `h-status-list-cascade-fold` — it is AUTHORITATIVE
|
|
8
|
+
* for the delegation, including the domain control proof cascade underneath it: a
|
|
9
|
+
* revoked or suspended control folds into this same bit, so `checkDelegationStatus`
|
|
10
|
+
* trusts a `valid`/`revoked`/`suspended` list read and does not also ask the endpoint
|
|
11
|
+
* (`h-delegation-verifier-status-fallthrough-gate`).
|
|
12
|
+
* 2. **The issuer's status endpoint.** Stays the only channel for three things the bit
|
|
13
|
+
* cannot express: `unknown_delegation` ("no record of this handle" is distinguishable
|
|
14
|
+
* from "we have a record and it is dead" only by a live call), a token carrying no
|
|
15
|
+
* status-list claim at all, and cascade-derived EXPIRY — the control's own `expires_at`
|
|
16
|
+
* is never packed into the bit (only revoked/suspended are), so a delegation whose
|
|
17
|
+
* effective lifetime is capped by an expired control, while its own list bit and its own
|
|
18
|
+
* `exp` claim both still read fine, is only visible through this endpoint.
|
|
19
|
+
*
|
|
20
|
+
* `h-fix-control-proof-reissue-shorter` closes the write side of that expiry gap going
|
|
21
|
+
* forward — a delegation can no longer be minted longer than its control, and reissuing a
|
|
22
|
+
* control shorter than an outstanding delegation now caps the reissue instead of leaving
|
|
23
|
+
* the delegation to outlive it — but two bounded residuals remain, both deliberately
|
|
24
|
+
* accepted there rather than fixed here: a control shortened before that task shipped
|
|
25
|
+
* (measured zero on staging, unmeasured in production), and that task's own fail-open
|
|
26
|
+
* when its registry-budget guard cannot run. A verifier package cannot special-case any
|
|
27
|
+
* one issuer's guarantees, so this residual is disclosed, not silently assumed away — see
|
|
28
|
+
* the package README's Failure reasons section.
|
|
29
|
+
*
|
|
30
|
+
* Both channels are authenticated: the status list is a signed JWT checked against the same
|
|
31
|
+
* key set as the delegation, and the uri inside the token is refused unless it belongs to the
|
|
32
|
+
* trusted issuer — it arrives INSIDE the artifact being verified, so treating it as an address
|
|
33
|
+
* to fetch would hand an attacker both the status authority and an SSRF primitive.
|
|
34
|
+
*/
|
|
35
|
+
import { base64UrlToBytes, isRecord, parseJwt } from './jwt.js';
|
|
36
|
+
import { getVerificationKeys, isSupportedAlgorithm, MAX_REMOTE_BODY_BYTES, verifySignature } from './jwks.js';
|
|
37
|
+
const STATUS_VALID = 0;
|
|
38
|
+
const STATUS_REVOKED = 1;
|
|
39
|
+
const STATUS_SUSPENDED = 2;
|
|
40
|
+
export function allowedStatusOrigins(issuer) {
|
|
41
|
+
const origins = new Set();
|
|
42
|
+
for (const candidate of [issuer.jwksUri, issuer.statusEndpoint, ...(issuer.statusListOrigins ?? [])]) {
|
|
43
|
+
if (!candidate)
|
|
44
|
+
continue;
|
|
45
|
+
try {
|
|
46
|
+
origins.add(new URL(candidate).origin);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// A malformed policy entry contributes no origin; the caller's own validation reports it.
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [...origins];
|
|
53
|
+
}
|
|
54
|
+
/** Inflate a zlib-wrapped DEFLATE payload without pulling in a compression dependency. */
|
|
55
|
+
async function inflate(bytes) {
|
|
56
|
+
const stream = new Blob([bytes]).stream().pipeThrough(new DecompressionStream('deflate'));
|
|
57
|
+
const buffer = await new Response(stream).arrayBuffer();
|
|
58
|
+
return new Uint8Array(buffer);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Read one 2-bit slot out of a decoded status list. Lowest index in the least significant
|
|
62
|
+
* bits, `bits` statuses per byte — the draft's byte order.
|
|
63
|
+
*/
|
|
64
|
+
export function readStatusSlot(bytes, index, bits) {
|
|
65
|
+
if (!Number.isInteger(index) || index < 0)
|
|
66
|
+
return null;
|
|
67
|
+
if (bits !== 1 && bits !== 2 && bits !== 4 && bits !== 8)
|
|
68
|
+
return null;
|
|
69
|
+
const perByte = 8 / bits;
|
|
70
|
+
const byteIndex = Math.floor(index / perByte);
|
|
71
|
+
if (byteIndex >= bytes.length)
|
|
72
|
+
return null;
|
|
73
|
+
const shift = (index % perByte) * bits;
|
|
74
|
+
const mask = (1 << bits) - 1;
|
|
75
|
+
return (bytes[byteIndex] >> shift) & mask;
|
|
76
|
+
}
|
|
77
|
+
export function readStatusListClaim(claims) {
|
|
78
|
+
const status = claims.status;
|
|
79
|
+
if (!isRecord(status))
|
|
80
|
+
return null;
|
|
81
|
+
const statusList = status.status_list;
|
|
82
|
+
if (!isRecord(statusList))
|
|
83
|
+
return null;
|
|
84
|
+
const { idx, uri } = statusList;
|
|
85
|
+
if (typeof idx !== 'number' || !Number.isInteger(idx) || idx < 0)
|
|
86
|
+
return null;
|
|
87
|
+
if (typeof uri !== 'string' || uri.length === 0)
|
|
88
|
+
return null;
|
|
89
|
+
return { idx, uri };
|
|
90
|
+
}
|
|
91
|
+
async function checkStatusList(params) {
|
|
92
|
+
const { claim, issuer } = params;
|
|
93
|
+
let uri;
|
|
94
|
+
try {
|
|
95
|
+
uri = new URL(claim.uri);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return { state: 'untrusted_uri', message: 'status list uri is not a valid URL' };
|
|
99
|
+
}
|
|
100
|
+
if (!allowedStatusOrigins(issuer).includes(uri.origin)) {
|
|
101
|
+
return {
|
|
102
|
+
state: 'untrusted_uri',
|
|
103
|
+
message: `status list uri origin ${uri.origin} is not an origin of trusted issuer "${issuer.issuer}"`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
let response;
|
|
107
|
+
try {
|
|
108
|
+
response = await params.fetch(uri.toString(), {
|
|
109
|
+
signal: AbortSignal.timeout(params.timeoutMs),
|
|
110
|
+
headers: { accept: 'application/statuslist+jwt, application/jwt, text/plain' },
|
|
111
|
+
// The origin guard above is only worth as much as the hop it guards: with the default
|
|
112
|
+
// `follow`, ANY open redirect on the issuer's origin would carry this fetch off it —
|
|
113
|
+
// the token controls the path — handing back both the status authority and an SSRF
|
|
114
|
+
// primitive against the verifying gateway's network.
|
|
115
|
+
redirect: 'error',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
return {
|
|
120
|
+
state: 'unavailable',
|
|
121
|
+
message: `status list fetch failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return { state: 'unavailable', message: `status list fetch failed with HTTP ${response.status}` };
|
|
126
|
+
}
|
|
127
|
+
const declaredLength = Number(response.headers.get('content-length'));
|
|
128
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_REMOTE_BODY_BYTES) {
|
|
129
|
+
return { state: 'unavailable', message: 'status list response is too large' };
|
|
130
|
+
}
|
|
131
|
+
const listToken = (await response.text()).trim();
|
|
132
|
+
if (listToken.length > MAX_REMOTE_BODY_BYTES) {
|
|
133
|
+
return { state: 'unavailable', message: 'status list response is too large' };
|
|
134
|
+
}
|
|
135
|
+
const parsed = parseJwt(listToken);
|
|
136
|
+
if (!parsed)
|
|
137
|
+
return { state: 'unavailable', message: 'status list is not a JWT' };
|
|
138
|
+
const alg = parsed.header.alg;
|
|
139
|
+
if (!isSupportedAlgorithm(alg)) {
|
|
140
|
+
return { state: 'unavailable', message: `status list uses unsupported algorithm ${String(alg)}` };
|
|
141
|
+
}
|
|
142
|
+
// The draft mandates this `typ`, and it is the one header that separates a status list from
|
|
143
|
+
// every other JWT the issuer signs with the SAME key — without it, any such token with a
|
|
144
|
+
// matching `sub` could stand in as a status authority.
|
|
145
|
+
if (parsed.header.typ !== 'statuslist+jwt') {
|
|
146
|
+
return {
|
|
147
|
+
state: 'unavailable',
|
|
148
|
+
message: `status list must carry typ "statuslist+jwt", got ${JSON.stringify(parsed.header.typ)}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
let keys;
|
|
152
|
+
try {
|
|
153
|
+
keys = await getVerificationKeys({
|
|
154
|
+
jwksUri: issuer.jwksUri,
|
|
155
|
+
kid: typeof parsed.header.kid === 'string' ? parsed.header.kid : undefined,
|
|
156
|
+
alg,
|
|
157
|
+
fetch: params.fetch,
|
|
158
|
+
timeoutMs: params.timeoutMs,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
state: 'unavailable',
|
|
164
|
+
message: `status list signing key unavailable: ${error instanceof Error ? error.message : String(error)}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (keys.length === 0) {
|
|
168
|
+
return { state: 'unavailable', message: 'status list is signed by an unknown key' };
|
|
169
|
+
}
|
|
170
|
+
let signatureOk = false;
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
if (await verifySignature(key, alg, parsed.signingInput, parsed.signature)) {
|
|
173
|
+
signatureOk = true;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!signatureOk) {
|
|
178
|
+
return { state: 'unavailable', message: 'status list signature is invalid' };
|
|
179
|
+
}
|
|
180
|
+
// The list must say it is the list we asked for, or a valid list from the same issuer could
|
|
181
|
+
// be replayed in place of another one.
|
|
182
|
+
if (parsed.payload.sub !== claim.uri) {
|
|
183
|
+
return { state: 'unavailable', message: 'status list `sub` does not match the uri it was fetched from' };
|
|
184
|
+
}
|
|
185
|
+
// A freshness channel with no expiry is not one: an `exp`-less list would be honoured
|
|
186
|
+
// forever, so its absence is treated as "cannot answer" rather than fail-open.
|
|
187
|
+
const nowSeconds = Math.floor(params.now.getTime() / 1000);
|
|
188
|
+
const exp = parsed.payload.exp;
|
|
189
|
+
if (typeof exp !== 'number') {
|
|
190
|
+
return { state: 'unavailable', message: 'status list carries no exp' };
|
|
191
|
+
}
|
|
192
|
+
if (nowSeconds > exp + params.clockToleranceSeconds) {
|
|
193
|
+
return { state: 'unavailable', message: 'status list has expired' };
|
|
194
|
+
}
|
|
195
|
+
const statusList = parsed.payload.status_list;
|
|
196
|
+
if (!isRecord(statusList) || typeof statusList.lst !== 'string') {
|
|
197
|
+
return { state: 'unavailable', message: 'status list payload has no `lst`' };
|
|
198
|
+
}
|
|
199
|
+
const bits = typeof statusList.bits === 'number' ? statusList.bits : 2;
|
|
200
|
+
let decoded;
|
|
201
|
+
try {
|
|
202
|
+
decoded = await inflate(base64UrlToBytes(statusList.lst));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return {
|
|
206
|
+
state: 'unavailable',
|
|
207
|
+
message: `status list payload could not be decompressed: ${error instanceof Error ? error.message : String(error)}`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
const slot = readStatusSlot(decoded, claim.idx, bits);
|
|
211
|
+
if (slot === null) {
|
|
212
|
+
// A slot beyond the list is not "valid" — the issuer has no answer for this token.
|
|
213
|
+
return { state: 'unavailable', message: `status list has no slot ${claim.idx}` };
|
|
214
|
+
}
|
|
215
|
+
if (slot === STATUS_REVOKED)
|
|
216
|
+
return { state: 'revoked', message: 'the issuer status list marks this delegation revoked' };
|
|
217
|
+
if (slot === STATUS_SUSPENDED) {
|
|
218
|
+
return { state: 'suspended', message: 'the issuer status list marks this delegation suspended' };
|
|
219
|
+
}
|
|
220
|
+
if (slot !== STATUS_VALID) {
|
|
221
|
+
return { state: 'unavailable', message: `status list slot carries unknown status ${slot}` };
|
|
222
|
+
}
|
|
223
|
+
return { state: 'valid' };
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Ask the issuer's status endpoint about this exact token. Contract:
|
|
227
|
+
* `POST` `{"proof_token": "<jwt>"}` → JSON with at least `{"valid": boolean}` and optionally
|
|
228
|
+
* `{"reason": "revoked" | "suspended" | "expired" | ...}`.
|
|
229
|
+
*/
|
|
230
|
+
async function checkStatusEndpoint(params) {
|
|
231
|
+
const endpoint = params.issuer.statusEndpoint;
|
|
232
|
+
if (!endpoint) {
|
|
233
|
+
return {
|
|
234
|
+
state: 'unavailable',
|
|
235
|
+
message: `trusted issuer "${params.issuer.issuer}" declares no statusEndpoint, so the control-proof cascade cannot be checked`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
let response;
|
|
239
|
+
try {
|
|
240
|
+
response = await params.fetch(endpoint, {
|
|
241
|
+
method: 'POST',
|
|
242
|
+
signal: AbortSignal.timeout(params.timeoutMs),
|
|
243
|
+
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
|
244
|
+
body: JSON.stringify({ proof_token: params.token }),
|
|
245
|
+
redirect: 'error',
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
return {
|
|
250
|
+
state: 'unavailable',
|
|
251
|
+
message: `status endpoint request failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
return { state: 'unavailable', message: `status endpoint answered HTTP ${response.status}` };
|
|
256
|
+
}
|
|
257
|
+
let body;
|
|
258
|
+
try {
|
|
259
|
+
body = await response.json();
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return { state: 'unavailable', message: 'status endpoint answered with a non-JSON body' };
|
|
263
|
+
}
|
|
264
|
+
if (!isRecord(body) || typeof body.valid !== 'boolean') {
|
|
265
|
+
return { state: 'unavailable', message: 'status endpoint answer carries no `valid` boolean' };
|
|
266
|
+
}
|
|
267
|
+
if (body.valid)
|
|
268
|
+
return { state: 'valid' };
|
|
269
|
+
// `valid: false` is not automatically "withdrawn". An issuer answers it for things that mean
|
|
270
|
+
// "I could not check" too — proof.holdings itself answers `status_unavailable` on a registry
|
|
271
|
+
// read failure. Reporting those as `revoked` would make a database blip at the issuer look
|
|
272
|
+
// like a permanent withdrawal to every gateway that asked, so only reasons that actually
|
|
273
|
+
// name a withdrawal are treated as one; anything unrecognised fails closed as unavailable.
|
|
274
|
+
const reason = typeof body.reason === 'string' ? body.reason : '';
|
|
275
|
+
switch (reason) {
|
|
276
|
+
case 'revoked':
|
|
277
|
+
return { state: 'revoked', message: 'the issuer reports this delegation revoked' };
|
|
278
|
+
case 'suspended':
|
|
279
|
+
return { state: 'suspended', message: 'the issuer reports this delegation suspended' };
|
|
280
|
+
case 'expired':
|
|
281
|
+
return { state: 'expired', message: 'the issuer reports this delegation expired' };
|
|
282
|
+
case 'unknown_delegation':
|
|
283
|
+
return { state: 'unknown', message: 'the issuer has no record of this delegation' };
|
|
284
|
+
default:
|
|
285
|
+
return {
|
|
286
|
+
state: 'unavailable',
|
|
287
|
+
message: `the issuer answered invalid without a recognised reason (${reason || 'none'})`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Combined status. A negative answer from EITHER channel is decisive; `unavailable` from
|
|
293
|
+
* either one is reported so the caller can fail closed.
|
|
294
|
+
*/
|
|
295
|
+
export async function checkDelegationStatus(params) {
|
|
296
|
+
const claim = readStatusListClaim(params.claims);
|
|
297
|
+
if (claim) {
|
|
298
|
+
const listOutcome = await checkStatusList({
|
|
299
|
+
claim,
|
|
300
|
+
issuer: params.issuer,
|
|
301
|
+
fetch: params.fetch,
|
|
302
|
+
timeoutMs: params.timeoutMs,
|
|
303
|
+
now: params.now,
|
|
304
|
+
clockToleranceSeconds: params.clockToleranceSeconds,
|
|
305
|
+
});
|
|
306
|
+
// Authoritative for `valid`/`revoked`/`suspended` — the fold (h-status-list-cascade-fold)
|
|
307
|
+
// makes those three the issuer's whole answer for this delegation's own bit.
|
|
308
|
+
// `unavailable`/`untrusted_uri` mean "could not read the list", not an answer from it, and
|
|
309
|
+
// still fall through to the endpoint below.
|
|
310
|
+
if (listOutcome.state !== 'unavailable' && listOutcome.state !== 'untrusted_uri') {
|
|
311
|
+
return listOutcome;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return checkStatusEndpoint({
|
|
315
|
+
token: params.token,
|
|
316
|
+
issuer: params.issuer,
|
|
317
|
+
fetch: params.fetch,
|
|
318
|
+
timeoutMs: params.timeoutMs,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"status.js","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public types of the Proof of Delegation reference verifier.
|
|
3
|
+
*
|
|
4
|
+
* The library verifies a delegation token — an attestation that the domain in `principal`
|
|
5
|
+
* authorized the artifact in `delegate` for a set of capability scopes. It attests NOTHING
|
|
6
|
+
* about the artifact's quality, safety or the business behind it, and it is not a statement
|
|
7
|
+
* that anyone "verified" the server.
|
|
8
|
+
*
|
|
9
|
+
* Two design rules separate this from an SDK, and both are load-bearing:
|
|
10
|
+
*
|
|
11
|
+
* 1. There is NO default trust list. `trustedIssuers` is supplied by the caller on every
|
|
12
|
+
* call — a verifier that trusts an issuer because the library shipped with its name
|
|
13
|
+
* hard-coded is a client for that issuer, not a verifier.
|
|
14
|
+
* 2. `delegate` is REQUIRED. The token is a bearer artifact: anyone can copy a genuine
|
|
15
|
+
* token into their own package. The comparison that defeats that is against the artifact
|
|
16
|
+
* identity the CALLER resolved independently — never against what the card says about
|
|
17
|
+
* itself.
|
|
18
|
+
*/
|
|
19
|
+
/** A typed artifact identifier, canonicalized before comparison. */
|
|
20
|
+
export type DelegateType = 'url' | 'purl';
|
|
21
|
+
export interface DelegateInput {
|
|
22
|
+
type: DelegateType;
|
|
23
|
+
/** e.g. `pkg:npm/postmark-mcp` or `https://api.example.com/mcp` */
|
|
24
|
+
value: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* One entry of the caller's trust policy.
|
|
28
|
+
*
|
|
29
|
+
* `statusEndpoint` is what lets the verifier see a revocation that a status list cannot show.
|
|
30
|
+
* An issuer's status list carries one bit per proof, so it reports the delegation's OWN
|
|
31
|
+
* revocation — but a delegation is only worth the domain control proof underneath it, and
|
|
32
|
+
* that cascade is resolved by the issuer, not encoded in the delegation's own bit. The
|
|
33
|
+
* endpoint contract is deliberately minimal: `POST` with `{"proof_token": "<jwt>"}` and a
|
|
34
|
+
* JSON answer carrying at least `{"valid": boolean}` (optionally `reason`).
|
|
35
|
+
*/
|
|
36
|
+
export interface TrustedIssuer {
|
|
37
|
+
/** Exact value the token's `iss` claim must equal. */
|
|
38
|
+
issuer: string;
|
|
39
|
+
/** Absolute https URL of the issuer's JWKS. */
|
|
40
|
+
jwksUri: string;
|
|
41
|
+
/** Absolute https URL of the issuer's public token-status endpoint. */
|
|
42
|
+
statusEndpoint?: string;
|
|
43
|
+
/**
|
|
44
|
+
* Extra origins this issuer may serve its status list from. The origins of `jwksUri` and
|
|
45
|
+
* `statusEndpoint` are always allowed; anything else must be listed here or the status-list
|
|
46
|
+
* uri inside the token is refused WITHOUT being fetched.
|
|
47
|
+
*/
|
|
48
|
+
statusListOrigins?: string[];
|
|
49
|
+
}
|
|
50
|
+
export type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
|
|
51
|
+
export interface VerifyOptions {
|
|
52
|
+
/** REQUIRED, no default. An empty list fails closed with `no_trust_policy`. */
|
|
53
|
+
trustedIssuers: TrustedIssuer[];
|
|
54
|
+
/** REQUIRED. The artifact identity the caller resolved independently. */
|
|
55
|
+
delegate: DelegateInput;
|
|
56
|
+
/**
|
|
57
|
+
* The domain you expect to have authorized this artifact. Optional in the type, but omitting
|
|
58
|
+
* it downgrades the answer — see `VerificationSuccess.principalChecked`. Compared after
|
|
59
|
+
* trimming and lowercasing.
|
|
60
|
+
*/
|
|
61
|
+
expectedPrincipal?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Scopes the delegation must grant. Compared as EXACT strings against the token's own
|
|
64
|
+
* `scope` (trimmed only): scope grammar belongs to the issuer, and imposing one issuer's
|
|
65
|
+
* rules here would make another issuer's grants unaskable.
|
|
66
|
+
*/
|
|
67
|
+
requiredScopes?: string[];
|
|
68
|
+
/**
|
|
69
|
+
* `required` (default) — the delegation's status list is checked, and the issuer's status
|
|
70
|
+
* endpoint is asked too whenever the list cannot answer on its own (see `status.ts` for
|
|
71
|
+
* exactly when); anything unreachable fails closed with `status_unavailable`. There is no
|
|
72
|
+
* option to tolerate a transient `status_unavailable` — a caller wanting a grace window
|
|
73
|
+
* around a flaky issuer must implement that itself (e.g. retry, or branch on `outcome:
|
|
74
|
+
* 'unconfirmed'`); this library does not offer a built-in one.
|
|
75
|
+
* `skip` — signature and claims only; the result carries `statusChecked: false`, which a
|
|
76
|
+
* caller must treat as "revocation not checked", never as "not revoked".
|
|
77
|
+
*/
|
|
78
|
+
statusCheck?: 'required' | 'skip';
|
|
79
|
+
/** Seconds of leeway on `exp`/`iat`. Default 5. */
|
|
80
|
+
clockToleranceSeconds?: number;
|
|
81
|
+
/** Verification time; defaults to now. */
|
|
82
|
+
now?: Date;
|
|
83
|
+
/** Injected fetch (tests, proxies, non-Node runtimes). Defaults to global fetch. */
|
|
84
|
+
fetch?: FetchLike;
|
|
85
|
+
/** Per-request network timeout in ms. Default 5000. */
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
export type FailureReason =
|
|
89
|
+
/** The call itself is wrong (missing `delegate`, malformed trust entry). */
|
|
90
|
+
'invalid_options'
|
|
91
|
+
/** `trustedIssuers` was empty — the library has no default trust. */
|
|
92
|
+
| 'no_trust_policy' | 'malformed_token' | 'untrusted_issuer' | 'unsupported_algorithm' | 'unknown_key' | 'bad_signature' | 'jwks_unavailable' | 'not_a_delegation' | 'unsupported_schema' | 'expired' | 'not_yet_valid' | 'malformed_claims' | 'principal_mismatch'
|
|
93
|
+
/** The token attests some OTHER artifact than the one the caller resolved. */
|
|
94
|
+
| 'delegate_mismatch' | 'scope_not_granted'
|
|
95
|
+
/** The status-list uri in the token does not belong to the trusted issuer. */
|
|
96
|
+
| 'status_uri_untrusted' | 'status_unavailable' | 'revoked' | 'suspended'
|
|
97
|
+
/** The issuer has no record of this delegation — distinct from "it could not answer". */
|
|
98
|
+
| 'unknown_delegation'
|
|
99
|
+
/** Card carries no delegation publication. */
|
|
100
|
+
| 'no_publication'
|
|
101
|
+
/** Card carries several publications — ambiguous, refused rather than guessed. */
|
|
102
|
+
| 'multiple_publications';
|
|
103
|
+
export type FailureOutcome = 'invalid' | 'unconfirmed';
|
|
104
|
+
/**
|
|
105
|
+
* Whether a `FailureReason` means "checked, and it's bad" (`invalid`) or "could not check"
|
|
106
|
+
* (`unconfirmed`). The three status-channel reasons that mean the revocation check itself
|
|
107
|
+
* failed are `unconfirmed`; every other reason is a completed check with a negative verdict,
|
|
108
|
+
* including a call-shape error — `verifyDelegation` always receives a well-formed
|
|
109
|
+
* `VerifyOptions` in practice, so those are `invalid` for exhaustiveness, not because they
|
|
110
|
+
* occur.
|
|
111
|
+
*/
|
|
112
|
+
export declare function classifyFailureReason(reason: FailureReason): FailureOutcome;
|
|
113
|
+
export interface VerifiedDelegation {
|
|
114
|
+
issuer: string;
|
|
115
|
+
/** The domain that authorized the artifact. */
|
|
116
|
+
principal: string;
|
|
117
|
+
/** Canonical artifact identifier, byte-equal to what the caller resolved. */
|
|
118
|
+
delegate: string;
|
|
119
|
+
scope: string[];
|
|
120
|
+
/** Public handle of the domain control proof underneath this delegation. */
|
|
121
|
+
controlProof: string;
|
|
122
|
+
/** Public handle of the delegation itself (`ph_dlg_*` for proof.holdings). */
|
|
123
|
+
delegationId: string;
|
|
124
|
+
issuedAt: Date;
|
|
125
|
+
expiresAt: Date;
|
|
126
|
+
}
|
|
127
|
+
export interface VerificationSuccess {
|
|
128
|
+
valid: true;
|
|
129
|
+
/** False only when `statusCheck: 'skip'` was requested. */
|
|
130
|
+
statusChecked: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* False when no `expectedPrincipal` was supplied — and then `valid: true` means LESS than it
|
|
133
|
+
* looks like. An issuer binds `delegate` to nothing: the domain in `principal` is whatever
|
|
134
|
+
* the requester proved control of, and the artifact is free-text they chose. So anyone who
|
|
135
|
+
* can verify `evil.example` can mint a genuine, signature-valid delegation naming SOMEONE
|
|
136
|
+
* ELSE's package. The required `delegate` argument defeats a COPIED token; only pinning
|
|
137
|
+
* `principal` defeats a token minted by the wrong party.
|
|
138
|
+
*
|
|
139
|
+
* A caller that already knows whose artifact this is (an agent installing `postmark-mcp`
|
|
140
|
+
* knows it expects `postmarkapp.com`) must pass `expectedPrincipal`. A caller that is
|
|
141
|
+
* discovering who claims an artifact may legitimately leave it off — and must then present
|
|
142
|
+
* the answer as "X says it authorized this", never as "verified".
|
|
143
|
+
*/
|
|
144
|
+
principalChecked: boolean;
|
|
145
|
+
delegation: VerifiedDelegation;
|
|
146
|
+
}
|
|
147
|
+
export interface VerificationFailure {
|
|
148
|
+
valid: false;
|
|
149
|
+
reason: FailureReason;
|
|
150
|
+
/** `classifyFailureReason(reason)` — set for every failure, never left for the caller to derive. */
|
|
151
|
+
outcome: FailureOutcome;
|
|
152
|
+
message: string;
|
|
153
|
+
}
|
|
154
|
+
export type VerificationResult = VerificationSuccess | VerificationFailure;
|
|
155
|
+
//# sourceMappingURL=types.d.ts.map
|