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