@x1id/resolve 0.23.0 → 0.44.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 (49) hide show
  1. package/README.md +97 -0
  2. package/dist/accounts.d.ts +6 -0
  3. package/dist/accounts.js +6 -1
  4. package/dist/agent.d.ts +184 -8
  5. package/dist/agent.js +351 -3
  6. package/dist/agentPolicy.d.ts +238 -0
  7. package/dist/agentPolicy.js +364 -0
  8. package/dist/attestation.d.ts +25 -0
  9. package/dist/attestation.js +25 -0
  10. package/dist/checkout.d.ts +216 -0
  11. package/dist/checkout.js +327 -0
  12. package/dist/credentials.d.ts +466 -0
  13. package/dist/credentials.js +669 -0
  14. package/dist/gate.d.ts +94 -4
  15. package/dist/gate.js +206 -7
  16. package/dist/gateClient.d.ts +12 -0
  17. package/dist/giftLink.d.ts +272 -0
  18. package/dist/giftLink.js +452 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.js +32 -0
  21. package/dist/loginButton.d.ts +69 -0
  22. package/dist/loginButton.js +207 -0
  23. package/dist/logo.d.ts +1 -0
  24. package/dist/logo.js +8 -0
  25. package/dist/messaging.d.ts +439 -0
  26. package/dist/messaging.js +979 -0
  27. package/dist/nsGate.d.ts +98 -0
  28. package/dist/nsGate.js +172 -0
  29. package/dist/oidc.d.ts +272 -0
  30. package/dist/oidc.js +641 -0
  31. package/dist/pay.d.ts +513 -0
  32. package/dist/pay.js +756 -0
  33. package/dist/payment.d.ts +9 -2
  34. package/dist/payment.js +9 -2
  35. package/dist/recovery.d.ts +186 -0
  36. package/dist/recovery.js +276 -0
  37. package/dist/recoveryNft.d.ts +183 -0
  38. package/dist/recoveryNft.js +291 -0
  39. package/dist/shield.d.ts +373 -0
  40. package/dist/shield.js +695 -0
  41. package/dist/subscription.d.ts +439 -0
  42. package/dist/subscription.js +642 -0
  43. package/dist/textRecords.d.ts +52 -6
  44. package/dist/textRecords.js +80 -8
  45. package/dist/types.d.ts +3 -0
  46. package/dist/x402Receipt.d.ts +246 -0
  47. package/dist/x402Receipt.js +374 -0
  48. package/package.json +18 -1
  49. package/schema/agent-manifest.json +91 -8
