@bwalletx/connect 0.1.0 → 0.2.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/BRAND.md +68 -0
- package/README.md +39 -0
- package/TRADEMARKS.md +9 -0
- package/assets/bwalletx-mark-circle.svg +1 -0
- package/assets/bwalletx-mark.svg +1 -0
- package/assets/signin-dark.svg +8 -0
- package/assets/signin-gold.svg +8 -0
- package/assets/signin-light.svg +8 -0
- package/assets/snippet.html +17 -0
- package/dist/bwalletx-connect.global.js +48 -2
- package/dist/bwalletx-connect.global.js.map +1 -1
- package/dist/chunk-NNMUSWPG.js +649 -0
- package/dist/chunk-NNMUSWPG.js.map +1 -0
- package/dist/element.cjs +665 -0
- package/dist/element.cjs.map +1 -0
- package/dist/element.d.cts +29 -0
- package/dist/element.d.ts +29 -0
- package/dist/element.js +24 -0
- package/dist/element.js.map +1 -0
- package/dist/index.cjs +218 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +31 -426
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +700 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +70 -0
- package/dist/react.d.ts +70 -0
- package/dist/react.js +59 -0
- package/dist/react.js.map +1 -0
- package/package.json +53 -6
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/react.ts
|
|
21
|
+
var react_exports = {};
|
|
22
|
+
__export(react_exports, {
|
|
23
|
+
BwalletxSignin: () => BwalletxSignin,
|
|
24
|
+
default: () => react_default
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(react_exports);
|
|
27
|
+
var import_react = require("react");
|
|
28
|
+
|
|
29
|
+
// src/discover.ts
|
|
30
|
+
var RDNS_EXTENSION = "com.bwalletx.extension";
|
|
31
|
+
var RDNS_MOBILE = "space.bwallet.mobile";
|
|
32
|
+
var BWALLETX_RDNS = [RDNS_EXTENSION, RDNS_MOBILE];
|
|
33
|
+
function discoverBwalletX(ms = 1200) {
|
|
34
|
+
if (typeof window === "undefined") return Promise.resolve(null);
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
let done = false;
|
|
37
|
+
const finish = (r) => {
|
|
38
|
+
if (done) return;
|
|
39
|
+
done = true;
|
|
40
|
+
window.removeEventListener("brc100:announceWallet", onAnnounce);
|
|
41
|
+
resolve(r);
|
|
42
|
+
};
|
|
43
|
+
const onAnnounce = (e) => {
|
|
44
|
+
const d = e.detail;
|
|
45
|
+
const rdns = d?.info?.rdns ?? "";
|
|
46
|
+
if (d?.wallet && BWALLETX_RDNS.includes(rdns)) {
|
|
47
|
+
finish({ wallet: d.wallet, method: rdns === RDNS_EXTENSION ? "extension" : "in-app", rdns });
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
window.addEventListener("brc100:announceWallet", onAnnounce);
|
|
51
|
+
window.dispatchEvent(new Event("brc100:requestWallet"));
|
|
52
|
+
setTimeout(() => {
|
|
53
|
+
const cwi = window.CWI;
|
|
54
|
+
finish(cwi ? { wallet: cwi, method: "in-app", rdns: null } : null);
|
|
55
|
+
}, ms);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/pair.ts
|
|
60
|
+
var import_sdk2 = require("@bsv/sdk");
|
|
61
|
+
|
|
62
|
+
// src/in-order.ts
|
|
63
|
+
function inOrder(handle, onError) {
|
|
64
|
+
let queue = Promise.resolve();
|
|
65
|
+
return (frame) => {
|
|
66
|
+
queue = queue.then(() => handle(frame)).catch((e) => onError?.(e));
|
|
67
|
+
return queue;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/protocol.ts
|
|
72
|
+
var import_sdk = require("@bsv/sdk");
|
|
73
|
+
var PAIR_VERSION = "1";
|
|
74
|
+
var PAIR_INFO = "bwallet-pair-v1";
|
|
75
|
+
var PAIR_HOST = "www.bwallet.space";
|
|
76
|
+
var DEFAULT_RELAY = "relay.bwallet.space";
|
|
77
|
+
var QR_LIFETIME_S = 120;
|
|
78
|
+
var enc = new TextEncoder();
|
|
79
|
+
var dec = new TextDecoder();
|
|
80
|
+
var binary = (bytes) => {
|
|
81
|
+
let s = "";
|
|
82
|
+
for (let i = 0; i < bytes.length; i += 32768) s += String.fromCharCode(...bytes.subarray(i, i + 32768));
|
|
83
|
+
return s;
|
|
84
|
+
};
|
|
85
|
+
var b64url = (bytes) => btoa(binary(bytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
86
|
+
var fromB64url = (s) => {
|
|
87
|
+
const t = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
88
|
+
const bin = atob(t + "=".repeat((4 - t.length % 4) % 4));
|
|
89
|
+
return Uint8Array.from(bin, (ch) => ch.charCodeAt(0));
|
|
90
|
+
};
|
|
91
|
+
var b64 = (bytes) => btoa(binary(bytes));
|
|
92
|
+
var fromB64 = (s) => Uint8Array.from(atob(s), (ch) => ch.charCodeAt(0));
|
|
93
|
+
var newChannel = () => b64url(crypto.getRandomValues(new Uint8Array(16)));
|
|
94
|
+
function pairUrl(link) {
|
|
95
|
+
const q = new URLSearchParams({ v: link.v, r: link.r, c: link.c, k: link.k, o: link.o, e: String(link.e) });
|
|
96
|
+
return `https://${PAIR_HOST}/pair?${q}`;
|
|
97
|
+
}
|
|
98
|
+
var relaySocketUrl = (relay, channel, role, expiry) => {
|
|
99
|
+
const local = /^(localhost|127\.0\.0\.1)(:\d+)?$/.test(relay);
|
|
100
|
+
const q = new URLSearchParams({ role });
|
|
101
|
+
if (expiry) q.set("e", String(expiry));
|
|
102
|
+
return `${local ? "ws" : "wss"}://${relay}/v1/c/${channel}?${q}`;
|
|
103
|
+
};
|
|
104
|
+
async function deriveSession(mine, theirs, channel) {
|
|
105
|
+
const point = mine.deriveSharedSecret(import_sdk.PublicKey.fromString(theirs));
|
|
106
|
+
const ikm = new Uint8Array(point.encode(true));
|
|
107
|
+
const base = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
|
|
108
|
+
const bits = new Uint8Array(
|
|
109
|
+
await crypto.subtle.deriveBits(
|
|
110
|
+
{ name: "HKDF", hash: "SHA-256", salt: fromB64url(channel), info: enc.encode(PAIR_INFO) },
|
|
111
|
+
base,
|
|
112
|
+
256
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
const key = await crypto.subtle.importKey("raw", bits, "AES-GCM", false, ["encrypt", "decrypt"]);
|
|
116
|
+
const h = new Uint8Array(await crypto.subtle.digest("SHA-256", bits));
|
|
117
|
+
const code = String((h[0] << 8 | h[1]) % 1e4).padStart(4, "0");
|
|
118
|
+
return { key, code };
|
|
119
|
+
}
|
|
120
|
+
var Sealer = class {
|
|
121
|
+
constructor(key, me) {
|
|
122
|
+
this.key = key;
|
|
123
|
+
this.me = me;
|
|
124
|
+
}
|
|
125
|
+
key;
|
|
126
|
+
me;
|
|
127
|
+
sent = 0;
|
|
128
|
+
lastSeen = 0;
|
|
129
|
+
async seal(msg) {
|
|
130
|
+
const s = ++this.sent;
|
|
131
|
+
const n = crypto.getRandomValues(new Uint8Array(12));
|
|
132
|
+
const ct = new Uint8Array(
|
|
133
|
+
await crypto.subtle.encrypt(
|
|
134
|
+
{ name: "AES-GCM", iv: n, additionalData: enc.encode(`${this.me}|${s}`) },
|
|
135
|
+
this.key,
|
|
136
|
+
enc.encode(JSON.stringify(msg))
|
|
137
|
+
)
|
|
138
|
+
);
|
|
139
|
+
return { s, n: b64(n), d: b64(ct) };
|
|
140
|
+
}
|
|
141
|
+
/** Decrypts a frame from the other side; null for replays, reflections or tampering. */
|
|
142
|
+
async open(f) {
|
|
143
|
+
if (!Number.isInteger(f.s) || f.s <= this.lastSeen) return null;
|
|
144
|
+
const from = this.me === "site" ? "wallet" : "site";
|
|
145
|
+
try {
|
|
146
|
+
const pt = await crypto.subtle.decrypt(
|
|
147
|
+
{ name: "AES-GCM", iv: fromB64(f.n), additionalData: enc.encode(`${from}|${f.s}`) },
|
|
148
|
+
this.key,
|
|
149
|
+
fromB64(f.d)
|
|
150
|
+
);
|
|
151
|
+
this.lastSeen = f.s;
|
|
152
|
+
return JSON.parse(dec.decode(pt));
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** For resuming a session after a reload: continue counters where they were. */
|
|
158
|
+
get counters() {
|
|
159
|
+
return { sent: this.sent, lastSeen: this.lastSeen };
|
|
160
|
+
}
|
|
161
|
+
restore(c) {
|
|
162
|
+
this.sent = c.sent;
|
|
163
|
+
this.lastSeen = c.lastSeen;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
var isSealed = (x) => !!x && typeof x === "object" && "d" in x && "n" in x && "s" in x;
|
|
167
|
+
|
|
168
|
+
// src/pair.ts
|
|
169
|
+
var DEFAULT_KEY = "bwalletx.connect.pairing.v1";
|
|
170
|
+
var OPEN = 1;
|
|
171
|
+
function memoryStorage() {
|
|
172
|
+
const m = /* @__PURE__ */ new Map();
|
|
173
|
+
return { getItem: (k) => m.get(k) ?? null, setItem: (k, v) => void m.set(k, v), removeItem: (k) => void m.delete(k) };
|
|
174
|
+
}
|
|
175
|
+
function defaultStorage() {
|
|
176
|
+
try {
|
|
177
|
+
if (typeof localStorage !== "undefined") return localStorage;
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
return memoryStorage();
|
|
181
|
+
}
|
|
182
|
+
function defaultSocket() {
|
|
183
|
+
return (url) => {
|
|
184
|
+
if (typeof WebSocket === "undefined") throw new Error("No WebSocket here. Pass options.socket.");
|
|
185
|
+
return new WebSocket(url);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function parseStored(raw) {
|
|
189
|
+
if (!raw) return null;
|
|
190
|
+
try {
|
|
191
|
+
const p = JSON.parse(raw);
|
|
192
|
+
const n = (x) => Number.isInteger(x) && x >= 0;
|
|
193
|
+
if (p.v !== 1) return null;
|
|
194
|
+
if (typeof p.c !== "string" || !/^[A-Za-z0-9_-]{22}$/.test(p.c)) return null;
|
|
195
|
+
if (typeof p.r !== "string" || !/^[a-z0-9.-]+(:\d+)?$/i.test(p.r)) return null;
|
|
196
|
+
if (typeof p.s !== "string" || !/^[0-9a-f]{64}$/i.test(p.s)) return null;
|
|
197
|
+
if (typeof p.k !== "string" || !/^0[23][0-9a-f]{64}$/i.test(p.k)) return null;
|
|
198
|
+
if (!n(p.sent) || !n(p.lastSeen) || !n(p.pairedAt)) return null;
|
|
199
|
+
return p;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
var Link = class {
|
|
205
|
+
constructor(p, sealer, storage, key, socket, timeoutMs) {
|
|
206
|
+
this.p = p;
|
|
207
|
+
this.sealer = sealer;
|
|
208
|
+
this.storage = storage;
|
|
209
|
+
this.key = key;
|
|
210
|
+
this.socket = socket;
|
|
211
|
+
this.timeoutMs = timeoutMs;
|
|
212
|
+
}
|
|
213
|
+
p;
|
|
214
|
+
sealer;
|
|
215
|
+
storage;
|
|
216
|
+
key;
|
|
217
|
+
socket;
|
|
218
|
+
timeoutMs;
|
|
219
|
+
ws = null;
|
|
220
|
+
opening = null;
|
|
221
|
+
waiting = /* @__PURE__ */ new Map();
|
|
222
|
+
forgotten = false;
|
|
223
|
+
/** Take over a socket that is already open (the one pairing used). */
|
|
224
|
+
adopt(ws) {
|
|
225
|
+
this.ws = ws;
|
|
226
|
+
this.wire(ws);
|
|
227
|
+
}
|
|
228
|
+
wire(ws) {
|
|
229
|
+
ws.onmessage = inOrder((ev) => this.onFrame(String(ev.data)));
|
|
230
|
+
ws.onerror = null;
|
|
231
|
+
ws.onclose = () => {
|
|
232
|
+
if (this.ws === ws) this.ws = null;
|
|
233
|
+
this.failAll(new Error("The connection to bWalletX closed"));
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
persist() {
|
|
237
|
+
if (this.forgotten) return;
|
|
238
|
+
this.p = { ...this.p, ...this.sealer.counters };
|
|
239
|
+
void Promise.resolve(this.storage.setItem(this.key, JSON.stringify(this.p))).catch(() => {
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
failAll(e) {
|
|
243
|
+
for (const w of this.waiting.values()) {
|
|
244
|
+
clearTimeout(w.timer);
|
|
245
|
+
w.reject(e);
|
|
246
|
+
}
|
|
247
|
+
this.waiting.clear();
|
|
248
|
+
}
|
|
249
|
+
async ensure() {
|
|
250
|
+
if (this.forgotten) throw new Error("This pairing was removed. Pair again.");
|
|
251
|
+
if (this.ws && this.ws.readyState === OPEN) return;
|
|
252
|
+
this.opening ??= this.reopen().finally(() => this.opening = null);
|
|
253
|
+
await this.opening;
|
|
254
|
+
}
|
|
255
|
+
reopen() {
|
|
256
|
+
const ws = this.socket(relaySocketUrl(this.p.r, this.p.c, "site", Math.floor(Date.now() / 1e3) + QR_LIFETIME_S - 10));
|
|
257
|
+
return new Promise((resolve, reject) => {
|
|
258
|
+
ws.onopen = () => {
|
|
259
|
+
this.ws = ws;
|
|
260
|
+
this.wire(ws);
|
|
261
|
+
resolve();
|
|
262
|
+
};
|
|
263
|
+
ws.onerror = () => reject(new Error("Could not reach the bWalletX pairing service"));
|
|
264
|
+
ws.onclose = () => reject(new Error("Could not reach the bWalletX pairing service"));
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
async onFrame(raw) {
|
|
268
|
+
let f;
|
|
269
|
+
try {
|
|
270
|
+
f = JSON.parse(raw);
|
|
271
|
+
} catch {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const t = f.t;
|
|
275
|
+
if (t === "relay" && f.error) {
|
|
276
|
+
return this.failAll(new Error(`bWalletX pairing service: ${f.error}`));
|
|
277
|
+
}
|
|
278
|
+
if (!isSealed(f)) return;
|
|
279
|
+
const msg = await this.sealer.open(f);
|
|
280
|
+
if (!msg) return;
|
|
281
|
+
this.persist();
|
|
282
|
+
if (msg.t === "res") {
|
|
283
|
+
const w = this.waiting.get(msg.id);
|
|
284
|
+
if (!w) return;
|
|
285
|
+
clearTimeout(w.timer);
|
|
286
|
+
this.waiting.delete(msg.id);
|
|
287
|
+
if (msg.error) w.reject(Object.assign(new Error(msg.error.message), { code: msg.error.code }));
|
|
288
|
+
else w.resolve(msg.result);
|
|
289
|
+
} else if (msg.t === "close") {
|
|
290
|
+
this.drop();
|
|
291
|
+
this.failAll(new Error("bWalletX disconnected this site. Pair again."));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async call(action, params) {
|
|
295
|
+
await this.ensure();
|
|
296
|
+
const ws = this.ws;
|
|
297
|
+
const id = `${action}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
298
|
+
const result = new Promise((resolve, reject) => {
|
|
299
|
+
const timer = setTimeout(() => {
|
|
300
|
+
if (this.waiting.delete(id)) reject(new Error("No answer from bWalletX. Open it, unlocked, and approve."));
|
|
301
|
+
}, this.timeoutMs);
|
|
302
|
+
this.waiting.set(id, { resolve, reject, timer });
|
|
303
|
+
});
|
|
304
|
+
ws.send(JSON.stringify(await this.sealer.seal({ t: "req", id, action, params })));
|
|
305
|
+
this.persist();
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
308
|
+
close() {
|
|
309
|
+
const ws = this.ws;
|
|
310
|
+
this.ws = null;
|
|
311
|
+
ws?.close();
|
|
312
|
+
}
|
|
313
|
+
drop() {
|
|
314
|
+
this.forgotten = true;
|
|
315
|
+
void Promise.resolve(this.storage.removeItem(this.key)).catch(() => {
|
|
316
|
+
});
|
|
317
|
+
this.close();
|
|
318
|
+
}
|
|
319
|
+
async forget() {
|
|
320
|
+
if (this.forgotten) return;
|
|
321
|
+
try {
|
|
322
|
+
if (this.ws && this.ws.readyState === OPEN) {
|
|
323
|
+
this.ws.send(JSON.stringify(await this.sealer.seal({ t: "close", reason: "site unpaired" })));
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
this.drop();
|
|
328
|
+
}
|
|
329
|
+
wallet() {
|
|
330
|
+
return new Proxy({}, {
|
|
331
|
+
get: (_t, method) => {
|
|
332
|
+
if (method === "close") return () => this.close();
|
|
333
|
+
if (method === "forget") return () => this.forget();
|
|
334
|
+
if (typeof method !== "string" || method === "then" || method === "toJSON") return void 0;
|
|
335
|
+
return (params) => this.call(method, params ?? {});
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
function pairBwalletX(opts) {
|
|
341
|
+
const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
|
|
342
|
+
const key = opts.storageKey ?? DEFAULT_KEY;
|
|
343
|
+
const socket = opts.socket ?? defaultSocket();
|
|
344
|
+
const relay = opts.relay ?? DEFAULT_RELAY;
|
|
345
|
+
const origin = opts.origin ?? (typeof location !== "undefined" ? location.origin : "");
|
|
346
|
+
if (!origin) return Promise.reject(new Error("pairBwalletX needs options.origin outside a browser"));
|
|
347
|
+
const siteKey = import_sdk2.PrivateKey.fromRandom();
|
|
348
|
+
const channel = newChannel();
|
|
349
|
+
const expiry = Math.floor(Date.now() / 1e3) + QR_LIFETIME_S;
|
|
350
|
+
return new Promise((resolve, reject) => {
|
|
351
|
+
if (opts.signal?.aborted) return reject(new Error("cancelled"));
|
|
352
|
+
let ws;
|
|
353
|
+
try {
|
|
354
|
+
ws = socket(relaySocketUrl(relay, channel, "site", expiry));
|
|
355
|
+
} catch (e) {
|
|
356
|
+
return reject(e);
|
|
357
|
+
}
|
|
358
|
+
let sealer = null;
|
|
359
|
+
let walletKey = "";
|
|
360
|
+
let settled = false;
|
|
361
|
+
const fail = (e) => {
|
|
362
|
+
if (settled) return;
|
|
363
|
+
settled = true;
|
|
364
|
+
clearTimeout(timer);
|
|
365
|
+
ws.close();
|
|
366
|
+
reject(e);
|
|
367
|
+
};
|
|
368
|
+
const timer = setTimeout(() => fail(new Error("The pairing code expired. Show a new one.")), QR_LIFETIME_S * 1e3);
|
|
369
|
+
opts.signal?.addEventListener("abort", () => fail(new Error("cancelled")));
|
|
370
|
+
ws.onerror = () => fail(new Error("Could not reach the bWalletX pairing service"));
|
|
371
|
+
ws.onclose = () => fail(new Error("The connection to bWalletX closed before pairing finished"));
|
|
372
|
+
ws.onmessage = inOrder(async (ev) => {
|
|
373
|
+
if (settled) return;
|
|
374
|
+
let f;
|
|
375
|
+
try {
|
|
376
|
+
f = JSON.parse(String(ev.data));
|
|
377
|
+
} catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (f.t === "hello" && !sealer) {
|
|
381
|
+
walletKey = f.k;
|
|
382
|
+
const s = await deriveSession(siteKey, walletKey, channel);
|
|
383
|
+
sealer = new Sealer(s.key, "site");
|
|
384
|
+
opts.onCode(s.code);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!sealer || !isSealed(f)) return;
|
|
388
|
+
const msg = await sealer.open(f);
|
|
389
|
+
if (!msg) return;
|
|
390
|
+
if (msg.t === "ready") {
|
|
391
|
+
settled = true;
|
|
392
|
+
clearTimeout(timer);
|
|
393
|
+
const p = { v: 1, c: channel, r: relay, s: siteKey.toHex(), k: walletKey, ...sealer.counters, pairedAt: Date.now() };
|
|
394
|
+
await Promise.resolve(storage.setItem(key, JSON.stringify(p))).catch(() => {
|
|
395
|
+
});
|
|
396
|
+
const link = new Link(p, sealer, storage, key, socket, opts.callTimeoutMs ?? 12e4);
|
|
397
|
+
link.adopt(ws);
|
|
398
|
+
resolve(link.wallet());
|
|
399
|
+
} else if (msg.t === "close") {
|
|
400
|
+
fail(new Error("Pairing was declined in bWalletX"));
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
opts.onLink(pairUrl({ v: PAIR_VERSION, r: relay, c: channel, k: siteKey.toPublicKey().toString(), o: origin, e: expiry }));
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
async function restorePairing(opts = {}) {
|
|
407
|
+
const storage = opts.storage === void 0 ? defaultStorage() : opts.storage ?? memoryStorage();
|
|
408
|
+
const key = opts.storageKey ?? DEFAULT_KEY;
|
|
409
|
+
const p = parseStored(await Promise.resolve(storage.getItem(key)).catch(() => null));
|
|
410
|
+
if (!p) return null;
|
|
411
|
+
const { key: aes } = await deriveSession(import_sdk2.PrivateKey.fromHex(p.s), p.k, p.c);
|
|
412
|
+
const sealer = new Sealer(aes, "site");
|
|
413
|
+
sealer.restore({ sent: p.sent, lastSeen: p.lastSeen });
|
|
414
|
+
return new Link(p, sealer, storage, key, opts.socket ?? defaultSocket(), opts.callTimeoutMs ?? 12e4).wallet();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// src/signin.ts
|
|
418
|
+
var LOGIN_PROTOCOL = [2, "bwallet sign in"];
|
|
419
|
+
async function connectBwalletX(opts = {}) {
|
|
420
|
+
if (opts.wallet) return { wallet: opts.wallet, method: "wallet" };
|
|
421
|
+
const found = await discoverBwalletX(opts.discoverMs);
|
|
422
|
+
if (found) return { wallet: found.wallet, method: found.method };
|
|
423
|
+
if (opts.reusePairing !== false) {
|
|
424
|
+
const restored = await restorePairing(opts.pairing ?? {});
|
|
425
|
+
if (restored) return { wallet: restored, method: "pairing" };
|
|
426
|
+
}
|
|
427
|
+
if (!opts.pairing) {
|
|
428
|
+
throw Object.assign(new Error("bWalletX was not found in this browser"), { code: "NOT_FOUND" });
|
|
429
|
+
}
|
|
430
|
+
return { wallet: await pairBwalletX(opts.pairing), method: "pairing" };
|
|
431
|
+
}
|
|
432
|
+
async function signInWithBwalletX(opts) {
|
|
433
|
+
const { wallet, method } = await connectBwalletX(opts);
|
|
434
|
+
if (method !== "pairing") await wallet.waitForAuthentication({}).catch(() => {
|
|
435
|
+
});
|
|
436
|
+
const { publicKey: identityKey } = await wallet.getPublicKey({ identityKey: true });
|
|
437
|
+
const c = await opts.challenge(identityKey);
|
|
438
|
+
const { signature } = await wallet.createSignature({
|
|
439
|
+
protocolID: c.protocolID ?? LOGIN_PROTOCOL,
|
|
440
|
+
keyID: c.keyID,
|
|
441
|
+
counterparty: "anyone",
|
|
442
|
+
data: Array.from(new TextEncoder().encode(c.message))
|
|
443
|
+
});
|
|
444
|
+
return { identityKey, signature: Array.from(signature), nonce: c.nonce, method, wallet };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/element.ts
|
|
448
|
+
var SIGNIN_TAG = "bwalletx-signin";
|
|
449
|
+
var GET_URL = "https://bwalletx.com/get";
|
|
450
|
+
var WEB_WALLET_URL = "https://web.bwalletx.com";
|
|
451
|
+
var VERBS = { signin: "Sign in with", continue: "Continue with", connect: "Connect" };
|
|
452
|
+
function signinText(label = "signin", brand = "bWalletX") {
|
|
453
|
+
return `${VERBS[label] ?? VERBS.signin} ${brand}`;
|
|
454
|
+
}
|
|
455
|
+
var uid = 0;
|
|
456
|
+
function markSvg(px = 20) {
|
|
457
|
+
const n = ++uid;
|
|
458
|
+
return `<svg viewBox="23 8 74 100" width="${Math.round(px * 0.74)}" height="${px}" aria-hidden="true" focusable="false"><defs><linearGradient id="bwxg${n}" gradientUnits="userSpaceOnUse" x1="0" y1="12" x2="0" y2="105"><stop offset="0" stop-color="#FFE58A"/><stop offset=".55" stop-color="#FFD24D"/><stop offset="1" stop-color="#C98F00"/></linearGradient><mask id="bwxh${n}"><rect width="140" height="140" fill="#fff"/><circle cx="60" cy="72" r="15" fill="#000"/></mask></defs><g fill="url(#bwxg${n})" mask="url(#bwxh${n})"><polygon points="45,12 45,76 27,76 27,30"/><circle cx="60" cy="72" r="33"/></g></svg>`;
|
|
459
|
+
}
|
|
460
|
+
var SIGNIN_CSS = `
|
|
461
|
+
:host{display:inline-block;vertical-align:middle;font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
462
|
+
:host([full-width]){display:block;width:100%}
|
|
463
|
+
:host([hidden]){display:none}
|
|
464
|
+
button{all:unset;box-sizing:border-box;display:inline-flex;align-items:center;justify-content:center;gap:12px;width:100%;cursor:pointer;
|
|
465
|
+
padding:16px 20px;border-radius:12px;font-size:16px;font-weight:700;line-height:1.2;text-align:left;
|
|
466
|
+
background:linear-gradient(180deg,#fcd34d,#f59e0b);color:#000;
|
|
467
|
+
box-shadow:0 10px 15px -3px rgba(245,158,11,.2),0 4px 6px -4px rgba(245,158,11,.2);
|
|
468
|
+
transition:background .15s,transform .1s,box-shadow .15s,border-color .15s;-webkit-tap-highlight-color:transparent}
|
|
469
|
+
button:hover{background:linear-gradient(180deg,#fde68a,#fbbf24)}
|
|
470
|
+
button:active{transform:translateY(1px);background:linear-gradient(180deg,#fbbf24,#d97706)}
|
|
471
|
+
button:focus-visible{outline:2px solid #b45309;outline-offset:3px}
|
|
472
|
+
button:disabled{opacity:.6;cursor:not-allowed;transform:none}
|
|
473
|
+
button[aria-busy=true]{cursor:wait}
|
|
474
|
+
.mark{display:inline-flex;align-items:center;justify-content:center;flex:none;width:28px;height:28px;border-radius:50%;background:#000}
|
|
475
|
+
.txt{display:flex;flex-direction:column;align-items:flex-start;min-width:0}
|
|
476
|
+
.txt b{font-weight:700;white-space:nowrap}
|
|
477
|
+
.txt small{font-size:11px;font-weight:500;color:rgba(0,0,0,.65);white-space:nowrap}
|
|
478
|
+
.spin{width:16px;height:16px;border-radius:50%;border:2px solid rgba(255,210,77,.35);border-top-color:#ffd24d;animation:r .8s linear infinite}
|
|
479
|
+
@keyframes r{to{transform:rotate(360deg)}}
|
|
480
|
+
@media (prefers-reduced-motion:reduce){button{transition:none}button:active{transform:none}.spin{animation:none;border-color:#ffd24d;border-right-color:transparent}}
|
|
481
|
+
/* sizes */
|
|
482
|
+
:host([size=md]) button{padding:10px 16px;border-radius:10px;font-size:15px;gap:10px}
|
|
483
|
+
:host([size=md]) .mark{width:24px;height:24px}
|
|
484
|
+
:host([size=md]) .txt small{font-size:10.5px}
|
|
485
|
+
:host([size=sm]) button{padding:6px 12px 6px 6px;border-radius:8px;font-size:13px;gap:8px;min-height:36px}
|
|
486
|
+
:host([size=sm]) .mark{width:22px;height:22px}
|
|
487
|
+
:host([size=icon]) button{width:48px;height:48px;padding:0;border-radius:50%;gap:0}
|
|
488
|
+
:host([size=icon]) .mark{width:34px;height:34px}
|
|
489
|
+
/* themes */
|
|
490
|
+
:host([theme=dark]) button{background:#0a0a0a;color:#fafafa;border:1px solid #3f3f46;box-shadow:none}
|
|
491
|
+
:host([theme=dark]) button:hover{background:#18181b;border-color:rgba(251,191,36,.6)}
|
|
492
|
+
:host([theme=dark]) button:active{background:#000}
|
|
493
|
+
:host([theme=dark]) button:focus-visible{outline-color:#fcd34d}
|
|
494
|
+
:host([theme=dark]) .mark{box-shadow:inset 0 0 0 1px #3f3f46}
|
|
495
|
+
:host([theme=dark]) .txt small{color:rgba(255,255,255,.65)}
|
|
496
|
+
:host([theme=light]) button{background:#fff;color:#09090b;border:1px solid #d4d4d8;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
|
497
|
+
:host([theme=light]) button:hover{background:#fafafa;border-color:#f59e0b}
|
|
498
|
+
:host([theme=light]) button:active{background:#f4f4f5}
|
|
499
|
+
:host([theme=light]) .txt small{color:#52525b}
|
|
500
|
+
/* not found */
|
|
501
|
+
.nf{margin-top:8px;font-size:12px;line-height:1.4;color:#71717a;text-align:center}
|
|
502
|
+
.nf a{color:inherit;font-weight:600;text-decoration:underline;text-underline-offset:2px}
|
|
503
|
+
.nf a:focus-visible{outline:2px solid #b45309;outline-offset:2px;border-radius:2px}
|
|
504
|
+
:host([theme=dark]) .nf,:host-context(.dark) .nf{color:#a1a1aa}
|
|
505
|
+
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
|
|
506
|
+
`;
|
|
507
|
+
function b64url2(bytes) {
|
|
508
|
+
let s = "";
|
|
509
|
+
for (const b of bytes) s += String.fromCharCode(b);
|
|
510
|
+
return btoa(s).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
511
|
+
}
|
|
512
|
+
async function buildAuthorizeUrl(p) {
|
|
513
|
+
const rnd = (n) => b64url2(crypto.getRandomValues(new Uint8Array(n)));
|
|
514
|
+
const verifier = rnd(32);
|
|
515
|
+
const state = rnd(16);
|
|
516
|
+
const nonce = rnd(16);
|
|
517
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)));
|
|
518
|
+
const u = new URL(p.authorizeUrl, typeof location !== "undefined" ? location.href : void 0);
|
|
519
|
+
const set = (k, v) => {
|
|
520
|
+
if (v) u.searchParams.set(k, v);
|
|
521
|
+
};
|
|
522
|
+
set("response_type", "code");
|
|
523
|
+
set("client_id", p.clientId);
|
|
524
|
+
set("redirect_uri", p.redirectUri);
|
|
525
|
+
set("scope", p.scope || "openid profile");
|
|
526
|
+
set("state", state);
|
|
527
|
+
set("nonce", nonce);
|
|
528
|
+
set("code_challenge", b64url2(digest));
|
|
529
|
+
set("code_challenge_method", "S256");
|
|
530
|
+
try {
|
|
531
|
+
sessionStorage.setItem("bwalletx:oauth", JSON.stringify({ verifier, state, nonce }));
|
|
532
|
+
} catch {
|
|
533
|
+
}
|
|
534
|
+
return u.toString();
|
|
535
|
+
}
|
|
536
|
+
async function postJson(url, body) {
|
|
537
|
+
const res = await fetch(url, { method: "POST", credentials: "include", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
538
|
+
const data = await res.json().catch(() => ({}));
|
|
539
|
+
if (!res.ok) throw new Error(data.error ?? `HTTP ${res.status}`);
|
|
540
|
+
return data;
|
|
541
|
+
}
|
|
542
|
+
var OBSERVED = ["size", "theme", "label", "brand", "subtitle", "loading", "disabled", "full-width", "aria-label", "get-url", "web-url"];
|
|
543
|
+
function createClass() {
|
|
544
|
+
return class BwalletxSigninElement extends HTMLElement {
|
|
545
|
+
static get observedAttributes() {
|
|
546
|
+
return OBSERVED;
|
|
547
|
+
}
|
|
548
|
+
#root;
|
|
549
|
+
#notFound = false;
|
|
550
|
+
#busy = false;
|
|
551
|
+
constructor() {
|
|
552
|
+
super();
|
|
553
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
554
|
+
}
|
|
555
|
+
connectedCallback() {
|
|
556
|
+
this.#render();
|
|
557
|
+
}
|
|
558
|
+
attributeChangedCallback() {
|
|
559
|
+
if (this.isConnected) this.#render();
|
|
560
|
+
}
|
|
561
|
+
/** The inner <button>, for focus() and tests. */
|
|
562
|
+
get button() {
|
|
563
|
+
return this.#root.querySelector("button");
|
|
564
|
+
}
|
|
565
|
+
get notFound() {
|
|
566
|
+
return this.#notFound;
|
|
567
|
+
}
|
|
568
|
+
focus(opts) {
|
|
569
|
+
this.button?.focus(opts);
|
|
570
|
+
}
|
|
571
|
+
#attr(name) {
|
|
572
|
+
return this.getAttribute(name);
|
|
573
|
+
}
|
|
574
|
+
#render() {
|
|
575
|
+
const size = this.#attr("size") ?? "lg";
|
|
576
|
+
const brand = this.#attr("brand") === "bWallet" ? "bWallet" : "bWalletX";
|
|
577
|
+
const text = signinText(this.#attr("label") ?? "signin", brand);
|
|
578
|
+
const subAttr = this.#attr("subtitle");
|
|
579
|
+
const subDefault = brand === "bWalletX" && (size === "lg" || size === "md") ? "also works with bWallet" : null;
|
|
580
|
+
const subtitle = subAttr === null || subAttr === "" || subAttr === "on" ? subAttr === "on" ? "also works with bWallet" : subDefault : subAttr === "off" ? null : subAttr;
|
|
581
|
+
const loading = this.hasAttribute("loading") || this.#busy;
|
|
582
|
+
const disabled = this.hasAttribute("disabled");
|
|
583
|
+
const markPx = size === "icon" ? 24 : size === "sm" ? 15 : size === "md" ? 17 : 20;
|
|
584
|
+
const aria = this.#attr("aria-label") ?? (subtitle && size !== "icon" ? `${text}, ${subtitle}` : text);
|
|
585
|
+
const getUrl = this.#attr("get-url") ?? GET_URL;
|
|
586
|
+
const webUrl = this.#attr("web-url") ?? WEB_WALLET_URL;
|
|
587
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
588
|
+
const label = size === "icon" ? "" : `<span class="txt" part="text"><b part="label">${esc(text)}</b>${subtitle ? `<small part="subtitle">${esc(subtitle)}</small>` : ""}</span>`;
|
|
589
|
+
const mark = loading ? '<span class="spin" aria-hidden="true"></span>' : markSvg(markPx);
|
|
590
|
+
const nf = this.#notFound ? `<p class="nf" part="not-found" role="status">bWalletX wasn't found in this browser. <a href="${esc(getUrl)}" target="_blank" rel="noopener">Get ${brand}</a> or <a href="${esc(webUrl)}" target="_blank" rel="noopener">use the web wallet</a>.</p>` : "";
|
|
591
|
+
this.#root.innerHTML = `<style>${SIGNIN_CSS}</style><button type="button" part="button" aria-label="${esc(aria)}" title="${esc(subtitle ? `${text} (${subtitle})` : text)}"${disabled ? " disabled" : ""}${loading ? ' aria-busy="true"' : ""}><span class="mark" part="mark">${mark}</span>${label}</button>${loading ? '<span class="sr" role="status">Connecting to bWalletX\u2026</span>' : ""}${nf}`;
|
|
592
|
+
this.button.addEventListener("click", () => void this.#click());
|
|
593
|
+
}
|
|
594
|
+
#emit(type, detail, cancelable = false) {
|
|
595
|
+
return this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true, cancelable }));
|
|
596
|
+
}
|
|
597
|
+
#setBusy(on) {
|
|
598
|
+
this.#busy = on;
|
|
599
|
+
this.#render();
|
|
600
|
+
}
|
|
601
|
+
async #click() {
|
|
602
|
+
if (this.hasAttribute("disabled") || this.hasAttribute("loading") || this.#busy) return;
|
|
603
|
+
if (!this.#emit("bwalletx-signin", { element: this }, true)) return;
|
|
604
|
+
const href = this.#attr("href");
|
|
605
|
+
const authorizeUrl = this.#attr("authorize-url");
|
|
606
|
+
if (authorizeUrl) {
|
|
607
|
+
location.assign(await buildAuthorizeUrl({ authorizeUrl, clientId: this.#attr("client-id"), redirectUri: this.#attr("redirect-uri"), scope: this.#attr("scope") }));
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
if (href) {
|
|
611
|
+
location.assign(href);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const challengeUrl = this.#attr("challenge-url");
|
|
615
|
+
if (!challengeUrl) return;
|
|
616
|
+
this.#notFound = false;
|
|
617
|
+
this.#setBusy(true);
|
|
618
|
+
try {
|
|
619
|
+
const ms = Number(this.#attr("discover-ms")) || 1200;
|
|
620
|
+
const found = await discoverBwalletX(ms);
|
|
621
|
+
if (!found) {
|
|
622
|
+
this.#notFound = true;
|
|
623
|
+
this.#emit("bwalletx-not-found");
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const r = await signInWithBwalletX({ wallet: found.wallet, challenge: (identityKey) => postJson(challengeUrl, { identityKey }) });
|
|
627
|
+
const verifyUrl = this.#attr("verify-url");
|
|
628
|
+
const response = verifyUrl ? await postJson(verifyUrl, { identityKey: r.identityKey, nonce: r.nonce, signature: r.signature }) : null;
|
|
629
|
+
this.#emit("bwalletx-signed-in", { identityKey: r.identityKey, method: found.method, nonce: r.nonce, signature: r.signature, response });
|
|
630
|
+
} catch (error) {
|
|
631
|
+
this.#emit("bwalletx-error", { error });
|
|
632
|
+
} finally {
|
|
633
|
+
this.#setBusy(false);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function defineBwalletxSignin(tag = SIGNIN_TAG) {
|
|
639
|
+
if (typeof window === "undefined" || typeof customElements === "undefined") return void 0;
|
|
640
|
+
const existing = customElements.get(tag);
|
|
641
|
+
if (existing) return existing;
|
|
642
|
+
const C = createClass();
|
|
643
|
+
customElements.define(tag, C);
|
|
644
|
+
return C;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// src/react.ts
|
|
648
|
+
function BwalletxSignin(props) {
|
|
649
|
+
const ref = (0, import_react.useRef)(null);
|
|
650
|
+
const cb = (0, import_react.useRef)(props);
|
|
651
|
+
cb.current = props;
|
|
652
|
+
(0, import_react.useEffect)(() => {
|
|
653
|
+
defineBwalletxSignin();
|
|
654
|
+
const el = ref.current;
|
|
655
|
+
if (!el) return;
|
|
656
|
+
const on = [
|
|
657
|
+
["bwalletx-signin", (e) => cb.current.onClick?.(e)],
|
|
658
|
+
["bwalletx-signed-in", (e) => cb.current.onSignedIn?.(e.detail)],
|
|
659
|
+
["bwalletx-not-found", () => cb.current.onNotFound?.()],
|
|
660
|
+
["bwalletx-error", (e) => cb.current.onError?.(e.detail.error)]
|
|
661
|
+
];
|
|
662
|
+
for (const [t, f] of on) el.addEventListener(t, f);
|
|
663
|
+
return () => {
|
|
664
|
+
for (const [t, f] of on) el.removeEventListener(t, f);
|
|
665
|
+
};
|
|
666
|
+
}, []);
|
|
667
|
+
const p = props;
|
|
668
|
+
const flag = (v) => v ? "" : void 0;
|
|
669
|
+
return (0, import_react.createElement)("bwalletx-signin", {
|
|
670
|
+
ref,
|
|
671
|
+
id: p.id,
|
|
672
|
+
class: p.className,
|
|
673
|
+
style: p.style,
|
|
674
|
+
size: p.size,
|
|
675
|
+
theme: p.theme,
|
|
676
|
+
label: p.label,
|
|
677
|
+
brand: p.brand,
|
|
678
|
+
subtitle: p.subtitle === false ? "off" : typeof p.subtitle === "string" ? p.subtitle : void 0,
|
|
679
|
+
"full-width": flag(p.fullWidth),
|
|
680
|
+
loading: flag(p.loading),
|
|
681
|
+
disabled: flag(p.disabled),
|
|
682
|
+
"challenge-url": p.challengeUrl,
|
|
683
|
+
"verify-url": p.verifyUrl,
|
|
684
|
+
"get-url": p.getUrl,
|
|
685
|
+
"web-url": p.webUrl,
|
|
686
|
+
href: p.href,
|
|
687
|
+
"authorize-url": p.authorizeUrl,
|
|
688
|
+
"client-id": p.clientId,
|
|
689
|
+
"redirect-uri": p.redirectUri,
|
|
690
|
+
scope: p.scope,
|
|
691
|
+
"discover-ms": p.discoverMs,
|
|
692
|
+
"aria-label": p["aria-label"]
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
var react_default = BwalletxSignin;
|
|
696
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
697
|
+
0 && (module.exports = {
|
|
698
|
+
BwalletxSignin
|
|
699
|
+
});
|
|
700
|
+
//# sourceMappingURL=react.cjs.map
|