@bwalletx/connect 0.3.0 → 0.5.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/CHANGELOG.md +34 -0
- package/README.md +145 -4
- package/assets/protocol/401.jpg +0 -0
- package/assets/protocol/401.mp4 +0 -0
- package/assets/protocol/402.jpg +0 -0
- package/assets/protocol/402.mp4 +0 -0
- package/assets/protocol/403.jpg +0 -0
- package/assets/protocol/403.mp4 +0 -0
- package/dist/access-Z6xQwKVF.d.cts +118 -0
- package/dist/access-Z6xQwKVF.d.ts +118 -0
- package/dist/bwalletx-connect.global.js +41 -3
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/{chunk-NNMUSWPG.js → chunk-CVLXFNAG.js} +123 -1
- package/dist/chunk-CVLXFNAG.js.map +1 -0
- package/dist/chunk-GF6GUDZL.js +404 -0
- package/dist/chunk-GF6GUDZL.js.map +1 -0
- package/dist/chunk-MWD4BCO5.js +182 -0
- package/dist/chunk-MWD4BCO5.js.map +1 -0
- package/dist/chunk-QRXFL66W.js +652 -0
- package/dist/chunk-QRXFL66W.js.map +1 -0
- package/dist/element.cjs +209 -0
- package/dist/element.cjs.map +1 -1
- package/dist/element.js +2 -1
- package/dist/element.js.map +1 -1
- package/dist/index.cjs +706 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +66 -62
- package/dist/index.d.ts +66 -62
- package/dist/index.js +63 -3
- package/dist/index.js.map +1 -1
- package/dist/next.cjs +783 -0
- package/dist/next.cjs.map +1 -0
- package/dist/next.d.cts +98 -0
- package/dist/next.d.ts +98 -0
- package/dist/next.js +198 -0
- package/dist/next.js.map +1 -0
- package/dist/react.cjs +795 -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 +177 -4
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +742 -10
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +314 -2
- package/dist/server.d.ts +314 -2
- package/dist/server.js +82 -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 +32 -6
- package/dist/chunk-NNMUSWPG.js.map +0 -1
package/dist/server.cjs
CHANGED
|
@@ -20,24 +20,721 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/server.ts
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
|
+
ACCESS_CHALLENGE_LIFETIME_S: () => ACCESS_CHALLENGE_LIFETIME_S,
|
|
24
|
+
ACCESS_ERROR_STATUS: () => ACCESS_ERROR_STATUS,
|
|
25
|
+
ACCESS_PROTOCOL: () => ACCESS_PROTOCOL,
|
|
26
|
+
ACCESS_VERSION: () => ACCESS_VERSION,
|
|
27
|
+
AccessError: () => AccessError,
|
|
23
28
|
CHALLENGE_LIFETIME_MS: () => CHALLENGE_LIFETIME_MS,
|
|
29
|
+
IdentityLinkConflictError: () => IdentityLinkConflictError,
|
|
24
30
|
LOGIN_PROTOCOL: () => LOGIN_PROTOCOL,
|
|
31
|
+
MAX_HOLDING_PROOFS: () => MAX_HOLDING_PROOFS,
|
|
32
|
+
MIN_SECRET_LENGTH: () => MIN_SECRET_LENGTH,
|
|
33
|
+
PROTOCOL_MEDIA: () => PROTOCOL_MEDIA,
|
|
34
|
+
PaymentVerificationError: () => PaymentVerificationError,
|
|
35
|
+
accessErrorMessage: () => accessErrorMessage,
|
|
36
|
+
atLeastRaw: () => atLeastRaw,
|
|
37
|
+
bitSignResolver: () => bitSignResolver,
|
|
38
|
+
canonicalJson: () => canonicalJson,
|
|
39
|
+
challengeBytes: () => challengeBytes,
|
|
40
|
+
checkTokenHolding: () => checkTokenHolding,
|
|
41
|
+
createAccessChallenge: () => createAccessChallenge,
|
|
25
42
|
createChallenge: () => createChallenge,
|
|
26
43
|
isIdentityKey: () => isIdentityKey,
|
|
44
|
+
kindFor: () => kindFor,
|
|
45
|
+
linkIdentity: () => linkIdentity,
|
|
27
46
|
loginMessage: () => loginMessage,
|
|
28
47
|
memoryStore: () => memoryStore,
|
|
48
|
+
normaliseRequirement: () => normaliseRequirement,
|
|
49
|
+
oneSatResolver: () => oneSatResolver,
|
|
29
50
|
originAllowed: () => originAllowed,
|
|
51
|
+
parseAccessChallenge: () => parseAccessChallenge,
|
|
52
|
+
pgNonceTableSql: () => pgNonceTableSql,
|
|
53
|
+
pgStore: () => pgStore,
|
|
54
|
+
protocolMediaUrl: () => protocolMediaUrl,
|
|
55
|
+
readCookie: () => readCookie,
|
|
56
|
+
redisStore: () => redisStore,
|
|
57
|
+
serializeCookie: () => serializeCookie,
|
|
58
|
+
signInOrLink: () => signInOrLink,
|
|
59
|
+
signSession: () => signSession,
|
|
60
|
+
tokenNeedRaw: () => tokenNeedRaw,
|
|
61
|
+
verifyAccess: () => verifyAccess,
|
|
62
|
+
verifyHoldingProof: () => verifyHoldingProof,
|
|
63
|
+
verifyPayment: () => verifyPayment,
|
|
64
|
+
verifySession: () => verifySession,
|
|
30
65
|
verifySignIn: () => verifySignIn
|
|
31
66
|
});
|
|
32
67
|
module.exports = __toCommonJS(server_exports);
|
|
68
|
+
var import_sdk3 = require("@bsv/sdk");
|
|
69
|
+
|
|
70
|
+
// src/nonce-stores.ts
|
|
71
|
+
var ttlSeconds = (c) => Math.max(1, Math.ceil((c.expiresAt - Date.now()) / 1e3));
|
|
72
|
+
function parse(v) {
|
|
73
|
+
if (v == null) return null;
|
|
74
|
+
try {
|
|
75
|
+
const c = typeof v === "string" ? JSON.parse(v) : v;
|
|
76
|
+
if (!c || typeof c.identityKey !== "string" || typeof c.message !== "string" || typeof c.expiresAt !== "number") return null;
|
|
77
|
+
return c.expiresAt < Date.now() ? null : c;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function redisStore(opts) {
|
|
83
|
+
const { client } = opts;
|
|
84
|
+
const prefix = opts.prefix ?? "bwalletx:nonce:";
|
|
85
|
+
const getdel = client.getdel?.bind(client) ?? client.getDel?.bind(client);
|
|
86
|
+
if (!getdel) throw new Error("redisStore: the client needs getdel (or getDel). Redis 6.2+.");
|
|
87
|
+
return {
|
|
88
|
+
async put(nonce, c) {
|
|
89
|
+
const s = ttlSeconds(c);
|
|
90
|
+
if (opts.flavor === "ioredis") await client.set(prefix + nonce, JSON.stringify(c), "EX", s);
|
|
91
|
+
else await client.set(prefix + nonce, JSON.stringify(c), { ex: s, EX: s });
|
|
92
|
+
},
|
|
93
|
+
async take(nonce) {
|
|
94
|
+
return parse(await getdel(prefix + nonce));
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function pgNonceTableSql(table = "bwalletx_nonces") {
|
|
99
|
+
const t = safeTable(table);
|
|
100
|
+
return `create table if not exists ${t} (
|
|
101
|
+
nonce text primary key,
|
|
102
|
+
challenge jsonb not null,
|
|
103
|
+
expires_at timestamptz not null
|
|
104
|
+
);
|
|
105
|
+
create index if not exists ${t.replace(".", "_")}_expires_idx on ${t} (expires_at);`;
|
|
106
|
+
}
|
|
107
|
+
function safeTable(t) {
|
|
108
|
+
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}`);
|
|
109
|
+
return t;
|
|
110
|
+
}
|
|
111
|
+
function pgStore(opts) {
|
|
112
|
+
const t = safeTable(opts.table ?? "bwalletx_nonces");
|
|
113
|
+
const rows = (r) => Array.isArray(r) ? r : r.rows;
|
|
114
|
+
return {
|
|
115
|
+
async put(nonce, c) {
|
|
116
|
+
await opts.query(`delete from ${t} where expires_at < now()`, []);
|
|
117
|
+
await opts.query(`insert into ${t} (nonce, challenge, expires_at) values ($1, $2, to_timestamp($3 / 1000.0))`, [
|
|
118
|
+
nonce,
|
|
119
|
+
JSON.stringify(c),
|
|
120
|
+
c.expiresAt
|
|
121
|
+
]);
|
|
122
|
+
},
|
|
123
|
+
async take(nonce) {
|
|
124
|
+
const r = rows(await opts.query(`delete from ${t} where nonce = $1 returning challenge`, [nonce]));
|
|
125
|
+
const row = r[0];
|
|
126
|
+
return parse(row?.challenge);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/session.ts
|
|
132
|
+
var MIN_SECRET_LENGTH = 32;
|
|
133
|
+
var enc = new TextEncoder();
|
|
134
|
+
var dec = new TextDecoder();
|
|
135
|
+
function b64u(bytes) {
|
|
136
|
+
let s = "";
|
|
137
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
138
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
139
|
+
}
|
|
140
|
+
function unb64u(s) {
|
|
141
|
+
if (!/^[A-Za-z0-9_-]*$/.test(s)) throw new Error("bad base64url");
|
|
142
|
+
const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4));
|
|
143
|
+
return Uint8Array.from(b, (c) => c.charCodeAt(0));
|
|
144
|
+
}
|
|
145
|
+
function assertSecret(secret) {
|
|
146
|
+
if (typeof secret !== "string" || secret.length < MIN_SECRET_LENGTH) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`[@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.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
var keys = /* @__PURE__ */ new Map();
|
|
153
|
+
function hmacKey(secret) {
|
|
154
|
+
let k = keys.get(secret);
|
|
155
|
+
if (!k) {
|
|
156
|
+
k = globalThis.crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
|
|
157
|
+
keys.set(secret, k);
|
|
158
|
+
}
|
|
159
|
+
return k;
|
|
160
|
+
}
|
|
161
|
+
async function signSession(claims, opts) {
|
|
162
|
+
assertSecret(opts.secret);
|
|
163
|
+
if (typeof claims.sub !== "string" || !claims.sub) throw new Error("session needs a sub (identity key)");
|
|
164
|
+
const iat = Math.floor((opts.now ?? Date.now()) / 1e3);
|
|
165
|
+
const payload = { ...claims, sub: claims.sub, iat, exp: iat + (opts.maxAge ?? 7 * 24 * 3600) };
|
|
166
|
+
const body = b64u(enc.encode(JSON.stringify(payload)));
|
|
167
|
+
const sig = new Uint8Array(await globalThis.crypto.subtle.sign("HMAC", await hmacKey(opts.secret), enc.encode(body)));
|
|
168
|
+
return `${body}.${b64u(sig)}`;
|
|
169
|
+
}
|
|
170
|
+
async function verifySession(token, opts) {
|
|
171
|
+
assertSecret(opts.secret);
|
|
172
|
+
if (typeof token !== "string" || token.length > 8192) return null;
|
|
173
|
+
const dot = token.indexOf(".");
|
|
174
|
+
if (dot < 1 || dot !== token.lastIndexOf(".")) return null;
|
|
175
|
+
const body = token.slice(0, dot);
|
|
176
|
+
try {
|
|
177
|
+
const sig = unb64u(token.slice(dot + 1));
|
|
178
|
+
const ok = await globalThis.crypto.subtle.verify("HMAC", await hmacKey(opts.secret), sig, enc.encode(body));
|
|
179
|
+
if (!ok) return null;
|
|
180
|
+
const p = JSON.parse(dec.decode(unb64u(body)));
|
|
181
|
+
if (!p || typeof p.sub !== "string" || typeof p.exp !== "number") return null;
|
|
182
|
+
if (Math.floor((opts.now ?? Date.now()) / 1e3) >= p.exp) return null;
|
|
183
|
+
return p;
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function serializeCookie(value, o) {
|
|
189
|
+
const parts = [`${o.name}=${value}`, `Path=${o.path ?? "/"}`, `Max-Age=${o.maxAge}`, "HttpOnly", `SameSite=${o.sameSite ?? "Lax"}`];
|
|
190
|
+
if (o.secure !== false) parts.push("Secure");
|
|
191
|
+
if (o.domain) parts.push(`Domain=${o.domain}`);
|
|
192
|
+
return parts.join("; ");
|
|
193
|
+
}
|
|
194
|
+
function readCookie(header, name) {
|
|
195
|
+
if (!header) return null;
|
|
196
|
+
for (const part of header.split(";")) {
|
|
197
|
+
const i = part.indexOf("=");
|
|
198
|
+
if (i > 0 && part.slice(0, i).trim() === name) return part.slice(i + 1).trim();
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/verify-payment.ts
|
|
33
204
|
var import_sdk = require("@bsv/sdk");
|
|
205
|
+
var PaymentVerificationError = class extends Error {
|
|
206
|
+
constructor(code, message, txid) {
|
|
207
|
+
super(message);
|
|
208
|
+
this.code = code;
|
|
209
|
+
this.txid = txid;
|
|
210
|
+
this.name = "PaymentVerificationError";
|
|
211
|
+
}
|
|
212
|
+
code;
|
|
213
|
+
txid;
|
|
214
|
+
};
|
|
215
|
+
var isHex = (s) => /^([0-9a-fA-F]{2})+$/.test(s);
|
|
216
|
+
var woc = (n) => `https://api.whatsonchain.com/v1/bsv/${n}`;
|
|
217
|
+
function parseTx(input) {
|
|
218
|
+
const bytes = typeof input === "string" ? import_sdk.Utils.toArray(input, "hex") : Array.from(input);
|
|
219
|
+
const tries = [
|
|
220
|
+
() => import_sdk.Transaction.fromAtomicBEEF(bytes),
|
|
221
|
+
() => import_sdk.Transaction.fromBEEF(bytes),
|
|
222
|
+
() => import_sdk.Transaction.fromBinary(bytes)
|
|
223
|
+
];
|
|
224
|
+
for (const t of tries) {
|
|
225
|
+
try {
|
|
226
|
+
return t();
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
throw new PaymentVerificationError("BAD_TX", "Could not parse the transaction (expected a txid, raw tx, BEEF or Atomic BEEF).");
|
|
231
|
+
}
|
|
232
|
+
function expectedScript(e) {
|
|
233
|
+
if (!Number.isSafeInteger(e.satoshis) || e.satoshis < 0) throw new PaymentVerificationError("BAD_EXPECTED", "expected.satoshis must be a non-negative integer");
|
|
234
|
+
if (e.script) {
|
|
235
|
+
if (!isHex(e.script)) throw new PaymentVerificationError("BAD_EXPECTED", "expected.script must be hex");
|
|
236
|
+
return e.script.toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
if (e.address) {
|
|
239
|
+
try {
|
|
240
|
+
return new import_sdk.P2PKH().lock(e.address).toHex();
|
|
241
|
+
} catch {
|
|
242
|
+
throw new PaymentVerificationError("BAD_EXPECTED", `Not a valid address: ${e.address}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
throw new PaymentVerificationError("BAD_EXPECTED", "Each expected output needs a script or an address");
|
|
246
|
+
}
|
|
247
|
+
async function getJson(f, url, headers = {}) {
|
|
248
|
+
let r;
|
|
249
|
+
try {
|
|
250
|
+
r = await f(url, { headers });
|
|
251
|
+
} catch (e) {
|
|
252
|
+
throw new PaymentVerificationError("NETWORK", `Could not reach ${new URL(url).host}: ${e.message}`);
|
|
253
|
+
}
|
|
254
|
+
const text = await r.text();
|
|
255
|
+
let body = text;
|
|
256
|
+
try {
|
|
257
|
+
body = JSON.parse(text);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
return { status: r.status, body };
|
|
261
|
+
}
|
|
262
|
+
async function checkStatus(o, f, txid) {
|
|
263
|
+
const mode = o.status ?? "woc";
|
|
264
|
+
if (mode === "none") return { state: "unchecked" };
|
|
265
|
+
if (mode === "arc") {
|
|
266
|
+
if (!o.arcUrl) throw new PaymentVerificationError("NETWORK", "status 'arc' needs arcUrl");
|
|
267
|
+
const { status: status2, body: body2 } = await getJson(f, `${o.arcUrl.replace(/\/$/, "")}/v1/tx/${txid}`, o.arcApiKey ? { Authorization: `Bearer ${o.arcApiKey}` } : {});
|
|
268
|
+
if (status2 === 404) return { state: "not-found", source: "arc" };
|
|
269
|
+
if (status2 >= 400) throw new PaymentVerificationError("NETWORK", `ARC answered ${status2}`, txid);
|
|
270
|
+
const b = body2;
|
|
271
|
+
const s = String(b.txStatus ?? "").toUpperCase();
|
|
272
|
+
if (s === "MINED" || s === "IMMUTABLE") return { state: "confirmed", confirmations: 1, source: "arc" };
|
|
273
|
+
if (s === "REJECTED" || s === "DOUBLE_SPEND_ATTEMPTED") return { state: "rejected", source: "arc", detail: b.extraInfo ?? s };
|
|
274
|
+
if (s === "UNKNOWN" || s === "") return { state: "not-found", source: "arc", detail: s };
|
|
275
|
+
return { state: "mempool", source: "arc", detail: s };
|
|
276
|
+
}
|
|
277
|
+
const { status, body } = await getJson(f, `${woc(o.network ?? "main")}/tx/hash/${txid}`);
|
|
278
|
+
if (status === 404) return { state: "not-found", source: "woc" };
|
|
279
|
+
if (status >= 400) throw new PaymentVerificationError("NETWORK", `WhatsOnChain answered ${status}`, txid);
|
|
280
|
+
const c = Number(body.confirmations ?? 0);
|
|
281
|
+
return c > 0 ? { state: "confirmed", confirmations: c, source: "woc" } : { state: "mempool", source: "woc" };
|
|
282
|
+
}
|
|
283
|
+
async function verifyPayment(o) {
|
|
284
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
285
|
+
if (!o.expected?.length) throw new PaymentVerificationError("BAD_EXPECTED", "Pass at least one expected output");
|
|
286
|
+
const want = o.expected.map((e) => ({ script: expectedScript(e), satoshis: e.satoshis }));
|
|
287
|
+
let tx;
|
|
288
|
+
if (typeof o.tx === "string" && /^[0-9a-fA-F]{64}$/.test(o.tx)) {
|
|
289
|
+
const txid2 = o.tx.toLowerCase();
|
|
290
|
+
let r;
|
|
291
|
+
try {
|
|
292
|
+
r = await f(`${woc(o.network ?? "main")}/tx/${txid2}/hex`);
|
|
293
|
+
} catch (e) {
|
|
294
|
+
throw new PaymentVerificationError("NETWORK", `Could not reach WhatsOnChain: ${e.message}`, txid2);
|
|
295
|
+
}
|
|
296
|
+
if (r.status === 404) throw new PaymentVerificationError("TX_NOT_FOUND", `Transaction ${txid2} was not found`, txid2);
|
|
297
|
+
if (!r.ok) throw new PaymentVerificationError("NETWORK", `WhatsOnChain answered ${r.status}`, txid2);
|
|
298
|
+
tx = parseTx((await r.text()).trim());
|
|
299
|
+
if (tx.id("hex") !== txid2) throw new PaymentVerificationError("BAD_TX", "The fetched transaction does not match the txid", txid2);
|
|
300
|
+
} else {
|
|
301
|
+
if (typeof o.tx === "string" && !isHex(o.tx)) throw new PaymentVerificationError("BAD_TX", "tx must be a txid or hex");
|
|
302
|
+
tx = parseTx(o.tx);
|
|
303
|
+
}
|
|
304
|
+
const txid = tx.id("hex");
|
|
305
|
+
if (o.spv) {
|
|
306
|
+
let ok = false;
|
|
307
|
+
try {
|
|
308
|
+
ok = await tx.verify(o.spv);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
throw new PaymentVerificationError("SPV_FAILED", `SPV check failed: ${e.message}`, txid);
|
|
311
|
+
}
|
|
312
|
+
if (!ok) throw new PaymentVerificationError("SPV_FAILED", "SPV check failed", txid);
|
|
313
|
+
}
|
|
314
|
+
const used = /* @__PURE__ */ new Set();
|
|
315
|
+
const matched = [];
|
|
316
|
+
for (const w of want) {
|
|
317
|
+
const vout = tx.outputs.findIndex(
|
|
318
|
+
(out, i) => !used.has(i) && out.lockingScript.toHex() === w.script && (out.satoshis ?? 0) >= w.satoshis
|
|
319
|
+
);
|
|
320
|
+
if (vout < 0) {
|
|
321
|
+
throw new PaymentVerificationError("OUTPUT_MISSING", `No output pays ${w.satoshis} sats to script ${w.script.slice(0, 16)}\u2026`, txid);
|
|
322
|
+
}
|
|
323
|
+
used.add(vout);
|
|
324
|
+
matched.push({ vout, satoshis: tx.outputs[vout].satoshis ?? 0, script: w.script });
|
|
325
|
+
}
|
|
326
|
+
const status = await checkStatus(o, f, txid);
|
|
327
|
+
if (status.state === "rejected") throw new PaymentVerificationError("REJECTED", `The network rejected ${txid}: ${status.detail ?? ""}`.trim(), txid);
|
|
328
|
+
if ((o.requireSeen || (o.minConfirmations ?? 0) > 0) && status.state !== "mempool" && status.state !== "confirmed") {
|
|
329
|
+
throw new PaymentVerificationError("NOT_SEEN", `The network has not seen ${txid} yet`, txid);
|
|
330
|
+
}
|
|
331
|
+
const conf = status.state === "confirmed" ? status.confirmations : 0;
|
|
332
|
+
if ((o.minConfirmations ?? 0) > conf) {
|
|
333
|
+
throw new PaymentVerificationError("NOT_SEEN", `${txid} has ${conf} confirmations, needs ${o.minConfirmations}`, txid);
|
|
334
|
+
}
|
|
335
|
+
return { ok: true, txid, matched, status };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/link.ts
|
|
339
|
+
var IdentityLinkConflictError = class extends Error {
|
|
340
|
+
constructor(identityKey, otherUserId) {
|
|
341
|
+
super("This bWalletX identity is already linked to a different account. Sign in with that account, or unlink it there first.");
|
|
342
|
+
this.identityKey = identityKey;
|
|
343
|
+
this.otherUserId = otherUserId;
|
|
344
|
+
this.name = "IdentityLinkConflictError";
|
|
345
|
+
}
|
|
346
|
+
identityKey;
|
|
347
|
+
otherUserId;
|
|
348
|
+
code = "IDENTITY_LINKED_TO_OTHER_USER";
|
|
349
|
+
};
|
|
350
|
+
var norm = (k) => {
|
|
351
|
+
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)");
|
|
352
|
+
return k.toLowerCase();
|
|
353
|
+
};
|
|
354
|
+
async function linkIdentity(userId, identityKey, cb) {
|
|
355
|
+
if (!userId) throw new Error("linkIdentity needs the current user id");
|
|
356
|
+
const key = norm(identityKey);
|
|
357
|
+
const owner = await cb.findUserByIdentityKey(key);
|
|
358
|
+
if (owner && owner !== userId) throw new IdentityLinkConflictError(key, owner);
|
|
359
|
+
if (owner === userId) return { status: "already-linked", userId, identityKey: key };
|
|
360
|
+
await cb.attach(userId, key);
|
|
361
|
+
return { status: "linked", userId, identityKey: key };
|
|
362
|
+
}
|
|
363
|
+
async function signInOrLink(identityKey, o) {
|
|
364
|
+
const key = norm(identityKey);
|
|
365
|
+
if (o.currentUserId) return linkIdentity(o.currentUserId, key, o);
|
|
366
|
+
const owner = await o.findUserByIdentityKey(key);
|
|
367
|
+
if (owner) return { status: "signed-in", userId: owner, identityKey: key };
|
|
368
|
+
if (!o.createUser) return { status: "unknown", userId: null, identityKey: key };
|
|
369
|
+
const userId = await o.createUser(key);
|
|
370
|
+
await o.attach(userId, key);
|
|
371
|
+
return { status: "created", userId, identityKey: key };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// src/access.ts
|
|
375
|
+
var ACCESS_VERSION = 1;
|
|
376
|
+
var ACCESS_PROTOCOL = [2, "bwalletx access"];
|
|
377
|
+
var ACCESS_CHALLENGE_LIFETIME_S = 120;
|
|
378
|
+
var MAX_HOLDING_PROOFS = 50;
|
|
379
|
+
var ACCESS_ERROR_STATUS = {
|
|
380
|
+
not_installed: 424,
|
|
381
|
+
user_rejected: 403,
|
|
382
|
+
wrong_account: 409,
|
|
383
|
+
insufficient_tokens: 403,
|
|
384
|
+
identity_level_too_low: 403,
|
|
385
|
+
kyc_required: 403,
|
|
386
|
+
indexer_unavailable: 503,
|
|
387
|
+
expired: 401,
|
|
388
|
+
replay: 401,
|
|
389
|
+
invalid_signature: 401,
|
|
390
|
+
bad_request: 400
|
|
391
|
+
};
|
|
392
|
+
var AccessError = class extends Error {
|
|
393
|
+
code;
|
|
394
|
+
have;
|
|
395
|
+
need;
|
|
396
|
+
constructor(code, message, extra = {}) {
|
|
397
|
+
super(message ?? code);
|
|
398
|
+
this.name = "AccessError";
|
|
399
|
+
this.code = code;
|
|
400
|
+
this.have = extra.have;
|
|
401
|
+
this.need = extra.need;
|
|
402
|
+
}
|
|
403
|
+
toJSON() {
|
|
404
|
+
const b = { error: this.code, message: this.message };
|
|
405
|
+
if (this.have !== void 0) b.have = this.have;
|
|
406
|
+
if (this.need !== void 0) b.need = this.need;
|
|
407
|
+
return b;
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
var PROTOCOL_MEDIA = {
|
|
411
|
+
"401": { name: "Identity", color: "#22c55e", video: "assets/protocol/401.mp4", poster: "assets/protocol/401.jpg" },
|
|
412
|
+
"402": { name: "Payment", color: "#3b82f6", video: "assets/protocol/402.mp4", poster: "assets/protocol/402.jpg" },
|
|
413
|
+
"403": { name: "Access", color: "#ef4444", video: "assets/protocol/403.mp4", poster: "assets/protocol/403.jpg" }
|
|
414
|
+
};
|
|
415
|
+
function protocolMediaUrl(kind, which, version = "latest") {
|
|
416
|
+
return `https://unpkg.com/@bwalletx/connect@${version}/${PROTOCOL_MEDIA[kind][which]}`;
|
|
417
|
+
}
|
|
418
|
+
function canonicalJson(v) {
|
|
419
|
+
if (v === null) return "null";
|
|
420
|
+
if (typeof v === "string" || typeof v === "boolean") return JSON.stringify(v);
|
|
421
|
+
if (typeof v === "number") {
|
|
422
|
+
if (!Number.isFinite(v)) throw new Error("canonicalJson: non-finite number");
|
|
423
|
+
return JSON.stringify(v);
|
|
424
|
+
}
|
|
425
|
+
if (Array.isArray(v)) return `[${v.map((x) => canonicalJson(x)).join(",")}]`;
|
|
426
|
+
if (typeof v === "object") {
|
|
427
|
+
const o = v;
|
|
428
|
+
const keys2 = Object.keys(o).filter((k) => o[k] !== void 0).sort();
|
|
429
|
+
return `{${keys2.map((k) => `${JSON.stringify(k)}:${canonicalJson(o[k])}`).join(",")}}`;
|
|
430
|
+
}
|
|
431
|
+
throw new Error(`canonicalJson: unsupported ${typeof v}`);
|
|
432
|
+
}
|
|
433
|
+
function challengeBytes(challenge) {
|
|
434
|
+
return Array.from(new TextEncoder().encode(challenge));
|
|
435
|
+
}
|
|
436
|
+
var TOKEN_ID = /^bsv21:[0-9a-f]{64}_\d{1,6}$/;
|
|
437
|
+
var RAW = /^[1-9]\d{0,38}$/;
|
|
438
|
+
function normaliseRequirement(r) {
|
|
439
|
+
if (!r || typeof r !== "object") throw new AccessError("bad_request", "require must be an object");
|
|
440
|
+
const req = r;
|
|
441
|
+
const out = {};
|
|
442
|
+
if (req.token !== void 0) {
|
|
443
|
+
const t = req.token;
|
|
444
|
+
if (!t || typeof t !== "object" || typeof t.id !== "string") throw new AccessError("bad_request", "require.token.id is required");
|
|
445
|
+
const id = t.id.trim().toLowerCase();
|
|
446
|
+
if (!TOKEN_ID.test(id)) throw new AccessError("bad_request", "require.token.id must be 'bsv21:<txid>_<vout>'");
|
|
447
|
+
const hasMin = t.min !== void 0;
|
|
448
|
+
const hasBps = t.minBps !== void 0;
|
|
449
|
+
if (hasMin === hasBps) throw new AccessError("bad_request", "require.token needs exactly one of min or minBps");
|
|
450
|
+
out.token = { id };
|
|
451
|
+
if (hasMin) {
|
|
452
|
+
if (typeof t.min !== "string" || !RAW.test(t.min)) throw new AccessError("bad_request", "require.token.min must be a positive integer string (raw units)");
|
|
453
|
+
out.token.min = t.min;
|
|
454
|
+
} else {
|
|
455
|
+
if (!Number.isInteger(t.minBps) || t.minBps < 1 || t.minBps > 1e4) {
|
|
456
|
+
throw new AccessError("bad_request", "require.token.minBps must be an integer 1..10000");
|
|
457
|
+
}
|
|
458
|
+
out.token.minBps = t.minBps;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (req.identityLevel !== void 0) {
|
|
462
|
+
if (!Number.isInteger(req.identityLevel) || req.identityLevel < 1 || req.identityLevel > 10) {
|
|
463
|
+
throw new AccessError("bad_request", "require.identityLevel must be an integer 1..10");
|
|
464
|
+
}
|
|
465
|
+
out.identityLevel = req.identityLevel;
|
|
466
|
+
}
|
|
467
|
+
if (req.kyc !== void 0) {
|
|
468
|
+
if (typeof req.kyc !== "boolean") throw new AccessError("bad_request", "require.kyc must be a boolean");
|
|
469
|
+
if (req.kyc) out.kyc = true;
|
|
470
|
+
}
|
|
471
|
+
if (!out.token && !out.identityLevel && !out.kyc) throw new AccessError("bad_request", "require is empty");
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
function kindFor(r) {
|
|
475
|
+
return r.token ? "403" : "401";
|
|
476
|
+
}
|
|
477
|
+
function parseAccessChallenge(s) {
|
|
478
|
+
if (typeof s !== "string" || s.length > 4096) throw new AccessError("bad_request", "challenge must be a string");
|
|
479
|
+
let c;
|
|
480
|
+
try {
|
|
481
|
+
c = JSON.parse(s);
|
|
482
|
+
} catch {
|
|
483
|
+
throw new AccessError("bad_request", "challenge is not JSON");
|
|
484
|
+
}
|
|
485
|
+
if (!c || typeof c !== "object" || c.v !== 1) throw new AccessError("bad_request", "unsupported challenge version");
|
|
486
|
+
if (c.kind !== "401" && c.kind !== "402" && c.kind !== "403") throw new AccessError("bad_request", "bad kind");
|
|
487
|
+
if (typeof c.origin !== "string" || typeof c.nonce !== "string" || !/^[A-Za-z0-9_-]{16,64}$/.test(c.nonce)) {
|
|
488
|
+
throw new AccessError("bad_request", "bad origin or nonce");
|
|
489
|
+
}
|
|
490
|
+
if (!Number.isInteger(c.iat) || !Number.isInteger(c.exp) || c.exp <= c.iat) throw new AccessError("bad_request", "bad iat/exp");
|
|
491
|
+
if (c.sub !== void 0 && (typeof c.sub !== "string" || !/^0[23][0-9a-f]{64}$/.test(c.sub))) throw new AccessError("bad_request", "bad sub");
|
|
492
|
+
c.require = normaliseRequirement(c.require);
|
|
493
|
+
if (canonicalJson(c) !== s) throw new AccessError("bad_request", "challenge is not canonical");
|
|
494
|
+
return c;
|
|
495
|
+
}
|
|
496
|
+
function tokenNeedRaw(t, supplyRaw) {
|
|
497
|
+
if (t.min) return t.min;
|
|
498
|
+
if (supplyRaw === void 0 || !/^\d+$/.test(supplyRaw)) throw new AccessError("indexer_unavailable", "token supply unknown");
|
|
499
|
+
const n = BigInt(supplyRaw) * BigInt(t.minBps);
|
|
500
|
+
const need = (n + 9999n) / 10000n;
|
|
501
|
+
return (need < 1n ? 1n : need).toString();
|
|
502
|
+
}
|
|
503
|
+
function atLeastRaw(have, need) {
|
|
504
|
+
try {
|
|
505
|
+
return BigInt(have) >= BigInt(need);
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
function accessErrorMessage(code, e = {}) {
|
|
511
|
+
switch (code) {
|
|
512
|
+
case "not_installed":
|
|
513
|
+
return "No bWalletX (or other BRC-100 wallet) was found.";
|
|
514
|
+
case "user_rejected":
|
|
515
|
+
return "The request was declined in the wallet.";
|
|
516
|
+
case "wrong_account":
|
|
517
|
+
return "The wallet is signed in to a different account than this site expects.";
|
|
518
|
+
case "insufficient_tokens":
|
|
519
|
+
return `Not enough tokens: holding ${e.have ?? 0}, need ${e.need ?? "?"}.`;
|
|
520
|
+
case "identity_level_too_low":
|
|
521
|
+
return `Identity level ${e.have ?? 0} is below the required level ${e.need ?? "?"}.`;
|
|
522
|
+
case "kyc_required":
|
|
523
|
+
return "A verified identity (KYC) is required.";
|
|
524
|
+
case "indexer_unavailable":
|
|
525
|
+
return "Holdings could not be checked right now. Try again shortly.";
|
|
526
|
+
case "expired":
|
|
527
|
+
return "The request expired. Try again.";
|
|
528
|
+
case "replay":
|
|
529
|
+
return "That request was already used. Try again.";
|
|
530
|
+
case "invalid_signature":
|
|
531
|
+
return "The wallet signature did not verify.";
|
|
532
|
+
case "bad_request":
|
|
533
|
+
return "The request was malformed.";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/access-server.ts
|
|
538
|
+
var import_sdk2 = require("@bsv/sdk");
|
|
539
|
+
var bare = (tokenId) => tokenId.replace(/^bsv21:/, "");
|
|
540
|
+
function oneSatResolver(o = {}) {
|
|
541
|
+
const base = (o.base ?? "https://api.1sat.app/1sat").replace(/\/$/, "");
|
|
542
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
543
|
+
const t = o.timeoutMs ?? 12e3;
|
|
544
|
+
const call = async (path, init) => {
|
|
545
|
+
const res = await f(`${base}${path}`, { ...init, signal: AbortSignal.timeout(t), cache: "no-store" });
|
|
546
|
+
if (!res.ok) throw new Error(`1sat ${res.status} for ${path}`);
|
|
547
|
+
return res.json();
|
|
548
|
+
};
|
|
549
|
+
return {
|
|
550
|
+
async tokenBalance(tokenId, addresses) {
|
|
551
|
+
const sorted = [...new Set(addresses)].sort();
|
|
552
|
+
let total = 0n;
|
|
553
|
+
for (let i = 0; i < sorted.length; i += 100) {
|
|
554
|
+
const j = await call(`/bsv21/${bare(tokenId)}/p2pkh/balance`, {
|
|
555
|
+
method: "POST",
|
|
556
|
+
headers: { "content-type": "application/json" },
|
|
557
|
+
body: JSON.stringify(sorted.slice(i, i + 100))
|
|
558
|
+
});
|
|
559
|
+
const b = j?.balance ?? 0;
|
|
560
|
+
const s = typeof b === "number" ? String(Math.trunc(b)) : String(b).split(".")[0] || "0";
|
|
561
|
+
if (!/^\d+$/.test(s)) throw new Error("1sat returned a bad balance");
|
|
562
|
+
total += BigInt(s);
|
|
563
|
+
}
|
|
564
|
+
return total.toString();
|
|
565
|
+
},
|
|
566
|
+
async tokenSupply(tokenId) {
|
|
567
|
+
const j = await call(`/bsv21/${bare(tokenId)}`);
|
|
568
|
+
const tok = (j?.token ?? j) || {};
|
|
569
|
+
const v = tok.amt ?? tok.max ?? tok.supply;
|
|
570
|
+
const s = v === void 0 || v === null ? "" : String(v).split(".")[0];
|
|
571
|
+
if (!/^\d+$/.test(s) || s === "0") throw new Error("1sat did not report a supply");
|
|
572
|
+
return s;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function bitSignResolver(o = {}) {
|
|
577
|
+
const base = (o.base ?? "https://bit-sign.online").replace(/\/$/, "");
|
|
578
|
+
const f = o.fetch ?? globalThis.fetch;
|
|
579
|
+
const tokens = o.tokens ?? oneSatResolver({ fetch: f });
|
|
580
|
+
return {
|
|
581
|
+
tokenBalance: (id, a) => tokens.tokenBalance(id, a),
|
|
582
|
+
tokenSupply: tokens.tokenSupply ? (id) => tokens.tokenSupply(id) : void 0,
|
|
583
|
+
async identity(identityKey, response) {
|
|
584
|
+
const res = await f(`${base}/api/access/verify`, {
|
|
585
|
+
method: "POST",
|
|
586
|
+
headers: { "content-type": "application/json" },
|
|
587
|
+
body: JSON.stringify({ response, only: "identity" }),
|
|
588
|
+
signal: AbortSignal.timeout(o.timeoutMs ?? 12e3),
|
|
589
|
+
cache: "no-store"
|
|
590
|
+
});
|
|
591
|
+
const j = await res.json().catch(() => null);
|
|
592
|
+
if (!j?.identity || j.identityKey !== identityKey) throw new Error(`bit-sign ${res.status}`);
|
|
593
|
+
return { level: Number(j.identity.level) || 0, kyc: j.identity.kyc === true };
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
function normalOrigin(origin) {
|
|
598
|
+
const u = new URL(origin);
|
|
599
|
+
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
600
|
+
return u.origin;
|
|
601
|
+
}
|
|
602
|
+
function randomNonce() {
|
|
603
|
+
const b = new Uint8Array(18);
|
|
604
|
+
globalThis.crypto.getRandomValues(b);
|
|
605
|
+
let s = "";
|
|
606
|
+
for (const x of b) s += String.fromCharCode(x);
|
|
607
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
608
|
+
}
|
|
609
|
+
async function createAccessChallenge(o) {
|
|
610
|
+
const origin = normalOrigin(o.origin);
|
|
611
|
+
const require2 = normaliseRequirement(o.require);
|
|
612
|
+
let sub;
|
|
613
|
+
if (o.identityKey !== void 0 && o.identityKey !== null && o.identityKey !== "") {
|
|
614
|
+
if (!isIdentityKey(o.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
615
|
+
sub = o.identityKey.toLowerCase();
|
|
616
|
+
}
|
|
617
|
+
const iat = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
618
|
+
const exp = iat + (o.lifetimeS ?? ACCESS_CHALLENGE_LIFETIME_S);
|
|
619
|
+
const nonce = randomNonce();
|
|
620
|
+
const c = { v: 1, kind: o.kind ?? kindFor(require2), origin, nonce, iat, exp, sub, require: require2 };
|
|
621
|
+
const challenge = canonicalJson(c);
|
|
622
|
+
await o.store.put(nonce, { identityKey: sub ?? "", origin, message: challenge, expiresAt: exp * 1e3 });
|
|
623
|
+
return { challenge, nonce, kind: c.kind, protocolID: ACCESS_PROTOCOL, keyID: nonce, exp };
|
|
624
|
+
}
|
|
625
|
+
var isByteArray = (s) => Array.isArray(s) && s.length >= 8 && s.length <= 80 && s.every((b) => Number.isInteger(b) && b >= 0 && b <= 255);
|
|
626
|
+
function verifyHoldingProof(challenge, p) {
|
|
627
|
+
const proof = p;
|
|
628
|
+
if (!proof || typeof proof.pubkey !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(proof.pubkey) || !isByteArray(proof.signature)) return null;
|
|
629
|
+
try {
|
|
630
|
+
const pub = import_sdk2.PublicKey.fromString(proof.pubkey);
|
|
631
|
+
const sig = import_sdk2.Signature.fromDER(proof.signature);
|
|
632
|
+
return pub.verify(challengeBytes(challenge), sig) ? pub.toAddress() : null;
|
|
633
|
+
} catch {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async function verifyAccess(o) {
|
|
638
|
+
const r = o.response;
|
|
639
|
+
if (!r || typeof r !== "object" || r.v !== 1) throw new AccessError("bad_request", "response must be { v: 1, challenge, identityKey, signature }");
|
|
640
|
+
const c = parseAccessChallenge(r.challenge);
|
|
641
|
+
const nowS = Math.floor((o.now ?? Date.now()) / 1e3);
|
|
642
|
+
const pending = await o.store.take(c.nonce);
|
|
643
|
+
if (!pending) throw new AccessError(nowS >= c.exp ? "expired" : "replay");
|
|
644
|
+
if (nowS >= c.exp || (o.now ?? Date.now()) > pending.expiresAt) throw new AccessError("expired");
|
|
645
|
+
if (pending.message !== r.challenge) throw new AccessError("replay", "challenge does not match the one issued");
|
|
646
|
+
let origin;
|
|
647
|
+
try {
|
|
648
|
+
origin = normalOrigin(o.origin);
|
|
649
|
+
} catch {
|
|
650
|
+
throw new AccessError("bad_request", "server origin is misconfigured");
|
|
651
|
+
}
|
|
652
|
+
if (c.origin !== origin || pending.origin !== origin) throw new AccessError("bad_request", "origin mismatch");
|
|
653
|
+
if (o.require && canonicalJson(normaliseRequirement(o.require)) !== canonicalJson(c.require)) {
|
|
654
|
+
throw new AccessError("bad_request", "challenge requirement does not match this gate");
|
|
655
|
+
}
|
|
656
|
+
if (!isIdentityKey(r.identityKey)) throw new AccessError("bad_request", "identityKey must be a compressed public key");
|
|
657
|
+
const identityKey = r.identityKey.toLowerCase();
|
|
658
|
+
if (c.sub && c.sub !== identityKey) throw new AccessError("wrong_account");
|
|
659
|
+
if (!isByteArray(r.signature)) throw new AccessError("invalid_signature");
|
|
660
|
+
let valid = false;
|
|
661
|
+
try {
|
|
662
|
+
({ valid } = await new import_sdk2.ProtoWallet("anyone").verifySignature({
|
|
663
|
+
protocolID: ACCESS_PROTOCOL,
|
|
664
|
+
keyID: c.nonce,
|
|
665
|
+
counterparty: identityKey,
|
|
666
|
+
data: challengeBytes(r.challenge),
|
|
667
|
+
signature: r.signature
|
|
668
|
+
}));
|
|
669
|
+
} catch {
|
|
670
|
+
valid = false;
|
|
671
|
+
}
|
|
672
|
+
if (!valid) throw new AccessError("invalid_signature");
|
|
673
|
+
const grant = { ok: true, identityKey, kind: c.kind, require: c.require, addresses: [], have: {} };
|
|
674
|
+
if (c.require.identityLevel || c.require.kyc) {
|
|
675
|
+
if (!o.resolver.identity) throw new AccessError("indexer_unavailable", "no identity resolver configured");
|
|
676
|
+
let id;
|
|
677
|
+
try {
|
|
678
|
+
id = await o.resolver.identity(identityKey, r);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
throw new AccessError("indexer_unavailable", `identity lookup failed: ${e.message}`);
|
|
681
|
+
}
|
|
682
|
+
grant.have.identityLevel = id.level;
|
|
683
|
+
grant.have.kyc = id.kyc;
|
|
684
|
+
if (c.require.kyc && !id.kyc) throw new AccessError("kyc_required");
|
|
685
|
+
if (c.require.identityLevel && id.level < c.require.identityLevel) {
|
|
686
|
+
throw new AccessError("identity_level_too_low", void 0, { have: id.level, need: c.require.identityLevel });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (c.require.token) {
|
|
690
|
+
const proofs = Array.isArray(r.holdings) ? r.holdings.slice(0, MAX_HOLDING_PROOFS) : [];
|
|
691
|
+
const addresses = /* @__PURE__ */ new Set();
|
|
692
|
+
for (const p of proofs) {
|
|
693
|
+
const a = verifyHoldingProof(r.challenge, p);
|
|
694
|
+
if (a) addresses.add(a);
|
|
695
|
+
}
|
|
696
|
+
grant.addresses = [...addresses].sort();
|
|
697
|
+
const check = await checkTokenHolding(c.require, grant.addresses, o.resolver);
|
|
698
|
+
grant.have.tokenRaw = check.have;
|
|
699
|
+
grant.have.needRaw = check.need;
|
|
700
|
+
}
|
|
701
|
+
return grant;
|
|
702
|
+
}
|
|
703
|
+
async function checkTokenHolding(require2, addresses, resolver) {
|
|
704
|
+
const t = require2.token;
|
|
705
|
+
if (!t) return { have: "0", need: "0" };
|
|
706
|
+
let supply;
|
|
707
|
+
if (t.minBps) {
|
|
708
|
+
if (!resolver.tokenSupply) throw new AccessError("indexer_unavailable", "no supply resolver configured");
|
|
709
|
+
try {
|
|
710
|
+
supply = await resolver.tokenSupply(t.id);
|
|
711
|
+
} catch (e) {
|
|
712
|
+
throw new AccessError("indexer_unavailable", `supply lookup failed: ${e.message}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const need = tokenNeedRaw(t, supply);
|
|
716
|
+
if (!addresses.length) throw new AccessError("insufficient_tokens", void 0, { have: "0", need });
|
|
717
|
+
let have;
|
|
718
|
+
try {
|
|
719
|
+
have = await resolver.tokenBalance(t.id, addresses);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
throw new AccessError("indexer_unavailable", `balance lookup failed: ${e.message}`);
|
|
722
|
+
}
|
|
723
|
+
if (!atLeastRaw(have, need)) throw new AccessError("insufficient_tokens", void 0, { have, need });
|
|
724
|
+
return { have, need };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// src/server.ts
|
|
34
728
|
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
35
729
|
var CHALLENGE_LIFETIME_MS = 2 * 60 * 1e3;
|
|
36
730
|
var warned = false;
|
|
37
731
|
function memoryStore(opts = {}) {
|
|
38
|
-
|
|
732
|
+
const prod = typeof process !== "undefined" && process.env?.NODE_ENV === "production";
|
|
733
|
+
if ((prod || !opts.quiet) && !warned) {
|
|
39
734
|
warned = true;
|
|
40
|
-
console.warn(
|
|
735
|
+
console.warn(
|
|
736
|
+
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."
|
|
737
|
+
);
|
|
41
738
|
}
|
|
42
739
|
const m = /* @__PURE__ */ new Map();
|
|
43
740
|
return {
|
|
@@ -58,13 +755,13 @@ var storeOrDefault = (s) => s ?? (defaultStore ??= memoryStore());
|
|
|
58
755
|
function isIdentityKey(x) {
|
|
59
756
|
if (typeof x !== "string" || !/^0[23][0-9a-fA-F]{64}$/.test(x)) return false;
|
|
60
757
|
try {
|
|
61
|
-
|
|
758
|
+
import_sdk3.PublicKey.fromString(x);
|
|
62
759
|
return true;
|
|
63
760
|
} catch {
|
|
64
761
|
return false;
|
|
65
762
|
}
|
|
66
763
|
}
|
|
67
|
-
function
|
|
764
|
+
function normalOrigin2(origin) {
|
|
68
765
|
const u = new URL(origin);
|
|
69
766
|
if (u.origin === "null" || u.origin !== origin.replace(/\/$/, "")) {
|
|
70
767
|
throw new Error(`origin must be scheme://host[:port], got ${origin}`);
|
|
@@ -82,7 +779,7 @@ function loginMessage(origin, nonce, expiresAt) {
|
|
|
82
779
|
`Expires: ${new Date(expiresAt).toISOString()}`
|
|
83
780
|
].join("\n");
|
|
84
781
|
}
|
|
85
|
-
function
|
|
782
|
+
function randomNonce2() {
|
|
86
783
|
const b = new Uint8Array(16);
|
|
87
784
|
globalThis.crypto.getRandomValues(b);
|
|
88
785
|
let s = "";
|
|
@@ -91,8 +788,8 @@ function randomNonce() {
|
|
|
91
788
|
}
|
|
92
789
|
async function createChallenge(opts) {
|
|
93
790
|
if (!isIdentityKey(opts.identityKey)) throw new Error("identityKey must be a compressed public key (66 hex chars)");
|
|
94
|
-
const origin =
|
|
95
|
-
const nonce =
|
|
791
|
+
const origin = normalOrigin2(opts.origin);
|
|
792
|
+
const nonce = randomNonce2();
|
|
96
793
|
const expiresAt = Date.now() + CHALLENGE_LIFETIME_MS;
|
|
97
794
|
const message = loginMessage(origin, nonce, expiresAt);
|
|
98
795
|
await storeOrDefault(opts.store).put(nonce, { identityKey: opts.identityKey.toLowerCase(), origin, message, expiresAt });
|
|
@@ -104,7 +801,7 @@ async function verifySignIn(opts) {
|
|
|
104
801
|
if (!c || Date.now() > c.expiresAt) return null;
|
|
105
802
|
let origin;
|
|
106
803
|
try {
|
|
107
|
-
origin =
|
|
804
|
+
origin = normalOrigin2(opts.origin);
|
|
108
805
|
} catch {
|
|
109
806
|
return null;
|
|
110
807
|
}
|
|
@@ -117,7 +814,7 @@ async function verifySignIn(opts) {
|
|
|
117
814
|
return null;
|
|
118
815
|
}
|
|
119
816
|
try {
|
|
120
|
-
const { valid } = await new
|
|
817
|
+
const { valid } = await new import_sdk3.ProtoWallet("anyone").verifySignature({
|
|
121
818
|
protocolID: LOGIN_PROTOCOL,
|
|
122
819
|
keyID: opts.nonce,
|
|
123
820
|
counterparty: identityKey,
|
|
@@ -131,20 +828,55 @@ async function verifySignIn(opts) {
|
|
|
131
828
|
}
|
|
132
829
|
function originAllowed(requestOrigin, origin) {
|
|
133
830
|
try {
|
|
134
|
-
return !!requestOrigin && requestOrigin ===
|
|
831
|
+
return !!requestOrigin && requestOrigin === normalOrigin2(origin);
|
|
135
832
|
} catch {
|
|
136
833
|
return false;
|
|
137
834
|
}
|
|
138
835
|
}
|
|
139
836
|
// Annotate the CommonJS export names for ESM import in node:
|
|
140
837
|
0 && (module.exports = {
|
|
838
|
+
ACCESS_CHALLENGE_LIFETIME_S,
|
|
839
|
+
ACCESS_ERROR_STATUS,
|
|
840
|
+
ACCESS_PROTOCOL,
|
|
841
|
+
ACCESS_VERSION,
|
|
842
|
+
AccessError,
|
|
141
843
|
CHALLENGE_LIFETIME_MS,
|
|
844
|
+
IdentityLinkConflictError,
|
|
142
845
|
LOGIN_PROTOCOL,
|
|
846
|
+
MAX_HOLDING_PROOFS,
|
|
847
|
+
MIN_SECRET_LENGTH,
|
|
848
|
+
PROTOCOL_MEDIA,
|
|
849
|
+
PaymentVerificationError,
|
|
850
|
+
accessErrorMessage,
|
|
851
|
+
atLeastRaw,
|
|
852
|
+
bitSignResolver,
|
|
853
|
+
canonicalJson,
|
|
854
|
+
challengeBytes,
|
|
855
|
+
checkTokenHolding,
|
|
856
|
+
createAccessChallenge,
|
|
143
857
|
createChallenge,
|
|
144
858
|
isIdentityKey,
|
|
859
|
+
kindFor,
|
|
860
|
+
linkIdentity,
|
|
145
861
|
loginMessage,
|
|
146
862
|
memoryStore,
|
|
863
|
+
normaliseRequirement,
|
|
864
|
+
oneSatResolver,
|
|
147
865
|
originAllowed,
|
|
866
|
+
parseAccessChallenge,
|
|
867
|
+
pgNonceTableSql,
|
|
868
|
+
pgStore,
|
|
869
|
+
protocolMediaUrl,
|
|
870
|
+
readCookie,
|
|
871
|
+
redisStore,
|
|
872
|
+
serializeCookie,
|
|
873
|
+
signInOrLink,
|
|
874
|
+
signSession,
|
|
875
|
+
tokenNeedRaw,
|
|
876
|
+
verifyAccess,
|
|
877
|
+
verifyHoldingProof,
|
|
878
|
+
verifyPayment,
|
|
879
|
+
verifySession,
|
|
148
880
|
verifySignIn
|
|
149
881
|
});
|
|
150
882
|
//# sourceMappingURL=server.cjs.map
|