@@ -0,0 +1,327 @@
1
+ /**
2
+ * X1ID Pay checkout widget ([Pay 7], docs/x1id-pay-checkout.md): the
3
+ * postMessage protocol between a merchant page and the X1 Pay invoice page (app.x1pay.me),
4
+ * plus the merchant-side controller that opens the checkout popup.
5
+ *
6
+ * The widget only PAYS an invoice that already exists on chain. The merchant
7
+ * creates the invoice (itself, or with a merchant-delegate key on its server),
8
+ * opens checkout for it, and then verifies payment SERVER-SIDE by reading the
9
+ * Invoice account ({@link verifyInvoicePaid} in pay.ts). What this protocol
10
+ * reports back to the merchant's browser is a UX hint only: a browser result
11
+ * can be forged by anyone who controls the merchant's page, so it must never
12
+ * release goods by itself.
13
+ *
14
+ * Flow (all messages are plain JSON-able objects, see {@link CheckoutMessage}):
15
+ *
16
+ * merchant page (origin M) popup: X1 Pay app (origin A)
17
+ * ─────────────────────── ──────────────────────────
18
+ * window.open(A/invoice?address=…&embed=1&origin=M&sid=S)
19
+ * ◀── ready {sid, invoice} (targetOrigin M)
20
+ * ack {sid} (targetOrigin A) ──▶
21
+ * ◀── result {sid, invoice, status, signature}
22
+ * popup closed without a result → status "closed" (detected locally)
23
+ *
24
+ * Origin checks on BOTH sides:
25
+ * - the popup posts only to `window.opener` with `targetOrigin` = the
26
+ * merchant origin it was opened for, so the browser drops the message if the
27
+ * opener is anything else; it trusts an `ack` only when `event.source` is
28
+ * its opener and `event.origin` is exactly that origin;
29
+ * - the merchant accepts a message only when `event.source` is the popup it
30
+ * opened, `event.origin` is exactly the X1 Pay app origin, and the session id
31
+ * (`sid`, 128 random bits per checkout) and invoice match.
32
+ *
33
+ * No iframe mode: the X1 Pay app keeps `frame-ancestors 'none'` /
34
+ * `X-Frame-Options: DENY` (clickjacking), and wallet extensions work in a
35
+ * top-level window.
36
+ *
37
+ * Dependency-free on purpose (no SDK imports), so the browser widget bundle
38
+ * stays tiny.
39
+ */
40
+ /** Protocol tag on every message. */
41
+ export const CHECKOUT_PROTOCOL = "x1id-checkout";
42
+ /** Protocol version on every message. */
43
+ export const CHECKOUT_VERSION = 1;
44
+ /** Where the hosted checkout lives. */
45
+ export const DEFAULT_CHECKOUT_APP_ORIGIN = "https://app.x1pay.me";
46
+ // ---------------------------------------------------------------------------
47
+ // validation
48
+ // ---------------------------------------------------------------------------
49
+ const B58 = "[1-9A-HJ-NP-Za-km-z]";
50
+ const ADDRESS_RE = new RegExp(`^${B58}{32,44}$`);
51
+ const SIGNATURE_RE = new RegExp(`^${B58}{64,88}$`);
52
+ const SID_RE = /^[0-9a-f]{32}$/;
53
+ /** Shape check for a base58 account address (32-44 chars). */
54
+ export function isCheckoutAddress(s) {
55
+ return typeof s === "string" && ADDRESS_RE.test(s);
56
+ }
57
+ /** Shape check for a base58 transaction signature (64-88 chars). */
58
+ export function isCheckoutSignature(s) {
59
+ return typeof s === "string" && SIGNATURE_RE.test(s);
60
+ }
61
+ /** A checkout session id: 32 lowercase hex chars (128 bits). */
62
+ export function isCheckoutSid(s) {
63
+ return typeof s === "string" && SID_RE.test(s);
64
+ }
65
+ /** 128 fresh random bits as a checkout session id (CSPRNG). */
66
+ export function newCheckoutSid() {
67
+ const c = globalThis.crypto;
68
+ if (!c?.getRandomValues)
69
+ throw new Error("No CSPRNG available (globalThis.crypto.getRandomValues)");
70
+ return Array.from(c.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
71
+ }
72
+ const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
73
+ /**
74
+ * A web origin the protocol will talk to, or null. It must be exactly an
75
+ * origin (`scheme://host[:port]`, no path, query, fragment or credentials),
76
+ * `https:`, or `http:` on a loopback host for local development. `"null"`
77
+ * (sandboxed / `file:` pages) is refused: messages could not be pinned to it.
78
+ */
79
+ export function normalizeCheckoutOrigin(raw) {
80
+ if (typeof raw !== "string" || raw.length === 0 || raw.length > 255)
81
+ return null;
82
+ let u;
83
+ try {
84
+ u = new URL(raw);
85
+ }
86
+ catch {
87
+ return null;
88
+ }
89
+ if (u.origin === "null" || u.origin !== raw)
90
+ return null;
91
+ if (u.protocol === "https:")
92
+ return u.origin;
93
+ if (u.protocol === "http:" && LOOPBACK.has(u.hostname))
94
+ return u.origin;
95
+ return null;
96
+ }
97
+ function isPlainObject(v) {
98
+ if (typeof v !== "object" || v === null || Array.isArray(v))
99
+ return false;
100
+ const proto = Object.getPrototypeOf(v);
101
+ return proto === Object.prototype || proto === null;
102
+ }
103
+ /**
104
+ * Parse an incoming `event.data` as a protocol message, or null. Strict:
105
+ * right protocol tag and version, a known `type`, only the fields that type
106
+ * carries, each well-formed. Says nothing about WHO sent it: check
107
+ * `event.origin` and `event.source` too (see {@link acceptCheckoutResult} and
108
+ * {@link acceptCheckoutAck}).
109
+ */
110
+ export function parseCheckoutMessage(data) {
111
+ if (!isPlainObject(data))
112
+ return null;
113
+ if (data.protocol !== CHECKOUT_PROTOCOL || data.version !== CHECKOUT_VERSION)
114
+ return null;
115
+ if (!isCheckoutSid(data.sid))
116
+ return null;
117
+ const keys = Object.keys(data).sort().join(",");
118
+ switch (data.type) {
119
+ case "ready":
120
+ if (keys !== "invoice,protocol,sid,type,version" || !isCheckoutAddress(data.invoice))
121
+ return null;
122
+ return { protocol: CHECKOUT_PROTOCOL, version: CHECKOUT_VERSION, type: "ready", sid: data.sid, invoice: data.invoice };
123
+ case "ack":
124
+ if (keys !== "protocol,sid,type,version")
125
+ return null;
126
+ return { protocol: CHECKOUT_PROTOCOL, version: CHECKOUT_VERSION, type: "ack", sid: data.sid };
127
+ case "result": {
128
+ if (keys !== "invoice,protocol,sid,signature,status,type,version" || !isCheckoutAddress(data.invoice))
129
+ return null;
130
+ const status = data.status;
131
+ if (status !== "paid" && status !== "cancelled")
132
+ return null;
133
+ const signature = data.signature;
134
+ if (signature !== null && !isCheckoutSignature(signature))
135
+ return null;
136
+ // Only a payment carries a signature.
137
+ if (status === "cancelled" && signature !== null)
138
+ return null;
139
+ return {
140
+ protocol: CHECKOUT_PROTOCOL,
141
+ version: CHECKOUT_VERSION,
142
+ type: "result",
143
+ sid: data.sid,
144
+ invoice: data.invoice,
145
+ status,
146
+ signature,
147
+ };
148
+ }
149
+ default:
150
+ return null;
151
+ }
152
+ }
153
+ export function checkoutReadyMessage(sid, invoice) {
154
+ if (!isCheckoutSid(sid))
155
+ throw new Error("bad checkout sid");
156
+ if (!isCheckoutAddress(invoice))
157
+ throw new Error("bad invoice address");
158
+ return { protocol: CHECKOUT_PROTOCOL, version: CHECKOUT_VERSION, type: "ready", sid, invoice };
159
+ }
160
+ export function checkoutAckMessage(sid) {
161
+ if (!isCheckoutSid(sid))
162
+ throw new Error("bad checkout sid");
163
+ return { protocol: CHECKOUT_PROTOCOL, version: CHECKOUT_VERSION, type: "ack", sid };
164
+ }
165
+ export function checkoutResultMessage(p) {
166
+ const msg = {
167
+ protocol: CHECKOUT_PROTOCOL,
168
+ version: CHECKOUT_VERSION,
169
+ type: "result",
170
+ sid: p.sid,
171
+ invoice: p.invoice,
172
+ status: p.status,
173
+ signature: p.status === "paid" ? p.signature : null,
174
+ };
175
+ if (!parseCheckoutMessage(msg))
176
+ throw new Error("bad checkout result");
177
+ return msg;
178
+ }
179
+ /**
180
+ * Merchant side: the result carried by `ev`, or null unless it comes from
181
+ * exactly `expect.source` (the popup window this page opened) at exactly
182
+ * `expect.appOrigin`, for this checkout's `sid` and `invoice`.
183
+ */
184
+ export function acceptCheckoutResult(ev, expect) {
185
+ if (expect.source == null || ev.source !== expect.source || ev.origin !== expect.appOrigin)
186
+ return null;
187
+ const msg = parseCheckoutMessage(ev.data);
188
+ if (!msg || msg.type !== "result" || msg.sid !== expect.sid || msg.invoice !== expect.invoice)
189
+ return null;
190
+ return { invoice: msg.invoice, signature: msg.signature, status: msg.status };
191
+ }
192
+ /**
193
+ * Popup side: true when `ev` is this checkout's `ack`, sent by the window that
194
+ * opened the popup (`expect.opener`) from exactly `expect.origin`.
195
+ */
196
+ export function acceptCheckoutAck(ev, expect) {
197
+ if (expect.opener == null || ev.source !== expect.opener || ev.origin !== expect.origin)
198
+ return false;
199
+ const msg = parseCheckoutMessage(ev.data);
200
+ return !!msg && msg.type === "ack" && msg.sid === expect.sid;
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // URLs
204
+ // ---------------------------------------------------------------------------
205
+ /** The hosted checkout URL for an invoice. */
206
+ export function buildCheckoutUrl(p) {
207
+ const app = normalizeCheckoutOrigin(p.appOrigin ?? DEFAULT_CHECKOUT_APP_ORIGIN);
208
+ if (!app)
209
+ throw new Error("appOrigin must be an https origin");
210
+ const origin = normalizeCheckoutOrigin(p.origin);
211
+ if (!origin)
212
+ throw new Error("checkout needs an https page (or http://localhost); this page's origin cannot receive results");
213
+ if (!isCheckoutAddress(p.invoice))
214
+ throw new Error("invoice must be a base58 invoice address");
215
+ if (!isCheckoutSid(p.sid))
216
+ throw new Error("bad checkout sid");
217
+ const q = new URLSearchParams({ address: p.invoice, embed: "1", origin, sid: p.sid });
218
+ return `${app}/invoice?${q.toString()}`;
219
+ }
220
+ /** Popup side: the checkout parameters of an invoice page URL, or null when it
221
+ * is not an embedded checkout (or any parameter is malformed). */
222
+ export function parseCheckoutParams(get) {
223
+ if (get("embed") !== "1")
224
+ return null;
225
+ const origin = normalizeCheckoutOrigin(get("origin"));
226
+ const sid = get("sid");
227
+ if (!origin || !isCheckoutSid(sid))
228
+ return null;
229
+ return { origin, sid };
230
+ }
231
+ /** Thrown by {@link openCheckout} when the browser blocked the popup (call it
232
+ * straight from a click handler). */
233
+ export class CheckoutPopupBlockedError extends Error {
234
+ constructor() {
235
+ super("The browser blocked the X1 Pay checkout window. Open checkout from a click handler.");
236
+ this.name = "CheckoutPopupBlockedError";
237
+ }
238
+ }
239
+ /**
240
+ * Open the hosted checkout for `opts.invoice` in a popup and listen for its
241
+ * result. Call it synchronously from a user gesture (a click), or the browser
242
+ * blocks the popup ({@link CheckoutPopupBlockedError}).
243
+ *
244
+ * The result is a HINT for your UI. Before you ship anything, have your server
245
+ * call `verifyInvoicePaid` with the invoice address and terms YOU stored when
246
+ * you created the invoice.
247
+ */
248
+ export function openCheckout(host, opts) {
249
+ const appOrigin = normalizeCheckoutOrigin(opts.appOrigin ?? DEFAULT_CHECKOUT_APP_ORIGIN);
250
+ if (!appOrigin)
251
+ throw new Error("appOrigin must be an https origin");
252
+ const sid = opts.sid ?? newCheckoutSid();
253
+ const url = buildCheckoutUrl({ invoice: opts.invoice, origin: host.location.origin, sid, appOrigin });
254
+ const width = opts.width ?? 440;
255
+ const height = opts.height ?? 720;
256
+ const left = Math.max(0, Math.round((host.screenX ?? 0) + ((host.outerWidth ?? width) - width) / 2));
257
+ const top = Math.max(0, Math.round((host.screenY ?? 0) + ((host.outerHeight ?? height) - height) / 2));
258
+ // NOT `noopener`: the popup reports back through window.opener.
259
+ const popup = host.open(url, `x1id-checkout-${sid}`, `popup=yes,width=${width},height=${height},left=${left},top=${top}`);
260
+ if (!popup)
261
+ throw new CheckoutPopupBlockedError();
262
+ let settled = false;
263
+ let closedSeen = false;
264
+ let resolveFn;
265
+ let rejectFn;
266
+ const result = new Promise((res, rej) => {
267
+ resolveFn = res;
268
+ rejectFn = rej;
269
+ });
270
+ const listener = (ev) => {
271
+ if (settled || ev.source !== popup || ev.origin !== appOrigin)
272
+ return;
273
+ const msg = parseCheckoutMessage(ev.data);
274
+ if (!msg || msg.sid !== sid)
275
+ return;
276
+ if (msg.type === "ready" && msg.invoice === opts.invoice) {
277
+ popup.postMessage(checkoutAckMessage(sid), appOrigin);
278
+ opts.onReady?.();
279
+ return;
280
+ }
281
+ const r = acceptCheckoutResult(ev, { appOrigin, source: popup, sid, invoice: opts.invoice });
282
+ if (r)
283
+ finish(r);
284
+ };
285
+ const timer = host.setInterval(() => {
286
+ if (settled)
287
+ return;
288
+ if (!popup.closed)
289
+ return;
290
+ // Give a result the popup posted just before closing one more tick to
291
+ // arrive (message delivery is queued) before reporting "closed".
292
+ if (!closedSeen) {
293
+ closedSeen = true;
294
+ return;
295
+ }
296
+ finish({ invoice: opts.invoice, signature: null, status: "closed" });
297
+ }, opts.pollMs ?? 400);
298
+ function cleanup() {
299
+ settled = true;
300
+ host.removeEventListener("message", listener);
301
+ host.clearInterval(timer);
302
+ }
303
+ function finish(r) {
304
+ if (settled)
305
+ return;
306
+ cleanup();
307
+ resolveFn(r);
308
+ }
309
+ host.addEventListener("message", listener);
310
+ popup.focus?.();
311
+ return {
312
+ sid,
313
+ result,
314
+ abort() {
315
+ if (settled)
316
+ return;
317
+ cleanup();
318
+ try {
319
+ popup.close();
320
+ }
321
+ catch {
322
+ /* already gone */
323
+ }
324
+ rejectFn(new Error("checkout aborted"));
325
+ },
326
+ };
327
+ }