@zadq/x402 0.2.0 → 0.3.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 +24 -2
- package/dist/badge.d.ts +51 -0
- package/dist/badge.js +103 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +46 -0
- package/dist/explorer.d.ts +17 -4
- package/dist/explorer.js +22 -10
- package/dist/extension.d.ts +20 -6
- package/dist/extension.js +55 -10
- package/dist/header.d.ts +15 -5
- package/dist/header.js +29 -16
- package/dist/holder/chain.d.ts +6 -1
- package/dist/holder/chain.js +24 -8
- package/dist/holder/cli.d.ts +2 -0
- package/dist/holder/cli.js +25 -4
- package/dist/holder/holder.d.ts +8 -0
- package/dist/holder/holder.js +81 -15
- package/dist/holder/index.d.ts +6 -6
- package/dist/holder/index.js +5 -5
- package/dist/holder/killswitch.d.ts +128 -45
- package/dist/holder/killswitch.js +236 -56
- package/dist/holder/process.d.ts +9 -0
- package/dist/holder/process.js +9 -0
- package/dist/holder/types.d.ts +52 -15
- package/dist/index.d.ts +6 -4
- package/dist/index.js +6 -4
- package/dist/keys.d.ts +22 -1
- package/dist/keys.js +25 -7
- package/dist/receipt.d.ts +8 -3
- package/dist/receipt.js +11 -5
- package/dist/verdict.d.ts +53 -8
- package/dist/verdict.js +121 -17
- package/dist/webhook.d.ts +4 -2
- package/dist/webhook.js +6 -12
- package/package.json +18 -2
package/README.md
CHANGED
|
@@ -30,8 +30,30 @@ When the service serves its own statement about the seller's guarantee deposit
|
|
|
30
30
|
beside the verdict, `readDepositDeclaration(verdict)` returns it — a state and
|
|
31
31
|
a level band, never an amount — and `null` otherwise.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
```ts
|
|
34
|
+
import { embedVerificationBadge } from '@zadq/x402/badge';
|
|
35
|
+
|
|
36
|
+
// Always renders: `human-backed · ≥ G`, `revoked` or `unknown`; never rejects.
|
|
37
|
+
await embedVerificationBadge(document.getElementById('badge')!, {
|
|
38
|
+
baseUrl: 'https://api.zadq.net',
|
|
39
|
+
subject: { chain: 'base', payTo: '0x…', resourceUrl: 'https://…' },
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A fetched verdict is structurally checked and must be about the subject asked;
|
|
44
|
+
anything else is no verdict, which every helper shows as `unknown`. The
|
|
45
|
+
signature check never throws (`valid`, `invalid` or `unverifiable`); keys are
|
|
46
|
+
selected by `keyId` from the published set, so a verdict signed before a key
|
|
47
|
+
rotation keeps verifying. An inline verdict on a 402 is projected to the
|
|
48
|
+
canonical v0 fields, so the emitted `identityPolicy` always fits MintID's closed
|
|
49
|
+
schema.
|
|
50
|
+
|
|
51
|
+
Subpath exports: `verdict`, `extension` (also as `payment-required`), `header`
|
|
52
|
+
(also as `x-payment`), `receipt`, `badge`, `explorer`, `keys`, `webhook`, `client`,
|
|
53
|
+
`holder` (Node only) — or everything from the root. The byte forms of the
|
|
54
|
+
signed verdict, the `X-PAYMENT-IDENTITY` envelope and the issuer receipt are
|
|
55
|
+
pinned by vectors in the test suite; a change to any of them is a major
|
|
56
|
+
version. The
|
|
35
57
|
contracts each module honours are documented at
|
|
36
58
|
<https://zadq.info/docs/sdk>; the modules are also available as copy-in files
|
|
37
59
|
in the repository's `snippets/` folder for integrators who prefer to vendor.
|
package/dist/badge.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Verdict, type VerdictSubject } from './verdict.js';
|
|
2
|
+
/** What the badge shows. Everything uncertain is "unknown". */
|
|
3
|
+
export type BadgeState = 'human-backed' | 'revoked' | 'unknown';
|
|
4
|
+
/** The tooltip when no verification signal is available. */
|
|
5
|
+
export declare const NO_SIGNAL_TITLE = "No verification signal available \u2014 nothing in this flow depends on it.";
|
|
6
|
+
/**
|
|
7
|
+
* The state a verdict maps to: `human_backed` → `human-backed`, `revoked` →
|
|
8
|
+
* `revoked`; `stale`, `no_signal`, no verdict and anything that is not a
|
|
9
|
+
* structurally valid verdict → `unknown`. Pure; never throws.
|
|
10
|
+
*/
|
|
11
|
+
export declare function badgeState(verdict: Verdict | undefined | null): BadgeState;
|
|
12
|
+
/**
|
|
13
|
+
* The badge's text: `human-backed · ≥ <assuranceLevel>` (U+00B7, U+2265,
|
|
14
|
+
* single spaces) for the positive claim with a grade; otherwise the state
|
|
15
|
+
* itself (`human-backed`, `revoked`, `unknown`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function badgeLabel(state: BadgeState, verdict?: Verdict | null): string;
|
|
18
|
+
/**
|
|
19
|
+
* The badge's tooltip: `Verified at <verifiedAt> · re-checked every <N> h`
|
|
20
|
+
* (the time as transported, and the cadence promise the verdict carries),
|
|
21
|
+
* plus ` · re-check overdue` when the verdict is older than its promise. With
|
|
22
|
+
* no verdict, or no `verifiedAt`, the no-signal sentence (`NO_SIGNAL_TITLE`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function badgeTitle(verdict: Verdict | undefined | null, now?: Date): string;
|
|
25
|
+
export type BadgeOptions = {
|
|
26
|
+
/** Base URL of the verification service (its free path is used). */
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
/** The endpoint the badge speaks about. */
|
|
29
|
+
subject: VerdictSubject;
|
|
30
|
+
/**
|
|
31
|
+
* Optional verification public key (base64, 32 raw bytes). When given, a
|
|
32
|
+
* positive claim whose signature is `invalid` renders "unknown"; one the
|
|
33
|
+
* runtime cannot check (`unverifiable`) is shown as delivered.
|
|
34
|
+
*/
|
|
35
|
+
publicKey?: string;
|
|
36
|
+
/** Fetch timeout in milliseconds (default 3000). */
|
|
37
|
+
timeoutMs?: number;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Fetches the endpoint's verdict and renders the badge into `container`: one
|
|
41
|
+
* `<span>` with the label as its text, the tooltip as its `title`,
|
|
42
|
+
* `data-verification-badge="<state>"` and an inline pill style, replacing the
|
|
43
|
+
* container's children. Resolves to the state shown. Never rejects: whatever
|
|
44
|
+
* the service answers — or if it does not — a badge is rendered.
|
|
45
|
+
*/
|
|
46
|
+
export declare function embedVerificationBadge(container: Element, options: BadgeOptions): Promise<BadgeState>;
|
|
47
|
+
/**
|
|
48
|
+
* Renders a badge for a verdict the caller already holds (inline in a 402,
|
|
49
|
+
* from a webhook, from the explorer client). Same element as the embed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function renderBadge(container: Element, state: BadgeState, verdict?: Verdict | null): void;
|
package/dist/badge.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Badge snippet — the verification badge an explorer, a listing or a seller's
|
|
2
|
+
// own page shows next to an x402 endpoint.
|
|
3
|
+
//
|
|
4
|
+
// Vendor this file with verdict.ts. Zero runtime dependencies; the DOM embed
|
|
5
|
+
// uses only `ownerDocument.createElement` and `replaceChildren`.
|
|
6
|
+
//
|
|
7
|
+
// Graceful degradation, by design: the badge always renders. No verdict, a
|
|
8
|
+
// malformed answer, an unreachable service or a positive claim whose
|
|
9
|
+
// signature does not check all render the "unknown" badge, and the rest of
|
|
10
|
+
// the page is unaffected — the embed never rejects.
|
|
11
|
+
import { BADGE_CLIENT } from "./client.js";
|
|
12
|
+
import { fetchVerdict, freshness, isVerdict, verifyVerdictSignature, } from "./verdict.js";
|
|
13
|
+
/** The tooltip when no verification signal is available. */
|
|
14
|
+
export const NO_SIGNAL_TITLE = 'No verification signal available — nothing in this flow depends on it.';
|
|
15
|
+
/**
|
|
16
|
+
* The state a verdict maps to: `human_backed` → `human-backed`, `revoked` →
|
|
17
|
+
* `revoked`; `stale`, `no_signal`, no verdict and anything that is not a
|
|
18
|
+
* structurally valid verdict → `unknown`. Pure; never throws.
|
|
19
|
+
*/
|
|
20
|
+
export function badgeState(verdict) {
|
|
21
|
+
if (!isVerdict(verdict))
|
|
22
|
+
return 'unknown';
|
|
23
|
+
if (verdict.outcome === 'human_backed')
|
|
24
|
+
return 'human-backed';
|
|
25
|
+
if (verdict.outcome === 'revoked')
|
|
26
|
+
return 'revoked';
|
|
27
|
+
return 'unknown';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The badge's text: `human-backed · ≥ <assuranceLevel>` (U+00B7, U+2265,
|
|
31
|
+
* single spaces) for the positive claim with a grade; otherwise the state
|
|
32
|
+
* itself (`human-backed`, `revoked`, `unknown`).
|
|
33
|
+
*/
|
|
34
|
+
export function badgeLabel(state, verdict) {
|
|
35
|
+
const grade = isVerdict(verdict) ? verdict.assuranceLevel : undefined;
|
|
36
|
+
if (state === 'human-backed' && grade !== undefined) {
|
|
37
|
+
return `human-backed · ≥ ${grade}`;
|
|
38
|
+
}
|
|
39
|
+
return state;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The badge's tooltip: `Verified at <verifiedAt> · re-checked every <N> h`
|
|
43
|
+
* (the time as transported, and the cadence promise the verdict carries),
|
|
44
|
+
* plus ` · re-check overdue` when the verdict is older than its promise. With
|
|
45
|
+
* no verdict, or no `verifiedAt`, the no-signal sentence (`NO_SIGNAL_TITLE`).
|
|
46
|
+
*/
|
|
47
|
+
export function badgeTitle(verdict, now = new Date()) {
|
|
48
|
+
if (!isVerdict(verdict) || verdict.verifiedAt === undefined)
|
|
49
|
+
return NO_SIGNAL_TITLE;
|
|
50
|
+
const base = `Verified at ${verdict.verifiedAt} · re-checked every ${verdict.recheckEveryHours} h`;
|
|
51
|
+
return freshness(verdict, now) === 'overdue' ? `${base} · re-check overdue` : base;
|
|
52
|
+
}
|
|
53
|
+
const COLOURS = {
|
|
54
|
+
'human-backed': '#1a7f37',
|
|
55
|
+
revoked: '#b42318',
|
|
56
|
+
unknown: '#6b7280',
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Fetches the endpoint's verdict and renders the badge into `container`: one
|
|
60
|
+
* `<span>` with the label as its text, the tooltip as its `title`,
|
|
61
|
+
* `data-verification-badge="<state>"` and an inline pill style, replacing the
|
|
62
|
+
* container's children. Resolves to the state shown. Never rejects: whatever
|
|
63
|
+
* the service answers — or if it does not — a badge is rendered.
|
|
64
|
+
*/
|
|
65
|
+
export async function embedVerificationBadge(container, options) {
|
|
66
|
+
let shown;
|
|
67
|
+
try {
|
|
68
|
+
shown = await fetchVerdict(options.baseUrl, options.subject, {
|
|
69
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
70
|
+
client: BADGE_CLIENT,
|
|
71
|
+
});
|
|
72
|
+
if (shown?.outcome === 'human_backed' && options.publicKey !== undefined) {
|
|
73
|
+
if ((await verifyVerdictSignature(shown, options.publicKey)) === 'invalid') {
|
|
74
|
+
shown = undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
shown = undefined;
|
|
80
|
+
}
|
|
81
|
+
const state = badgeState(shown);
|
|
82
|
+
try {
|
|
83
|
+
renderBadge(container, state, shown);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// A container that cannot take children leaves the page as it was.
|
|
87
|
+
}
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Renders a badge for a verdict the caller already holds (inline in a 402,
|
|
92
|
+
* from a webhook, from the explorer client). Same element as the embed.
|
|
93
|
+
*/
|
|
94
|
+
export function renderBadge(container, state, verdict) {
|
|
95
|
+
const span = container.ownerDocument.createElement('span');
|
|
96
|
+
span.textContent = badgeLabel(state, verdict);
|
|
97
|
+
span.title = badgeTitle(verdict);
|
|
98
|
+
span.setAttribute('data-verification-badge', state);
|
|
99
|
+
span.setAttribute('style', 'display:inline-block;padding:0.125em 0.6em;border-radius:999px;' +
|
|
100
|
+
'font:500 0.75rem/1.5 system-ui,sans-serif;color:#fff;white-space:nowrap;' +
|
|
101
|
+
`background:${COLOURS[state]}`);
|
|
102
|
+
container.replaceChildren(span);
|
|
103
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** This library's version, as its package manifest states it (a test keeps them equal). */
|
|
2
|
+
export declare const SDK_VERSION = "0.3.0";
|
|
3
|
+
/** The service's own clients. */
|
|
4
|
+
export declare const CLIENT_CLASSES: readonly ["sdk", "mcp", "badge"];
|
|
5
|
+
export type ClientClass = (typeof CLIENT_CLASSES)[number];
|
|
6
|
+
/** Which client is calling: its class and its version (`major.minor.patch`). */
|
|
7
|
+
export type ClientDeclaration = {
|
|
8
|
+
client: ClientClass;
|
|
9
|
+
version: string;
|
|
10
|
+
};
|
|
11
|
+
/** The library's own declaration, the default for every helper that calls the service. */
|
|
12
|
+
export declare const SDK_CLIENT: ClientDeclaration;
|
|
13
|
+
/** The library's badge (`badge.ts`): its own class, the library's version. */
|
|
14
|
+
export declare const BADGE_CLIENT: ClientDeclaration;
|
|
15
|
+
/** The header that carries the declaration. */
|
|
16
|
+
export declare const CLIENT_HEADER = "user-agent";
|
|
17
|
+
/**
|
|
18
|
+
* `<brand>-<class>/<version>`, e.g. the SDK's `…-sdk/0.2.0`; `undefined`
|
|
19
|
+
* for a class outside the enumeration or a version that is not three
|
|
20
|
+
* dot-separated numbers — nothing else can be carried.
|
|
21
|
+
*/
|
|
22
|
+
export declare function clientUserAgent(declaration?: ClientDeclaration): string | undefined;
|
|
23
|
+
/** True where the runtime owns its User-Agent; false in a browser (window or worker). */
|
|
24
|
+
export declare function runtimeDeclaresClient(): boolean;
|
|
25
|
+
/** The headers a request to the service carries for the declaration: one, or none. */
|
|
26
|
+
export declare function clientHeaders(declaration?: ClientDeclaration): Record<string, string>;
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The client class: the service's own clients say which one they are, so
|
|
2
|
+
// the free path's aggregate counters can tell the SDK, the MCP server and
|
|
3
|
+
// the badge apart from any other caller. The declaration is a fixed
|
|
4
|
+
// User-Agent, `<brand>-<class>/<version>` — the client's name and version
|
|
5
|
+
// and nothing else: never an identifier of the integrator, the page, the
|
|
6
|
+
// agent or the person, and never anything a caller could fill in (the
|
|
7
|
+
// class is a closed enumeration and the version is digits and dots).
|
|
8
|
+
//
|
|
9
|
+
// Browsers are left alone: there a User-Agent set by script is ignored or,
|
|
10
|
+
// cross-origin, turns a simple request into a CORS preflight, so the
|
|
11
|
+
// declaration is sent only where the runtime owns its User-Agent (Node,
|
|
12
|
+
// Deno, Bun, edge workers). Nothing depends on it: a request without it is
|
|
13
|
+
// answered exactly the same, and every helper still degrades to "unknown".
|
|
14
|
+
import { BRAND } from "./brand.js";
|
|
15
|
+
/** This library's version, as its package manifest states it (a test keeps them equal). */
|
|
16
|
+
export const SDK_VERSION = '0.3.0';
|
|
17
|
+
/** The service's own clients. */
|
|
18
|
+
export const CLIENT_CLASSES = ['sdk', 'mcp', 'badge'];
|
|
19
|
+
/** The library's own declaration, the default for every helper that calls the service. */
|
|
20
|
+
export const SDK_CLIENT = { client: 'sdk', version: SDK_VERSION };
|
|
21
|
+
/** The library's badge (`badge.ts`): its own class, the library's version. */
|
|
22
|
+
export const BADGE_CLIENT = { client: 'badge', version: SDK_VERSION };
|
|
23
|
+
/** The header that carries the declaration. */
|
|
24
|
+
export const CLIENT_HEADER = 'user-agent';
|
|
25
|
+
/**
|
|
26
|
+
* `<brand>-<class>/<version>`, e.g. the SDK's `…-sdk/0.2.0`; `undefined`
|
|
27
|
+
* for a class outside the enumeration or a version that is not three
|
|
28
|
+
* dot-separated numbers — nothing else can be carried.
|
|
29
|
+
*/
|
|
30
|
+
export function clientUserAgent(declaration = SDK_CLIENT) {
|
|
31
|
+
if (!CLIENT_CLASSES.includes(declaration.client))
|
|
32
|
+
return undefined;
|
|
33
|
+
if (!/^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(declaration.version))
|
|
34
|
+
return undefined;
|
|
35
|
+
return `${BRAND.toLowerCase()}-${declaration.client}/${declaration.version}`;
|
|
36
|
+
}
|
|
37
|
+
/** True where the runtime owns its User-Agent; false in a browser (window or worker). */
|
|
38
|
+
export function runtimeDeclaresClient() {
|
|
39
|
+
const ua = globalThis.navigator?.userAgent;
|
|
40
|
+
return !(typeof ua === 'string' && ua.startsWith('Mozilla/'));
|
|
41
|
+
}
|
|
42
|
+
/** The headers a request to the service carries for the declaration: one, or none. */
|
|
43
|
+
export function clientHeaders(declaration = SDK_CLIENT) {
|
|
44
|
+
const ua = clientUserAgent(declaration);
|
|
45
|
+
return ua !== undefined && runtimeDeclaresClient() ? { [CLIENT_HEADER]: ua } : {};
|
|
46
|
+
}
|
package/dist/explorer.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ClientDeclaration } from './client.js';
|
|
2
|
+
import { type VerificationKey } from './keys.js';
|
|
1
3
|
import { type Freshness, type Verdict, type VerdictSubject } from './verdict.js';
|
|
2
4
|
/** Display signal for one endpoint row. Everything uncertain is "unknown". */
|
|
3
5
|
export type VerificationSignal = {
|
|
@@ -23,10 +25,19 @@ export type VerificationClientOptions = {
|
|
|
23
25
|
* as delivered — the listing keeps working either way.
|
|
24
26
|
*/
|
|
25
27
|
publicKey?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Optional published key set (`fetchVerificationKeys`), used instead of
|
|
30
|
+
* `publicKey`: each positive claim is checked with the key its `keyId`
|
|
31
|
+
* names, `current` or `previous`. A positive claim naming no published key
|
|
32
|
+
* is demoted to "unknown".
|
|
33
|
+
*/
|
|
34
|
+
keys?: readonly VerificationKey[];
|
|
26
35
|
/** Fetch timeout in milliseconds (default 3000). */
|
|
27
36
|
timeoutMs?: number;
|
|
28
37
|
/** In-memory cache TTL in milliseconds (default 5 minutes; 0 disables). */
|
|
29
38
|
cacheTtlMs?: number;
|
|
39
|
+
/** Which of the service's clients is calling (default: this library). See client.ts. */
|
|
40
|
+
client?: ClientDeclaration;
|
|
30
41
|
};
|
|
31
42
|
export type VerificationClient = {
|
|
32
43
|
/** Resolves the display signal for one endpoint. Never rejects. */
|
|
@@ -40,8 +51,10 @@ export type VerificationClient = {
|
|
|
40
51
|
export declare function createVerificationClient(options: VerificationClientOptions): VerificationClient;
|
|
41
52
|
/**
|
|
42
53
|
* Maps a verdict (or its absence) to the display signal, optionally checking
|
|
43
|
-
* the signature of positive claims locally
|
|
44
|
-
*
|
|
45
|
-
*
|
|
54
|
+
* the signature of positive claims locally — against one public key (base64)
|
|
55
|
+
* or against a published key set, selected by the verdict's `keyId`. Exported
|
|
56
|
+
* for callers that already hold a verdict — e.g. one delivered inline in a
|
|
57
|
+
* `PaymentRequired` extension (extension.ts) or by a webhook. Anything that is
|
|
58
|
+
* not a structurally valid verdict maps to "unknown"; never rejects.
|
|
46
59
|
*/
|
|
47
|
-
export declare function toSignal(verdict: Verdict | undefined,
|
|
60
|
+
export declare function toSignal(verdict: Verdict | undefined | null, key?: string | readonly VerificationKey[]): Promise<VerificationSignal>;
|
package/dist/explorer.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// for. Graceful degradation, by design: a listing keeps rendering exactly as
|
|
6
6
|
// before when no verification response is available — the signal degrades to
|
|
7
7
|
// "unknown", it never becomes an error state, a spinner or a blocked row.
|
|
8
|
-
import {
|
|
8
|
+
import { keyFor } from "./keys.js";
|
|
9
|
+
import { canonicalPayTo, fetchVerdict, freshness, isVerdict, verifyVerdictSignature, } from "./verdict.js";
|
|
9
10
|
/** The signal rendered when no verdict is available. Rendering never blocks on it. */
|
|
10
11
|
export const UNKNOWN_SIGNAL = { state: 'unknown', freshness: 'unknown' };
|
|
11
12
|
/**
|
|
@@ -17,8 +18,11 @@ export function createVerificationClient(options) {
|
|
|
17
18
|
const ttl = options.cacheTtlMs ?? 300_000;
|
|
18
19
|
const cache = new Map();
|
|
19
20
|
async function resolve(subject) {
|
|
20
|
-
const verdict = await fetchVerdict(options.baseUrl, subject,
|
|
21
|
-
|
|
21
|
+
const verdict = await fetchVerdict(options.baseUrl, subject, {
|
|
22
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
23
|
+
...(options.client === undefined ? {} : { client: options.client }),
|
|
24
|
+
});
|
|
25
|
+
return toSignal(verdict, options.keys ?? options.publicKey);
|
|
22
26
|
}
|
|
23
27
|
return {
|
|
24
28
|
async signalFor(subject) {
|
|
@@ -43,16 +47,24 @@ export function createVerificationClient(options) {
|
|
|
43
47
|
}
|
|
44
48
|
/**
|
|
45
49
|
* Maps a verdict (or its absence) to the display signal, optionally checking
|
|
46
|
-
* the signature of positive claims locally
|
|
47
|
-
*
|
|
48
|
-
*
|
|
50
|
+
* the signature of positive claims locally — against one public key (base64)
|
|
51
|
+
* or against a published key set, selected by the verdict's `keyId`. Exported
|
|
52
|
+
* for callers that already hold a verdict — e.g. one delivered inline in a
|
|
53
|
+
* `PaymentRequired` extension (extension.ts) or by a webhook. Anything that is
|
|
54
|
+
* not a structurally valid verdict maps to "unknown"; never rejects.
|
|
49
55
|
*/
|
|
50
|
-
export async function toSignal(verdict,
|
|
51
|
-
if (verdict
|
|
56
|
+
export async function toSignal(verdict, key) {
|
|
57
|
+
if (!isVerdict(verdict)) {
|
|
52
58
|
return UNKNOWN_SIGNAL;
|
|
53
59
|
}
|
|
54
|
-
if (verdict.outcome === 'human_backed' &&
|
|
55
|
-
const
|
|
60
|
+
if (verdict.outcome === 'human_backed' && key !== undefined) {
|
|
61
|
+
const publicKey = typeof key === 'string' ? key : keyFor(verdict, key);
|
|
62
|
+
// A signed positive claim naming no published key cannot match the set.
|
|
63
|
+
const check = publicKey !== undefined
|
|
64
|
+
? await verifyVerdictSignature(verdict, publicKey)
|
|
65
|
+
: verdict.signature === undefined
|
|
66
|
+
? 'unverifiable'
|
|
67
|
+
: 'invalid';
|
|
56
68
|
if (check === 'invalid') {
|
|
57
69
|
// An unverified positive claim is not displayed; the row still renders.
|
|
58
70
|
return UNKNOWN_SIGNAL;
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Verdict } from './verdict.js';
|
|
2
2
|
/**
|
|
3
3
|
* The identity policy a seller advertises on its 402 response, following the
|
|
4
4
|
* bIP-0002 profile — sub-profile `bip-0002/0-seller`, the canonical v0 seller
|
|
@@ -59,17 +59,31 @@ export declare const IDENTITY_POLICY_EXTENSION = "identityPolicy";
|
|
|
59
59
|
export declare const PERIMETER_EXTENSION = "trustPerimeter";
|
|
60
60
|
/**
|
|
61
61
|
* Seller side: returns a copy of the 402 body with the identity policy
|
|
62
|
-
* attached under `extensions.identityPolicy`.
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
62
|
+
* attached under `extensions.identityPolicy`. The input is not mutated;
|
|
63
|
+
* existing extensions are preserved and `x402Version`, `error` and `accepts`
|
|
64
|
+
* are never touched. Only the canonical fields are emitted — the inline
|
|
65
|
+
* verdict projected by `canonicalInlineVerdict` — so the object validates
|
|
66
|
+
* against the closed v0 seller schema whatever else the caller's policy value
|
|
67
|
+
* carried, a live verdict's `extensions` included.
|
|
66
68
|
*/
|
|
67
69
|
export declare function withIdentityPolicy<T extends PaymentRequiredBody>(body: T, policy: IdentityPolicy): T;
|
|
70
|
+
/**
|
|
71
|
+
* The inline verdict reduced to the fields of the canonical v0 seller object:
|
|
72
|
+
* `outcome`, `reason` (never beside `human_backed`), `subject` {chain, payTo,
|
|
73
|
+
* resourceUrl}, `assuranceLevel`, `verifiedAt`, `recheckEveryHours`,
|
|
74
|
+
* `signature` and `keyId`. Everything else — the verifying party's own
|
|
75
|
+
* `extensions` beside the verdict included — is dropped, so a verdict taken
|
|
76
|
+
* straight from the free path keeps the closed schema valid; the signed bytes
|
|
77
|
+
* are all kept, so it verifies exactly as the fetched one does. Anything that
|
|
78
|
+
* is not a structurally valid verdict gives `undefined` (no inline verdict).
|
|
79
|
+
*/
|
|
80
|
+
export declare function canonicalInlineVerdict(verdict: unknown): Verdict | undefined;
|
|
68
81
|
/**
|
|
69
82
|
* Counterparty side: reads the identity policy from a 402 body, if present
|
|
70
83
|
* and well-formed. Returns `undefined` otherwise — an absent or malformed
|
|
71
84
|
* policy is a normal state, and the payment flow proceeds unchanged. Fields
|
|
72
|
-
* outside the canonical object are dropped, never fatal
|
|
85
|
+
* outside the canonical object are dropped, never fatal; an inline verdict
|
|
86
|
+
* that is not structurally valid is dropped and the policy kept.
|
|
73
87
|
*/
|
|
74
88
|
export declare function readIdentityPolicy(body: PaymentRequiredBody): IdentityPolicy | undefined;
|
|
75
89
|
/**
|
package/dist/extension.js
CHANGED
|
@@ -10,16 +10,19 @@
|
|
|
10
10
|
// and with no round-trip, or later via the free verification path. Absence of
|
|
11
11
|
// the extension, of a verdict, or of any verification response changes
|
|
12
12
|
// nothing in either party's flow.
|
|
13
|
+
import { isVerdict } from "./verdict.js";
|
|
13
14
|
/** Extension key defined by the bIP-0002 profile for the identity policy. */
|
|
14
15
|
export const IDENTITY_POLICY_EXTENSION = 'identityPolicy';
|
|
15
16
|
/** The vendor-owned extension key the trust-perimeter invitation rides under. */
|
|
16
17
|
export const PERIMETER_EXTENSION = 'trustPerimeter';
|
|
17
18
|
/**
|
|
18
19
|
* Seller side: returns a copy of the 402 body with the identity policy
|
|
19
|
-
* attached under `extensions.identityPolicy`.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
* attached under `extensions.identityPolicy`. The input is not mutated;
|
|
21
|
+
* existing extensions are preserved and `x402Version`, `error` and `accepts`
|
|
22
|
+
* are never touched. Only the canonical fields are emitted — the inline
|
|
23
|
+
* verdict projected by `canonicalInlineVerdict` — so the object validates
|
|
24
|
+
* against the closed v0 seller schema whatever else the caller's policy value
|
|
25
|
+
* carried, a live verdict's `extensions` included.
|
|
23
26
|
*/
|
|
24
27
|
export function withIdentityPolicy(body, policy) {
|
|
25
28
|
const canonical = {
|
|
@@ -27,20 +30,59 @@ export function withIdentityPolicy(body, policy) {
|
|
|
27
30
|
assuranceAtLeast: policy.assuranceAtLeast,
|
|
28
31
|
verifyUrl: policy.verifyUrl,
|
|
29
32
|
};
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
const verdict = canonicalInlineVerdict(policy.verdict);
|
|
34
|
+
if (verdict !== undefined)
|
|
35
|
+
canonical.verdict = verdict;
|
|
32
36
|
return {
|
|
33
37
|
...body,
|
|
34
38
|
extensions: { ...body.extensions, [IDENTITY_POLICY_EXTENSION]: canonical },
|
|
35
39
|
};
|
|
36
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The inline verdict reduced to the fields of the canonical v0 seller object:
|
|
43
|
+
* `outcome`, `reason` (never beside `human_backed`), `subject` {chain, payTo,
|
|
44
|
+
* resourceUrl}, `assuranceLevel`, `verifiedAt`, `recheckEveryHours`,
|
|
45
|
+
* `signature` and `keyId`. Everything else — the verifying party's own
|
|
46
|
+
* `extensions` beside the verdict included — is dropped, so a verdict taken
|
|
47
|
+
* straight from the free path keeps the closed schema valid; the signed bytes
|
|
48
|
+
* are all kept, so it verifies exactly as the fetched one does. Anything that
|
|
49
|
+
* is not a structurally valid verdict gives `undefined` (no inline verdict).
|
|
50
|
+
*/
|
|
51
|
+
export function canonicalInlineVerdict(verdict) {
|
|
52
|
+
if (!isVerdict(verdict))
|
|
53
|
+
return undefined;
|
|
54
|
+
const out = {
|
|
55
|
+
outcome: verdict.outcome,
|
|
56
|
+
subject: {
|
|
57
|
+
chain: verdict.subject.chain,
|
|
58
|
+
payTo: verdict.subject.payTo,
|
|
59
|
+
resourceUrl: verdict.subject.resourceUrl,
|
|
60
|
+
},
|
|
61
|
+
recheckEveryHours: verdict.recheckEveryHours,
|
|
62
|
+
};
|
|
63
|
+
if (verdict.reason !== undefined && verdict.outcome !== 'human_backed') {
|
|
64
|
+
out.reason = verdict.reason;
|
|
65
|
+
}
|
|
66
|
+
if (verdict.assuranceLevel !== undefined)
|
|
67
|
+
out.assuranceLevel = verdict.assuranceLevel;
|
|
68
|
+
if (verdict.verifiedAt !== undefined)
|
|
69
|
+
out.verifiedAt = verdict.verifiedAt;
|
|
70
|
+
if (verdict.signature !== undefined)
|
|
71
|
+
out.signature = verdict.signature;
|
|
72
|
+
if (verdict.keyId !== undefined)
|
|
73
|
+
out.keyId = verdict.keyId;
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
37
76
|
/**
|
|
38
77
|
* Counterparty side: reads the identity policy from a 402 body, if present
|
|
39
78
|
* and well-formed. Returns `undefined` otherwise — an absent or malformed
|
|
40
79
|
* policy is a normal state, and the payment flow proceeds unchanged. Fields
|
|
41
|
-
* outside the canonical object are dropped, never fatal
|
|
80
|
+
* outside the canonical object are dropped, never fatal; an inline verdict
|
|
81
|
+
* that is not structurally valid is dropped and the policy kept.
|
|
42
82
|
*/
|
|
43
83
|
export function readIdentityPolicy(body) {
|
|
84
|
+
if (typeof body !== 'object' || body === null)
|
|
85
|
+
return undefined;
|
|
44
86
|
const raw = body.extensions?.[IDENTITY_POLICY_EXTENSION];
|
|
45
87
|
if (typeof raw !== 'object' || raw === null) {
|
|
46
88
|
return undefined;
|
|
@@ -56,10 +98,11 @@ export function readIdentityPolicy(body) {
|
|
|
56
98
|
assuranceAtLeast: policy['assuranceAtLeast'],
|
|
57
99
|
verifyUrl: policy['verifyUrl'],
|
|
58
100
|
};
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
// An inline verdict that is not structurally valid is dropped, the policy
|
|
102
|
+
// kept; a valid one is read back in its canonical fields only.
|
|
103
|
+
const verdict = canonicalInlineVerdict(policy['verdict']);
|
|
104
|
+
if (verdict !== undefined)
|
|
61
105
|
read.verdict = verdict;
|
|
62
|
-
}
|
|
63
106
|
return read;
|
|
64
107
|
}
|
|
65
108
|
/**
|
|
@@ -83,6 +126,8 @@ export function withPerimeterInvitation(body, invitation) {
|
|
|
83
126
|
* exactly as before.
|
|
84
127
|
*/
|
|
85
128
|
export function readPerimeterInvitation(body) {
|
|
129
|
+
if (typeof body !== 'object' || body === null)
|
|
130
|
+
return undefined;
|
|
86
131
|
const raw = body.extensions?.[PERIMETER_EXTENSION];
|
|
87
132
|
if (typeof raw !== 'object' || raw === null)
|
|
88
133
|
return undefined;
|
package/dist/header.d.ts
CHANGED
|
@@ -17,14 +17,24 @@ export type IdentityProof = {
|
|
|
17
17
|
};
|
|
18
18
|
type HeaderRecord = Record<string, string>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* The exact header value for a proof: base64url without padding (RFC 4648
|
|
21
|
+
* §5) of the compact UTF-8 JSON `{"version":1,"format":…,"payload":…}`, keys
|
|
22
|
+
* in that order and nothing else. Any change to these bytes is a major
|
|
23
|
+
* version of the package.
|
|
24
|
+
*/
|
|
25
|
+
export declare function identityProofHeaderValue(proof: IdentityProof): string;
|
|
26
|
+
/**
|
|
27
|
+
* Sender side: attaches the identity proof next to `X-PAYMENT` (which is
|
|
28
|
+
* never read or modified). Mutates and returns the same headers
|
|
29
|
+
* object/record for chaining.
|
|
22
30
|
*/
|
|
23
31
|
export declare function attachIdentityProof<T extends Headers | HeaderRecord>(headers: T, proof: IdentityProof): T;
|
|
24
32
|
/**
|
|
25
|
-
* Receiver side: reads the identity proof, if present and well-formed
|
|
26
|
-
*
|
|
27
|
-
*
|
|
33
|
+
* Receiver side: reads the identity proof, if present and well-formed — the
|
|
34
|
+
* header name case-insensitively, the value with or without padding. Returns
|
|
35
|
+
* `undefined` otherwise — an absent or malformed proof is a normal state;
|
|
36
|
+
* handle the request exactly as you would without the header. Only the three
|
|
37
|
+
* envelope fields are returned.
|
|
28
38
|
*/
|
|
29
39
|
export declare function readIdentityProof(headers: Headers | HeaderRecord): IdentityProof | undefined;
|
|
30
40
|
export {};
|
package/dist/header.js
CHANGED
|
@@ -16,11 +16,21 @@ export const X_PAYMENT_HEADER = 'X-PAYMENT';
|
|
|
16
16
|
/** Companion header defined by the bIP-0002 profile for the identity proof. */
|
|
17
17
|
export const X_PAYMENT_IDENTITY_HEADER = 'X-PAYMENT-IDENTITY';
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* The exact header value for a proof: base64url without padding (RFC 4648
|
|
20
|
+
* §5) of the compact UTF-8 JSON `{"version":1,"format":…,"payload":…}`, keys
|
|
21
|
+
* in that order and nothing else. Any change to these bytes is a major
|
|
22
|
+
* version of the package.
|
|
23
|
+
*/
|
|
24
|
+
export function identityProofHeaderValue(proof) {
|
|
25
|
+
return base64UrlEncode(JSON.stringify({ version: proof.version, format: proof.format, payload: proof.payload }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sender side: attaches the identity proof next to `X-PAYMENT` (which is
|
|
29
|
+
* never read or modified). Mutates and returns the same headers
|
|
30
|
+
* object/record for chaining.
|
|
21
31
|
*/
|
|
22
32
|
export function attachIdentityProof(headers, proof) {
|
|
23
|
-
const value =
|
|
33
|
+
const value = identityProofHeaderValue(proof);
|
|
24
34
|
if (headers instanceof Headers) {
|
|
25
35
|
headers.set(X_PAYMENT_IDENTITY_HEADER, value);
|
|
26
36
|
}
|
|
@@ -30,25 +40,28 @@ export function attachIdentityProof(headers, proof) {
|
|
|
30
40
|
return headers;
|
|
31
41
|
}
|
|
32
42
|
/**
|
|
33
|
-
* Receiver side: reads the identity proof, if present and well-formed
|
|
34
|
-
*
|
|
35
|
-
*
|
|
43
|
+
* Receiver side: reads the identity proof, if present and well-formed — the
|
|
44
|
+
* header name case-insensitively, the value with or without padding. Returns
|
|
45
|
+
* `undefined` otherwise — an absent or malformed proof is a normal state;
|
|
46
|
+
* handle the request exactly as you would without the header. Only the three
|
|
47
|
+
* envelope fields are returned.
|
|
36
48
|
*/
|
|
37
49
|
export function readIdentityProof(headers) {
|
|
38
|
-
const value = headers instanceof Headers
|
|
39
|
-
? headers.get(X_PAYMENT_IDENTITY_HEADER)
|
|
40
|
-
: findHeader(headers, X_PAYMENT_IDENTITY_HEADER);
|
|
41
|
-
if (value === null || value === undefined || value === '') {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
50
|
try {
|
|
51
|
+
const value = headers instanceof Headers
|
|
52
|
+
? headers.get(X_PAYMENT_IDENTITY_HEADER)
|
|
53
|
+
: findHeader(headers, X_PAYMENT_IDENTITY_HEADER);
|
|
54
|
+
if (typeof value !== 'string' || value === '') {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
45
57
|
const parsed = JSON.parse(base64UrlDecode(value));
|
|
46
|
-
if (parsed
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
59
|
+
return undefined;
|
|
60
|
+
const p = parsed;
|
|
61
|
+
if (p['version'] !== 1 || typeof p['format'] !== 'string' || typeof p['payload'] !== 'string') {
|
|
49
62
|
return undefined;
|
|
50
63
|
}
|
|
51
|
-
return
|
|
64
|
+
return { version: 1, format: p['format'], payload: p['payload'] };
|
|
52
65
|
}
|
|
53
66
|
catch {
|
|
54
67
|
return undefined;
|
package/dist/holder/chain.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** The chain query path of the issuer-scoped read. */
|
|
2
2
|
export declare const RECORDED_NULLIFIERS_QUERY = "/identity.v1.selfrevoke.Query/RecordedNullifiers";
|
|
3
|
+
/** After how long the chain prunes a record (`retention_seconds`, both networks). */
|
|
4
|
+
export declare const RECORD_RETENTION_SECONDS = 3600;
|
|
3
5
|
/** A recorded self-revocation's state on the chain, by revocation value (hex). */
|
|
4
6
|
export type RecordedState = 'pending' | 'folded';
|
|
5
7
|
export type ChainReadOptions = {
|
|
@@ -12,7 +14,10 @@ export declare function bytesField(field: number, value: Uint8Array): Uint8Array
|
|
|
12
14
|
export declare function varintField(field: number, value: number): Uint8Array;
|
|
13
15
|
export declare function hexToBytes(hex: string): Uint8Array;
|
|
14
16
|
export declare function bytesToHex(bytes: Uint8Array): string;
|
|
15
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* `QueryRecordedNullifiersRequest{issuer_id = 1}` — `0a20‖issuer_id`; names
|
|
19
|
+
* no revocation value. The issuer id is exactly 32 bytes.
|
|
20
|
+
*/
|
|
16
21
|
export declare function encodeIssuerQuery(issuerIdHex: string): Uint8Array;
|
|
17
22
|
/**
|
|
18
23
|
* `QueryRecordedNullifiersResponse{repeated RecordedNullifier records = 1}`
|