@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.
@@ -0,0 +1,452 @@
1
+ import { Addresses, Chain } from '@cef-ai/wallet-identity';
2
+ import { EthersSigner, PolkadotSigner, SolanaSigner } from '@cef-ai/wallet-signers';
3
+ import { WalletErrorCode } from '@cef-ai/wallet-api-client';
4
+
5
+ /**
6
+ * The `@cef-ai/signer` contract, declared locally.
7
+ *
8
+ * `@cef-ai/signer` is published from the CEF-AI/sdk repo; until it is released
9
+ * we mirror its shape here so `EmbedWallet.asSigner()` can produce a
10
+ * structurally-compatible signer that `@cef-ai/account` consumes. When the
11
+ * package ships, replace this with `import type { Signer, SignIntent } from
12
+ * '@cef-ai/signer'`. The shape MUST stay in sync with that interface.
13
+ */
14
+ type SignerType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum';
15
+ type SignIntent = 'data' | 'token' | 'extrinsic';
16
+ interface CefSigner {
17
+ readonly type: SignerType;
18
+ readonly address: string;
19
+ readonly publicKey: Uint8Array;
20
+ isReady(): Promise<boolean>;
21
+ /**
22
+ * Sign raw bytes. `intent` keys the wallet's consent UI ('extrinsic' raises
23
+ * the extrinsic-consent screen). `opts.humanReadable` is an out-of-band
24
+ * description shown best-effort on that screen — it is NOT part of the
25
+ * minimal `@cef-ai/signer` interface, just an extra the CEF wallet honors.
26
+ */
27
+ sign(bytes: Uint8Array, intent?: SignIntent, opts?: {
28
+ humanReadable?: string;
29
+ }): Promise<Uint8Array>;
30
+ }
31
+
32
+ interface AppContext {
33
+ appId: string;
34
+ name: string;
35
+ origin: string;
36
+ }
37
+ /** Spec §5.2. Messages sent from a host page to a wallet popup. */
38
+ type HostToPopup = {
39
+ type: 'wallet:hello';
40
+ id: string;
41
+ appContext: AppContext;
42
+ intent: 'register' | 'login';
43
+ label?: string;
44
+ } | {
45
+ type: 'wallet:requestDelegation';
46
+ id: string;
47
+ scope: DelegationScope;
48
+ ttl: string;
49
+ } | {
50
+ type: 'wallet:sign';
51
+ id: string;
52
+ chain: Chain;
53
+ payload: Uint8Array;
54
+ requestKind: string;
55
+ humanReadable?: string;
56
+ } | ({
57
+ type: 'wallet:saveApplication';
58
+ id: string;
59
+ } & ApplicationBody) | ({
60
+ type: 'wallet:findApplications';
61
+ id: string;
62
+ } & ApplicationFilter) | {
63
+ type: 'wallet:logout';
64
+ id: string;
65
+ };
66
+ /**
67
+ * Spec §5.2. Messages sent from a wallet popup back to the host.
68
+ *
69
+ * On `wallet:error`, `message` carries the human-readable detail
70
+ * (what `WalletError.detail` returns), NOT the compound `Error.message`
71
+ * (`"${code}: ${detail}"`). The compound is reconstructable from `code + message`
72
+ * on the receiving side. Serialisers in the popup MUST send `err.detail ?? ''`,
73
+ * not `err.message`.
74
+ */
75
+ type PopupToHost = {
76
+ type: 'wallet:ready';
77
+ id: string;
78
+ } | {
79
+ type: 'wallet:login:ok';
80
+ id: string;
81
+ addresses: Addresses;
82
+ credentialId: string;
83
+ } | {
84
+ type: 'wallet:result';
85
+ id: string;
86
+ result: unknown;
87
+ } | {
88
+ type: 'wallet:error';
89
+ id: string;
90
+ code: WalletErrorCode;
91
+ message: string;
92
+ traceId?: string;
93
+ };
94
+ /** Spec §5.5. Channel name: `scp-wallet-v2`. Wallet-origin tabs only. */
95
+ type BroadcastChannelMessage = {
96
+ type: 'request-session';
97
+ requestId: string;
98
+ } | {
99
+ type: 'offer-session';
100
+ requestId: string;
101
+ edSeed: Uint8Array;
102
+ secpKey: Uint8Array;
103
+ addresses: Addresses;
104
+ /** absolute Unix epoch ms when session keys expire */ expMs: number;
105
+ } | {
106
+ type: 'logout';
107
+ };
108
+ declare const BROADCAST_CHANNEL_NAME = "scp-wallet-v2";
109
+
110
+ /**
111
+ * The minimal subset of `Window` that we use for postMessage + lifecycle.
112
+ * Both real browser windows and `MessageChannel`-backed test doubles can
113
+ * satisfy this shape.
114
+ */
115
+ interface PopupHandle {
116
+ postMessage(message: HostToPopup, targetOrigin: string): void;
117
+ close(): void;
118
+ readonly closed: boolean;
119
+ }
120
+ /** Opens a popup window. Defaults to `window.open(url, '_blank', features)`. */
121
+ type PopupOpener = (url: string, features?: string) => PopupHandle | null;
122
+ /**
123
+ * Subscriber for inbound messages on the host side. Both event.origin and
124
+ * event.data are passed through; the transport verifies origin before
125
+ * dispatching.
126
+ */
127
+ type HostMessageListener = (event: {
128
+ data: PopupToHost;
129
+ origin: string;
130
+ }) => void;
131
+ /**
132
+ * Inbound message handler on the popup side. Receives only messages whose
133
+ * origin matched the expected host origin.
134
+ */
135
+ type PopupMessageHandler<T extends HostToPopup = HostToPopup> = (message: T, meta: {
136
+ origin: string;
137
+ }) => Promise<void> | void;
138
+
139
+ interface PopupTransportOptions {
140
+ /** Expected origin of the wallet popup, e.g. 'https://wallet.example.com'. */
141
+ walletOrigin: string;
142
+ /** Defaults to `(url, features) => window.open(url, '_blank', features)`. */
143
+ openPopup?: PopupOpener;
144
+ /** Window object hosting the event listener. Defaults to `globalThis.window`. */
145
+ hostWindow?: Window;
146
+ /** Default per-request timeout. Defaults to 60_000 ms. */
147
+ defaultTimeoutMs?: number;
148
+ }
149
+ /**
150
+ * Host-side popup transport. Spec §5.
151
+ *
152
+ * Use one `PopupTransport` instance per `EmbedWallet`. The instance owns a
153
+ * single popup window at a time, opens it on demand, auto-reopens on the next
154
+ * `request()` after it's been closed, and correlates each in-flight request
155
+ * by its message `id`.
156
+ *
157
+ * Wire handshake: every freshly opened popup must send `wallet:ready` before
158
+ * this transport will post any `HostToPopup` envelope. Without the handshake,
159
+ * `postMessage` may fire before the popup document has executed its `message`
160
+ * listener and the message is dropped. Outbound messages are queued in
161
+ * `outbox` while `popupReady === false`; `wallet:ready` flushes them.
162
+ */
163
+ declare class PopupTransport {
164
+ private readonly opts;
165
+ private popup;
166
+ private pending;
167
+ private listener;
168
+ private popupReady;
169
+ private outbox;
170
+ constructor(options: PopupTransportOptions);
171
+ request<T extends PopupToHost>(message: HostToPopup, expectedTypes: T['type'][], init?: {
172
+ timeoutMs?: number;
173
+ }): Promise<T>;
174
+ /** Close the popup if open. Pending requests will time out normally. */
175
+ close(): void;
176
+ /**
177
+ * Tear down the transport entirely. Closes the popup AND removes the host-
178
+ * window message listener. After `destroy()`, the transport instance cannot
179
+ * be reused — callers should drop the reference.
180
+ *
181
+ * Use `close()` if you only want to close the popup but keep the transport
182
+ * alive for a later re-open.
183
+ */
184
+ destroy(): void;
185
+ private ensureListening;
186
+ private onMessage;
187
+ }
188
+
189
+ interface EmbedWalletOptions {
190
+ appId: string;
191
+ /**
192
+ * Origin where the wallet SPA is deployed, e.g. `https://wallet.example.com`.
193
+ * Required for real use (the SDK opens a popup to `${walletOrigin}/embed/wallet`
194
+ * and pins postMessage to this origin) — there is no default; the constructor
195
+ * throws if it is absent. Optional only when injecting a transport via the
196
+ * `__internal__` test seam, which already carries the origin.
197
+ */
198
+ walletOrigin?: string;
199
+ popup?: {
200
+ width?: number;
201
+ height?: number;
202
+ };
203
+ /** Optional name for the appContext sent via wallet:hello. Defaults to ''. */
204
+ appName?: string;
205
+ /**
206
+ * Test seam. Injects an alternative `PopupTransport` instance. Production
207
+ * callers leave this undefined and the SDK constructs its own transport.
208
+ */
209
+ __internal__?: {
210
+ transport?: PopupTransport;
211
+ };
212
+ }
213
+
214
+ interface LoginResult {
215
+ addresses: Addresses;
216
+ credentialId: string;
217
+ }
218
+ interface DelegationScope {
219
+ capabilities: string[];
220
+ appId?: string;
221
+ agentPubkey?: string;
222
+ constraints?: Record<string, unknown>;
223
+ }
224
+ interface DelegationRequest extends DelegationScope {
225
+ ttl: string;
226
+ }
227
+ interface DelegationResult {
228
+ token: string;
229
+ expiresAt: string;
230
+ issuer: string;
231
+ }
232
+ interface SignerSpec {
233
+ chain: Chain;
234
+ }
235
+ interface ApplicationFilter {
236
+ appId?: string;
237
+ }
238
+ interface ApplicationBody {
239
+ permissions: Record<string, unknown>;
240
+ email?: string;
241
+ }
242
+ /**
243
+ * Per-app record returned by `findApplications` / `saveApplication`. Mirrors
244
+ * the `Application` shape returned by wallet-api; duplicated here so
245
+ * embed-sdk consumers don't have to depend on `@cef-ai/wallet-api-client` for a
246
+ * type.
247
+ */
248
+ interface ApplicationRecord {
249
+ appId: string;
250
+ address: string;
251
+ permissions: Record<string, unknown>;
252
+ email?: string;
253
+ }
254
+ type WalletEvent = {
255
+ type: 'login';
256
+ addresses: Addresses;
257
+ } | {
258
+ type: 'logout';
259
+ } | {
260
+ type: 'error';
261
+ code: WalletErrorCode;
262
+ message: string;
263
+ };
264
+ type WalletEventListener<T extends WalletEvent['type']> = (ev: Extract<WalletEvent, {
265
+ type: T;
266
+ }>) => void;
267
+
268
+ /**
269
+ * Public SDK class. v2.0.0. Spec §4.
270
+ *
271
+ * Drives the popup transport: every method that does meaningful work opens
272
+ * (or reuses) a popup at `walletOrigin/embed/*`, sends a `HostToPopup`
273
+ * message, awaits the matching `PopupToHost` response, and updates local
274
+ * state. See spec §5 for the wire protocol.
275
+ */
276
+ declare class EmbedWallet {
277
+ readonly appId: string;
278
+ readonly walletOrigin: string;
279
+ readonly popup: {
280
+ width: number;
281
+ height: number;
282
+ };
283
+ readonly appName: string;
284
+ private _addresses;
285
+ private _credentialId;
286
+ private _listeners;
287
+ private readonly transport;
288
+ constructor(opts: EmbedWalletOptions);
289
+ get isLoggedIn(): boolean;
290
+ get addresses(): Addresses | null;
291
+ get credentialId(): string | null;
292
+ register(opts?: {
293
+ label?: string;
294
+ }): Promise<LoginResult>;
295
+ login(): Promise<LoginResult>;
296
+ logout(): Promise<void>;
297
+ getSigner(spec: {
298
+ chain: 'evm';
299
+ }): EthersSigner;
300
+ getSigner(spec: {
301
+ chain: 'cere';
302
+ }): PolkadotSigner;
303
+ getSigner(spec: {
304
+ chain: 'solana';
305
+ }): SolanaSigner;
306
+ getSigner(spec: SignerSpec): EthersSigner | PolkadotSigner | SolanaSigner;
307
+ /**
308
+ * Expose the wallet as a chain-free `@cef-ai/signer` over the Cere
309
+ * Ed25519 key — the pluggable-signer seam `@cef-ai/account` consumes for DDC
310
+ * and `@cef-ai/chain` adapts for extrinsics.
311
+ *
312
+ * `publicKey` is recovered from the (public) Solana address — no key material
313
+ * leaves the popup's `SessionVault`. `sign(bytes, 'extrinsic', { humanReadable })`
314
+ * routes through the popup's extrinsic-consent screen.
315
+ */
316
+ asSigner(): CefSigner;
317
+ requestDelegation(req: DelegationRequest): Promise<DelegationResult>;
318
+ findApplications(filter?: ApplicationFilter): Promise<ApplicationRecord[]>;
319
+ saveApplication(body: ApplicationBody): Promise<ApplicationRecord>;
320
+ on<T extends WalletEvent['type']>(type: T, listener: WalletEventListener<T>): this;
321
+ off<T extends WalletEvent['type']>(type: T, listener: WalletEventListener<T>): this;
322
+ /**
323
+ * Tear down the wallet instance. Closes any open popup, removes transport
324
+ * listeners, clears in-memory state. Call from your app's unmount hook.
325
+ *
326
+ * After dispose(), this instance is not reusable — construct a new one.
327
+ */
328
+ dispose(): void;
329
+ private appContext;
330
+ private helloAndAwaitLogin;
331
+ private emit;
332
+ private assertLoggedIn;
333
+ }
334
+
335
+ /**
336
+ * Origin verification helpers used by both ends of the popup transport.
337
+ * Spec §5.3.
338
+ */
339
+ /**
340
+ * Schemes that we accept for origins. Production uses https only; we allow
341
+ * http here so test setups and `localhost:port` work — callers can layer
342
+ * tighter rules (e.g. "only https in production") on top.
343
+ */
344
+ declare const ALLOWED_ORIGIN_SCHEMES: readonly string[];
345
+ /**
346
+ * Strict origin equality check. Returns true iff both inputs are non-empty,
347
+ * URL-parseable, and have matching `origin` (scheme + host + port).
348
+ */
349
+ declare function isMatchingOrigin(a: string | undefined, b: string | undefined): boolean;
350
+
351
+ interface PopupHostBridgeOptions {
352
+ /** Expected origin of the host page (the page that opened this popup). */
353
+ hostOrigin: string;
354
+ /** The window this bridge runs in. Defaults to `globalThis`. */
355
+ popupWindow?: Window;
356
+ }
357
+ /**
358
+ * Popup-side bridge. Spec §5.
359
+ *
360
+ * The wallet SPA constructs one of these at boot, registers handlers via
361
+ * `on(type, fn)`, and calls `start()` to begin listening. Each inbound
362
+ * `HostToPopup` message that matches a registered handler is dispatched;
363
+ * the handler is responsible for calling `bridge.send(...)` to reply.
364
+ *
365
+ * Handlers may throw `WalletError`; the bridge automatically converts the
366
+ * throw into a `wallet:error` response keyed by the inbound message id.
367
+ */
368
+ declare class PopupHostBridge {
369
+ private readonly opts;
370
+ private readonly handlers;
371
+ private listener;
372
+ constructor(options: PopupHostBridgeOptions);
373
+ /** Register a handler for a specific message type. Replaces any prior handler. */
374
+ on<T extends HostToPopup['type']>(type: T, handler: PopupMessageHandler<Extract<HostToPopup, {
375
+ type: T;
376
+ }>>): this;
377
+ /**
378
+ * Send a `PopupToHost` message back to the opener. The bridge does not
379
+ * remember the host-window reference itself — it reads `globalThis.opener`
380
+ * each call, so it survives popup re-opens.
381
+ */
382
+ send(message: PopupToHost): void;
383
+ /**
384
+ * Start listening for inbound messages.
385
+ *
386
+ * If `announceReady` is provided, sends a `wallet:ready` message immediately.
387
+ * Use this when the popup wants to signal "I'm here" without waiting for the
388
+ * host's first call.
389
+ */
390
+ start(opts?: {
391
+ announceReady?: {
392
+ id: string;
393
+ };
394
+ }): void;
395
+ stop(): void;
396
+ private onMessage;
397
+ }
398
+
399
+ interface BroadcastSession {
400
+ edSeed: Uint8Array;
401
+ secpKey: Uint8Array;
402
+ addresses: Addresses;
403
+ /** Absolute Unix epoch ms when the session expires. */
404
+ expMs: number;
405
+ }
406
+ interface BroadcastSessionShareOptions {
407
+ /**
408
+ * Returns the local session if this tab has one to offer. Called whenever
409
+ * another tab broadcasts `request-session`.
410
+ */
411
+ getSession: () => BroadcastSession | null;
412
+ /**
413
+ * Called when another tab broadcasts a `logout` message. The local handler
414
+ * should wipe its own session.
415
+ */
416
+ onLogout?: () => void;
417
+ }
418
+ /**
419
+ * Popup-side cross-tab session sharing. Spec §5.5.
420
+ *
421
+ * - `request()`: broadcast `request-session`, wait up to 200ms for an offer.
422
+ * - `start()`: listen for incoming requests; reply with `offer-session` if we
423
+ * have a non-expired session and `getSession()` returns it.
424
+ * - `broadcastLogout()`: notify peers to wipe their sessions too.
425
+ *
426
+ * Channel name is the spec-locked `scp-wallet-v2`. Only wallet-origin popups
427
+ * join. Host pages do not participate.
428
+ */
429
+ declare class BroadcastSessionShare {
430
+ private readonly channel;
431
+ private readonly opts;
432
+ private listening;
433
+ constructor(opts: BroadcastSessionShareOptions);
434
+ /**
435
+ * Broadcast a `request-session` and wait for an `offer-session` from another
436
+ * tab. Resolves with the offered session, or null if nobody offered before
437
+ * the timeout.
438
+ */
439
+ request(opts?: {
440
+ timeoutMs?: number;
441
+ }): Promise<BroadcastSession | null>;
442
+ /** Begin offering this tab's session in response to requests, and handle logouts. */
443
+ start(): void;
444
+ stop(): void;
445
+ /** Notify peers that the user logged out. Peers wipe their sessions. */
446
+ broadcastLogout(): void;
447
+ /** Free the underlying channel handle. Call from unload handlers. */
448
+ close(): void;
449
+ private onMessage;
450
+ }
451
+
452
+ export { ALLOWED_ORIGIN_SCHEMES, type AppContext, type ApplicationBody, type ApplicationFilter, type ApplicationRecord, BROADCAST_CHANNEL_NAME, type BroadcastChannelMessage, type BroadcastSession, BroadcastSessionShare, type BroadcastSessionShareOptions, type CefSigner, type DelegationRequest, type DelegationResult, type DelegationScope, EmbedWallet, type EmbedWalletOptions, type HostMessageListener, type HostToPopup, type LoginResult, type PopupHandle, PopupHostBridge, type PopupHostBridgeOptions, type PopupMessageHandler, type PopupOpener, type PopupToHost, PopupTransport, type PopupTransportOptions, type SignIntent, type SignerSpec, type SignerType, type WalletEvent, type WalletEventListener, isMatchingOrigin };