@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.
- package/README.md +97 -0
- package/dist/accounts.d.ts +6 -0
- package/dist/accounts.js +6 -1
- package/dist/agent.d.ts +184 -8
- package/dist/agent.js +351 -3
- package/dist/agentPolicy.d.ts +238 -0
- package/dist/agentPolicy.js +364 -0
- package/dist/attestation.d.ts +25 -0
- package/dist/attestation.js +25 -0
- package/dist/checkout.d.ts +216 -0
- package/dist/checkout.js +327 -0
- package/dist/credentials.d.ts +466 -0
- package/dist/credentials.js +669 -0
- package/dist/gate.d.ts +94 -4
- package/dist/gate.js +206 -7
- package/dist/gateClient.d.ts +12 -0
- package/dist/giftLink.d.ts +272 -0
- package/dist/giftLink.js +452 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +32 -0
- package/dist/loginButton.d.ts +69 -0
- package/dist/loginButton.js +207 -0
- package/dist/logo.d.ts +1 -0
- package/dist/logo.js +8 -0
- package/dist/messaging.d.ts +439 -0
- package/dist/messaging.js +979 -0
- package/dist/nsGate.d.ts +98 -0
- package/dist/nsGate.js +172 -0
- package/dist/oidc.d.ts +272 -0
- package/dist/oidc.js +641 -0
- package/dist/pay.d.ts +513 -0
- package/dist/pay.js +756 -0
- package/dist/payment.d.ts +9 -2
- package/dist/payment.js +9 -2
- package/dist/recovery.d.ts +186 -0
- package/dist/recovery.js +276 -0
- package/dist/recoveryNft.d.ts +183 -0
- package/dist/recoveryNft.js +291 -0
- package/dist/shield.d.ts +373 -0
- package/dist/shield.js +695 -0
- package/dist/subscription.d.ts +439 -0
- package/dist/subscription.js +642 -0
- package/dist/textRecords.d.ts +52 -6
- package/dist/textRecords.js +80 -8
- package/dist/types.d.ts +3 -0
- package/dist/x402Receipt.d.ts +246 -0
- package/dist/x402Receipt.js +374 -0
- package/package.json +18 -1
- package/schema/agent-manifest.json +91 -8
package/dist/checkout.js
ADDED
|
@@ -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
|
+
}
|