@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.
Files changed (79) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +172 -0
  3. package/dist/ceremony/cartMandate.d.ts +61 -0
  4. package/dist/ceremony/cartMandate.js +76 -0
  5. package/dist/ceremony/challengeToken.d.ts +5 -0
  6. package/dist/ceremony/challengeToken.js +43 -0
  7. package/dist/ceremony/checkout-page.d.ts +85 -0
  8. package/dist/ceremony/checkout-page.js +269 -0
  9. package/dist/ceremony/completion.d.ts +41 -0
  10. package/dist/ceremony/completion.js +90 -0
  11. package/dist/ceremony/credential-gate/dcql.d.ts +10 -0
  12. package/dist/ceremony/credential-gate/dcql.js +12 -0
  13. package/dist/ceremony/credential-gate/doc-spec.d.ts +3 -0
  14. package/dist/ceremony/credential-gate/doc-spec.js +16 -0
  15. package/dist/ceremony/credential-gate/mdoc-verify.d.ts +15 -0
  16. package/dist/ceremony/credential-gate/mdoc-verify.js +29 -0
  17. package/dist/ceremony/credential-gate/page.d.ts +20 -0
  18. package/dist/ceremony/credential-gate/page.js +136 -0
  19. package/dist/ceremony/credential-gate/request.d.ts +15 -0
  20. package/dist/ceremony/credential-gate/request.js +43 -0
  21. package/dist/ceremony/credential-gate/routes.d.ts +2 -0
  22. package/dist/ceremony/credential-gate/routes.js +200 -0
  23. package/dist/ceremony/credential-gate/verify.d.ts +51 -0
  24. package/dist/ceremony/credential-gate/verify.js +146 -0
  25. package/dist/ceremony/dc-payment/dcql.d.ts +5 -0
  26. package/dist/ceremony/dc-payment/dcql.js +23 -0
  27. package/dist/ceremony/dc-payment/page.d.ts +18 -0
  28. package/dist/ceremony/dc-payment/page.js +195 -0
  29. package/dist/ceremony/dc-payment/request.d.ts +17 -0
  30. package/dist/ceremony/dc-payment/request.js +50 -0
  31. package/dist/ceremony/dc-payment/routes.d.ts +2 -0
  32. package/dist/ceremony/dc-payment/routes.js +147 -0
  33. package/dist/ceremony/dc-payment/txData.d.ts +19 -0
  34. package/dist/ceremony/dc-payment/txData.js +34 -0
  35. package/dist/ceremony/dc-payment/verify.d.ts +108 -0
  36. package/dist/ceremony/dc-payment/verify.js +208 -0
  37. package/dist/ceremony/mandate.d.ts +71 -0
  38. package/dist/ceremony/mandate.js +116 -0
  39. package/dist/ceremony/mdoc/mdoc-iso.d.ts +44 -0
  40. package/dist/ceremony/mdoc/mdoc-iso.js +260 -0
  41. package/dist/ceremony/mdoc/mdoc.d.ts +17 -0
  42. package/dist/ceremony/mdoc/mdoc.js +94 -0
  43. package/dist/ceremony/mdoc/reader.d.ts +10 -0
  44. package/dist/ceremony/mdoc/reader.js +43 -0
  45. package/dist/ceremony/mdoc/readerContext.d.ts +8 -0
  46. package/dist/ceremony/mdoc/readerContext.js +29 -0
  47. package/dist/ceremony/mount.d.ts +57 -0
  48. package/dist/ceremony/mount.js +96 -0
  49. package/dist/ceremony/origin.d.ts +10 -0
  50. package/dist/ceremony/origin.js +9 -0
  51. package/dist/ceremony/passkey/page.d.ts +6 -0
  52. package/dist/ceremony/passkey/page.js +136 -0
  53. package/dist/ceremony/passkey/routes.d.ts +2 -0
  54. package/dist/ceremony/passkey/routes.js +170 -0
  55. package/dist/ceremony/passkey/verify.d.ts +15 -0
  56. package/dist/ceremony/passkey/verify.js +56 -0
  57. package/dist/ceremony/reconciliation.d.ts +34 -0
  58. package/dist/ceremony/reconciliation.js +21 -0
  59. package/dist/ceremony/theme.d.ts +63 -0
  60. package/dist/ceremony/theme.js +285 -0
  61. package/dist/ceremony/types.d.ts +95 -0
  62. package/dist/ceremony/types.js +1 -0
  63. package/dist/client.d.ts +39 -0
  64. package/dist/client.js +84 -0
  65. package/dist/credentials.d.ts +48 -0
  66. package/dist/credentials.js +127 -0
  67. package/dist/envelope.d.ts +62 -0
  68. package/dist/envelope.js +72 -0
  69. package/dist/gated.d.ts +39 -0
  70. package/dist/gated.js +41 -0
  71. package/dist/index.d.ts +18 -0
  72. package/dist/index.js +49 -0
  73. package/dist/manifest.d.ts +28 -0
  74. package/dist/manifest.js +76 -0
  75. package/dist/store.d.ts +7 -0
  76. package/dist/store.js +16 -0
  77. package/dist/types.d.ts +146 -0
  78. package/dist/types.js +7 -0
  79. package/package.json +62 -0
