@cef-ai/wallet 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @cef-ai/wallet
2
+
3
+ Embed SCP Wallet into a host application. Passkey-native authentication
4
+ via WebAuthn + PRF. No MetaMask, no seed phrases, no popup-blocker headaches.
5
+
6
+ For runtime topology and the popup/cross-tab channels this SDK speaks,
7
+ see [`ARCHITECTURE.md`](../../ARCHITECTURE.md) in the repo root.
8
+
9
+ > **Status:** v2.0.0-alpha. Public API surface is stable but pre-1.0.
10
+ > `private: true` in `package.json` — not yet published to npm.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @cef-ai/wallet
16
+ ```
17
+
18
+ ## TODO(publish) — must be addressed before flipping `private: false`
19
+
20
+ 1. **Workspace dep versions.** `@cef-ai/wallet-api-client`, `@cef-ai/wallet-identity`,
21
+ `@cef-ai/wallet-signers` are pinned at `0.0.0` (workspace placeholder). Choose:
22
+
23
+ - **Option A**: publish those three packages with real semver versions first,
24
+ then bump this package's dependencies to match.
25
+ - **Option B**: drop `external: [/^@cef-ai\/wallet/]` from `tsup.config.ts` so
26
+ the SDK bundle inlines them. Smaller dep tree for consumers; larger SDK.
27
+
28
+ 2. **Downstream type resolution.** Even after Option A above, the workspace
29
+ packages must publish their own `dist/` with `.d.ts` files. The current
30
+ workspace `package.json` files point `types` at `./src` (source). For publish
31
+ they'll need their own tsup configs and matching publishConfig.
32
+
33
+ 3. **Pack validation.** Add a `pack-dry-run` CI step that runs
34
+ `npm pack --dry-run --json` and asserts `dist/index.js`, `dist/index.cjs`,
35
+ `dist/index.d.ts`, `dist/index.d.cts` are all present and no source `.ts`
36
+ files leak through.
37
+
38
+ ## Quick start
39
+
40
+ ```ts
41
+ import { EmbedWallet } from '@cef-ai/wallet';
42
+
43
+ const wallet = new EmbedWallet({
44
+ appId: 'my-app',
45
+ appName: 'My App',
46
+ walletOrigin: 'https://wallet.example.com', // required: your wallet deployment origin
47
+ });
48
+
49
+ // First-time users register a passkey; returning users call login().
50
+ const session = await wallet.login();
51
+ console.log('Cere address:', session.addresses.cere);
52
+
53
+ // Signing is per-chain. Pick a signer for the chain you need, then call its
54
+ // `signMessage(bytes)` (returns raw signature bytes).
55
+ const signer = wallet.getSigner({ chain: 'cere' });
56
+ const sig = await signer.signMessage(new TextEncoder().encode('Hello'));
57
+ console.log('Signature bytes:', sig);
58
+ ```
59
+
60
+ ## Architecture
61
+
62
+ - A popup-window-based transport (`PopupTransport`) opens `wallet.example.com/embed/wallet?origin=...`
63
+ - The popup runs the wallet UI, performs the WebAuthn ceremony, and posts results back
64
+ - Cross-tab session sharing via `BroadcastChannel('scp-wallet-v2')` so a second host tab discovers an existing session
65
+
66
+ Design specs and ADRs are maintained in the company memory bank.
67
+
68
+ ## Capabilities
69
+
70
+ - `register({ label? })` / `login()` — opens the popup, performs the WebAuthn ceremony, returns the session
71
+ - `getSigner({ chain })` — returns an `EthersSigner` (`'evm'`), `PolkadotSigner` (`'cere'`), or `SolanaSigner` (`'solana'`); each exposes `signMessage(bytes: Uint8Array): Promise<Uint8Array>`
72
+ - `requestDelegation({ capabilities, ttl, appId?, agentPubkey?, constraints? })` — requests a scoped delegation token; the popup shows a consent UI
73
+ - `saveApplication(record)` / `findApplications(filter)` — manage app context records
74
+ - `logout()` — closes the session locally + broadcasts so other tabs do the same
75
+ - `dispose()` — tear-down hook; closes any open popup and removes listeners
76
+
77
+ ## License
78
+
79
+ MIT.
package/dist/index.cjs ADDED
@@ -0,0 +1,597 @@
1
+ 'use strict';
2
+
3
+ var walletApiClient = require('@cef-ai/wallet-api-client');
4
+ var walletIdentity = require('@cef-ai/wallet-identity');
5
+ var walletSigners = require('@cef-ai/wallet-signers');
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
12
+ var __spreadValues = (a, b) => {
13
+ for (var prop in b || (b = {}))
14
+ if (__hasOwnProp.call(b, prop))
15
+ __defNormalProp(a, prop, b[prop]);
16
+ if (__getOwnPropSymbols)
17
+ for (var prop of __getOwnPropSymbols(b)) {
18
+ if (__propIsEnum.call(b, prop))
19
+ __defNormalProp(a, prop, b[prop]);
20
+ }
21
+ return a;
22
+ };
23
+ var __async = (__this, __arguments, generator) => {
24
+ return new Promise((resolve, reject) => {
25
+ var fulfilled = (value) => {
26
+ try {
27
+ step(generator.next(value));
28
+ } catch (e) {
29
+ reject(e);
30
+ }
31
+ };
32
+ var rejected = (value) => {
33
+ try {
34
+ step(generator.throw(value));
35
+ } catch (e) {
36
+ reject(e);
37
+ }
38
+ };
39
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
40
+ step((generator = generator.apply(__this, __arguments)).next());
41
+ });
42
+ };
43
+
44
+ // src/transport/origin.ts
45
+ var ALLOWED_ORIGIN_SCHEMES = ["https:", "http:"];
46
+ function isMatchingOrigin(a, b) {
47
+ if (!a || !b) return false;
48
+ try {
49
+ const ua = new URL(a);
50
+ const ub = new URL(b);
51
+ return ua.origin === ub.origin;
52
+ } catch (e) {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ // src/transport/PopupTransport.ts
58
+ var DEFAULT_FEATURES = "width=420,height=640,resizable=yes,scrollbars=yes";
59
+ var PopupTransport = class {
60
+ constructor(options) {
61
+ this.popup = null;
62
+ this.pending = /* @__PURE__ */ new Map();
63
+ this.listener = null;
64
+ this.popupReady = false;
65
+ this.outbox = [];
66
+ this.opts = __spreadValues({
67
+ openPopup: defaultOpener,
68
+ hostWindow: globalThis.window,
69
+ defaultTimeoutMs: 6e4
70
+ }, options);
71
+ }
72
+ request(_0, _1) {
73
+ return __async(this, arguments, function* (message, expectedTypes, init = {}) {
74
+ var _a, _b, _c;
75
+ if (!this.popup || this.popup.closed) {
76
+ const hostOrigin = (_b = (_a = globalThis.location) == null ? void 0 : _a.origin) != null ? _b : "";
77
+ const url = `${this.opts.walletOrigin}/embed/wallet?origin=${encodeURIComponent(hostOrigin)}`;
78
+ this.popup = this.opts.openPopup(url, DEFAULT_FEATURES);
79
+ if (!this.popup) {
80
+ throw new walletApiClient.WalletError("popup-blocked", "popup window failed to open");
81
+ }
82
+ this.popupReady = false;
83
+ this.outbox = [];
84
+ this.ensureListening();
85
+ }
86
+ const id = message.id;
87
+ const timeoutMs = (_c = init.timeoutMs) != null ? _c : this.opts.defaultTimeoutMs;
88
+ return new Promise((resolve, reject) => {
89
+ const timer = setTimeout(() => {
90
+ if (this.pending.has(id)) {
91
+ this.pending.delete(id);
92
+ this.outbox = this.outbox.filter((m) => m.id !== id);
93
+ reject(new walletApiClient.WalletError("internal", `request "${message.type}" (id=${id}) timed out after ${timeoutMs}ms`));
94
+ }
95
+ }, timeoutMs);
96
+ this.pending.set(id, {
97
+ expectedTypes,
98
+ resolve: (msg) => resolve(msg),
99
+ reject,
100
+ timer
101
+ });
102
+ if (this.popupReady) {
103
+ this.popup.postMessage(message, this.opts.walletOrigin);
104
+ } else {
105
+ this.outbox.push(message);
106
+ }
107
+ });
108
+ });
109
+ }
110
+ /** Close the popup if open. Pending requests will time out normally. */
111
+ close() {
112
+ if (this.popup && !this.popup.closed) {
113
+ this.popup.close();
114
+ }
115
+ this.popup = null;
116
+ this.popupReady = false;
117
+ this.outbox = [];
118
+ }
119
+ /**
120
+ * Tear down the transport entirely. Closes the popup AND removes the host-
121
+ * window message listener. After `destroy()`, the transport instance cannot
122
+ * be reused — callers should drop the reference.
123
+ *
124
+ * Use `close()` if you only want to close the popup but keep the transport
125
+ * alive for a later re-open.
126
+ */
127
+ destroy() {
128
+ this.close();
129
+ if (this.listener) {
130
+ this.opts.hostWindow.removeEventListener("message", this.listener);
131
+ this.listener = null;
132
+ }
133
+ }
134
+ // ---- internal -------------------------------------------------------------
135
+ ensureListening() {
136
+ if (this.listener) return;
137
+ this.listener = (event) => this.onMessage(event);
138
+ this.opts.hostWindow.addEventListener("message", this.listener);
139
+ }
140
+ onMessage(event) {
141
+ if (!isMatchingOrigin(event.origin, this.opts.walletOrigin)) {
142
+ const data2 = event.data;
143
+ if (data2 && typeof data2.id === "string" && this.pending.has(data2.id)) {
144
+ const pending2 = this.pending.get(data2.id);
145
+ this.pending.delete(data2.id);
146
+ clearTimeout(pending2.timer);
147
+ pending2.reject(
148
+ new walletApiClient.WalletError(
149
+ "origin-mismatch",
150
+ `message from "${event.origin}" rejected (expected "${this.opts.walletOrigin}")`
151
+ )
152
+ );
153
+ }
154
+ return;
155
+ }
156
+ const data = event.data;
157
+ if (!data || typeof data.type !== "string" || typeof data.id !== "string") return;
158
+ if (data.type === "wallet:ready") {
159
+ if (this.popupReady) return;
160
+ this.popupReady = true;
161
+ const toFlush = this.outbox;
162
+ this.outbox = [];
163
+ if (this.popup && !this.popup.closed) {
164
+ for (const m of toFlush) {
165
+ this.popup.postMessage(m, this.opts.walletOrigin);
166
+ }
167
+ }
168
+ return;
169
+ }
170
+ const pending = this.pending.get(data.id);
171
+ if (!pending) return;
172
+ clearTimeout(pending.timer);
173
+ this.pending.delete(data.id);
174
+ if (data.type === "wallet:error") {
175
+ pending.reject(new walletApiClient.WalletError(data.code, data.message, data.traceId));
176
+ return;
177
+ }
178
+ if (!pending.expectedTypes.includes(data.type)) {
179
+ pending.reject(
180
+ new walletApiClient.WalletError("internal", `received unexpected response type "${data.type}" for id "${data.id}"`)
181
+ );
182
+ return;
183
+ }
184
+ pending.resolve(data);
185
+ }
186
+ };
187
+ function defaultOpener(url, features) {
188
+ const w = globalThis.window;
189
+ const opened = w == null ? void 0 : w.open(url, "_blank", features);
190
+ return opened;
191
+ }
192
+
193
+ // src/EmbedWallet.ts
194
+ var EmbedWallet = class {
195
+ constructor(opts) {
196
+ this._addresses = null;
197
+ this._credentialId = null;
198
+ this._listeners = {};
199
+ var _a, _b, _c, _d, _e, _f, _g;
200
+ const injectedTransport = (_a = opts.__internal__) == null ? void 0 : _a.transport;
201
+ if (!injectedTransport && !opts.walletOrigin) {
202
+ throw new Error("EmbedWallet: `walletOrigin` is required (e.g. https://wallet.example.com).");
203
+ }
204
+ this.appId = opts.appId;
205
+ this.walletOrigin = (_b = opts.walletOrigin) != null ? _b : "";
206
+ this.popup = { width: (_d = (_c = opts.popup) == null ? void 0 : _c.width) != null ? _d : 420, height: (_f = (_e = opts.popup) == null ? void 0 : _e.height) != null ? _f : 640 };
207
+ this.appName = (_g = opts.appName) != null ? _g : "";
208
+ this.transport = injectedTransport != null ? injectedTransport : new PopupTransport({
209
+ walletOrigin: this.walletOrigin
210
+ });
211
+ }
212
+ get isLoggedIn() {
213
+ return this._addresses !== null;
214
+ }
215
+ get addresses() {
216
+ return this._addresses;
217
+ }
218
+ get credentialId() {
219
+ return this._credentialId;
220
+ }
221
+ register() {
222
+ return __async(this, arguments, function* (opts = {}) {
223
+ return this.helloAndAwaitLogin("register", opts);
224
+ });
225
+ }
226
+ login() {
227
+ return __async(this, null, function* () {
228
+ return this.helloAndAwaitLogin("login");
229
+ });
230
+ }
231
+ logout() {
232
+ return __async(this, null, function* () {
233
+ if (!this._addresses) {
234
+ this.transport.close();
235
+ return;
236
+ }
237
+ const id = crypto.randomUUID();
238
+ try {
239
+ yield this.transport.request({ type: "wallet:logout", id }, [
240
+ "wallet:result"
241
+ ]);
242
+ } catch (_err) {
243
+ }
244
+ this._addresses = null;
245
+ this._credentialId = null;
246
+ this.transport.close();
247
+ this.emit("logout", { type: "logout" });
248
+ });
249
+ }
250
+ getSigner(spec) {
251
+ this.assertLoggedIn("getSigner");
252
+ const sign = (chain, payload) => __async(this, null, function* () {
253
+ const id = crypto.randomUUID();
254
+ const result = yield this.transport.request(
255
+ { type: "wallet:sign", id, chain, payload, requestKind: "sdk" },
256
+ ["wallet:result"]
257
+ );
258
+ const sig = result.result;
259
+ if (!(sig instanceof Uint8Array)) {
260
+ throw new walletApiClient.WalletError("internal", `popup returned non-Uint8Array sign result (got ${typeof sig})`);
261
+ }
262
+ return sig;
263
+ });
264
+ const addresses = this._addresses;
265
+ switch (spec.chain) {
266
+ case "evm":
267
+ return new walletSigners.EthersSigner({ address: addresses.evm, sign });
268
+ case "cere":
269
+ return new walletSigners.PolkadotSigner({ address: addresses.cere, sign });
270
+ case "solana":
271
+ return new walletSigners.SolanaSigner({ address: addresses.solana, sign });
272
+ default:
273
+ throw new walletApiClient.WalletError("validation", `getSigner: unknown chain "${String(spec.chain)}"`);
274
+ }
275
+ }
276
+ /**
277
+ * Expose the wallet as a chain-free `@cef-ai/signer` over the Cere
278
+ * Ed25519 key — the pluggable-signer seam `@cef-ai/account` consumes for DDC
279
+ * and `@cef-ai/chain` adapts for extrinsics.
280
+ *
281
+ * `publicKey` is recovered from the (public) Solana address — no key material
282
+ * leaves the popup's `SessionVault`. `sign(bytes, 'extrinsic', { humanReadable })`
283
+ * routes through the popup's extrinsic-consent screen.
284
+ */
285
+ asSigner() {
286
+ this.assertLoggedIn("asSigner");
287
+ const addresses = this._addresses;
288
+ const send = (payload, intent, humanReadable) => __async(this, null, function* () {
289
+ if (!this.isLoggedIn) {
290
+ throw new walletApiClient.WalletError("unauthorized", "asSigner: wallet is no longer logged in");
291
+ }
292
+ const id = crypto.randomUUID();
293
+ const result = yield this.transport.request(
294
+ { type: "wallet:sign", id, chain: "cere", payload, requestKind: intent != null ? intent : "sdk", humanReadable },
295
+ ["wallet:result"]
296
+ );
297
+ const sig = result.result;
298
+ if (!(sig instanceof Uint8Array)) {
299
+ throw new walletApiClient.WalletError("internal", `popup returned non-Uint8Array sign result (got ${typeof sig})`);
300
+ }
301
+ return sig;
302
+ });
303
+ return {
304
+ type: "ed25519",
305
+ address: addresses.cere,
306
+ publicKey: walletIdentity.decodeEd25519Pubkey(addresses.solana),
307
+ isReady: () => __async(this, null, function* () {
308
+ return this.isLoggedIn;
309
+ }),
310
+ sign: (bytes, intent, opts) => send(bytes, intent, opts == null ? void 0 : opts.humanReadable)
311
+ };
312
+ }
313
+ requestDelegation(req) {
314
+ return __async(this, null, function* () {
315
+ this.assertLoggedIn("requestDelegation");
316
+ const id = crypto.randomUUID();
317
+ const scope = {
318
+ capabilities: req.capabilities,
319
+ appId: req.appId,
320
+ agentPubkey: req.agentPubkey,
321
+ constraints: req.constraints
322
+ };
323
+ const result = yield this.transport.request(
324
+ { type: "wallet:requestDelegation", id, scope, ttl: req.ttl },
325
+ ["wallet:result"]
326
+ );
327
+ return result.result;
328
+ });
329
+ }
330
+ findApplications() {
331
+ return __async(this, arguments, function* (filter = {}) {
332
+ this.assertLoggedIn("findApplications");
333
+ const id = crypto.randomUUID();
334
+ const result = yield this.transport.request(
335
+ { type: "wallet:findApplications", id, appId: filter.appId },
336
+ ["wallet:result"]
337
+ );
338
+ return result.result;
339
+ });
340
+ }
341
+ saveApplication(body) {
342
+ return __async(this, null, function* () {
343
+ this.assertLoggedIn("saveApplication");
344
+ const id = crypto.randomUUID();
345
+ const result = yield this.transport.request(
346
+ { type: "wallet:saveApplication", id, permissions: body.permissions, email: body.email },
347
+ ["wallet:result"]
348
+ );
349
+ return result.result;
350
+ });
351
+ }
352
+ on(type, listener) {
353
+ var _a;
354
+ const map = this._listeners;
355
+ ((_a = map[type]) != null ? _a : map[type] = /* @__PURE__ */ new Set()).add(listener);
356
+ return this;
357
+ }
358
+ off(type, listener) {
359
+ var _a;
360
+ const map = this._listeners;
361
+ (_a = map[type]) == null ? void 0 : _a.delete(listener);
362
+ return this;
363
+ }
364
+ /**
365
+ * Tear down the wallet instance. Closes any open popup, removes transport
366
+ * listeners, clears in-memory state. Call from your app's unmount hook.
367
+ *
368
+ * After dispose(), this instance is not reusable — construct a new one.
369
+ */
370
+ dispose() {
371
+ this._addresses = null;
372
+ this._credentialId = null;
373
+ this._listeners = {};
374
+ this.transport.destroy();
375
+ }
376
+ // ---- private --------------------------------------------------------------
377
+ appContext() {
378
+ const origin = typeof window !== "undefined" ? window.location.origin : "";
379
+ return { appId: this.appId, name: this.appName, origin };
380
+ }
381
+ helloAndAwaitLogin(_0) {
382
+ return __async(this, arguments, function* (intent, opts = {}) {
383
+ const id = crypto.randomUUID();
384
+ const helloMessage = {
385
+ type: "wallet:hello",
386
+ id,
387
+ appContext: this.appContext(),
388
+ intent,
389
+ label: opts.label
390
+ };
391
+ const result = yield this.transport.request(helloMessage, [
392
+ "wallet:login:ok"
393
+ ]);
394
+ this._addresses = result.addresses;
395
+ this._credentialId = result.credentialId;
396
+ this.emit("login", { type: "login", addresses: result.addresses });
397
+ return { addresses: result.addresses, credentialId: result.credentialId };
398
+ });
399
+ }
400
+ emit(type, ev) {
401
+ const set = this._listeners[type];
402
+ if (!set) return;
403
+ for (const listener of set) {
404
+ try {
405
+ listener(ev);
406
+ } catch (e) {
407
+ }
408
+ }
409
+ }
410
+ assertLoggedIn(method) {
411
+ if (!this._addresses) {
412
+ throw new walletApiClient.WalletError("unauthorized", `${method}: not logged in`);
413
+ }
414
+ }
415
+ };
416
+
417
+ // src/protocol.ts
418
+ var BROADCAST_CHANNEL_NAME = "scp-wallet-v2";
419
+ var PopupHostBridge = class {
420
+ constructor(options) {
421
+ this.handlers = {};
422
+ this.listener = null;
423
+ this.opts = __spreadValues({
424
+ popupWindow: globalThis.window
425
+ }, options);
426
+ }
427
+ /** Register a handler for a specific message type. Replaces any prior handler. */
428
+ on(type, handler) {
429
+ this.handlers[type] = handler;
430
+ return this;
431
+ }
432
+ /**
433
+ * Send a `PopupToHost` message back to the opener. The bridge does not
434
+ * remember the host-window reference itself — it reads `globalThis.opener`
435
+ * each call, so it survives popup re-opens.
436
+ */
437
+ send(message) {
438
+ const opener = globalThis.opener;
439
+ if (!opener || typeof opener.postMessage !== "function") {
440
+ return;
441
+ }
442
+ opener.postMessage(message, this.opts.hostOrigin);
443
+ }
444
+ /**
445
+ * Start listening for inbound messages.
446
+ *
447
+ * If `announceReady` is provided, sends a `wallet:ready` message immediately.
448
+ * Use this when the popup wants to signal "I'm here" without waiting for the
449
+ * host's first call.
450
+ */
451
+ start(opts = {}) {
452
+ if (this.listener) return;
453
+ this.listener = (event) => this.onMessage(event);
454
+ this.opts.popupWindow.addEventListener("message", this.listener);
455
+ if (opts.announceReady) {
456
+ this.send({ type: "wallet:ready", id: opts.announceReady.id });
457
+ }
458
+ }
459
+ stop() {
460
+ if (!this.listener) return;
461
+ this.opts.popupWindow.removeEventListener("message", this.listener);
462
+ this.listener = null;
463
+ }
464
+ // ---- internal -------------------------------------------------------------
465
+ onMessage(event) {
466
+ return __async(this, null, function* () {
467
+ var _a;
468
+ if (!isMatchingOrigin(event.origin, this.opts.hostOrigin)) {
469
+ return;
470
+ }
471
+ const data = event.data;
472
+ if (!data || typeof data.type !== "string" || typeof data.id !== "string") return;
473
+ const handler = this.handlers[data.type];
474
+ if (!handler) {
475
+ this.send({
476
+ type: "wallet:error",
477
+ id: data.id,
478
+ code: "internal",
479
+ message: `no handler registered for "${data.type}"`
480
+ });
481
+ return;
482
+ }
483
+ try {
484
+ yield handler(data, { origin: event.origin });
485
+ } catch (err) {
486
+ if (err instanceof walletApiClient.WalletError) {
487
+ this.send({
488
+ type: "wallet:error",
489
+ id: data.id,
490
+ code: err.code,
491
+ message: (_a = err.detail) != null ? _a : "",
492
+ traceId: err.traceId
493
+ });
494
+ } else {
495
+ this.send({
496
+ type: "wallet:error",
497
+ id: data.id,
498
+ code: "internal",
499
+ message: err instanceof Error ? err.message : String(err)
500
+ });
501
+ }
502
+ }
503
+ });
504
+ }
505
+ };
506
+
507
+ // src/transport/BroadcastSessionShare.ts
508
+ var DEFAULT_REQUEST_TIMEOUT_MS = 200;
509
+ var BroadcastSessionShare = class {
510
+ constructor(opts) {
511
+ this.listening = false;
512
+ this.onMessage = (ev) => {
513
+ var _a, _b;
514
+ const msg = ev.data;
515
+ if (msg.type === "request-session") {
516
+ const session = this.opts.getSession();
517
+ if (!session || session.expMs <= Date.now()) return;
518
+ this.channel.postMessage({
519
+ type: "offer-session",
520
+ requestId: msg.requestId,
521
+ edSeed: session.edSeed,
522
+ secpKey: session.secpKey,
523
+ addresses: session.addresses,
524
+ expMs: session.expMs
525
+ });
526
+ return;
527
+ }
528
+ if (msg.type === "logout") {
529
+ (_b = (_a = this.opts).onLogout) == null ? void 0 : _b.call(_a);
530
+ return;
531
+ }
532
+ };
533
+ this.opts = opts;
534
+ this.channel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
535
+ }
536
+ /**
537
+ * Broadcast a `request-session` and wait for an `offer-session` from another
538
+ * tab. Resolves with the offered session, or null if nobody offered before
539
+ * the timeout.
540
+ */
541
+ request() {
542
+ return __async(this, arguments, function* (opts = {}) {
543
+ var _a;
544
+ const requestId = crypto.randomUUID();
545
+ const timeoutMs = (_a = opts.timeoutMs) != null ? _a : DEFAULT_REQUEST_TIMEOUT_MS;
546
+ return new Promise((resolve) => {
547
+ const onMsg = (ev) => {
548
+ if (ev.data.type === "offer-session" && ev.data.requestId === requestId && ev.data.expMs > Date.now()) {
549
+ this.channel.removeEventListener("message", onMsg);
550
+ clearTimeout(timer);
551
+ resolve({
552
+ edSeed: ev.data.edSeed,
553
+ secpKey: ev.data.secpKey,
554
+ addresses: ev.data.addresses,
555
+ expMs: ev.data.expMs
556
+ });
557
+ }
558
+ };
559
+ this.channel.addEventListener("message", onMsg);
560
+ const timer = setTimeout(() => {
561
+ this.channel.removeEventListener("message", onMsg);
562
+ resolve(null);
563
+ }, timeoutMs);
564
+ this.channel.postMessage({ type: "request-session", requestId });
565
+ });
566
+ });
567
+ }
568
+ /** Begin offering this tab's session in response to requests, and handle logouts. */
569
+ start() {
570
+ if (this.listening) return;
571
+ this.listening = true;
572
+ this.channel.addEventListener("message", this.onMessage);
573
+ }
574
+ stop() {
575
+ this.channel.removeEventListener("message", this.onMessage);
576
+ this.listening = false;
577
+ }
578
+ /** Notify peers that the user logged out. Peers wipe their sessions. */
579
+ broadcastLogout() {
580
+ this.channel.postMessage({ type: "logout" });
581
+ }
582
+ /** Free the underlying channel handle. Call from unload handlers. */
583
+ close() {
584
+ this.stop();
585
+ this.channel.close();
586
+ }
587
+ };
588
+
589
+ exports.ALLOWED_ORIGIN_SCHEMES = ALLOWED_ORIGIN_SCHEMES;
590
+ exports.BROADCAST_CHANNEL_NAME = BROADCAST_CHANNEL_NAME;
591
+ exports.BroadcastSessionShare = BroadcastSessionShare;
592
+ exports.EmbedWallet = EmbedWallet;
593
+ exports.PopupHostBridge = PopupHostBridge;
594
+ exports.PopupTransport = PopupTransport;
595
+ exports.isMatchingOrigin = isMatchingOrigin;
596
+ //# sourceMappingURL=index.cjs.map
597
+ //# sourceMappingURL=index.cjs.map