@openmobilehub/attestomcp-gate 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 +201 -0
- package/README.md +172 -0
- package/dist/ceremony/cartMandate.d.ts +61 -0
- package/dist/ceremony/cartMandate.js +76 -0
- package/dist/ceremony/challengeToken.d.ts +5 -0
- package/dist/ceremony/challengeToken.js +43 -0
- package/dist/ceremony/checkout-page.d.ts +85 -0
- package/dist/ceremony/checkout-page.js +269 -0
- package/dist/ceremony/completion.d.ts +41 -0
- package/dist/ceremony/completion.js +90 -0
- package/dist/ceremony/credential-gate/dcql.d.ts +10 -0
- package/dist/ceremony/credential-gate/dcql.js +12 -0
- package/dist/ceremony/credential-gate/doc-spec.d.ts +3 -0
- package/dist/ceremony/credential-gate/doc-spec.js +16 -0
- package/dist/ceremony/credential-gate/mdoc-verify.d.ts +15 -0
- package/dist/ceremony/credential-gate/mdoc-verify.js +29 -0
- package/dist/ceremony/credential-gate/page.d.ts +20 -0
- package/dist/ceremony/credential-gate/page.js +136 -0
- package/dist/ceremony/credential-gate/request.d.ts +15 -0
- package/dist/ceremony/credential-gate/request.js +43 -0
- package/dist/ceremony/credential-gate/routes.d.ts +2 -0
- package/dist/ceremony/credential-gate/routes.js +200 -0
- package/dist/ceremony/credential-gate/verify.d.ts +51 -0
- package/dist/ceremony/credential-gate/verify.js +146 -0
- package/dist/ceremony/dc-payment/dcql.d.ts +5 -0
- package/dist/ceremony/dc-payment/dcql.js +23 -0
- package/dist/ceremony/dc-payment/page.d.ts +18 -0
- package/dist/ceremony/dc-payment/page.js +195 -0
- package/dist/ceremony/dc-payment/request.d.ts +17 -0
- package/dist/ceremony/dc-payment/request.js +50 -0
- package/dist/ceremony/dc-payment/routes.d.ts +2 -0
- package/dist/ceremony/dc-payment/routes.js +147 -0
- package/dist/ceremony/dc-payment/txData.d.ts +19 -0
- package/dist/ceremony/dc-payment/txData.js +34 -0
- package/dist/ceremony/dc-payment/verify.d.ts +108 -0
- package/dist/ceremony/dc-payment/verify.js +208 -0
- package/dist/ceremony/mandate.d.ts +71 -0
- package/dist/ceremony/mandate.js +116 -0
- package/dist/ceremony/mdoc/mdoc-iso.d.ts +44 -0
- package/dist/ceremony/mdoc/mdoc-iso.js +260 -0
- package/dist/ceremony/mdoc/mdoc.d.ts +17 -0
- package/dist/ceremony/mdoc/mdoc.js +94 -0
- package/dist/ceremony/mdoc/reader.d.ts +10 -0
- package/dist/ceremony/mdoc/reader.js +43 -0
- package/dist/ceremony/mdoc/readerContext.d.ts +8 -0
- package/dist/ceremony/mdoc/readerContext.js +29 -0
- package/dist/ceremony/mount.d.ts +57 -0
- package/dist/ceremony/mount.js +96 -0
- package/dist/ceremony/origin.d.ts +10 -0
- package/dist/ceremony/origin.js +9 -0
- package/dist/ceremony/passkey/page.d.ts +6 -0
- package/dist/ceremony/passkey/page.js +136 -0
- package/dist/ceremony/passkey/routes.d.ts +2 -0
- package/dist/ceremony/passkey/routes.js +170 -0
- package/dist/ceremony/passkey/verify.d.ts +15 -0
- package/dist/ceremony/passkey/verify.js +56 -0
- package/dist/ceremony/reconciliation.d.ts +34 -0
- package/dist/ceremony/reconciliation.js +21 -0
- package/dist/ceremony/theme.d.ts +63 -0
- package/dist/ceremony/theme.js +285 -0
- package/dist/ceremony/types.d.ts +95 -0
- package/dist/ceremony/types.js +1 -0
- package/dist/client.d.ts +39 -0
- package/dist/client.js +84 -0
- package/dist/credentials.d.ts +48 -0
- package/dist/credentials.js +127 -0
- package/dist/envelope.d.ts +62 -0
- package/dist/envelope.js +72 -0
- package/dist/gated.d.ts +39 -0
- package/dist/gated.js +41 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +49 -0
- package/dist/manifest.d.ts +28 -0
- package/dist/manifest.js +76 -0
- package/dist/store.d.ts +7 -0
- package/dist/store.js +16 -0
- package/dist/types.d.ts +146 -0
- package/dist/types.js +7 -0
- package/package.json +62 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Policy builders — the typed surface a developer writes (Principle I).
|
|
2
|
+
//
|
|
3
|
+
// required(age.over(21).when(hasAlcohol)) // 21+, only when the predicate is true
|
|
4
|
+
// optional(membership.discount(10)) // 10% off if a loyalty credential is presented
|
|
5
|
+
// required(payment.in("usd")) // amount derived from the order; settles last
|
|
6
|
+
//
|
|
7
|
+
// Each builder returns a `Credential` carrying an `effect`; `.when()` attaches a
|
|
8
|
+
// call-site conditional. These hold FUNCTIONS — they live in your server and are
|
|
9
|
+
// resolved to data by `requirements()` (the code→data boundary). Nothing here
|
|
10
|
+
// crosses the wire.
|
|
11
|
+
import { ageDcql } from "./envelope.js";
|
|
12
|
+
// ── Effects (tagged data the resolver interprets) ──────────────────────────
|
|
13
|
+
export function gate() {
|
|
14
|
+
return { kind: "gate" };
|
|
15
|
+
}
|
|
16
|
+
export function discount(opts) {
|
|
17
|
+
return { kind: "discount", ...opts };
|
|
18
|
+
}
|
|
19
|
+
export function authorize() {
|
|
20
|
+
return { kind: "authorize" };
|
|
21
|
+
}
|
|
22
|
+
// ── DCQL sugar ─────────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* Concise DCQL for a single mdoc credential: name the doctype and the claim
|
|
25
|
+
* leaves, get back the full verifier-shaped `DcqlQuery`. Selective disclosure
|
|
26
|
+
* (never retain) is the default.
|
|
27
|
+
*/
|
|
28
|
+
export function dcql(spec) {
|
|
29
|
+
return {
|
|
30
|
+
credentials: [
|
|
31
|
+
{
|
|
32
|
+
id: spec.docType.split(".").pop() ?? spec.docType,
|
|
33
|
+
format: "mso_mdoc",
|
|
34
|
+
meta: { doctype_value: spec.docType },
|
|
35
|
+
claims: spec.claims.map((leaf) => ({ path: [spec.docType, leaf], intent_to_retain: false })),
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// ── Credential factory (non-mutating, chainable `.when()`) ─────────────────
|
|
41
|
+
/**
|
|
42
|
+
* Build a Credential and attach the chainable `.when()`. `.when()` returns a
|
|
43
|
+
* fresh Credential whose predicate is AND-ed onto any existing `appliesTo`, so
|
|
44
|
+
* `defineCredential`'s definition-time conditional and a call-site `.when()`
|
|
45
|
+
* compose (both must hold for the gate to apply).
|
|
46
|
+
*/
|
|
47
|
+
function makeCredential(base) {
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
when(predicate) {
|
|
51
|
+
const prev = base.appliesTo;
|
|
52
|
+
return makeCredential({
|
|
53
|
+
...base,
|
|
54
|
+
appliesTo: prev ? (order) => prev(order) && predicate(order) : predicate,
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// ── Built-in credentials ───────────────────────────────────────────────────
|
|
60
|
+
/** Age verification. `age.over(21)` proves the `age_over_21` claim (explicit positive). */
|
|
61
|
+
export const age = {
|
|
62
|
+
over(minAge) {
|
|
63
|
+
return makeCredential({
|
|
64
|
+
id: "age",
|
|
65
|
+
request: ageDcql(),
|
|
66
|
+
// Security invariant 5: require the explicit positive claim at THIS threshold
|
|
67
|
+
// (an 18+ proof must not satisfy a 21+ gate).
|
|
68
|
+
verify: (claims) => claims[`age_over_${minAge}`] === true,
|
|
69
|
+
effect: gate(),
|
|
70
|
+
ui: { label: `Age ${minAge}+`, action: `Verify you are ${minAge} or older` },
|
|
71
|
+
params: { minAge },
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/** Loyalty / membership — optional; presenting it applies a discount. */
|
|
76
|
+
export const membership = {
|
|
77
|
+
discount(percent) {
|
|
78
|
+
return makeCredential({
|
|
79
|
+
id: "membership",
|
|
80
|
+
// Real, interoperable loyalty doctype a wallet actually holds (Multipaz),
|
|
81
|
+
// matching the demo — NOT a branded placeholder, or the wallet finds nothing.
|
|
82
|
+
request: dcql({ docType: "org.multipaz.loyalty.1", claims: ["membership_number", "tier"] }),
|
|
83
|
+
verify: (claims) => typeof claims.membership_number === "string" && claims.membership_number.length > 0,
|
|
84
|
+
effect: discount({ percent }),
|
|
85
|
+
ui: { label: `${percent}% member discount`, action: "Present your membership" },
|
|
86
|
+
params: { percent },
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Payment authorization. Settles LAST (the resolver sorts authorize-effect
|
|
92
|
+
* entries to the end). The amount is derived from the order server-side, never
|
|
93
|
+
* passed as a field (Principle IV / Security invariant 2).
|
|
94
|
+
*/
|
|
95
|
+
export const payment = {
|
|
96
|
+
in(currency) {
|
|
97
|
+
return makeCredential({
|
|
98
|
+
id: "payment",
|
|
99
|
+
request: dcql({ docType: "org.openwallet.payment.1", claims: ["account"] }),
|
|
100
|
+
verify: (claims) => claims.authorized === true,
|
|
101
|
+
effect: authorize(),
|
|
102
|
+
ui: { label: `Pay (${currency.toUpperCase()})`, action: "Authorize payment" },
|
|
103
|
+
params: { currency },
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
// ── Extensibility ──────────────────────────────────────────────────────────
|
|
108
|
+
/** Define a custom credential — gate ANY consequential action with ANY credential. */
|
|
109
|
+
export function defineCredential(c) {
|
|
110
|
+
return makeCredential({
|
|
111
|
+
id: c.id,
|
|
112
|
+
request: c.request,
|
|
113
|
+
verify: c.verify,
|
|
114
|
+
effect: c.effect,
|
|
115
|
+
appliesTo: c.appliesTo,
|
|
116
|
+
ui: c.ui,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── Policy entries ─────────────────────────────────────────────────────────
|
|
120
|
+
/** A required gate — present in the manifest whenever it applies. */
|
|
121
|
+
export function required(c) {
|
|
122
|
+
return { credential: c, required: true };
|
|
123
|
+
}
|
|
124
|
+
/** An optional gate — surfaced but never blocking. */
|
|
125
|
+
export function optional(c) {
|
|
126
|
+
return { credential: c, required: false };
|
|
127
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { DcqlQuery, TrustLevel } from "./types.js";
|
|
2
|
+
export declare const ENVELOPE_VERSION: "attestomcp.verification/v1";
|
|
3
|
+
export declare const ENVELOPE_SENTINEL: "verification_required";
|
|
4
|
+
/** Built-in credential kinds the envelope describes. */
|
|
5
|
+
export type BuiltinKind = "age" | "membership" | "payment";
|
|
6
|
+
/**
|
|
7
|
+
* The age DCQL, matching the reference verifier (ISO 18013-5 mDL + EU PID).
|
|
8
|
+
* Mirrors the server's credential-gate/dcql.ts so the envelope describes the
|
|
9
|
+
* request the wallet will actually receive.
|
|
10
|
+
*/
|
|
11
|
+
export declare function ageDcql(): DcqlQuery;
|
|
12
|
+
export interface VerificationRequired {
|
|
13
|
+
/** Sentinel an agent/client keys on to detect a consent handshake. */
|
|
14
|
+
_attestomcp: typeof ENVELOPE_SENTINEL;
|
|
15
|
+
version: typeof ENVELOPE_VERSION;
|
|
16
|
+
order: {
|
|
17
|
+
id: string;
|
|
18
|
+
total: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
};
|
|
21
|
+
reason: {
|
|
22
|
+
gate: string;
|
|
23
|
+
pass: false;
|
|
24
|
+
detail: string;
|
|
25
|
+
};
|
|
26
|
+
present: {
|
|
27
|
+
credential: BuiltinKind;
|
|
28
|
+
/** Age threshold, when the credential is `age`. */
|
|
29
|
+
min_age?: number;
|
|
30
|
+
/** The DCQL the wallet will receive. */
|
|
31
|
+
request: DcqlQuery;
|
|
32
|
+
/** Per-order link the buyer opens to prove the credential on their phone. */
|
|
33
|
+
approve_url: string;
|
|
34
|
+
};
|
|
35
|
+
/** How the agent resumes once the buyer has proven the credential. */
|
|
36
|
+
resume: {
|
|
37
|
+
tool: string;
|
|
38
|
+
poll: string;
|
|
39
|
+
};
|
|
40
|
+
trust_level: TrustLevel;
|
|
41
|
+
}
|
|
42
|
+
export interface BuildEnvelopeArgs {
|
|
43
|
+
order: {
|
|
44
|
+
id: string;
|
|
45
|
+
total: number;
|
|
46
|
+
currency: string;
|
|
47
|
+
};
|
|
48
|
+
credential: BuiltinKind;
|
|
49
|
+
request: DcqlQuery;
|
|
50
|
+
approveUrl: string;
|
|
51
|
+
detail: string;
|
|
52
|
+
minAge?: number;
|
|
53
|
+
gate?: string;
|
|
54
|
+
resumeTool?: string;
|
|
55
|
+
trustLevel?: TrustLevel;
|
|
56
|
+
}
|
|
57
|
+
/** Build the typed refusal an agent can drive. Pure — no I/O. */
|
|
58
|
+
export declare function buildVerificationRequired(args: BuildEnvelopeArgs): VerificationRequired;
|
|
59
|
+
/** True if a tool result is a verification_required envelope (for agents/clients). */
|
|
60
|
+
export declare function isVerificationRequired(v: unknown): v is VerificationRequired;
|
|
61
|
+
/** A one-line, agent-facing instruction string to carry alongside the envelope. */
|
|
62
|
+
export declare function envelopeInstruction(env: VerificationRequired): string;
|
package/dist/envelope.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// The `verification_required` envelope — AttestoMcp's Mode-B / roadmap primitive.
|
|
2
|
+
//
|
|
3
|
+
// Mode A (consolidated checkout) is the v0.1 default: the tool mints the link +
|
|
4
|
+
// surfaces a `requires` manifest, and the page runs the gates. This envelope is
|
|
5
|
+
// the BLOCKING shape for page-less tools (Mode B, roadmap): a tool that has no
|
|
6
|
+
// checkout page returns a typed refusal an agent can DRIVE (why it stopped,
|
|
7
|
+
// which credential, a per-order approve link, the tool to poll) instead of a
|
|
8
|
+
// dead error string. Its wire shape is a tested contract — do NOT break it.
|
|
9
|
+
export const ENVELOPE_VERSION = "attestomcp.verification/v1";
|
|
10
|
+
export const ENVELOPE_SENTINEL = "verification_required";
|
|
11
|
+
/**
|
|
12
|
+
* The age DCQL, matching the reference verifier (ISO 18013-5 mDL + EU PID).
|
|
13
|
+
* Mirrors the server's credential-gate/dcql.ts so the envelope describes the
|
|
14
|
+
* request the wallet will actually receive.
|
|
15
|
+
*/
|
|
16
|
+
export function ageDcql() {
|
|
17
|
+
return {
|
|
18
|
+
credentials: [
|
|
19
|
+
{
|
|
20
|
+
id: "mdl",
|
|
21
|
+
format: "mso_mdoc",
|
|
22
|
+
meta: { doctype_value: "org.iso.18013.5.1.mDL" },
|
|
23
|
+
claims: [
|
|
24
|
+
{ path: ["org.iso.18013.5.1", "age_over_21"], intent_to_retain: false },
|
|
25
|
+
{ path: ["org.iso.18013.5.1", "age_over_18"], intent_to_retain: false },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "eupid",
|
|
30
|
+
format: "mso_mdoc",
|
|
31
|
+
meta: { doctype_value: "eu.europa.ec.eudi.pid.1" },
|
|
32
|
+
claims: [{ path: ["eu.europa.ec.eudi.pid.1", "age_over_18"], intent_to_retain: false }],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/** Build the typed refusal an agent can drive. Pure — no I/O. */
|
|
38
|
+
export function buildVerificationRequired(args) {
|
|
39
|
+
return {
|
|
40
|
+
_attestomcp: ENVELOPE_SENTINEL,
|
|
41
|
+
version: ENVELOPE_VERSION,
|
|
42
|
+
order: { id: args.order.id, total: args.order.total, currency: args.order.currency },
|
|
43
|
+
reason: {
|
|
44
|
+
gate: args.gate ?? (args.minAge != null ? `Age over ${args.minAge}` : "Verification"),
|
|
45
|
+
pass: false,
|
|
46
|
+
detail: args.detail,
|
|
47
|
+
},
|
|
48
|
+
present: {
|
|
49
|
+
credential: args.credential,
|
|
50
|
+
...(args.minAge != null ? { min_age: args.minAge } : {}),
|
|
51
|
+
request: args.request,
|
|
52
|
+
approve_url: args.approveUrl,
|
|
53
|
+
},
|
|
54
|
+
resume: { tool: args.resumeTool ?? "get-order-status", poll: "until status=completed or refused" },
|
|
55
|
+
trust_level: args.trustLevel ?? "presence-only-demo",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** True if a tool result is a verification_required envelope (for agents/clients). */
|
|
59
|
+
export function isVerificationRequired(v) {
|
|
60
|
+
return (typeof v === "object" &&
|
|
61
|
+
v !== null &&
|
|
62
|
+
v._attestomcp === ENVELOPE_SENTINEL);
|
|
63
|
+
}
|
|
64
|
+
/** A one-line, agent-facing instruction string to carry alongside the envelope. */
|
|
65
|
+
export function envelopeInstruction(env) {
|
|
66
|
+
const what = env.present.credential === "age"
|
|
67
|
+
? `age verification (${env.present.min_age ?? 21}+)`
|
|
68
|
+
: `a ${env.present.credential} credential`;
|
|
69
|
+
return (`This order needs ${what} before it can be placed. Share this link with the buyer to ` +
|
|
70
|
+
`prove it on their phone: ${env.present.approve_url} — then poll \`${env.resume.tool}\` ` +
|
|
71
|
+
`until it completes. Do not tell the user the order is placed until then.`);
|
|
72
|
+
}
|
package/dist/gated.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { DcqlQuery, GateOrder } from "./types.js";
|
|
2
|
+
export interface MinimalToolResult {
|
|
3
|
+
structuredContent?: Record<string, unknown>;
|
|
4
|
+
content: {
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
isError?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface EasyGatePolicy {
|
|
11
|
+
/** Require age verification. `true` uses the cart's strictest item threshold. */
|
|
12
|
+
age?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface GateDeps<A, O extends GateOrder> {
|
|
15
|
+
/** Resolve the order from the tool args (created ONCE, so the id is stable). */
|
|
16
|
+
resolveOrder: (args: A) => O | Promise<O>;
|
|
17
|
+
/** True iff this order is age-restricted AND has no recorded age verification. */
|
|
18
|
+
isAgeUnverified: (order: O) => boolean | Promise<boolean>;
|
|
19
|
+
/** Per-order link the buyer opens to prove age. */
|
|
20
|
+
approveUrl: (order: O) => string;
|
|
21
|
+
/** The age threshold for this order (e.g. 21), or undefined. */
|
|
22
|
+
minAge?: (order: O) => number | undefined;
|
|
23
|
+
/** The DCQL to request; defaults to `ageDcql()`. */
|
|
24
|
+
request?: DcqlQuery;
|
|
25
|
+
resumeTool?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* @deprecated v0.1 uses consolidated Mode A — wrap your checkout tool with
|
|
29
|
+
* `AttestoMcp.requirements(order, policy)` instead. `gated()` is the Mode-B
|
|
30
|
+
* blocking shim, kept for page-less tools / one minor version.
|
|
31
|
+
*
|
|
32
|
+
* Wrap an MCP tool handler so it returns a `verification_required` envelope when
|
|
33
|
+
* the age gate isn't met, instead of completing. The handler receives the
|
|
34
|
+
* resolved order so it never re-creates it (a fresh id would desync the approve
|
|
35
|
+
* link from the verified order).
|
|
36
|
+
*/
|
|
37
|
+
export declare function gated<A, O extends GateOrder>(handler: (args: A, ctx: {
|
|
38
|
+
order: O;
|
|
39
|
+
}) => MinimalToolResult | Promise<MinimalToolResult>, policy: EasyGatePolicy, deps: GateDeps<A, O>): (args: A) => Promise<MinimalToolResult>;
|
package/dist/gated.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// gated() — the v0-overnight blocking wrapper, kept as a DEPRECATED shim.
|
|
2
|
+
//
|
|
3
|
+
// v0.1 is consolidated Mode A: the checkout tool mints the link + surfaces a
|
|
4
|
+
// `requires` manifest (see AttestoMcp.requirements), and the page runs the gates.
|
|
5
|
+
// gated() is the Mode-B blocking shape — it withholds completion and returns a
|
|
6
|
+
// `verification_required` envelope. Retained for page-less tools / one minor
|
|
7
|
+
// version; prefer `requirements()` for checkout. Will be removed after v0.2.
|
|
8
|
+
import { ageDcql, buildVerificationRequired, envelopeInstruction } from "./envelope.js";
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated v0.1 uses consolidated Mode A — wrap your checkout tool with
|
|
11
|
+
* `AttestoMcp.requirements(order, policy)` instead. `gated()` is the Mode-B
|
|
12
|
+
* blocking shim, kept for page-less tools / one minor version.
|
|
13
|
+
*
|
|
14
|
+
* Wrap an MCP tool handler so it returns a `verification_required` envelope when
|
|
15
|
+
* the age gate isn't met, instead of completing. The handler receives the
|
|
16
|
+
* resolved order so it never re-creates it (a fresh id would desync the approve
|
|
17
|
+
* link from the verified order).
|
|
18
|
+
*/
|
|
19
|
+
export function gated(handler, policy, deps) {
|
|
20
|
+
return async (args) => {
|
|
21
|
+
const order = await deps.resolveOrder(args);
|
|
22
|
+
if (policy.age && (await deps.isAgeUnverified(order))) {
|
|
23
|
+
const minAge = deps.minAge?.(order);
|
|
24
|
+
const env = buildVerificationRequired({
|
|
25
|
+
order,
|
|
26
|
+
credential: "age",
|
|
27
|
+
request: deps.request ?? ageDcql(),
|
|
28
|
+
approveUrl: deps.approveUrl(order),
|
|
29
|
+
detail: `Cart contains age-restricted items. No age verification on file for order ${order.id}.`,
|
|
30
|
+
minAge,
|
|
31
|
+
resumeTool: deps.resumeTool,
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
// VerificationRequired is a plain JSON object; widen to the tool-result shape.
|
|
35
|
+
structuredContent: env,
|
|
36
|
+
content: [{ type: "text", text: envelopeInstruction(env) }],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return handler(args, { order });
|
|
40
|
+
};
|
|
41
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { AttestoMcp } from "./client.js";
|
|
2
|
+
export type { ExpressApp } from "./client.js";
|
|
3
|
+
export { age, membership, payment, required, optional, defineCredential, dcql, gate, discount, authorize } from "./credentials.js";
|
|
4
|
+
export { MemoryVerificationStore } from "./store.js";
|
|
5
|
+
export { completeOrder } from "./ceremony/completion.js";
|
|
6
|
+
export { issueCartMandate, verifyCartMandate, DEFAULT_CART_MANDATE_TTL_MS } from "./ceremony/cartMandate.js";
|
|
7
|
+
export type { CartMandate, CartMandateLine, CartMandateRefusal, CartMandateVerdict, IssueCartMandateArgs } from "./ceremony/cartMandate.js";
|
|
8
|
+
export { reconcileCartPayment } from "./ceremony/reconciliation.js";
|
|
9
|
+
export type { PaymentBinding, ReconcileRefusal, ReconcileVerdict } from "./ceremony/reconciliation.js";
|
|
10
|
+
export { renderRequirements } from "./ceremony/checkout-page.js";
|
|
11
|
+
export type { RenderOrder, RenderOrderLine, RenderVerification, RenderPaid, PaymentMethod, PaymentOptions, RenderRequirementsOptions, } from "./ceremony/checkout-page.js";
|
|
12
|
+
export type { CompletionContext, CompletedRecord, CompletedOrderStore, ClearableCart, SettlementRecordLike, } from "./ceremony/completion.js";
|
|
13
|
+
export type { CeremonyOrder, CeremonyOrderLine, CeremonyOrderStore, CeremonyCatalog, CartItemRef, RepriceOpts, CompletionInput, CompletionResult, CompletionSeam, SettlementSeam, GateOutcome, } from "./ceremony/types.js";
|
|
14
|
+
export type { AttestoMcpOptions, GateOrder, OrderLine, Credential, Step, Effect, VerificationManifestEntry, VerificationStore, VerificationRecord, TrustLevel, DcqlQuery, DcqlClaim, DcqlCredentialOption, } from "./types.js";
|
|
15
|
+
export { ageDcql, buildVerificationRequired, isVerificationRequired, envelopeInstruction, ENVELOPE_VERSION, ENVELOPE_SENTINEL, } from "./envelope.js";
|
|
16
|
+
export type { VerificationRequired, BuildEnvelopeArgs, BuiltinKind } from "./envelope.js";
|
|
17
|
+
export { gated } from "./gated.js";
|
|
18
|
+
export type { EasyGatePolicy, GateDeps, MinimalToolResult } from "./gated.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @openmobilehub/attestomcp-gate — the consent layer for AI agents (v0.1).
|
|
2
|
+
//
|
|
3
|
+
// Require a verifiable credential from the user's phone wallet before a
|
|
4
|
+
// consequential MCP tool completes. Identity leads; payments is one application.
|
|
5
|
+
//
|
|
6
|
+
// The v0.1 surface (consolidated Mode A):
|
|
7
|
+
// • new AttestoMcp({ walletOrigin }) — configure once
|
|
8
|
+
// • attestomcp.requirements(order, policy) — Context 1: policy → serializable manifest
|
|
9
|
+
// • attestomcp.mount(app) — Context 2: ceremony seam
|
|
10
|
+
// • required/optional over age/membership/payment builders, .when() conditional
|
|
11
|
+
// • defineCredential + gate/discount/authorize + dcql — gate ANY credential
|
|
12
|
+
// The `verification_required` envelope + gated() are retained as the Mode-B /
|
|
13
|
+
// roadmap blocking primitive (page-less tools); see ROADMAP.
|
|
14
|
+
// ── Client ───────────────────────────────────────────────────────────────
|
|
15
|
+
export { AttestoMcp } from "./client.js";
|
|
16
|
+
// ── Policy builders + extensibility ────────────────────────────────────────
|
|
17
|
+
export { age, membership, payment, required, optional, defineCredential, dcql, gate, discount, authorize } from "./credentials.js";
|
|
18
|
+
// ── Store ────────────────────────────────────────────────────────────────
|
|
19
|
+
export { MemoryVerificationStore } from "./store.js";
|
|
20
|
+
// ── Ceremony composition (host-side: bind completion over YOUR stores) ──────
|
|
21
|
+
// A composing host (e.g. @openmobilehub/attestomcp-storefront) binds `completeOrder`
|
|
22
|
+
// to its completed-order / cart stores + catalog and exposes it as the `completion`
|
|
23
|
+
// seam on `app.locals.attestomcp`, so a finished ceremony records + clears through the
|
|
24
|
+
// SAME shared path every rail uses (FR-008). The ceremony entity types let the host
|
|
25
|
+
// type those seam adapters without re-declaring them.
|
|
26
|
+
export { completeOrder } from "./ceremony/completion.js";
|
|
27
|
+
// ── Cart Mandate (ap2.CartMandate) — signed, tamper-evident cart envelope ────
|
|
28
|
+
// Additive + fail-closed: `issueCartMandate` seals a server-priced cart with the host's
|
|
29
|
+
// HMAC key; `verifyCartMandate` (and `completeOrder`, when given a `cartMandate` +
|
|
30
|
+
// `signingKey`) refuses a tampered / replayed / expired cart BEFORE re-pricing. The
|
|
31
|
+
// catalog stays the price authority (invariant 2); trust_level is presence-only-demo.
|
|
32
|
+
export { issueCartMandate, verifyCartMandate, DEFAULT_CART_MANDATE_TTL_MS } from "./ceremony/cartMandate.js";
|
|
33
|
+
// ── Cart ↔ Payment reconciliation — signed cart + signed payment agree on amount ──
|
|
34
|
+
// When BOTH a Cart Mandate and a Payment Mandate ride along, `completeOrder`
|
|
35
|
+
// reconciles them at the shared seam: same order, consistent currency, and the
|
|
36
|
+
// cart's sealed total == the catalog-re-derived total == the Payment Mandate's bound
|
|
37
|
+
// amount. One amount binding across every payment path (invariant 3); refuses on any
|
|
38
|
+
// mismatch. Exposed for hosts that reconcile outside the bundled completion seam.
|
|
39
|
+
export { reconcileCartPayment } from "./ceremony/reconciliation.js";
|
|
40
|
+
// ── Ceremony presentation (the ONE shared three-gate checkout page) ─────────
|
|
41
|
+
// Both the committed demo and @openmobilehub/attestomcp-storefront render their
|
|
42
|
+
// checkout page through `renderRequirements(order, manifest, verification)` — one
|
|
43
|
+
// polished, route-agnostic page driven by the `requires` manifest (each gate links
|
|
44
|
+
// to its OWN approveUrl) so the two surfaces never drift (T030).
|
|
45
|
+
export { renderRequirements } from "./ceremony/checkout-page.js";
|
|
46
|
+
// ── Retained: Mode-B / roadmap blocking primitive (do NOT break the wire shape) ──
|
|
47
|
+
export { ageDcql, buildVerificationRequired, isVerificationRequired, envelopeInstruction, ENVELOPE_VERSION, ENVELOPE_SENTINEL, } from "./envelope.js";
|
|
48
|
+
// gated() — deprecated Mode-B shim (use requirements() for checkout).
|
|
49
|
+
export { gated } from "./gated.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { GateOrder, Step, TrustLevel, VerificationManifestEntry } from "./types.js";
|
|
2
|
+
export interface ResolveContext {
|
|
3
|
+
/** Origin the per-order approve link binds to. */
|
|
4
|
+
walletOrigin: string;
|
|
5
|
+
/** mdoc trust honestly stated in every entry; v0.1 is presence-only. */
|
|
6
|
+
trustLevel?: TrustLevel;
|
|
7
|
+
/**
|
|
8
|
+
* Where the gates are enforced. v0.1 is consolidated Mode A: every gate runs
|
|
9
|
+
* on the checkout page (Context 2), so entries are `"checkout"`. (`"tool"` is
|
|
10
|
+
* the Mode-B / blocking shape — roadmap.)
|
|
11
|
+
*/
|
|
12
|
+
enforcedAt?: "tool" | "checkout";
|
|
13
|
+
/**
|
|
14
|
+
* Set once `attestomcp.mount()` has wired the ceremony rails onto the host app, so
|
|
15
|
+
* the approve links resolve to THIS server's mounted `/attestomcp/*` routes instead
|
|
16
|
+
* of the legacy `/credential-gate/*` shape. age/membership share the credential
|
|
17
|
+
* page (`?cred=…`); payment authorizes on the dc-payment page.
|
|
18
|
+
*/
|
|
19
|
+
mountedRoutes?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a policy against an order into the serializable manifest.
|
|
23
|
+
*
|
|
24
|
+
* Ordering: declared order is preserved EXCEPT that `authorize`-effect entries
|
|
25
|
+
* (payment) are moved to the end — payment always settles last, even when a
|
|
26
|
+
* developer declares it earlier in the policy (Principle IV / contract CT3).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRequirements(order: GateOrder, policy: Step[], ctx: ResolveContext): VerificationManifestEntry[];
|
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// requirements() — the code→data boundary (Principle VI).
|
|
2
|
+
//
|
|
3
|
+
// Runs each Step's `appliesTo` predicate server-side (Context 1), drops the
|
|
4
|
+
// gates that don't apply, orders them (payment / authorize settles LAST), and
|
|
5
|
+
// emits a flat, JSON-safe manifest: NO functions, NO closures. This is the only
|
|
6
|
+
// place policy code becomes wire data — `structuredContent.requires` is exactly
|
|
7
|
+
// what the agent and the widget receive.
|
|
8
|
+
/** Per-order approve link, e.g. `https://shop.example/credential-gate/age?order=ORD-1`. */
|
|
9
|
+
function approveUrlFor(walletOrigin, credentialId, orderId) {
|
|
10
|
+
const origin = walletOrigin.replace(/\/$/, "");
|
|
11
|
+
return `${origin}/credential-gate/${credentialId}?order=${encodeURIComponent(orderId)}`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Per-order approve link onto the MOUNTED ceremony routes (`attestomcp.mount()`):
|
|
15
|
+
* payment/authorize → `…/attestomcp/dc-payment?order=…` (passkey is the alt rail)
|
|
16
|
+
* age / membership / other gate → `…/attestomcp/credential?order=…&cred=<id>`
|
|
17
|
+
* Same origin the checkout link uses, so the host can re-home it onto its own base.
|
|
18
|
+
*/
|
|
19
|
+
function mountedApproveUrlFor(walletOrigin, credentialId, effect, orderId) {
|
|
20
|
+
const origin = walletOrigin.replace(/\/$/, "");
|
|
21
|
+
const order = encodeURIComponent(orderId);
|
|
22
|
+
if (effect === "authorize" || credentialId === "payment") {
|
|
23
|
+
return `${origin}/attestomcp/dc-payment?order=${order}`;
|
|
24
|
+
}
|
|
25
|
+
return `${origin}/attestomcp/credential?order=${order}&cred=${encodeURIComponent(credentialId)}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a policy against an order into the serializable manifest.
|
|
29
|
+
*
|
|
30
|
+
* Ordering: declared order is preserved EXCEPT that `authorize`-effect entries
|
|
31
|
+
* (payment) are moved to the end — payment always settles last, even when a
|
|
32
|
+
* developer declares it earlier in the policy (Principle IV / contract CT3).
|
|
33
|
+
*/
|
|
34
|
+
export function resolveRequirements(order, policy, ctx) {
|
|
35
|
+
const trust_level = ctx.trustLevel ?? "presence-only-demo";
|
|
36
|
+
const enforcedAt = ctx.enforcedAt ?? "checkout";
|
|
37
|
+
const entries = policy
|
|
38
|
+
// Drop gates whose inclusion predicate (defineCredential appliesTo + any
|
|
39
|
+
// composed .when()) is present and returns false.
|
|
40
|
+
.filter((step) => {
|
|
41
|
+
const applies = step.credential.appliesTo;
|
|
42
|
+
return applies ? applies(order) : true;
|
|
43
|
+
})
|
|
44
|
+
.map((step) => {
|
|
45
|
+
const c = step.credential;
|
|
46
|
+
const effect = c.effect.kind;
|
|
47
|
+
const entry = {
|
|
48
|
+
credential: c.id,
|
|
49
|
+
required: step.required,
|
|
50
|
+
effect,
|
|
51
|
+
enforcedAt,
|
|
52
|
+
trust_level,
|
|
53
|
+
label: c.ui.label,
|
|
54
|
+
};
|
|
55
|
+
if (c.params?.minAge != null)
|
|
56
|
+
entry.minAge = c.params.minAge;
|
|
57
|
+
if (effect === "discount" && c.params?.percent != null)
|
|
58
|
+
entry.discountPct = c.params.percent;
|
|
59
|
+
if (ctx.mountedRoutes) {
|
|
60
|
+
// Ceremony is mounted: every entry that maps to a `/attestomcp/*` route gets a
|
|
61
|
+
// per-order approve link — including the membership discount, which is
|
|
62
|
+
// proven on the same credential page (so the buyer can opt into the discount).
|
|
63
|
+
entry.approveUrl = mountedApproveUrlFor(ctx.walletOrigin, c.id, effect, order.id);
|
|
64
|
+
}
|
|
65
|
+
else if (effect === "gate" || effect === "authorize") {
|
|
66
|
+
// Legacy `/credential-gate/*` shape — a gate/authorize is proven via a
|
|
67
|
+
// per-order ceremony link; a discount is merely presented, no approve link.
|
|
68
|
+
entry.approveUrl = approveUrlFor(ctx.walletOrigin, c.id, order.id);
|
|
69
|
+
}
|
|
70
|
+
return entry;
|
|
71
|
+
});
|
|
72
|
+
// Stable payment-last: non-authorize entries keep their order, then authorize.
|
|
73
|
+
const settleLast = entries.filter((e) => e.effect === "authorize");
|
|
74
|
+
const rest = entries.filter((e) => e.effect !== "authorize");
|
|
75
|
+
return [...rest, ...settleLast];
|
|
76
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { VerificationRecord, VerificationStore } from "./types.js";
|
|
2
|
+
export declare class MemoryVerificationStore implements VerificationStore {
|
|
3
|
+
private readonly records;
|
|
4
|
+
read(orderId: string): VerificationRecord | undefined;
|
|
5
|
+
write(orderId: string, record: VerificationRecord): void;
|
|
6
|
+
clear(orderId: string): void;
|
|
7
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Per-order verification store. Keyed by order id — NEVER process-global, so one
|
|
2
|
+
// shopper's verification can never unlock another's checkout (Security invariant 4).
|
|
3
|
+
// Default is in-process; swap in Redis (Upstash) for serverless by implementing
|
|
4
|
+
// VerificationStore.
|
|
5
|
+
export class MemoryVerificationStore {
|
|
6
|
+
records = new Map();
|
|
7
|
+
read(orderId) {
|
|
8
|
+
return this.records.get(orderId);
|
|
9
|
+
}
|
|
10
|
+
write(orderId, record) {
|
|
11
|
+
this.records.set(orderId, record);
|
|
12
|
+
}
|
|
13
|
+
clear(orderId) {
|
|
14
|
+
this.records.delete(orderId);
|
|
15
|
+
}
|
|
16
|
+
}
|