@@ -0,0 +1,285 @@
1
+ // theme.ts — the SHARED AttestoMcp design system for the browser ceremony flow.
2
+ //
3
+ // The checkout hub (checkout-page.ts) and the two gate pages (credential-gate/page.ts,
4
+ // dc-payment/page.ts) all render through THIS module so they read as ONE branded flow:
5
+ // the same wordmark, the same card surfaces, the same teal accent, the same discreet
6
+ // honesty footer. Each page composes the pieces below around its OWN logic — the chrome
7
+ // is presentation-only and never touches a completion path.
8
+ //
9
+ // Design language (opinionated, build to this):
10
+ // • ONE accent — teal #0d9488 (hover #0f766e). Used sparingly: primary CTA, active
11
+ // step, the discount row, a verified ✓.
12
+ // • ink #0f172a · muted #64748b · hairline #e2e8f0 · surface #fff on app bg #f8fafc.
13
+ // • success #047857 · danger #b91c1c.
14
+ // • System type stack. Money is tabular-nums. Single column, max-width 460px, 14px
15
+ // card radius, a soft two-layer shadow, mobile-first (great at 390px).
16
+ //
17
+ // Honesty (FR-011 / Principle VII): the trust footer is the single presence-only surface.
18
+ // It MUST keep the literal token "presence-only-demo" so the honesty tests and the FR
19
+ // stay satisfied — the wire crypto is real; the issuer trust anchor is not.
20
+ function escapeHtml(s) {
21
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
22
+ }
23
+ function money(amount, currency) {
24
+ return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
25
+ }
26
+ // ── The design-system stylesheet ────────────────────────────────────────────
27
+ // One <style> block shared by all three pages so they are visually identical chrome.
28
+ // Pages add only the few component styles unique to them (e.g. the QR notice).
29
+ const DESIGN_CSS = `
30
+ :root {
31
+ --accent: #0d9488; --accent-hover: #0f766e;
32
+ --ink: #0f172a; --muted: #64748b; --hairline: #e2e8f0;
33
+ --surface: #ffffff; --app-bg: #f8fafc;
34
+ --success: #047857; --danger: #b91c1c;
35
+ --shadow: 0 1px 3px rgba(15,23,42,.08), 0 1px 2px rgba(15,23,42,.04);
36
+ }
37
+ * { box-sizing: border-box; }
38
+ body {
39
+ font-family: -apple-system, "Segoe UI", Roboto, system-ui, sans-serif;
40
+ background: var(--app-bg); color: var(--ink);
41
+ margin: 0; padding: 20px 16px 40px;
42
+ line-height: 1.55; -webkit-font-smoothing: antialiased;
43
+ }
44
+ .wrap { max-width: 460px; margin: 0 auto; }
45
+ h1 { font-size: 1.5rem; font-weight: 700; line-height: 1.2; margin: 0 0 6px; color: var(--ink); }
46
+ p.lede { font-size: .95rem; color: var(--muted); margin: 0 0 4px; }
47
+ small, .small { font-size: .8rem; color: var(--muted); }
48
+ .num { text-align: right; font-variant-numeric: tabular-nums; }
49
+
50
+ /* Brand header */
51
+ .brand { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; }
52
+ .wordmark { font-size: .78rem; letter-spacing: .22em; font-weight: 700; color: var(--muted); }
53
+ .demo-pill {
54
+ font-size: .62rem; letter-spacing: .14em; font-weight: 700; color: var(--muted);
55
+ border: 1px solid var(--hairline); border-radius: 999px; padding: 3px 9px; background: var(--surface);
56
+ }
57
+ .head { margin-bottom: 18px; }
58
+ .head .tagline { font-size: .95rem; color: var(--muted); margin: 0; }
59
+
60
+ /* Card surface */
61
+ .card {
62
+ background: var(--surface); border: 1px solid var(--hairline);
63
+ border-radius: 14px; box-shadow: var(--shadow);
64
+ padding: 18px; margin-bottom: 16px;
65
+ }
66
+ .card-title { font-size: .8rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 700; margin: 0 0 12px; }
67
+
68
+ /* Order summary */
69
+ .summary table { width: 100%; border-collapse: collapse; }
70
+ .summary td { padding: 8px 0; font-size: .95rem; }
71
+ .summary .line td { border-bottom: 1px solid var(--hairline); }
72
+ .summary .qty { color: var(--muted); font-variant-numeric: tabular-nums; }
73
+ .summary .disc td { color: var(--accent); font-weight: 600; }
74
+ .summary .total td { padding-top: 12px; border-top: 1px solid var(--hairline); font-weight: 700; font-size: 1.05rem; }
75
+
76
+ /* Progress rail (Age · Membership · Pay) */
77
+ .rail { display: flex; align-items: flex-start; justify-content: space-between; position: relative; margin: 4px 2px 18px; }
78
+ .rail::before { content: ""; position: absolute; top: 11px; left: 11%; right: 11%; height: 2px; background: var(--hairline); z-index: 0; }
79
+ .rail-step { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; gap: 6px; flex: 1; }
80
+ .rail-dot {
81
+ width: 22px; height: 22px; border-radius: 999px; background: var(--surface);
82
+ border: 2px solid var(--hairline); display: flex; align-items: center; justify-content: center;
83
+ font-size: .7rem; font-weight: 700; color: var(--muted);
84
+ }
85
+ .rail-step.done .rail-dot { background: var(--accent); border-color: var(--accent); color: #fff; }
86
+ .rail-step.current .rail-dot { border-color: var(--accent); color: var(--accent); box-shadow: 0 0 0 3px rgba(13,148,136,.14); }
87
+ .rail-label { font-size: .68rem; letter-spacing: .02em; color: var(--muted); text-align: center; }
88
+ .rail-step.done .rail-label, .rail-step.current .rail-label { color: var(--ink); font-weight: 600; }
89
+
90
+ /* Buttons */
91
+ .btn {
92
+ display: block; width: 100%; height: 48px; line-height: 1; border-radius: 10px;
93
+ font-size: .95rem; font-weight: 600; text-align: center; text-decoration: none;
94
+ border: 1px solid transparent; cursor: pointer; transition: background .12s, transform .04s;
95
+ display: flex; align-items: center; justify-content: center;
96
+ }
97
+ .btn-primary { background: var(--accent); color: #fff; border-color: var(--accent); }
98
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
99
+ .btn-primary:active { transform: translateY(1px); }
100
+ .btn-secondary { background: transparent; color: var(--accent); border-color: var(--hairline); }
101
+ .btn-secondary:hover { border-color: var(--accent); }
102
+ .btn-danger { background: var(--accent); color: #fff; border-color: var(--accent); }
103
+ .btn + .btn { margin-top: 10px; }
104
+ .btn:disabled { opacity: .55; cursor: default; }
105
+
106
+ /* Status rows + verify log */
107
+ .row-ok { color: var(--success); font-weight: 600; font-size: .95rem; display: flex; align-items: center; gap: 8px; }
108
+ .row-pending { color: var(--ink); font-size: .95rem; }
109
+ .step { padding: 5px 0; font-size: .85rem; display: flex; gap: 8px; align-items: baseline; }
110
+ .step.ok { color: var(--success); }
111
+ .step.err { color: var(--danger); white-space: pre-wrap; }
112
+ .notice {
113
+ margin-top: 14px; padding: 12px 14px; background: #f1f5f9; border: 1px solid var(--hairline);
114
+ border-radius: 10px; font-size: .88rem; color: var(--ink);
115
+ }
116
+
117
+ /* Payment-method group (Shopify-style radio group + one Pay CTA) */
118
+ .pm-head { font-size: .8rem; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); font-weight: 700; margin: 0 0 12px; }
119
+ .pm-group { border: 1px solid var(--hairline); border-radius: 10px; overflow: hidden; }
120
+ .pm-row { display: flex; gap: 10px; align-items: flex-start; padding: 12px 14px; cursor: pointer; border-bottom: 1px solid var(--hairline); }
121
+ .pm-row:last-child { border-bottom: none; }
122
+ .pm-row:has(input:checked) { background: #f0fdfa; box-shadow: inset 3px 0 0 var(--accent); }
123
+ .pm-row input { margin-top: 3px; accent-color: var(--accent); }
124
+ .pm-name { display: block; font-size: .9rem; font-weight: 600; color: var(--ink); }
125
+ .pm-desc { display: block; font-size: .8rem; color: var(--muted); margin-top: 2px; }
126
+ .step-no { display: inline-block; min-width: 1.4em; color: var(--muted); font-variant-numeric: tabular-nums; }
127
+
128
+ /* Calm payment-lock state (never alarming) */
129
+ .lock {
130
+ display: flex; align-items: center; gap: 8px; justify-content: center;
131
+ color: var(--muted); font-size: .9rem; padding: 14px;
132
+ background: #f1f5f9; border: 1px solid var(--hairline); border-radius: 10px;
133
+ }
134
+
135
+ /* Discreet trust footer */
136
+ .trust { margin-top: 22px; text-align: center; }
137
+ .trust .trust-line { font-size: .78rem; color: var(--muted); }
138
+
139
+ /* Tidy success / receipt card */
140
+ .receipt-banner {
141
+ background: var(--accent); color: #fff; border-radius: 12px; padding: 16px;
142
+ text-align: center; font-weight: 700; font-size: 1.05rem; margin-bottom: 12px;
143
+ }
144
+ .receipt-banner .sub { font-weight: 500; font-size: .85rem; opacity: .95; margin-top: 4px; }
145
+ .receipt-banner a { color: #fff; text-decoration: underline; }
146
+
147
+ /* Prominent end-of-ceremony handoff — shown when the WHOLE ceremony is done
148
+ (payment is the last gate). Bigger than the inline receipt banner: the order is
149
+ complete and the buyer can close the window; the agent (MCP host) polls
150
+ order-status and continues the conversation. */
151
+ .complete-banner {
152
+ background: var(--accent); color: #fff; border-radius: 14px; padding: 22px 18px 20px;
153
+ text-align: center; margin-bottom: 14px; box-shadow: var(--shadow);
154
+ }
155
+ .complete-banner .big { font-size: 1.35rem; font-weight: 800; line-height: 1.2; }
156
+ .complete-banner .sub { font-weight: 500; font-size: .92rem; opacity: .97; margin-top: 8px; line-height: 1.5; }
157
+ .complete-banner .sub strong { font-weight: 800; }
158
+ .complete-banner .ret { display: inline-block; margin-top: 12px; font-size: .82rem; opacity: .92; }
159
+ .complete-banner a { color: #fff; text-decoration: underline; }
160
+
161
+ /* Indeterminate settling bar — shown while x402 settles on-chain (~10s). A teal
162
+ sliver slides across a hairline track so the buyer sees the wait is live work,
163
+ not a hang. Hidden until a page adds .on; both payment rails use it. */
164
+ .settling-bar { display: none; margin: 14px 0 2px; height: 6px; background: var(--hairline); border-radius: 999px; overflow: hidden; }
165
+ .settling-bar.on { display: block; }
166
+ .settling-bar > i { display: block; width: 35%; height: 100%; background: var(--accent); border-radius: 999px; animation: settle-slide 1.15s ease-in-out infinite; }
167
+ @keyframes settle-slide { from { margin-left: -35%; } to { margin-left: 100%; } }
168
+
169
+ /* x402 settlement receipt — on-chain proof, design-system styled. The settle card
170
+ reuses the surface chrome; the teal left rail marks it as the success path. */
171
+ .settle {
172
+ margin-top: 14px; padding: 14px 16px; background: #f0fdfa;
173
+ border: 1px solid var(--hairline); border-left: 3px solid var(--accent);
174
+ border-radius: 10px;
175
+ }
176
+ .settle .settle-head { font-weight: 700; font-size: .95rem; color: var(--success); margin: 0 0 8px; }
177
+ .settle dl.kv { display: grid; grid-template-columns: 64px 1fr; gap: 4px 12px; margin: 0; font-size: .9rem; }
178
+ .settle dl.kv dt { color: var(--muted); font-size: .8rem; padding-top: 1px; }
179
+ .settle dl.kv dd { margin: 0; word-break: break-word; }
180
+ .settle .dim { color: var(--muted); font-weight: 400; font-size: .78rem; }
181
+ .settle .mono { font-family: ui-monospace, Menlo, monospace; font-size: .78rem; word-break: break-all; }
182
+ /* Prominent, tappable HashScan link — the buyer is on their phone; one tap to the
183
+ live explorer is the third-party proof (no QR; the package has no qr route). */
184
+ .settle .hashscan {
185
+ display: flex; align-items: center; justify-content: center; gap: 8px;
186
+ margin-top: 12px; height: 44px; border-radius: 10px;
187
+ background: var(--accent); color: #fff; font-weight: 600; font-size: .92rem;
188
+ text-decoration: none;
189
+ }
190
+ .settle .hashscan:hover { background: var(--accent-hover); }
191
+ /* Calm "authorized, not settled" line — never alarming red wall; a muted danger row. */
192
+ .settle-failed {
193
+ margin-top: 14px; padding: 12px 14px; background: #fef2f2;
194
+ border: 1px solid var(--hairline); border-left: 3px solid var(--danger);
195
+ border-radius: 10px; font-size: .88rem; color: var(--danger);
196
+ }
197
+ `;
198
+ /** <head> with the shared design-system CSS. `extraCss` lets a page add the few
199
+ * component styles unique to it without forking the design language. */
200
+ export function pageHead(title, extraCss = "") {
201
+ return `<head>
202
+ <meta charset="utf-8" />
203
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
204
+ <title>${escapeHtml(title)}</title>
205
+ <style>${DESIGN_CSS}${extraCss}</style>
206
+ </head>`;
207
+ }
208
+ /** The ATTESTOMCP wordmark + a discreet DEMO pill, with an optional confident h1 +
209
+ * identity-first tagline underneath. Pass `h1`/`tagline` to render the heading block;
210
+ * omit them to render just the brand row (a page can lay out its own heading). */
211
+ export function brandHeader(opts = {}) {
212
+ const heading = opts.h1 != null
213
+ ? `<div class="head"><h1>${escapeHtml(opts.h1)}</h1>${opts.tagline != null ? `<p class="tagline">${escapeHtml(opts.tagline)}</p>` : ""}</div>`
214
+ : "";
215
+ return `<div class="brand"><span class="wordmark">ATTESTOMCP</span><span class="demo-pill">DEMO</span></div>${heading}`;
216
+ }
217
+ /** An indeterminate "settling…" progress bar (hidden until JS adds `.on`). Shown on
218
+ * the payment rails while x402 settlement runs on-chain (~10s) so the wait reads as
219
+ * live work. `id` defaults to "settling" for the page script to toggle. */
220
+ export function settlingBar(id = "settling") {
221
+ return `<div class="settling-bar" id="${id}"><i></i></div>`;
222
+ }
223
+ /**
224
+ * The prominent end-of-ceremony handoff banner: every attestation + payment is done,
225
+ * so the order is COMPLETE. It tells the buyer they can close the window and continue
226
+ * in their agent — the MCP host polls order-status and resumes the conversation
227
+ * automatically (Mode A: the agent never runs the ceremony, it only orchestrates +
228
+ * polls). An optional secondary link returns to the checkout hub for a pure-browser
229
+ * flow. Built server-side and embedded into the gate page's receipt script.
230
+ */
231
+ export function completionHandoffBanner(returnUrl) {
232
+ const ret = returnUrl
233
+ ? `<a class="ret" href="${escapeHtml(returnUrl)}">Staying in the browser? Return to checkout ›</a>`
234
+ : "";
235
+ return `<div class="complete-banner"><div class="big">✓ Order complete</div><div class="sub">You can <strong>close this window</strong> and continue in your agent — it has your order and will pick up from here.</div>${ret}</div>`;
236
+ }
237
+ /** The order summary card: line items, an accent discount row, a bold Total with a
238
+ * top hairline. Money is tabular. Shared by all three pages so the cart reads the
239
+ * same everywhere. */
240
+ export function orderSummaryCard(args) {
241
+ const cur = args.currency;
242
+ const rows = args.lines
243
+ .map((l) => `<tr class="line"><td>${escapeHtml(l.name)} <span class="qty">×${l.quantity}</span></td><td class="num">${money(l.lineTotal, l.currency ?? cur)}</td></tr>`)
244
+ .join("\n");
245
+ const discount = args.discount ?? 0;
246
+ const discRow = discount > 0
247
+ ? `<tr class="disc"><td>${escapeHtml(args.discountLabel ?? "Discount")}</td><td class="num">-${money(discount, cur)}</td></tr>`
248
+ : "";
249
+ const caption = args.caption ? `<p class="card-title">${escapeHtml(args.caption)}</p>` : "";
250
+ return `<div class="card summary">
251
+ ${caption}<table>
252
+ ${rows}
253
+ ${discRow}
254
+ <tr class="total"><td>Total</td><td class="num">${money(args.total, cur)}</td></tr>
255
+ </table>
256
+ </div>`;
257
+ }
258
+ /**
259
+ * The Age · Membership · Pay stepper. DONE = filled accent with ✓; CURRENT (the step
260
+ * at `currentIndex`, when not already done) = accent ring; everything else = muted.
261
+ * The hub passes real status; each gate page marks its OWN step current.
262
+ */
263
+ export function progressRail(steps, currentIndex) {
264
+ if (steps.length === 0)
265
+ return "";
266
+ const dots = steps
267
+ .map((s, i) => {
268
+ const isDone = !!s.done;
269
+ const isCurrent = i === currentIndex && !isDone;
270
+ const cls = isDone ? "done" : isCurrent ? "current" : "";
271
+ const mark = isDone ? "✓" : String(i + 1);
272
+ return `<div class="rail-step ${cls}"><div class="rail-dot">${mark}</div><div class="rail-label">${escapeHtml(s.label)}</div></div>`;
273
+ })
274
+ .join("");
275
+ return `<div class="rail" role="list" aria-label="Progress">${dots}</div>`;
276
+ }
277
+ // ── Trust footer ────────────────────────────────────────────────────────────
278
+ /**
279
+ * The single, DISCREET presence-only honesty line (replaces the old yellow warning
280
+ * box). It MUST keep the literal "presence-only-demo" token (FR-011 + the honesty
281
+ * tests) — the wire crypto is real; the issuer trust anchor is not.
282
+ */
283
+ export function trustFooter() {
284
+ return `<div class="trust"><div class="trust-line">🔒 presence-only-demo · secured by AttestoMcp · the wire crypto is real; issuer trust anchor is not</div></div>`;
285
+ }
@@ -0,0 +1,95 @@
1
+ import type { CartMandate } from "./cartMandate.js";
2
+ import type { SettlementRecordLike } from "./completion.js";
3
+ export type MaybePromise<T> = T | Promise<T>;
4
+ export interface CeremonyOrderLine {
5
+ /** Product id. */
6
+ id: string;
7
+ /** Display name (optional; the catalog is the source of truth). */
8
+ name?: string;
9
+ /** Unit price (catalog-authoritative). */
10
+ unitPrice: number;
11
+ /** Quantity. */
12
+ quantity: number;
13
+ /** unitPrice × quantity. */
14
+ lineTotal: number;
15
+ /** ISO 4217 (optional; order-level currency is authoritative). */
16
+ currency?: string;
17
+ /** Per-product age threshold (e.g. 21), re-derived from the catalog. */
18
+ minimumAge?: number;
19
+ /** Product category — available to custom `.when()` predicates. */
20
+ category?: string;
21
+ }
22
+ export interface CeremonyOrder {
23
+ /** Stable per checkout. */
24
+ id: string;
25
+ lines: CeremonyOrderLine[];
26
+ itemCount?: number;
27
+ subtotal: number;
28
+ /** Re-derived from the catalog + this order's verified loyalty state. */
29
+ discount: number;
30
+ /** subtotal − discount; re-derived, never trusted from the token. */
31
+ total: number;
32
+ currency: string;
33
+ createdAt?: string;
34
+ }
35
+ export interface CartItemRef {
36
+ productId: string;
37
+ quantity: number;
38
+ }
39
+ export interface RepriceOpts {
40
+ ageVerified?: boolean;
41
+ loyaltyApplied?: boolean;
42
+ }
43
+ export interface CeremonyCatalog {
44
+ /**
45
+ * Re-price an order's lines from the catalog — the SERVER-SIDE source of truth
46
+ * (Security invariant 2: never trust the token's totals). The id is preserved;
47
+ * the loyalty discount is applied only when THIS order's verification opts in.
48
+ * Mirrors the demo's `createOrder(items, id, opts)` so it injects with no glue.
49
+ */
50
+ createOrder(items: CartItemRef[], orderId: string, opts?: RepriceOpts): CeremonyOrder;
51
+ }
52
+ export interface CeremonyOrderStore {
53
+ /**
54
+ * Resolve a created order by id. Totals are re-derived from the catalog
55
+ * regardless (CT3) — this only recovers the line items + id, never the price.
56
+ */
57
+ read(orderId: string): MaybePromise<CeremonyOrder | null | undefined>;
58
+ }
59
+ export interface GateOutcome {
60
+ gate: string;
61
+ pass: boolean;
62
+ detail: string;
63
+ }
64
+ export interface CompletionInput {
65
+ order: CeremonyOrder;
66
+ mandateId: string;
67
+ amount: number;
68
+ currency: string;
69
+ method: string;
70
+ instrument?: unknown;
71
+ gates: GateOutcome[];
72
+ /** Optional signed Cart Mandate (ap2.CartMandate). When present AND the context has a
73
+ * `signingKey`, completion verifies it (signature + order-id binding + expiry) BEFORE
74
+ * re-pricing and refuses a tampered/replayed/expired cart — additive, fail-closed
75
+ * defense-in-depth; the catalog stays the price authority either way. */
76
+ cartMandate?: CartMandate;
77
+ }
78
+ export interface CompletionResult {
79
+ completed: boolean;
80
+ settlement?: SettlementRecordLike;
81
+ settlementError?: string;
82
+ /** Why a non-completion happened — a failed ceremony ("gates"), a tampered/replayed/
83
+ * expired Cart Mandate ("cart-mandate"), a tampered token re-priced against the
84
+ * catalog ("reprice"), a signed Cart Mandate and signed Payment Mandate that
85
+ * disagree on order/amount/currency ("reconcile"), or an age-restricted order with
86
+ * no proven per-order age claim ("age"). */
87
+ reason?: "gates" | "cart-mandate" | "reprice" | "reconcile" | "age";
88
+ }
89
+ /**
90
+ * The host-bound completion seam mount() hands to each rail. The host pre-binds
91
+ * it to its completed-order + cart stores; the package ships `completeOrder`
92
+ * (completion.ts) as one ready implementation over the injected ceremony context.
93
+ */
94
+ export type CompletionSeam = (input: CompletionInput) => MaybePromise<CompletionResult>;
95
+ export type SettlementSeam = (order: CeremonyOrder) => Promise<SettlementRecordLike>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import type { AttestoMcpOptions, GateOrder, Step, VerificationManifestEntry, VerificationStore } from "./types.js";
2
+ import { type CeremonySeams } from "./ceremony/mount.js";
3
+ /** The ceremony seams the host supplies to `mount()`; the per-order
4
+ * verification store is AttestoMcp's own, so the host never passes it here. */
5
+ export type MountCeremony = Omit<Partial<CeremonySeams>, "verificationStore">;
6
+ /**
7
+ * Minimal structural type for an Express app — the package stays dependency-free
8
+ * (no `express` import). `mount()` only needs `app.locals` for the store seam.
9
+ */
10
+ export interface ExpressApp {
11
+ locals: Record<string, unknown>;
12
+ }
13
+ export declare class AttestoMcp {
14
+ readonly walletOrigin: string;
15
+ readonly store: VerificationStore;
16
+ private mountedRoutes;
17
+ constructor(opts?: AttestoMcpOptions);
18
+ /**
19
+ * Context 1 — resolve a policy against a server-priced order into the flat,
20
+ * JSON-safe `requires` manifest. Runs `.when()`/`appliesTo` predicates,
21
+ * payment-last; no functions cross the wire.
22
+ */
23
+ requirements(order: GateOrder, policy: Step[]): VerificationManifestEntry[];
24
+ /**
25
+ * Context 2 — wire the verification ceremony onto your Express app.
26
+ *
27
+ * Pass the ceremony seams (`{ orderStore, catalog, completion, signingKey, … }`)
28
+ * to register the gate's routes through `mountCeremony`: it validates the seams,
29
+ * FAILS FAST on a missing required one (CT2), and attaches each rail. AttestoMcp's
30
+ * own per-order store is injected as the `verificationStore` (keyed by order id,
31
+ * never process-global — Security invariant 4), so the host never passes it.
32
+ *
33
+ * Called WITHOUT seams it keeps the v0.1 behavior: expose the per-order store
34
+ * via `app.locals.attestomcp` so a host's existing fail-closed `/credential-gate/*`
35
+ * routes resolve verification state THROUGH AttestoMcp. The rails register only
36
+ * when seams are supplied; with none extracted yet, that path attaches no routes.
37
+ */
38
+ mount(app: ExpressApp, ceremony?: MountCeremony): void;
39
+ }
package/dist/client.js ADDED
@@ -0,0 +1,84 @@
1
+ // The configure-once client (Principle I): construct with your wallet origin,
2
+ // then declarative calls. `requirements(order, policy)` resolves a policy to the
3
+ // serializable manifest (Context 1); `mount(app)` is the Context-2 seam.
4
+ import { resolveRequirements } from "./manifest.js";
5
+ import { MemoryVerificationStore } from "./store.js";
6
+ import { mountCeremony } from "./ceremony/mount.js";
7
+ /** Zero-config default so `new AttestoMcp()` works for local dev. */
8
+ const DEFAULT_WALLET_ORIGIN = `http://localhost:${process.env.PORT ?? 3000}`;
9
+ export class AttestoMcp {
10
+ walletOrigin;
11
+ store;
12
+ // True once the ceremony rails are wired onto a host app (so `/attestomcp/*` routes
13
+ // exist on this server). `requirements()` then emits approve links that resolve
14
+ // to those mounted routes rather than the legacy `/credential-gate/*` shape.
15
+ mountedRoutes = false;
16
+ constructor(opts = {}) {
17
+ let origin = opts.walletOrigin?.trim();
18
+ if (!origin) {
19
+ // Zero-config: default to localhost so the getting-started example just runs.
20
+ origin = DEFAULT_WALLET_ORIGIN;
21
+ }
22
+ else if (!/^https?:\/\//.test(origin)) {
23
+ // Wallet ceremonies are origin-bound, so a scheme-less value can't work.
24
+ // Warn and fall back rather than hard-failing (DX over a thrown error).
25
+ console.warn(`[attestomcp] walletOrigin "${origin}" is not an absolute http(s) origin; using ${DEFAULT_WALLET_ORIGIN}. ` +
26
+ `Pass an absolute origin (e.g. https://shop.example) for any deployed environment.`);
27
+ origin = DEFAULT_WALLET_ORIGIN;
28
+ }
29
+ // OpenID4VP / WebAuthn are origin-bound, so a localhost origin in production
30
+ // mints approve links a buyer's phone can't reach. Warn loudly — not fatal.
31
+ if (process.env.NODE_ENV === "production" && /^https?:\/\/(localhost|127\.0\.0\.1)/.test(origin)) {
32
+ console.warn(`[attestomcp] walletOrigin is ${origin} in production — buyers can't open localhost approve links. ` +
33
+ `Set { walletOrigin } to your public origin.`);
34
+ }
35
+ this.walletOrigin = origin.replace(/\/$/, "");
36
+ this.store = opts.store ?? new MemoryVerificationStore();
37
+ }
38
+ /**
39
+ * Context 1 — resolve a policy against a server-priced order into the flat,
40
+ * JSON-safe `requires` manifest. Runs `.when()`/`appliesTo` predicates,
41
+ * payment-last; no functions cross the wire.
42
+ */
43
+ requirements(order, policy) {
44
+ return resolveRequirements(order, policy, { walletOrigin: this.walletOrigin, mountedRoutes: this.mountedRoutes });
45
+ }
46
+ /**
47
+ * Context 2 — wire the verification ceremony onto your Express app.
48
+ *
49
+ * Pass the ceremony seams (`{ orderStore, catalog, completion, signingKey, … }`)
50
+ * to register the gate's routes through `mountCeremony`: it validates the seams,
51
+ * FAILS FAST on a missing required one (CT2), and attaches each rail. AttestoMcp's
52
+ * own per-order store is injected as the `verificationStore` (keyed by order id,
53
+ * never process-global — Security invariant 4), so the host never passes it.
54
+ *
55
+ * Called WITHOUT seams it keeps the v0.1 behavior: expose the per-order store
56
+ * via `app.locals.attestomcp` so a host's existing fail-closed `/credential-gate/*`
57
+ * routes resolve verification state THROUGH AttestoMcp. The rails register only
58
+ * when seams are supplied; with none extracted yet, that path attaches no routes.
59
+ */
60
+ mount(app, ceremony) {
61
+ if (ceremony) {
62
+ mountCeremony(app, { ...ceremony, verificationStore: this.store });
63
+ this.mountedRoutes = true;
64
+ return;
65
+ }
66
+ // Zero-arg compose (the quickstart): a host (e.g. attestomcp-storefront) has
67
+ // already populated the ceremony seams on `app.locals.attestomcp`. Wire the rails
68
+ // straight from those seams — including the host's OWN verificationStore when it
69
+ // supplied one, so its `completion` seam shares the exact per-order state the
70
+ // rails write (invariant 4). Falls back to AttestoMcp's own store otherwise.
71
+ const locals = (app.locals.attestomcp ?? {});
72
+ if (locals.orderStore && locals.catalog && locals.completion) {
73
+ mountCeremony(app, locals.verificationStore ? {} : { verificationStore: this.store });
74
+ this.mountedRoutes = true;
75
+ return;
76
+ }
77
+ // Legacy (no seams): expose the per-order store so a host's existing
78
+ // fail-closed routes resolve verification THROUGH AttestoMcp.
79
+ const existing = app.locals.attestomcp;
80
+ if (existing?.store === this.store)
81
+ return; // idempotent
82
+ app.locals.attestomcp = { store: this.store, walletOrigin: this.walletOrigin };
83
+ }
84
+ }
@@ -0,0 +1,48 @@
1
+ import type { Credential, DcqlQuery, Effect, GateOrder, Step } from "./types.js";
2
+ export declare function gate(): Effect;
3
+ export declare function discount(opts: {
4
+ percent?: number;
5
+ amount?: number;
6
+ }): Effect;
7
+ export declare function authorize(): Effect;
8
+ /**
9
+ * Concise DCQL for a single mdoc credential: name the doctype and the claim
10
+ * leaves, get back the full verifier-shaped `DcqlQuery`. Selective disclosure
11
+ * (never retain) is the default.
12
+ */
13
+ export declare function dcql(spec: {
14
+ docType: string;
15
+ claims: string[];
16
+ }): DcqlQuery;
17
+ /** Age verification. `age.over(21)` proves the `age_over_21` claim (explicit positive). */
18
+ export declare const age: {
19
+ over(minAge: number): Credential;
20
+ };
21
+ /** Loyalty / membership — optional; presenting it applies a discount. */
22
+ export declare const membership: {
23
+ discount(percent: number): Credential;
24
+ };
25
+ /**
26
+ * Payment authorization. Settles LAST (the resolver sorts authorize-effect
27
+ * entries to the end). The amount is derived from the order server-side, never
28
+ * passed as a field (Principle IV / Security invariant 2).
29
+ */
30
+ export declare const payment: {
31
+ in(currency: string): Credential;
32
+ };
33
+ /** Define a custom credential — gate ANY consequential action with ANY credential. */
34
+ export declare function defineCredential(c: {
35
+ id: string;
36
+ request: DcqlQuery;
37
+ verify: (claims: Record<string, unknown>) => boolean;
38
+ effect: Effect;
39
+ appliesTo?: (order: GateOrder) => boolean;
40
+ ui: {
41
+ label: string;
42
+ action: string;
43
+ };
44
+ }): Credential;
45
+ /** A required gate — present in the manifest whenever it applies. */
46
+ export declare function required(c: Credential): Step;
47
+ /** An optional gate — surfaced but never blocking. */
48
+ export declare function optional(c: Credential): Step;