@bwalletx/connect 0.3.0 → 0.4.1
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/CHANGELOG.md +25 -0
- package/README.md +145 -4
- package/dist/bwalletx-connect.global.js +41 -3
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/chunk-7NAKOJGZ.js +442 -0
- package/dist/chunk-7NAKOJGZ.js.map +1 -0
- package/dist/chunk-FUQ46RJV.js +404 -0
- package/dist/chunk-FUQ46RJV.js.map +1 -0
- package/dist/index.cjs +425 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +23 -62
- package/dist/index.d.ts +23 -62
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +415 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +48 -0
- package/dist/next.d.ts +48 -0
- package/dist/next.js +107 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +586 -36
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +74 -28
- package/dist/react.d.ts +74 -28
- package/dist/react.js +175 -3
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +339 -4
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +204 -2
- package/dist/server.d.ts +204 -2
- package/dist/server.js +36 -107
- package/dist/server.js.map +1 -1
- package/dist/wallets-VOKIKBbO.d.cts +250 -0
- package/dist/wallets-VOKIKBbO.d.ts +250 -0
- package/package.json +38 -13
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { ProtoWallet, PublicKey } from "@bsv/sdk";
|
|
3
|
+
|
|
4
|
+
// src/nonce-stores.ts
|
|
5
|
+
var ttlSeconds = (c) => Math.max(1, Math.ceil((c.expiresAt - Date.now()) / 1e3));
|
|
6
|
+
function parse(v) {
|
|
7
|
+
if (v == null) return null;
|
|
8
|
+
try {
|
|
9
|
+
const c = typeof v === "string" ? JSON.parse(v) : v;
|
|
10
|
+
if (!c || typeof c.identityKey !== "string" || typeof c.message !== "string" || typeof c.expiresAt !== "number") return null;
|
|
11
|
+
return c.expiresAt < Date.now() ? null : c;
|
|
12
|
+
} catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function redisStore(opts) {
|
|
17
|
+
const { client } = opts;
|
|
18
|
+
const prefix = opts.prefix ?? "bwalletx:nonce:";
|
|
19
|
+
const getdel = client.getdel?.bind(client) ?? client.getDel?.bind(client);
|
|
20
|
+
if (!getdel) throw new Error("redisStore: the client needs getdel (or getDel). Redis 6.2+.");
|
|
21
|
+
return {
|
|
22
|
+
async put(nonce, c) {
|
|
23
|
+
const s = ttlSeconds(c);
|
|
24
|
+
if (opts.flavor === "ioredis") await client.set(prefix + nonce, JSON.stringify(c), "EX", s);
|
|
25
|
+
else await client.set(prefix + nonce, JSON.stringify(c), { ex: s, EX: s });
|
|
26
|
+
},
|
|
27
|
+
async take(nonce) {
|
|
28
|
+
return parse(await getdel(prefix + nonce));
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function pgNonceTableSql(table = "bwalletx_nonces") {
|
|
33
|
+
const t = safeTable(table);
|
|
34
|
+
return `create table if not exists ${t} (
|
|
35
|
+
nonce text primary key,
|
|
36
|
+
challenge jsonb not null,
|
|
37
|
+
expires_at timestamptz not null
|
|
38
|
+
);
|
|
39
|
+
create index if not exists ${t.replace(".", "_")}_expires_idx on ${t} (expires_at);`;
|
|
40
|
+
}
|
|
41
|
+
function safeTable(t) {
|
|
42
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t)) throw new Error(`pgStore: bad table name ${t}`);
|
|
43
|
+
return t;
|
|
44
|
+
}
|
|
45
|
+
function pgStore(opts) {
|
|
46
|
+
const t = safeTable(opts.table ?? "bwalletx_nonces");
|
|
47
|
+
const rows = (r) => Array.isArray(r) ? r : r.rows;
|
|
48
|
+
return {
|
|
49
|
+
async put(nonce, c) {
|
|
50
|
+
await opts.query(`delete from ${t} where expires_at < now()`, []);
|
|
51
|
+
await opts.query(`insert into ${t} (nonce, challenge, expires_at) values ($1, $2, to_timestamp($3 / 1000.0))`, [
|
|
52
|
+
nonce,
|
|
53
|
+
JSON.stringify(c),
|
|
54
|
+
c.expiresAt
|
|
55
|
+
]);
|
|
56
|
+
},
|
|
57
|
+
async take(nonce) {
|
|
58
|
+
const r = rows(await opts.query(`delete from ${t} where nonce = $1 returning challenge`, [nonce]));
|
|
59
|
+
const row = r[0];
|
|
60
|
+
return parse(row?.challenge);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/session.ts
|
|
66
|
+
var MIN_SECRET_LENGTH = 32;
|
|
67
|
+
var enc = new TextEncoder();
|
|
68
|
+
var dec = new TextDecoder();
|
|
69
|
+
function b64u(bytes) {
|
|
70
|
+
let s = "";
|
|
71
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
72
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
73
|
+
}
|
|
74
|
+
function unb64u(s) {
|
|
75
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error("bad base64url");
|
|
76
|
+
const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
|
|
77
|
+
return Uint8Array.from(b, (c) => c.charCodeAt(0));
|
|
78
|
+
}
|
|
79
|
+
function assertSecret(secret) {
|
|
80
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[@bwalletx/connect] A session secret of at least ${MIN_SECRET_LENGTH} characters is required. Set it in an environment variable (e.g. BWALLETX_SESSION_SECRET). There is no default.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
var keys = /* @__PURE__ */ new Map();
|
|
87
|
+
function hmacKey(secret) {
|
|
88
|
+
let k = keys.get(secret);
|
|
89
|
+
if (!k) {
|
|
90
|
+
k = globalThis.crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
91
|
+
keys.set(secret, k);
|
|
92
|
+
}
|
|
93
|
+
return k;
|
|
94
|
+
}
|
|
95
|
+
async function signSession(claims, opts) {
|
|
96
|
+
assertSecret(opts.secret);
|
|
97
|
+
if (typeof claims.sub !== "string" || !claims.sub) throw new Error("session needs a sub (identity key)");
|
|
98
|
+
const iat = Math.floor((opts.now ?? Date.now()) / 1e3);
|
|
99
|
+
const payload = { ...claims, sub: claims.sub, iat, exp: iat + (opts.maxAge ?? 7 * 24 * 3600) };
|
|
100
|
+
const body = b64u(enc.encode(JSON.stringify(payload)));
|
|
101
|
+
const sig = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(opts.secret), enc.encode(body)));
|
|
102
|
+
return `${body}.${b64u(sig)}`;
|
|
103
|
+
}
|
|
104
|
+
async function verifySession(token, opts) {
|
|
105
|
+
assertSecret(opts.secret);
|
|
106
|
+
if (typeof token !== "string" || token.length > 8192) return null;
|
|
107
|
+
const dot = token.indexOf(".");
|
|
108
|
+
if (dot < 1 || dot !== token.lastIndexOf(".")) return null;
|
|
109
|
+
const body = token.slice(0, dot);
|
|
110
|
+
try {
|
|
111
|
+
const sig = unb64u(token.slice(dot + 1));
|
|
112
|
+
const ok = await globalThis.crypto.subtle.verify("HMAC", await hmacKey(opts.secret), sig, enc.encode(body));
|
|
113
|
+
if (!ok) return null;
|
|
114
|
+
const p = JSON.parse(dec.decode(unb64u(body)));
|
|
115
|
+
if (!p || typeof p.sub !== "string" || typeof p.exp !== "number") return null;
|
|
116
|
+
if (Math.floor((opts.now ?? Date.now()) / 1e3) >= p.exp) return null;
|
|
117
|
+
return p;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function serializeCookie(value, o) {
|
|
123
|
+
const parts = [`${o.name}=${value}`, `Path=${o.path ?? "/"}`, `Max-Age=${o.maxAge}`, "HttpOnly", `SameSite=${o.sameSite ?? "Lax"}`];
|
|
124
|
+
if (o.secure !== false) parts.push("Secure");
|
|
125
|
+
if (o.domain) parts.push(`Domain=${o.domain}`);
|
|
126
|
+
return parts.join("; ");
|
|
127
|
+
}
|
|
128
|
+
function readCookie(header, name) {
|
|
129
|
+
if (!header) return null;
|
|
130
|
+
for (const part of header.split(";")) {
|
|
131
|
+
const i = part.indexOf("=");
|
|
132
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/verify-payment.ts
|
|
138
|
+
import { P2PKH, Transaction, Utils } from "@bsv/sdk";
|
|
139
|
+
var PaymentVerificationError = class extends Error {
|
|
140
|
+
constructor(code, message, txid) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.code = code;
|
|
143
|
+
this.txid = txid;
|
|
144
|
+
this.name = "PaymentVerificationError";
|
|
145
|
+
}
|
|
146
|
+
code;
|
|
147
|
+
txid;
|
|
148
|
+
};
|
|
149
|
+
var isHex = (s) => /^([0-9a-fA-F]{2})+$/.test(s);
|
|
150
|
+
var woc = (n) => `https://api.whatsonchain.com/v1/bsv/${n}`;
|
|
151
|
+
function parseTx(input) {
|
|
152
|
+
const bytes = typeof input === "string" ? Utils.toArray(input, "hex") : Array.from(input);
|
|
153
|
+
const tries = [
|
|
154
|
+
() => Transaction.fromAtomicBEEF(bytes),
|
|
155
|
+
() => Transaction.fromBEEF(bytes),
|
|
156
|
+
() => Transaction.fromBinary(bytes)
|
|
157
|
+
];
|
|
158
|
+
for (const t of tries) {
|
|
159
|
+
try {
|
|
160
|
+
return t();
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new PaymentVerificationError("BAD_TX", "Could not parse the transaction (expected a txid, raw tx, BEEF or Atomic BEEF).");
|
|
165
|
+
}
|
|
166
|
+
function expectedScript(e) {
|
|
167
|
+
if (!Number.isSafeInteger(e.satoshis) || e.satoshis < 0) throw new PaymentVerificationError("BAD_EXPECTED", "expected.satoshis must be a non-negative integer");
|
|
168
|
+
if (e.script) {
|
|
169
|
+
if (!isHex(e.script)) throw new PaymentVerificationError("BAD_EXPECTED", "expected.script must be hex");
|
|
170
|
+
return e.script.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
if (e.address) {
|
|
173
|
+
try {
|
|
174
|
+
return new P2PKH().lock(e.address).toHex();
|
|
175
|
+
} catch {
|
|
176
|
+
throw new PaymentVerificationError("BAD_EXPECTED", `Not a valid address: ${e.address}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
throw new PaymentVerificationError("BAD_EXPECTED", "Each expected output needs a script or an address");
|
|
180
|
+
}
|
|
181
|
+
async function getJson(f, url, headers = {}) {
|
|
182
|
+
let r;
|
|
183
|
+
try {
|
|
184
|
+
r = await f(url, { headers });
|
|
185
|
+
} catch (e) {
|
|
186
|
+
throw new PaymentVerificationError("NETWORK", `Could not reach ${new URL(url).host}: ${e.message}`);
|
|
187
|
+
}
|
|
188
|
+
const text = await r.text();
|
|
189
|
+
let body = text;
|
|
190
|
+
try {
|
|
191
|
+
body = JSON.parse(text);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
return { status: r.status, body };
|
|
195
|
+
}
|
|
196
|
+
async function checkStatus(o, f, txid) {
|
|
197
|
+
const mode = o.status ?? "woc";
|
|
198
|
+
if (mode === "none") return { state: "unchecked" };
|
|
199
|
+
if (mode === "arc") {
|
|
200
|
+
if (!o.arcUrl) throw new PaymentVerificationError("NETWORK", "status 'arc' needs arcUrl");
|
|
201
|
+
const { status: status2, body: body2 } = await getJson(f, `${o.arcUrl.replace(/\/$/, "")}/v1/tx/${txid}`, o.arcApiKey ? { Authorization: `Bearer ${o.arcApiKey}` } : {});
|
|
202
|
+
if (status2 === 404) return { state: "not-found", source: "arc" };
|
|
203
|
+
if (status2 >= 400) throw new PaymentVerificationError("NETWORK", `ARC answered ${status2}`, txid);
|
|
204
|
+
const b = body2;
|
|
205
|
+
const s = String(b.txStatus ?? "").toUpperCase();
|
|
206
|
+
if (s === "MINED" || s === "IMMUTABLE") return { state: "confirmed", confirmations: 1, source: "arc" };
|
|
207
|
+
if (s === "REJECTED" || s === "DOUBLE_SPEND_ATTEMPTED") return { state: "rejected", source: "arc", detail: b.extraInfo ?? s };
|
|
208
|
+
if (s === "UNKNOWN" || s === "") return { state: "not-found", source: "arc", detail: s };
|
|
209
|
+
return { state: "mempool", source: "arc", detail: s };
|
|
210
|
+
}
|
|
211
|
+
const { status, body } = await getJson(f, `${woc(o.network ?? "main")}/tx/hash/${txid}`);
|
|
212
|
+
if (status === 404) return { state: "not-found", source: "woc" };
|
|
213
|
+
if (status >= 400) throw new PaymentVerificationError("NETWORK", `WhatsOnChain answered ${status}`, txid);
|
|
214
|
+
const c = Number(body.confirmations ?? 0);
|
|
215
|
+
return c > 0 ? { state: "confirmed", confirmations: c, source: "woc" } : { state: "mempool", source: "woc" };
|
|
216
|
+
}
|
|
217
|
+
async function verifyPayment(o) {
|
|
218
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
219
|
+
if (!o.expected?.length) throw new PaymentVerificationError("BAD_EXPECTED", "Pass at least one expected output");
|
|
220
|
+
const want = o.expected.map((e) => ({ script: expectedScript(e), satoshis: e.satoshis }));
|
|
221
|
+
let tx;
|
|
222
|
+
if (typeof o.tx === "string" && /^[0-9a-fA-F]{64}$/.test(o.tx)) {
|
|
223
|
+
const txid2 = o.tx.toLowerCase();
|
|
224
|
+
let r;
|
|
225
|
+
try {
|
|
226
|
+
r = await f(`${woc(o.network ?? "main")}/tx/${txid2}/hex`);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
throw new PaymentVerificationError("NETWORK", `Could not reach WhatsOnChain: ${e.message}`, txid2);
|
|
229
|
+
}
|
|
230
|
+
if (r.status === 404) throw new PaymentVerificationError("TX_NOT_FOUND", `Transaction ${txid2} was not found`, txid2);
|
|
231
|
+
if (!r.ok) throw new PaymentVerificationError("NETWORK", `WhatsOnChain answered ${r.status}`, txid2);
|
|
232
|
+
tx = parseTx((await r.text()).trim());
|
|
233
|
+
if (tx.id("hex") !== txid2) throw new PaymentVerificationError("BAD_TX", "The fetched transaction does not match the txid", txid2);
|
|
234
|
+
} else {
|
|
235
|
+
if (typeof o.tx === "string" && !isHex(o.tx)) throw new PaymentVerificationError("BAD_TX", "tx must be a txid or hex");
|
|
236
|
+
tx = parseTx(o.tx);
|
|
237
|
+
}
|
|
238
|
+
const txid = tx.id("hex");
|
|
239
|
+
if (o.spv) {
|
|
240
|
+
let ok = false;
|
|
241
|
+
try {
|
|
242
|
+
ok = await tx.verify(o.spv);
|
|
243
|
+
} catch (e) {
|
|
244
|
+
throw new PaymentVerificationError("SPV_FAILED", `SPV check failed: ${e.message}`, txid);
|
|
245
|
+
}
|
|
246
|
+
if (!ok) throw new PaymentVerificationError("SPV_FAILED", "SPV check failed", txid);
|
|
247
|
+
}
|
|
248
|
+
const used = /* @__PURE__ */ new Set();
|
|
249
|
+
const matched = [];
|
|
250
|
+
for (const w of want) {
|
|
251
|
+
const vout = tx.outputs.findIndex(
|
|
252
|
+
(out, i) => !used.has(i) && out.lockingScript.toHex() === w.script && (out.satoshis ?? 0) >= w.satoshis
|
|
253
|
+
);
|
|
254
|
+
if (vout < 0) {
|
|
255
|
+
throw new PaymentVerificationError("OUTPUT_MISSING", `No output pays ${w.satoshis} sats to script ${w.script.slice(0, 16)}\u2026`, txid);
|
|
256
|
+
}
|
|
257
|
+
used.add(vout);
|
|
258
|
+
matched.push({ vout, satoshis: tx.outputs[vout].satoshis ?? 0, script: w.script });
|
|
259
|
+
}
|
|
260
|
+
const status = await checkStatus(o, f, txid);
|
|
261
|
+
if (status.state === "rejected") throw new PaymentVerificationError("REJECTED", `The network rejected ${txid}: ${status.detail ?? ""}`.trim(), txid);
|
|
262
|
+
if ((o.requireSeen || (o.minConfirmations ?? 0) > 0) && status.state !== "mempool" && status.state !== "confirmed") {
|
|
263
|
+
throw new PaymentVerificationError("NOT_SEEN", `The network has not seen ${txid} yet`, txid);
|
|
264
|
+
}
|
|
265
|
+
const conf = status.state === "confirmed" ? status.confirmations : 0;
|
|
266
|
+
if ((o.minConfirmations ?? 0) > conf) {
|
|
267
|
+
throw new PaymentVerificationError("NOT_SEEN", `${txid} has ${conf} confirmations, needs ${o.minConfirmations}`, txid);
|
|
268
|
+
}
|
|
269
|
+
return { ok: true, txid, matched, status };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/link.ts
|
|
273
|
+
var IdentityLinkConflictError = class extends Error {
|
|
274
|
+
constructor(identityKey, otherUserId) {
|
|
275
|
+
super("This bWalletX identity is already linked to a different account. Sign in with that account, or unlink it there first.");
|
|
276
|
+
this.identityKey = identityKey;
|
|
277
|
+
this.otherUserId = otherUserId;
|
|
278
|
+
this.name = "IdentityLinkConflictError";
|
|
279
|
+
}
|
|
280
|
+
identityKey;
|
|
281
|
+
otherUserId;
|
|
282
|
+
code = "IDENTITY_LINKED_TO_OTHER_USER";
|
|
283
|
+
};
|
|
284
|
+
var norm = (k) => {
|
|
285
|
+
if (typeof k !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(k)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
|
|
286
|
+
return k.toLowerCase();
|
|
287
|
+
};
|
|
288
|
+
async function linkIdentity(userId, identityKey, cb) {
|
|
289
|
+
if (!userId) throw new Error("linkIdentity needs the current user id");
|
|
290
|
+
const key = norm(identityKey);
|
|
291
|
+
const owner = await cb.findUserByIdentityKey(key);
|
|
292
|
+
if (owner && owner !== userId) throw new IdentityLinkConflictError(key, owner);
|
|
293
|
+
if (owner === userId) return { status: "already-linked", userId, identityKey: key };
|
|
294
|
+
await cb.attach(userId, key);
|
|
295
|
+
return { status: "linked", userId, identityKey: key };
|
|
296
|
+
}
|
|
297
|
+
async function signInOrLink(identityKey, o) {
|
|
298
|
+
const key = norm(identityKey);
|
|
299
|
+
if (o.currentUserId) return linkIdentity(o.currentUserId, key, o);
|
|
300
|
+
const owner = await o.findUserByIdentityKey(key);
|
|
301
|
+
if (owner) return { status: "signed-in", userId: owner, identityKey: key };
|
|
302
|
+
if (!o.createUser) return { status: "unknown", userId: null, identityKey: key };
|
|
303
|
+
const userId = await o.createUser(key);
|
|
304
|
+
await o.attach(userId, key);
|
|
305
|
+
return { status: "created", userId, identityKey: key };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/server.ts
|
|
309
|
+
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
310
|
+
var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
|
|
311
|
+
var warned = false;
|
|
312
|
+
function memoryStore(opts = {}) {
|
|
313
|
+
const prod = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
314
|
+
if ((prod || !opts.quiet) && !warned) {
|
|
315
|
+
warned = true;
|
|
316
|
+
console.warn(
|
|
317
|
+
prod ? "[@bwalletx/connect] WARNING: the in-memory nonce store is running in production. It is not shared between instances and breaks on serverless. Use redisStore or pgStore." : "[@bwalletx/connect] Using the in-memory nonce store. It is for development only; pass a shared store (redisStore, pgStore) in production."
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
const m = /* @__PURE__ */ new Map();
|
|
321
|
+
return {
|
|
322
|
+
put(nonce, c) {
|
|
323
|
+
const now = Date.now();
|
|
324
|
+
for (const [k, v] of m) if (v.expiresAt < now) m.delete(k);
|
|
325
|
+
m.set(nonce, c);
|
|
326
|
+
},
|
|
327
|
+
take(nonce) {
|
|
328
|
+
const c = m.get(nonce) ?? null;
|
|
329
|
+
m.delete(nonce);
|
|
330
|
+
return c;
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
var defaultStore = null;
|
|
335
|
+
var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
|
|
336
|
+
function isIdentityKey(x) {
|
|
337
|
+
if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
|
|
338
|
+
try {
|
|
339
|
+
PublicKey.fromString(x);
|
|
340
|
+
return true;
|
|
341
|
+
} catch {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function normalOrigin(origin) {
|
|
346
|
+
const u = new URL(origin);
|
|
347
|
+
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
|
|
348
|
+
throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
349
|
+
}
|
|
350
|
+
return u.origin;
|
|
351
|
+
}
|
|
352
|
+
function loginMessage(origin, nonce, expiresAt) {
|
|
353
|
+
return [
|
|
354
|
+
`Sign in to ${new URL(origin).host} with bWallet`,
|
|
355
|
+
"",
|
|
356
|
+
"This proves you hold this wallet. It does not spend anything.",
|
|
357
|
+
"",
|
|
358
|
+
`Origin: ${origin}`,
|
|
359
|
+
`Nonce: ${nonce}`,
|
|
360
|
+
`Expires: ${new Date(expiresAt).toISOString()}`
|
|
361
|
+
].join("\n");
|
|
362
|
+
}
|
|
363
|
+
function randomNonce() {
|
|
364
|
+
const b = new Uint8Array(16);
|
|
365
|
+
globalThis.crypto.getRandomValues(b);
|
|
366
|
+
let s = "";
|
|
367
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
368
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
369
|
+
}
|
|
370
|
+
async function createChallenge(opts) {
|
|
371
|
+
if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
|
|
372
|
+
const origin = normalOrigin(opts.origin);
|
|
373
|
+
const nonce = randomNonce();
|
|
374
|
+
const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
|
|
375
|
+
const message = loginMessage(origin, nonce, expiresAt);
|
|
376
|
+
await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
|
|
377
|
+
return { nonce, message, protocolID: LOGIN_PROTOCOL, keyID: nonce, expiresAt };
|
|
378
|
+
}
|
|
379
|
+
async function verifySignIn(opts) {
|
|
380
|
+
if (typeof opts.nonce !== "string" || opts.nonce.length > 64) return null;
|
|
381
|
+
const c = await storeOrDefault(opts.store).take(opts.nonce);
|
|
382
|
+
if (!c || Date.now() > c.expiresAt) return null;
|
|
383
|
+
let origin;
|
|
384
|
+
try {
|
|
385
|
+
origin = normalOrigin(opts.origin);
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
if (c.origin !== origin) return null;
|
|
390
|
+
if (!isIdentityKey(opts.identityKey)) return null;
|
|
391
|
+
const identityKey = opts.identityKey.toLowerCase();
|
|
392
|
+
if (c.identityKey !== identityKey) return null;
|
|
393
|
+
const sig = opts.signature;
|
|
394
|
+
if (!Array.isArray(sig) || sig.length < 8 || sig.length > 80 || !sig.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const { valid } = await new ProtoWallet("anyone").verifySignature({
|
|
399
|
+
protocolID: LOGIN_PROTOCOL,
|
|
400
|
+
keyID: opts.nonce,
|
|
401
|
+
counterparty: identityKey,
|
|
402
|
+
data: Array.from(new TextEncoder().encode(c.message)),
|
|
403
|
+
signature: sig
|
|
404
|
+
});
|
|
405
|
+
return valid ? identityKey : null;
|
|
406
|
+
} catch {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
function originAllowed(requestOrigin, origin) {
|
|
411
|
+
try {
|
|
412
|
+
return !!requestOrigin && requestOrigin === normalOrigin(origin);
|
|
413
|
+
} catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export {
|
|
419
|
+
redisStore,
|
|
420
|
+
pgNonceTableSql,
|
|
421
|
+
pgStore,
|
|
422
|
+
MIN_SECRET_LENGTH,
|
|
423
|
+
assertSecret,
|
|
424
|
+
signSession,
|
|
425
|
+
verifySession,
|
|
426
|
+
serializeCookie,
|
|
427
|
+
readCookie,
|
|
428
|
+
PaymentVerificationError,
|
|
429
|
+
verifyPayment,
|
|
430
|
+
IdentityLinkConflictError,
|
|
431
|
+
linkIdentity,
|
|
432
|
+
signInOrLink,
|
|
433
|
+
LOGIN_PROTOCOL,
|
|
434
|
+
CHALLENGE_LIFETIME_MS,
|
|
435
|
+
memoryStore,
|
|
436
|
+
isIdentityKey,
|
|
437
|
+
loginMessage,
|
|
438
|
+
createChallenge,
|
|
439
|
+
verifySignIn,
|
|
440
|
+
originAllowed
|
|
441
|
+
};
|
|
442
|
+
//# sourceMappingURL=chunk-7NAKOJGZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/server.ts","../src/nonce-stores.ts","../src/session.ts","../src/verify-payment.ts","../src/link.ts"],"sourcesContent":["import { ProtoWallet, PublicKey, type WalletProtocol } from '@bsv/sdk';\n\n/** Security level 2, a protocol name used for nothing else. Keep it constant. */\nexport const LOGIN_PROTOCOL: WalletProtocol = [2, 'bwallet sign in'];\nexport const CHALLENGE_LIFETIME_MS = 2 * 60 * 1000;\n\nexport interface PendingChallenge {\n identityKey: string;\n origin: string;\n message: string;\n expiresAt: number;\n}\n\n/**\n * Where challenges wait between the two routes. Use Redis or a database table in production.\n * `take` must read and delete in one step, so each nonce can be tried once.\n */\nexport interface NonceStore {\n put(nonce: string, c: PendingChallenge): Promise<void> | void;\n take(nonce: string): Promise<PendingChallenge | null> | PendingChallenge | null;\n}\n\nlet warned = false;\n\n/**\n * In-memory store. For development and tests only: it is lost on restart and not shared between\n * server processes or serverless instances. Expired entries are pruned on each put.\n */\nexport function memoryStore(opts: { quiet?: boolean } = {}): NonceStore {\n const prod = typeof process !== 'undefined' && process.env?.NODE_ENV === 'production';\n if ((prod || !opts.quiet) && !warned) {\n warned = true;\n console.warn(\n prod\n ? '[@bwalletx/connect] WARNING: the in-memory nonce store is running in production. It is not shared between instances and breaks on serverless. Use redisStore or pgStore.'\n : '[@bwalletx/connect] Using the in-memory nonce store. It is for development only; pass a shared store (redisStore, pgStore) in production.',\n );\n }\n const m = new Map<string, PendingChallenge>();\n return {\n put(nonce, c) {\n const now = Date.now();\n for (const [k, v] of m) if (v.expiresAt < now) m.delete(k);\n m.set(nonce, c);\n },\n take(nonce) {\n const c = m.get(nonce) ?? null;\n m.delete(nonce);\n return c;\n },\n };\n}\n\nlet defaultStore: NonceStore | null = null;\nconst storeOrDefault = (s?: NonceStore) => s ?? (defaultStore ??= memoryStore());\n\nexport function isIdentityKey(x: unknown): x is string {\n if (typeof x !== 'string' || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;\n try {\n PublicKey.fromString(x);\n return true;\n } catch {\n return false;\n }\n}\n\nfunction normalOrigin(origin: string): string {\n const u = new URL(origin);\n if (u.origin === 'null' || u.origin !== origin.replace(/\\/$/, '')) {\n throw new Error(`origin must be scheme://host[:port], got ${origin}`);\n }\n return u.origin;\n}\n\n/** The text the user signs. Lines joined with \\n, no trailing newline. */\nexport function loginMessage(origin: string, nonce: string, expiresAt: number): string {\n return [\n `Sign in to ${new URL(origin).host} with bWallet`,\n '',\n 'This proves you hold this wallet. It does not spend anything.',\n '',\n `Origin: ${origin}`,\n `Nonce: ${nonce}`,\n `Expires: ${new Date(expiresAt).toISOString()}`,\n ].join('\\n');\n}\n\nfunction randomNonce(): string {\n const b = new Uint8Array(16);\n globalThis.crypto.getRandomValues(b);\n let s = '';\n for (const x of b) s += String.fromCharCode(x);\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport interface CreateChallengeOptions {\n /** YOUR site's origin from config, e.g. \"https://example.com\". Never take it from the request. */\n origin: string;\n /** The identity key the browser claims. Checked again at verify. */\n identityKey: unknown;\n /** Default: a process-wide in-memory store (dev only). */\n store?: NonceStore;\n}\n\nexport interface ChallengeResponse {\n nonce: string;\n message: string;\n protocolID: WalletProtocol;\n keyID: string;\n expiresAt: number;\n}\n\n/** Step 1. Throws if identityKey is not a valid compressed public key. Send the result to the browser. */\nexport async function createChallenge(opts: CreateChallengeOptions): Promise<ChallengeResponse> {\n if (!isIdentityKey(opts.identityKey)) throw new Error('identityKey must be a compressed public key (66 hex chars)');\n const origin = normalOrigin(opts.origin);\n const nonce = randomNonce();\n const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;\n const message = loginMessage(origin, nonce, expiresAt);\n await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });\n return { nonce, message, protocolID: LOGIN_PROTOCOL, keyID: nonce, expiresAt };\n}\n\nexport interface VerifySignInOptions {\n /** YOUR site's origin from config. Must equal the origin the challenge was made for. */\n origin: string;\n /** From the request body. */\n identityKey: unknown;\n nonce: unknown;\n signature: unknown;\n store?: NonceStore;\n}\n\n/**\n * Step 2. Returns the proven identity key (lowercase hex), or null.\n * The nonce is deleted before anything is checked, so each challenge can be tried once.\n */\nexport async function verifySignIn(opts: VerifySignInOptions): Promise<string | null> {\n if (typeof opts.nonce !== 'string' || opts.nonce.length > 64) return null;\n const c = await storeOrDefault(opts.store).take(opts.nonce);\n if (!c || Date.now() > c.expiresAt) return null;\n let origin: string;\n try {\n origin = normalOrigin(opts.origin);\n } catch {\n return null;\n }\n if (c.origin !== origin) return null;\n if (!isIdentityKey(opts.identityKey)) return null;\n const identityKey = opts.identityKey.toLowerCase();\n if (c.identityKey !== identityKey) return null;\n\n const sig = opts.signature;\n if (!Array.isArray(sig) || sig.length < 8 || sig.length > 80 || !sig.every((b) => Number.isInteger(b) && b >= 0 && b <= 255)) {\n return null;\n }\n try {\n const { valid } = await new ProtoWallet('anyone').verifySignature({\n protocolID: LOGIN_PROTOCOL,\n keyID: opts.nonce,\n counterparty: identityKey,\n data: Array.from(new TextEncoder().encode(c.message)),\n signature: sig as number[],\n });\n return valid ? identityKey : null;\n } catch {\n return null; // @bsv/sdk throws on a bad signature instead of returning valid: false\n }\n}\n\n/** True when a request's Origin header is exactly your configured origin. Check it on both routes. */\nexport function originAllowed(requestOrigin: string | null | undefined, origin: string): boolean {\n try {\n return !!requestOrigin && requestOrigin === normalOrigin(origin);\n } catch {\n return false;\n }\n}\n\nexport { redisStore, pgStore, pgNonceTableSql } from './nonce-stores.js';\nexport type { RedisLike, RedisStoreOptions, PgQuery, PgStoreOptions } from './nonce-stores.js';\nexport { signSession, verifySession, serializeCookie, readCookie, MIN_SECRET_LENGTH } from './session.js';\nexport type { SessionPayload, SignSessionOptions, CookieOptions } from './session.js';\nexport { verifyPayment, PaymentVerificationError } from './verify-payment.js';\nexport type { VerifyPaymentOptions, VerifyPaymentResult, ExpectedOutput, TxStatus } from './verify-payment.js';\nexport { linkIdentity, signInOrLink, IdentityLinkConflictError } from './link.js';\nexport type { LinkCallbacks, LinkResult, SignInOrLinkOptions, SignInOrLinkResult } from './link.js';\n","/**\n * Shared NonceStore adapters. No database driver is imported: you pass your own client or query\n * function. Every adapter makes `take` read-and-delete in one step, so each nonce works once.\n */\nimport type { NonceStore, PendingChallenge } from './server.js';\n\nconst ttlSeconds = (c: PendingChallenge) => Math.max(1, Math.ceil((c.expiresAt - Date.now()) / 1000));\n\nfunction parse(v: unknown): PendingChallenge | null {\n if (v == null) return null;\n try {\n const c = (typeof v === 'string' ? JSON.parse(v) : v) as PendingChallenge;\n if (!c || typeof c.identityKey !== 'string' || typeof c.message !== 'string' || typeof c.expiresAt !== 'number') return null;\n return c.expiresAt < Date.now() ? null : c;\n } catch {\n return null;\n }\n}\n\n/**\n * The Redis commands used. Fits @upstash/redis and node-redis v4+ as they are\n * (`set(key, value, { ex | EX })`, `getdel` / `getDel`). For ioredis pass `flavor: 'ioredis'`.\n */\nexport interface RedisLike {\n set(key: string, value: string, ...args: unknown[]): Promise<unknown>;\n getdel?(key: string): Promise<unknown>;\n getDel?(key: string): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n client: RedisLike;\n /** Key prefix. Default 'bwalletx:nonce:'. */\n prefix?: string;\n /** 'ioredis' uses positional `set(key, value, 'EX', s)`. Default 'object' (Upstash, node-redis). */\n flavor?: 'object' | 'ioredis';\n}\n\n/** Redis / Upstash / Vercel KV. Needs Redis 6.2+ for GETDEL. */\nexport function redisStore(opts: RedisStoreOptions): NonceStore {\n const { client } = opts;\n const prefix = opts.prefix ?? 'bwalletx:nonce:';\n const getdel = client.getdel?.bind(client) ?? client.getDel?.bind(client);\n if (!getdel) throw new Error('redisStore: the client needs getdel (or getDel). Redis 6.2+.');\n return {\n async put(nonce, c) {\n const s = ttlSeconds(c);\n if (opts.flavor === 'ioredis') await client.set(prefix + nonce, JSON.stringify(c), 'EX', s);\n else await client.set(prefix + nonce, JSON.stringify(c), { ex: s, EX: s });\n },\n async take(nonce) {\n return parse(await getdel(prefix + nonce));\n },\n };\n}\n\n/** `query(sql, params)` resolving to `{ rows }` (node-postgres, @vercel/postgres, Neon) or a bare array (postgres.js `sql.unsafe`). */\nexport type PgQuery = (sql: string, params: unknown[]) => Promise<{ rows: unknown[] } | unknown[]>;\n\nexport interface PgStoreOptions {\n query: PgQuery;\n /** Table name (letters, digits, _ and one optional schema dot). Default 'bwalletx_nonces'. */\n table?: string;\n}\n\n/** Run this once (a migration) before using pgStore. */\nexport function pgNonceTableSql(table = 'bwalletx_nonces'): string {\n const t = safeTable(table);\n return `create table if not exists ${t} (\n nonce text primary key,\n challenge jsonb not null,\n expires_at timestamptz not null\n);\ncreate index if not exists ${t.replace('.', '_')}_expires_idx on ${t} (expires_at);`;\n}\n\nfunction safeTable(t: string): string {\n if (!/^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)?$/.test(t)) throw new Error(`pgStore: bad table name ${t}`);\n return t;\n}\n\n/** Postgres (e.g. self-hosted Supabase). Single-use via DELETE ... RETURNING. */\nexport function pgStore(opts: PgStoreOptions): NonceStore {\n const t = safeTable(opts.table ?? 'bwalletx_nonces');\n const rows = (r: { rows: unknown[] } | unknown[]) => (Array.isArray(r) ? r : r.rows);\n return {\n async put(nonce, c) {\n await opts.query(`delete from ${t} where expires_at < now()`, []);\n await opts.query(`insert into ${t} (nonce, challenge, expires_at) values ($1, $2, to_timestamp($3 / 1000.0))`, [\n nonce,\n JSON.stringify(c),\n c.expiresAt,\n ]);\n },\n async take(nonce) {\n const r = rows(await opts.query(`delete from ${t} where nonce = $1 returning challenge`, [nonce]));\n const row = r[0] as { challenge?: unknown } | undefined;\n return parse(row?.challenge);\n },\n };\n}\n","/**\n * Signed session tokens: base64url(JSON payload) + \".\" + base64url(HMAC-SHA256).\n * WebCrypto only, so it runs in Node 20+, Vercel Edge and Cloudflare Workers.\n * There is no default secret: pass one (32+ chars) or every call throws.\n */\n\nexport interface SessionPayload {\n /** The proven identity key (lowercase hex). */\n sub: string;\n /** Issued at, seconds since the epoch. */\n iat: number;\n /** Expires at, seconds since the epoch. */\n exp: number;\n /** Your app's user id, if the identity key is linked to one. */\n uid?: string;\n handle?: string;\n paymail?: string;\n [claim: string]: unknown;\n}\n\nexport const MIN_SECRET_LENGTH = 32;\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\nfunction b64u(bytes: Uint8Array): string {\n let s = '';\n for (const b of bytes) s += String.fromCharCode(b);\n return btoa(s).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nfunction unb64u(s: string): Uint8Array<ArrayBuffer> {\n if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error('bad base64url');\n const b = atob(s.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (s.length % 4)) % 4));\n return Uint8Array.from(b, (c) => c.charCodeAt(0));\n}\n\nexport function assertSecret(secret: unknown): asserts secret is string {\n if (typeof secret !== 'string' || secret.length < MIN_SECRET_LENGTH) {\n throw new Error(\n `[@bwalletx/connect] A session secret of at least ${MIN_SECRET_LENGTH} characters is required. ` +\n 'Set it in an environment variable (e.g. BWALLETX_SESSION_SECRET). There is no default.',\n );\n }\n}\n\nconst keys = new Map<string, Promise<CryptoKey>>();\nfunction hmacKey(secret: string): Promise<CryptoKey> {\n let k = keys.get(secret);\n if (!k) {\n k = globalThis.crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign', 'verify']);\n keys.set(secret, k);\n }\n return k;\n}\n\nexport interface SignSessionOptions {\n secret: string;\n /** Lifetime in seconds. Default 7 days. */\n maxAge?: number;\n /** For tests. */\n now?: number;\n}\n\n/** Sign a session. `claims.sub` (identity key) is required. */\nexport async function signSession(\n claims: Omit<SessionPayload, 'iat' | 'exp'> & Partial<Pick<SessionPayload, 'iat' | 'exp'>>,\n opts: SignSessionOptions,\n): Promise<string> {\n assertSecret(opts.secret);\n if (typeof claims.sub !== 'string' || !claims.sub) throw new Error('session needs a sub (identity key)');\n const iat = Math.floor((opts.now ?? Date.now()) / 1000);\n const payload: SessionPayload = { ...claims, sub: claims.sub, iat, exp: iat + (opts.maxAge ?? 7 * 24 * 3600) };\n const body = b64u(enc.encode(JSON.stringify(payload)));\n const sig = new Uint8Array(await globalThis.crypto.subtle.sign('HMAC', await hmacKey(opts.secret), enc.encode(body)));\n return `${body}.${b64u(sig)}`;\n}\n\n/** The payload if the token is authentic and unexpired, else null. Never throws on bad input (only on a missing secret). */\nexport async function verifySession(token: unknown, opts: { secret: string; now?: number }): Promise<SessionPayload | null> {\n assertSecret(opts.secret);\n if (typeof token !== 'string' || token.length > 8192) return null;\n const dot = token.indexOf('.');\n if (dot < 1 || dot !== token.lastIndexOf('.')) return null;\n const body = token.slice(0, dot);\n try {\n const sig = unb64u(token.slice(dot + 1));\n // crypto.subtle.verify compares in constant time.\n const ok = await globalThis.crypto.subtle.verify('HMAC', await hmacKey(opts.secret), sig, enc.encode(body));\n if (!ok) return null;\n const p = JSON.parse(dec.decode(unb64u(body))) as SessionPayload;\n if (!p || typeof p.sub !== 'string' || typeof p.exp !== 'number') return null;\n if (Math.floor((opts.now ?? Date.now()) / 1000) >= p.exp) return null;\n return p;\n } catch {\n return null;\n }\n}\n\nexport interface CookieOptions {\n name: string;\n maxAge: number;\n secure?: boolean;\n sameSite?: 'Lax' | 'Strict' | 'None';\n path?: string;\n domain?: string;\n}\n\n/** A Set-Cookie header value. Always HttpOnly. */\nexport function serializeCookie(value: string, o: CookieOptions): string {\n const parts = [`${o.name}=${value}`, `Path=${o.path ?? '/'}`, `Max-Age=${o.maxAge}`, 'HttpOnly', `SameSite=${o.sameSite ?? 'Lax'}`];\n if (o.secure !== false) parts.push('Secure');\n if (o.domain) parts.push(`Domain=${o.domain}`);\n return parts.join('; ');\n}\n\n/** Read one cookie from a Cookie header. */\nexport function readCookie(header: string | null | undefined, name: string): string | null {\n if (!header) return null;\n for (const part of header.split(';')) {\n const i = part.indexOf('=');\n if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();\n }\n return null;\n}\n","/**\n * Server side: check that a transaction really pays what you expected before you deliver anything.\n * Read-only: it never broadcasts, signs or retries a spend.\n */\nimport { P2PKH, Transaction, Utils, type ChainTracker } from '@bsv/sdk';\n\nexport interface ExpectedOutput {\n /** Locking script hex. Give this or `address`. */\n script?: string;\n /** A P2PKH address. */\n address?: string;\n /** Minimum satoshis this output must carry. */\n satoshis: number;\n}\n\nexport type TxStatus =\n | { state: 'confirmed'; confirmations: number; source: 'woc' | 'arc' }\n | { state: 'mempool'; source: 'woc' | 'arc'; detail?: string }\n | { state: 'not-found' | 'rejected'; source: 'woc' | 'arc'; detail?: string }\n | { state: 'unchecked' };\n\nexport interface VerifyPaymentOptions {\n /** A txid (fetched from WhatsOnChain), raw tx / BEEF / Atomic BEEF as hex, or BEEF bytes. */\n tx: string | number[] | Uint8Array;\n /** Every one must be matched by a different output. */\n expected: ExpectedOutput[];\n network?: 'main' | 'test';\n /** Where to ask for status. Default 'woc'. 'none' skips the network (BEEF only). */\n status?: 'woc' | 'arc' | 'none';\n /** ARC base URL, e.g. https://arc.taal.com. Needed when status is 'arc'. */\n arcUrl?: string;\n /** Sent as Bearer. Read it from env; never hardcode. */\n arcApiKey?: string;\n /** Fail unless the network has seen the tx (mempool or confirmed). Default false. */\n requireSeen?: boolean;\n /** Minimum confirmations. Default 0. */\n minConfirmations?: number;\n /** Also run SPV (merkle proofs + scripts) on a BEEF with this chain tracker. */\n spv?: ChainTracker;\n /** Injected fetch (tests, custom agents). Default globalThis.fetch. */\n fetch?: typeof fetch;\n}\n\nexport interface VerifyPaymentResult {\n ok: true;\n txid: string;\n /** For each expected output, the index of the output that paid it. */\n matched: { vout: number; satoshis: number; script: string }[];\n status: TxStatus;\n}\n\nexport type PaymentErrorCode = 'BAD_TX' | 'TX_NOT_FOUND' | 'OUTPUT_MISSING' | 'NOT_SEEN' | 'REJECTED' | 'SPV_FAILED' | 'NETWORK' | 'BAD_EXPECTED';\n\nexport class PaymentVerificationError extends Error {\n constructor(\n readonly code: PaymentErrorCode,\n message: string,\n readonly txid?: string,\n ) {\n super(message);\n this.name = 'PaymentVerificationError';\n }\n}\n\nconst isHex = (s: string) => /^([0-9a-fA-F]{2})+$/.test(s);\nconst woc = (n: 'main' | 'test') => `https://api.whatsonchain.com/v1/bsv/${n}`;\n\nfunction parseTx(input: string | number[] | Uint8Array): Transaction {\n const bytes = typeof input === 'string' ? Utils.toArray(input, 'hex') : Array.from(input);\n const tries: (() => Transaction)[] = [\n () => Transaction.fromAtomicBEEF(bytes),\n () => Transaction.fromBEEF(bytes),\n () => Transaction.fromBinary(bytes),\n ];\n for (const t of tries) {\n try {\n return t();\n } catch {\n /* next format */\n }\n }\n throw new PaymentVerificationError('BAD_TX', 'Could not parse the transaction (expected a txid, raw tx, BEEF or Atomic BEEF).');\n}\n\nfunction expectedScript(e: ExpectedOutput): string {\n if (!Number.isSafeInteger(e.satoshis) || e.satoshis < 0) throw new PaymentVerificationError('BAD_EXPECTED', 'expected.satoshis must be a non-negative integer');\n if (e.script) {\n if (!isHex(e.script)) throw new PaymentVerificationError('BAD_EXPECTED', 'expected.script must be hex');\n return e.script.toLowerCase();\n }\n if (e.address) {\n try {\n return new P2PKH().lock(e.address).toHex();\n } catch {\n throw new PaymentVerificationError('BAD_EXPECTED', `Not a valid address: ${e.address}`);\n }\n }\n throw new PaymentVerificationError('BAD_EXPECTED', 'Each expected output needs a script or an address');\n}\n\nasync function getJson(f: typeof fetch, url: string, headers: Record<string, string> = {}): Promise<{ status: number; body: unknown }> {\n let r: Response;\n try {\n r = await f(url, { headers });\n } catch (e) {\n throw new PaymentVerificationError('NETWORK', `Could not reach ${new URL(url).host}: ${(e as Error).message}`);\n }\n const text = await r.text();\n let body: unknown = text;\n try {\n body = JSON.parse(text);\n } catch {\n /* plain text */\n }\n return { status: r.status, body };\n}\n\nasync function checkStatus(o: VerifyPaymentOptions, f: typeof fetch, txid: string): Promise<TxStatus> {\n const mode = o.status ?? 'woc';\n if (mode === 'none') return { state: 'unchecked' };\n if (mode === 'arc') {\n if (!o.arcUrl) throw new PaymentVerificationError('NETWORK', \"status 'arc' needs arcUrl\");\n const { status, body } = await getJson(f, `${o.arcUrl.replace(/\\/$/, '')}/v1/tx/${txid}`, o.arcApiKey ? { Authorization: `Bearer ${o.arcApiKey}` } : {});\n if (status === 404) return { state: 'not-found', source: 'arc' };\n if (status >= 400) throw new PaymentVerificationError('NETWORK', `ARC answered ${status}`, txid);\n const b = body as { txStatus?: string; blockHeight?: number; extraInfo?: string };\n const s = String(b.txStatus ?? '').toUpperCase();\n if (s === 'MINED' || s === 'IMMUTABLE') return { state: 'confirmed', confirmations: 1, source: 'arc' };\n if (s === 'REJECTED' || s === 'DOUBLE_SPEND_ATTEMPTED') return { state: 'rejected', source: 'arc', detail: b.extraInfo ?? s };\n if (s === 'UNKNOWN' || s === '') return { state: 'not-found', source: 'arc', detail: s };\n return { state: 'mempool', source: 'arc', detail: s };\n }\n const { status, body } = await getJson(f, `${woc(o.network ?? 'main')}/tx/hash/${txid}`);\n if (status === 404) return { state: 'not-found', source: 'woc' };\n if (status >= 400) throw new PaymentVerificationError('NETWORK', `WhatsOnChain answered ${status}`, txid);\n const c = Number((body as { confirmations?: number }).confirmations ?? 0);\n return c > 0 ? { state: 'confirmed', confirmations: c, source: 'woc' } : { state: 'mempool', source: 'woc' };\n}\n\n/**\n * Throws PaymentVerificationError (with a `code`) unless every expected output is paid by a\n * distinct output of the transaction with at least the expected satoshis.\n */\nexport async function verifyPayment(o: VerifyPaymentOptions): Promise<VerifyPaymentResult> {\n const f = o.fetch ?? globalThis.fetch;\n if (!o.expected?.length) throw new PaymentVerificationError('BAD_EXPECTED', 'Pass at least one expected output');\n const want = o.expected.map((e) => ({ script: expectedScript(e), satoshis: e.satoshis }));\n\n let tx: Transaction;\n if (typeof o.tx === 'string' && /^[0-9a-fA-F]{64}$/.test(o.tx)) {\n const txid = o.tx.toLowerCase();\n let r: Response;\n try {\n r = await f(`${woc(o.network ?? 'main')}/tx/${txid}/hex`);\n } catch (e) {\n throw new PaymentVerificationError('NETWORK', `Could not reach WhatsOnChain: ${(e as Error).message}`, txid);\n }\n if (r.status === 404) throw new PaymentVerificationError('TX_NOT_FOUND', `Transaction ${txid} was not found`, txid);\n if (!r.ok) throw new PaymentVerificationError('NETWORK', `WhatsOnChain answered ${r.status}`, txid);\n tx = parseTx((await r.text()).trim());\n if (tx.id('hex') !== txid) throw new PaymentVerificationError('BAD_TX', 'The fetched transaction does not match the txid', txid);\n } else {\n if (typeof o.tx === 'string' && !isHex(o.tx)) throw new PaymentVerificationError('BAD_TX', 'tx must be a txid or hex');\n tx = parseTx(o.tx);\n }\n const txid = tx.id('hex');\n\n if (o.spv) {\n let ok = false;\n try {\n ok = await tx.verify(o.spv);\n } catch (e) {\n throw new PaymentVerificationError('SPV_FAILED', `SPV check failed: ${(e as Error).message}`, txid);\n }\n if (!ok) throw new PaymentVerificationError('SPV_FAILED', 'SPV check failed', txid);\n }\n\n const used = new Set<number>();\n const matched: VerifyPaymentResult['matched'] = [];\n for (const w of want) {\n const vout = tx.outputs.findIndex(\n (out, i) => !used.has(i) && out.lockingScript.toHex() === w.script && (out.satoshis ?? 0) >= w.satoshis,\n );\n if (vout < 0) {\n throw new PaymentVerificationError('OUTPUT_MISSING', `No output pays ${w.satoshis} sats to script ${w.script.slice(0, 16)}…`, txid);\n }\n used.add(vout);\n matched.push({ vout, satoshis: tx.outputs[vout].satoshis ?? 0, script: w.script });\n }\n\n const status = await checkStatus(o, f, txid);\n if (status.state === 'rejected') throw new PaymentVerificationError('REJECTED', `The network rejected ${txid}: ${status.detail ?? ''}`.trim(), txid);\n if ((o.requireSeen || (o.minConfirmations ?? 0) > 0) && status.state !== 'mempool' && status.state !== 'confirmed') {\n throw new PaymentVerificationError('NOT_SEEN', `The network has not seen ${txid} yet`, txid);\n }\n const conf = status.state === 'confirmed' ? status.confirmations : 0;\n if ((o.minConfirmations ?? 0) > conf) {\n throw new PaymentVerificationError('NOT_SEEN', `${txid} has ${conf} confirmations, needs ${o.minConfirmations}`, txid);\n }\n return { ok: true, txid, matched, status };\n}\n","/**\n * Link a proven identity key to a user in YOUR auth system (Supabase, next-auth, your own table).\n * You supply the storage callbacks; this module only holds the rules.\n */\n\nexport interface LinkCallbacks {\n /** The user id this identity key is linked to, or null. */\n findUserByIdentityKey(identityKey: string): Promise<string | null> | string | null;\n /** Save the link. Make (identity_key) unique in your table so a race cannot double-link. */\n attach(userId: string, identityKey: string): Promise<void> | void;\n}\n\nexport type LinkResult = { status: 'linked' | 'already-linked'; userId: string; identityKey: string };\n\nexport class IdentityLinkConflictError extends Error {\n readonly code = 'IDENTITY_LINKED_TO_OTHER_USER';\n constructor(\n readonly identityKey: string,\n readonly otherUserId: string,\n ) {\n super('This bWalletX identity is already linked to a different account. Sign in with that account, or unlink it there first.');\n this.name = 'IdentityLinkConflictError';\n }\n}\n\nconst norm = (k: string) => {\n if (typeof k !== 'string' || !/^0[23][0-9a-fA-F]{64}$/.test(k)) throw new Error('identityKey must be a compressed public key (66 hex chars)');\n return k.toLowerCase();\n};\n\n/**\n * Link `identityKey` (already verified with verifySignIn) to the signed-in `userId`.\n * Throws IdentityLinkConflictError if it belongs to someone else. Idempotent for the same user.\n */\nexport async function linkIdentity(userId: string, identityKey: string, cb: LinkCallbacks): Promise<LinkResult> {\n if (!userId) throw new Error('linkIdentity needs the current user id');\n const key = norm(identityKey);\n const owner = await cb.findUserByIdentityKey(key);\n if (owner && owner !== userId) throw new IdentityLinkConflictError(key, owner);\n if (owner === userId) return { status: 'already-linked', userId, identityKey: key };\n await cb.attach(userId, key);\n return { status: 'linked', userId, identityKey: key };\n}\n\nexport interface SignInOrLinkOptions extends LinkCallbacks {\n /** The user already signed in some other way (e.g. HandCash, email), if any. */\n currentUserId?: string | null;\n /** Make a new user for an unknown identity key. Without it, unknown keys return status 'unknown'. */\n createUser?(identityKey: string): Promise<string> | string;\n}\n\nexport type SignInOrLinkResult =\n | { status: 'signed-in' | 'linked' | 'already-linked' | 'created'; userId: string; identityKey: string }\n | { status: 'unknown'; userId: null; identityKey: string };\n\n/**\n * The usual verify-route logic: signed in already → link; known key → that user; else create (or report unknown).\n * Throws IdentityLinkConflictError when a signed-in user tries to link a key owned by another user.\n */\nexport async function signInOrLink(identityKey: string, o: SignInOrLinkOptions): Promise<SignInOrLinkResult> {\n const key = norm(identityKey);\n if (o.currentUserId) return linkIdentity(o.currentUserId, key, o);\n const owner = await o.findUserByIdentityKey(key);\n if (owner) return { status: 'signed-in', userId: owner, identityKey: key };\n if (!o.createUser) return { status: 'unknown', userId: null, identityKey: key };\n const userId = await o.createUser(key);\n await o.attach(userId, key);\n return { status: 'created', userId, identityKey: key };\n}\n"],"mappings":"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","names":["status","body","txid"]}
|