@cef-ai/wallet-identity 1.0.0 → 1.1.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.d.cts CHANGED
@@ -7,12 +7,69 @@ interface Addresses {
7
7
  evm: string;
8
8
  }
9
9
  type Chain = 'cere' | 'solana' | 'evm';
10
+ /**
11
+ * A transferable snapshot of the current session — the raw chain-key material
12
+ * plus the addresses it yields and an absolute expiry. This is the payload the
13
+ * register-popup → iframe handoff moves across the same-origin
14
+ * `BroadcastChannel('scp-wallet-v2')` (see embed-sdk's `BroadcastSessionShare`).
15
+ *
16
+ * SECURITY: unlike `VaultSnapshot`, this DOES carry the raw seeds. It exists
17
+ * only so a same-origin sibling surface (the persistent iframe) can adopt a
18
+ * session a punched-out popup just created — Safari blocks the WebAuthn
19
+ * `create()` ceremony in a cross-origin iframe (Task 6), so the popup runs the
20
+ * ceremony and hands its result back. It must NEVER cross an origin boundary or
21
+ * reach a host page. Spec §3.6 invariant 1 (keys never leave the wallet origin)
22
+ * still holds: the channel is same-origin and wallet-only.
23
+ *
24
+ * The field shape intentionally matches embed-sdk's `BroadcastSession`. It is
25
+ * declared here (not imported from embed-sdk) to avoid a circular package
26
+ * dependency — embed-sdk already depends on this package.
27
+ */
28
+ interface IdentitySession {
29
+ /** Ed25519 32-byte secret key seed; Cere + Solana signing. */
30
+ edSeed: Uint8Array;
31
+ /** secp256k1 32-byte secret key; EVM signing. */
32
+ secpKey: Uint8Array;
33
+ addresses: Addresses;
34
+ /** Absolute Unix epoch ms when the session expires. */
35
+ expMs: number;
36
+ }
37
+ /**
38
+ * Same-origin session-handoff capability. Kept OFF the core `Identity`
39
+ * interface deliberately: it is an optional, wallet-origin-only concern (the
40
+ * register-popup → iframe handoff), and forcing every `Identity` implementation
41
+ * — including thin test doubles — to provide raw-key export/import would widen
42
+ * the security surface for no benefit. Concrete `IdentityImpl` implements it;
43
+ * SPA wiring feature-detects via `supportsSessionHandoff()`.
44
+ */
45
+ interface SessionHandoff {
46
+ /**
47
+ * Export the current session for a same-origin handoff, or `null` when no
48
+ * live session exists (vault closed / expired / no captured keys). See
49
+ * `IdentitySession` for the security contract.
50
+ */
51
+ exportSession(): IdentitySession | null;
52
+ /**
53
+ * Adopt a session handed over from a same-origin sibling surface (e.g. the
54
+ * register popup). Rebuilds the derived keys from the transported seeds, opens
55
+ * the vault, and syncs public state — WITHOUT running a WebAuthn ceremony.
56
+ */
57
+ adoptSession(session: IdentitySession): void;
58
+ }
59
+ /**
60
+ * Narrow an `Identity` to one that supports same-origin session handoff.
61
+ * `IdentityImpl` returns true; thin test doubles return false and are simply
62
+ * skipped by the responder/requester wiring.
63
+ */
64
+ declare function supportsSessionHandoff(identity: Identity): identity is Identity & SessionHandoff;
10
65
  interface Identity {
11
66
  readonly isAuthenticated: boolean;
12
67
  readonly addresses: Addresses | null;
13
68
  readonly credentialId: string | null;
14
69
  register(opts?: {
15
70
  label?: string;
71
+ name?: string;
72
+ email?: string;
16
73
  }): Promise<void>;
17
74
  login(): Promise<void>;
18
75
  logout(): void;
@@ -39,11 +96,12 @@ declare const PRF_INPUT_LABEL = "cere-wallet-prf-v1";
39
96
  /**
40
97
  * SHA-256("cere-wallet-prf-v1") — 32 bytes. Fed to WebAuthn `prf.eval.first`.
41
98
  *
42
- * Implementation note: `createHash` runs at module evaluation time. In the test
43
- * environment (Vitest on Node) this works natively via `node:crypto`. In the
44
- * browser this depends on a `node:crypto` polyfill — `vite-plugin-node-polyfills`
45
- * is wired in by Cycle 1 Task 8. A follow-up cycle should replace this expression
46
- * with a hard-coded `Uint8Array` literal to eliminate the polyfill dependency.
99
+ * Hard-coded as a byte literal so this module is browser-safe with NO
100
+ * `node:crypto` dependency it is evaluated at module-load time, and relying on
101
+ * `createHash` there forced every browser consumer to wire a `node:crypto`
102
+ * polyfill (`vite-plugin-node-polyfills`). The literal is verified to equal
103
+ * `sha256(PRF_INPUT_LABEL)` in `constants.test.ts`, so the constant cannot drift
104
+ * silently. Rotating the label (`-v2`) means recomputing these bytes.
47
105
  */
48
106
  declare const PRF_INPUT_SEED: Uint8Array;
49
107
  /** Spec §3.2. Used as HKDF-SHA256 info to derive the secp256k1 32-byte secret. */
@@ -183,8 +241,18 @@ interface RegisterOptions {
183
241
  userHandle: Uint8Array;
184
242
  /** 32-byte PRF input (typically `PRF_INPUT_SEED`). */
185
243
  prfInput: Uint8Array;
186
- /** Human-readable label for the credential. Currently a hint, not stored. */
244
+ /**
245
+ * Legacy single label. Used as a fallback for both `user.displayName` and
246
+ * `user.name` when `name`/`email` are not supplied. Prefer `name` + `email`.
247
+ */
187
248
  label?: string;
249
+ /** Friendly account name → WebAuthn `user.displayName` (e.g. "Alex Müller"). */
250
+ name?: string;
251
+ /**
252
+ * Account identifier → WebAuthn `user.name` (e.g. "alex@example.com"). This is
253
+ * the field most OS/browser passkey managers surface to distinguish credentials.
254
+ */
255
+ email?: string;
188
256
  }
189
257
  interface RegisterResult {
190
258
  credentialId: string;
@@ -340,12 +408,27 @@ interface IdentityImplOptions {
340
408
  apiClient: ApiClient;
341
409
  ceremony: CeremonyAdapter;
342
410
  }
343
- declare class IdentityImpl implements Identity {
411
+ declare class IdentityImpl implements Identity, SessionHandoff {
344
412
  private readonly opts;
345
413
  private readonly vault;
346
414
  private cachedJwt;
347
415
  /** Server-returned credential ID (used as the public identity.credentialId). */
348
416
  private serverCredentialId;
417
+ /**
418
+ * Raw derived keys for the CURRENT session, captured at every `vault.set()`
419
+ * call site (register / login / adoptSession). The vault deliberately hides
420
+ * the seeds behind its closure (Spec §3.6 invariant 1); this field is the
421
+ * single, explicit, same-origin-only seam that lets `exportSession()` hand a
422
+ * live session to a sibling wallet-origin surface (the persistent iframe).
423
+ * Cleared on logout / cross-tab logout alongside the vault. Never serialised
424
+ * to storage and never sent to a host page.
425
+ */
426
+ private sessionKeys;
427
+ /**
428
+ * Absolute expiry (epoch ms) for `exportSession()`. Tracks the cached JWT
429
+ * expiry after register/login; set from the adopted session in adoptSession.
430
+ */
431
+ private sessionExpMs;
349
432
  private readonly _isAuthenticated;
350
433
  private readonly _addresses;
351
434
  private readonly _credentialId;
@@ -372,6 +455,8 @@ declare class IdentityImpl implements Identity {
372
455
  private syncPublicState;
373
456
  register(opts?: {
374
457
  label?: string;
458
+ name?: string;
459
+ email?: string;
375
460
  }): Promise<void>;
376
461
  login(): Promise<void>;
377
462
  logout(): void;
@@ -384,6 +469,24 @@ declare class IdentityImpl implements Identity {
384
469
  * side use — keys never reach the host. Spec §3.6 invariant 1.
385
470
  */
386
471
  getVault(): SessionVault;
472
+ /**
473
+ * Export the current session for a same-origin handoff (register popup →
474
+ * iframe). Returns `null` unless the vault is open, we captured the derived
475
+ * keys, and the session has not expired. The raw seeds ARE included — this is
476
+ * the deliberate, narrow seam described on `IdentitySession`; callers must
477
+ * only pass the result across the same-origin wallet BroadcastChannel.
478
+ */
479
+ exportSession(): IdentitySession | null;
480
+ /**
481
+ * Adopt a session handed over from a same-origin sibling surface. Rebuilds
482
+ * the `DerivedKeys` from the transported seeds (public keys are recomputed
483
+ * from the seeds; we do NOT re-run the HKDF step so the exact transported
484
+ * secpKey is preserved), opens the vault, and syncs public state — no
485
+ * ceremony. There is no credentialId in the handoff, so login() would need a
486
+ * fresh ceremony; but the session key is warm, so `sign`/`claim` (which use
487
+ * the vault directly, no JWT) work immediately.
488
+ */
489
+ adoptSession(session: IdentitySession): void;
387
490
  /**
388
491
  * Release the BroadcastChannel + cross-tab subscriber. Idempotent.
389
492
  *
@@ -440,4 +543,4 @@ declare class CrossTabSync {
440
543
  close(): void;
441
544
  }
442
545
 
443
- export { type Addresses, CERE_SS58_PREFIX, type CeremonyAdapter, type Chain, type CrossTabMessage, CrossTabSync, type DerivedKeys, EVM_HKDF_INFO, type Identity, IdentityImpl, type IdentityImplOptions, type InternalSign, type LoginOptions, type LoginResult, PRF_INPUT_LABEL, PRF_INPUT_SEED, type PrfSupportResult, type RegisterOptions, type RegisterResult, type SessionVault, SoftAuthenticator, type VaultSnapshot, WebAuthnCeremonyAdapter, type WebAuthnCeremonyAdapterOptions, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult };
546
+ export { type Addresses, CERE_SS58_PREFIX, type CeremonyAdapter, type Chain, type CrossTabMessage, CrossTabSync, type DerivedKeys, EVM_HKDF_INFO, type Identity, IdentityImpl, type IdentityImplOptions, type IdentitySession, type InternalSign, type LoginOptions, type LoginResult, PRF_INPUT_LABEL, PRF_INPUT_SEED, type PrfSupportResult, type RegisterOptions, type RegisterResult, type SessionHandoff, type SessionVault, SoftAuthenticator, type VaultSnapshot, WebAuthnCeremonyAdapter, type WebAuthnCeremonyAdapterOptions, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult, supportsSessionHandoff };
package/dist/index.d.ts CHANGED
@@ -7,12 +7,69 @@ interface Addresses {
7
7
  evm: string;
8
8
  }
9
9
  type Chain = 'cere' | 'solana' | 'evm';
10
+ /**
11
+ * A transferable snapshot of the current session — the raw chain-key material
12
+ * plus the addresses it yields and an absolute expiry. This is the payload the
13
+ * register-popup → iframe handoff moves across the same-origin
14
+ * `BroadcastChannel('scp-wallet-v2')` (see embed-sdk's `BroadcastSessionShare`).
15
+ *
16
+ * SECURITY: unlike `VaultSnapshot`, this DOES carry the raw seeds. It exists
17
+ * only so a same-origin sibling surface (the persistent iframe) can adopt a
18
+ * session a punched-out popup just created — Safari blocks the WebAuthn
19
+ * `create()` ceremony in a cross-origin iframe (Task 6), so the popup runs the
20
+ * ceremony and hands its result back. It must NEVER cross an origin boundary or
21
+ * reach a host page. Spec §3.6 invariant 1 (keys never leave the wallet origin)
22
+ * still holds: the channel is same-origin and wallet-only.
23
+ *
24
+ * The field shape intentionally matches embed-sdk's `BroadcastSession`. It is
25
+ * declared here (not imported from embed-sdk) to avoid a circular package
26
+ * dependency — embed-sdk already depends on this package.
27
+ */
28
+ interface IdentitySession {
29
+ /** Ed25519 32-byte secret key seed; Cere + Solana signing. */
30
+ edSeed: Uint8Array;
31
+ /** secp256k1 32-byte secret key; EVM signing. */
32
+ secpKey: Uint8Array;
33
+ addresses: Addresses;
34
+ /** Absolute Unix epoch ms when the session expires. */
35
+ expMs: number;
36
+ }
37
+ /**
38
+ * Same-origin session-handoff capability. Kept OFF the core `Identity`
39
+ * interface deliberately: it is an optional, wallet-origin-only concern (the
40
+ * register-popup → iframe handoff), and forcing every `Identity` implementation
41
+ * — including thin test doubles — to provide raw-key export/import would widen
42
+ * the security surface for no benefit. Concrete `IdentityImpl` implements it;
43
+ * SPA wiring feature-detects via `supportsSessionHandoff()`.
44
+ */
45
+ interface SessionHandoff {
46
+ /**
47
+ * Export the current session for a same-origin handoff, or `null` when no
48
+ * live session exists (vault closed / expired / no captured keys). See
49
+ * `IdentitySession` for the security contract.
50
+ */
51
+ exportSession(): IdentitySession | null;
52
+ /**
53
+ * Adopt a session handed over from a same-origin sibling surface (e.g. the
54
+ * register popup). Rebuilds the derived keys from the transported seeds, opens
55
+ * the vault, and syncs public state — WITHOUT running a WebAuthn ceremony.
56
+ */
57
+ adoptSession(session: IdentitySession): void;
58
+ }
59
+ /**
60
+ * Narrow an `Identity` to one that supports same-origin session handoff.
61
+ * `IdentityImpl` returns true; thin test doubles return false and are simply
62
+ * skipped by the responder/requester wiring.
63
+ */
64
+ declare function supportsSessionHandoff(identity: Identity): identity is Identity & SessionHandoff;
10
65
  interface Identity {
11
66
  readonly isAuthenticated: boolean;
12
67
  readonly addresses: Addresses | null;
13
68
  readonly credentialId: string | null;
14
69
  register(opts?: {
15
70
  label?: string;
71
+ name?: string;
72
+ email?: string;
16
73
  }): Promise<void>;
17
74
  login(): Promise<void>;
18
75
  logout(): void;
@@ -39,11 +96,12 @@ declare const PRF_INPUT_LABEL = "cere-wallet-prf-v1";
39
96
  /**
40
97
  * SHA-256("cere-wallet-prf-v1") — 32 bytes. Fed to WebAuthn `prf.eval.first`.
41
98
  *
42
- * Implementation note: `createHash` runs at module evaluation time. In the test
43
- * environment (Vitest on Node) this works natively via `node:crypto`. In the
44
- * browser this depends on a `node:crypto` polyfill — `vite-plugin-node-polyfills`
45
- * is wired in by Cycle 1 Task 8. A follow-up cycle should replace this expression
46
- * with a hard-coded `Uint8Array` literal to eliminate the polyfill dependency.
99
+ * Hard-coded as a byte literal so this module is browser-safe with NO
100
+ * `node:crypto` dependency it is evaluated at module-load time, and relying on
101
+ * `createHash` there forced every browser consumer to wire a `node:crypto`
102
+ * polyfill (`vite-plugin-node-polyfills`). The literal is verified to equal
103
+ * `sha256(PRF_INPUT_LABEL)` in `constants.test.ts`, so the constant cannot drift
104
+ * silently. Rotating the label (`-v2`) means recomputing these bytes.
47
105
  */
48
106
  declare const PRF_INPUT_SEED: Uint8Array;
49
107
  /** Spec §3.2. Used as HKDF-SHA256 info to derive the secp256k1 32-byte secret. */
@@ -183,8 +241,18 @@ interface RegisterOptions {
183
241
  userHandle: Uint8Array;
184
242
  /** 32-byte PRF input (typically `PRF_INPUT_SEED`). */
185
243
  prfInput: Uint8Array;
186
- /** Human-readable label for the credential. Currently a hint, not stored. */
244
+ /**
245
+ * Legacy single label. Used as a fallback for both `user.displayName` and
246
+ * `user.name` when `name`/`email` are not supplied. Prefer `name` + `email`.
247
+ */
187
248
  label?: string;
249
+ /** Friendly account name → WebAuthn `user.displayName` (e.g. "Alex Müller"). */
250
+ name?: string;
251
+ /**
252
+ * Account identifier → WebAuthn `user.name` (e.g. "alex@example.com"). This is
253
+ * the field most OS/browser passkey managers surface to distinguish credentials.
254
+ */
255
+ email?: string;
188
256
  }
189
257
  interface RegisterResult {
190
258
  credentialId: string;
@@ -340,12 +408,27 @@ interface IdentityImplOptions {
340
408
  apiClient: ApiClient;
341
409
  ceremony: CeremonyAdapter;
342
410
  }
343
- declare class IdentityImpl implements Identity {
411
+ declare class IdentityImpl implements Identity, SessionHandoff {
344
412
  private readonly opts;
345
413
  private readonly vault;
346
414
  private cachedJwt;
347
415
  /** Server-returned credential ID (used as the public identity.credentialId). */
348
416
  private serverCredentialId;
417
+ /**
418
+ * Raw derived keys for the CURRENT session, captured at every `vault.set()`
419
+ * call site (register / login / adoptSession). The vault deliberately hides
420
+ * the seeds behind its closure (Spec §3.6 invariant 1); this field is the
421
+ * single, explicit, same-origin-only seam that lets `exportSession()` hand a
422
+ * live session to a sibling wallet-origin surface (the persistent iframe).
423
+ * Cleared on logout / cross-tab logout alongside the vault. Never serialised
424
+ * to storage and never sent to a host page.
425
+ */
426
+ private sessionKeys;
427
+ /**
428
+ * Absolute expiry (epoch ms) for `exportSession()`. Tracks the cached JWT
429
+ * expiry after register/login; set from the adopted session in adoptSession.
430
+ */
431
+ private sessionExpMs;
349
432
  private readonly _isAuthenticated;
350
433
  private readonly _addresses;
351
434
  private readonly _credentialId;
@@ -372,6 +455,8 @@ declare class IdentityImpl implements Identity {
372
455
  private syncPublicState;
373
456
  register(opts?: {
374
457
  label?: string;
458
+ name?: string;
459
+ email?: string;
375
460
  }): Promise<void>;
376
461
  login(): Promise<void>;
377
462
  logout(): void;
@@ -384,6 +469,24 @@ declare class IdentityImpl implements Identity {
384
469
  * side use — keys never reach the host. Spec §3.6 invariant 1.
385
470
  */
386
471
  getVault(): SessionVault;
472
+ /**
473
+ * Export the current session for a same-origin handoff (register popup →
474
+ * iframe). Returns `null` unless the vault is open, we captured the derived
475
+ * keys, and the session has not expired. The raw seeds ARE included — this is
476
+ * the deliberate, narrow seam described on `IdentitySession`; callers must
477
+ * only pass the result across the same-origin wallet BroadcastChannel.
478
+ */
479
+ exportSession(): IdentitySession | null;
480
+ /**
481
+ * Adopt a session handed over from a same-origin sibling surface. Rebuilds
482
+ * the `DerivedKeys` from the transported seeds (public keys are recomputed
483
+ * from the seeds; we do NOT re-run the HKDF step so the exact transported
484
+ * secpKey is preserved), opens the vault, and syncs public state — no
485
+ * ceremony. There is no credentialId in the handoff, so login() would need a
486
+ * fresh ceremony; but the session key is warm, so `sign`/`claim` (which use
487
+ * the vault directly, no JWT) work immediately.
488
+ */
489
+ adoptSession(session: IdentitySession): void;
387
490
  /**
388
491
  * Release the BroadcastChannel + cross-tab subscriber. Idempotent.
389
492
  *
@@ -440,4 +543,4 @@ declare class CrossTabSync {
440
543
  close(): void;
441
544
  }
442
545
 
443
- export { type Addresses, CERE_SS58_PREFIX, type CeremonyAdapter, type Chain, type CrossTabMessage, CrossTabSync, type DerivedKeys, EVM_HKDF_INFO, type Identity, IdentityImpl, type IdentityImplOptions, type InternalSign, type LoginOptions, type LoginResult, PRF_INPUT_LABEL, PRF_INPUT_SEED, type PrfSupportResult, type RegisterOptions, type RegisterResult, type SessionVault, SoftAuthenticator, type VaultSnapshot, WebAuthnCeremonyAdapter, type WebAuthnCeremonyAdapterOptions, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult };
546
+ export { type Addresses, CERE_SS58_PREFIX, type CeremonyAdapter, type Chain, type CrossTabMessage, CrossTabSync, type DerivedKeys, EVM_HKDF_INFO, type Identity, IdentityImpl, type IdentityImplOptions, type IdentitySession, type InternalSign, type LoginOptions, type LoginResult, PRF_INPUT_LABEL, PRF_INPUT_SEED, type PrfSupportResult, type RegisterOptions, type RegisterResult, type SessionHandoff, type SessionVault, SoftAuthenticator, type VaultSnapshot, WebAuthnCeremonyAdapter, type WebAuthnCeremonyAdapterOptions, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult, supportsSessionHandoff };
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createHash } from 'crypto';
2
1
  import bs58 from 'bs58';
3
2
  import { blake2b } from '@noble/hashes/blake2b';
4
3
  import { keccak_256 } from '@noble/hashes/sha3';
@@ -30,8 +29,48 @@ var __async = (__this, __arguments, generator) => {
30
29
  step((generator = generator.apply(__this, __arguments)).next());
31
30
  });
32
31
  };
32
+
33
+ // src/types.ts
34
+ function supportsSessionHandoff(identity) {
35
+ return typeof identity.exportSession === "function" && typeof identity.adoptSession === "function";
36
+ }
37
+
38
+ // src/constants.ts
33
39
  var PRF_INPUT_LABEL = "cere-wallet-prf-v1";
34
- var PRF_INPUT_SEED = new Uint8Array(createHash("sha256").update(PRF_INPUT_LABEL).digest());
40
+ var PRF_INPUT_SEED = new Uint8Array([
41
+ 241,
42
+ 55,
43
+ 252,
44
+ 128,
45
+ 200,
46
+ 121,
47
+ 231,
48
+ 173,
49
+ 149,
50
+ 167,
51
+ 104,
52
+ 105,
53
+ 124,
54
+ 215,
55
+ 6,
56
+ 125,
57
+ 113,
58
+ 218,
59
+ 113,
60
+ 114,
61
+ 238,
62
+ 51,
63
+ 232,
64
+ 185,
65
+ 156,
66
+ 209,
67
+ 26,
68
+ 46,
69
+ 78,
70
+ 6,
71
+ 58,
72
+ 137
73
+ ]);
35
74
  var EVM_HKDF_INFO = "cere-wallet-evm-secp256k1-v1";
36
75
 
37
76
  // src/browser.ts
@@ -144,7 +183,7 @@ var WebAuthnCeremonyAdapter = class {
144
183
  }
145
184
  register(opts) {
146
185
  return __async(this, null, function* () {
147
- var _a, _b, _c, _d;
186
+ var _a, _b, _c, _d, _e, _f;
148
187
  if (!this.credentials) {
149
188
  throw new Error("WebAuthnCeremonyAdapter: navigator.credentials is unavailable");
150
189
  }
@@ -154,8 +193,11 @@ var WebAuthnCeremonyAdapter = class {
154
193
  rp: { id: opts.rpId, name: opts.rpId },
155
194
  user: {
156
195
  id: opts.userHandle,
157
- name: (_a = opts.label) != null ? _a : "scp-wallet",
158
- displayName: (_b = opts.label) != null ? _b : "SCP Wallet"
196
+ // WebAuthn convention: `name` is the account identifier (email), shown
197
+ // by passkey managers; `displayName` is the friendly name. Fall back to
198
+ // the legacy single `label`, then the product defaults.
199
+ name: (_b = (_a = opts.email) != null ? _a : opts.label) != null ? _b : "scp-wallet",
200
+ displayName: (_d = (_c = opts.name) != null ? _c : opts.label) != null ? _d : "SCP Wallet"
159
201
  },
160
202
  pubKeyCredParams: [
161
203
  { alg: -8, type: "public-key" },
@@ -185,7 +227,7 @@ var WebAuthnCeremonyAdapter = class {
185
227
  const response = cred.response;
186
228
  const transports = typeof response.getTransports === "function" ? response.getTransports() : [];
187
229
  const ext = cred.getClientExtensionResults();
188
- const prfFirst = (_d = (_c = ext == null ? void 0 : ext.prf) == null ? void 0 : _c.results) == null ? void 0 : _d.first;
230
+ const prfFirst = (_f = (_e = ext == null ? void 0 : ext.prf) == null ? void 0 : _e.results) == null ? void 0 : _f.first;
189
231
  return {
190
232
  credentialId: cred.id,
191
233
  clientDataJSON: bytesToB64u(response.clientDataJSON),
@@ -459,6 +501,21 @@ var IdentityImpl = class {
459
501
  this.cachedJwt = null;
460
502
  /** Server-returned credential ID (used as the public identity.credentialId). */
461
503
  this.serverCredentialId = null;
504
+ /**
505
+ * Raw derived keys for the CURRENT session, captured at every `vault.set()`
506
+ * call site (register / login / adoptSession). The vault deliberately hides
507
+ * the seeds behind its closure (Spec §3.6 invariant 1); this field is the
508
+ * single, explicit, same-origin-only seam that lets `exportSession()` hand a
509
+ * live session to a sibling wallet-origin surface (the persistent iframe).
510
+ * Cleared on logout / cross-tab logout alongside the vault. Never serialised
511
+ * to storage and never sent to a host page.
512
+ */
513
+ this.sessionKeys = null;
514
+ /**
515
+ * Absolute expiry (epoch ms) for `exportSession()`. Tracks the cached JWT
516
+ * expiry after register/login; set from the adopted session in adoptSession.
517
+ */
518
+ this.sessionExpMs = null;
462
519
  // Public observable state — derived from vault snapshot + serverCredentialId.
463
520
  // Kept in sync via syncPublicState() so React `observer()` wrappers around
464
521
  // components reading isAuthenticated/addresses/credentialId actually re-render
@@ -482,6 +539,8 @@ var IdentityImpl = class {
482
539
  this.vault.clear();
483
540
  this.cachedJwt = null;
484
541
  this.serverCredentialId = null;
542
+ this.sessionKeys = null;
543
+ this.sessionExpMs = null;
485
544
  this.syncPublicState();
486
545
  }
487
546
  });
@@ -525,7 +584,9 @@ var IdentityImpl = class {
525
584
  challenge: b64uDecode(start.challenge),
526
585
  userHandle: b64uDecode(start.userHandle),
527
586
  prfInput: PRF_INPUT_SEED,
528
- label: opts.label
587
+ label: opts.label,
588
+ name: opts.name,
589
+ email: opts.email
529
590
  });
530
591
  if (!cer.prfOutput) {
531
592
  throw new WalletError("prf-unsupported", "authenticator did not return a PRF output");
@@ -555,6 +616,8 @@ var IdentityImpl = class {
555
616
  });
556
617
  this.serverCredentialId = finish.credentialId;
557
618
  this.cachedJwt = { token: finish.token, expMs: decodeJwtExpMs(finish.token) };
619
+ this.sessionKeys = keys;
620
+ this.sessionExpMs = this.cachedJwt.expMs;
558
621
  this.syncPublicState();
559
622
  });
560
623
  }
@@ -592,6 +655,8 @@ var IdentityImpl = class {
592
655
  });
593
656
  this.serverCredentialId = finish.credentialId;
594
657
  this.cachedJwt = { token: finish.token, expMs: decodeJwtExpMs(finish.token) };
658
+ this.sessionKeys = keys;
659
+ this.sessionExpMs = this.cachedJwt.expMs;
595
660
  this.syncPublicState();
596
661
  });
597
662
  }
@@ -599,6 +664,8 @@ var IdentityImpl = class {
599
664
  this.vault.clear();
600
665
  this.cachedJwt = null;
601
666
  this.serverCredentialId = null;
667
+ this.sessionKeys = null;
668
+ this.sessionExpMs = null;
602
669
  this.syncPublicState();
603
670
  this.crossTabSync.broadcast({ type: "logout" });
604
671
  }
@@ -627,6 +694,59 @@ var IdentityImpl = class {
627
694
  getVault() {
628
695
  return this.vault;
629
696
  }
697
+ /**
698
+ * Export the current session for a same-origin handoff (register popup →
699
+ * iframe). Returns `null` unless the vault is open, we captured the derived
700
+ * keys, and the session has not expired. The raw seeds ARE included — this is
701
+ * the deliberate, narrow seam described on `IdentitySession`; callers must
702
+ * only pass the result across the same-origin wallet BroadcastChannel.
703
+ */
704
+ exportSession() {
705
+ var _a, _b, _c;
706
+ if (!this.vault.isOpen() || !this.sessionKeys) return null;
707
+ const snap = this.vault.snapshot();
708
+ if (!snap) return null;
709
+ const expMs = (_c = (_b = this.sessionExpMs) != null ? _b : (_a = this.cachedJwt) == null ? void 0 : _a.expMs) != null ? _c : 0;
710
+ if (expMs <= Date.now()) return null;
711
+ return {
712
+ // Copy so the caller (and the structured clone the BroadcastChannel makes)
713
+ // cannot mutate our live key buffers.
714
+ edSeed: new Uint8Array(this.sessionKeys.edSeed),
715
+ secpKey: new Uint8Array(this.sessionKeys.secpKey),
716
+ addresses: snap.addresses,
717
+ expMs
718
+ };
719
+ }
720
+ /**
721
+ * Adopt a session handed over from a same-origin sibling surface. Rebuilds
722
+ * the `DerivedKeys` from the transported seeds (public keys are recomputed
723
+ * from the seeds; we do NOT re-run the HKDF step so the exact transported
724
+ * secpKey is preserved), opens the vault, and syncs public state — no
725
+ * ceremony. There is no credentialId in the handoff, so login() would need a
726
+ * fresh ceremony; but the session key is warm, so `sign`/`claim` (which use
727
+ * the vault directly, no JWT) work immediately.
728
+ */
729
+ adoptSession(session) {
730
+ if (session.expMs <= Date.now()) return;
731
+ const edSeed = new Uint8Array(session.edSeed);
732
+ const secpKey = new Uint8Array(session.secpKey);
733
+ const keys = {
734
+ edSeed,
735
+ secpKey,
736
+ edPubkey: ed25519.getPublicKey(edSeed),
737
+ secpPubkeyUncompressed: secp256k1.getPublicKey(secpKey, false)
738
+ };
739
+ this.vault.set({
740
+ keys,
741
+ addresses: session.addresses,
742
+ // No credentialId travels in the handoff. login() (which needs it) will
743
+ // fall back to a fresh ceremony; the adopted key covers ceremony-free ops.
744
+ credentialId: ""
745
+ });
746
+ this.sessionKeys = keys;
747
+ this.sessionExpMs = session.expMs;
748
+ this.syncPublicState();
749
+ }
630
750
  /**
631
751
  * Release the BroadcastChannel + cross-tab subscriber. Idempotent.
632
752
  *
@@ -662,6 +782,6 @@ var IdentityImpl = class {
662
782
  }
663
783
  };
664
784
 
665
- export { CERE_SS58_PREFIX, CrossTabSync, EVM_HKDF_INFO, IdentityImpl, PRF_INPUT_LABEL, PRF_INPUT_SEED, SoftAuthenticator, WebAuthnCeremonyAdapter, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult };
785
+ export { CERE_SS58_PREFIX, CrossTabSync, EVM_HKDF_INFO, IdentityImpl, PRF_INPUT_LABEL, PRF_INPUT_SEED, SoftAuthenticator, WebAuthnCeremonyAdapter, b64uToBytes, bytesToB64u, createSessionVault, decodeEd25519Pubkey, deriveCereAddress, deriveEvmAddress, deriveSolanaAddress, derivedKeys, detectPrfSupport, isPrfSupportedResult, supportsSessionHandoff };
666
786
  //# sourceMappingURL=index.js.map
667
787
  //# sourceMappingURL=index.js.map