@cef-ai/wallet-identity 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.cjs ADDED
@@ -0,0 +1,690 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var bs58 = require('bs58');
5
+ var blake2b = require('@noble/hashes/blake2b');
6
+ var sha3 = require('@noble/hashes/sha3');
7
+ var utils = require('@noble/hashes/utils');
8
+ var ed25519 = require('@noble/curves/ed25519');
9
+ var secp256k1 = require('@noble/curves/secp256k1');
10
+ var hkdf = require('@noble/hashes/hkdf');
11
+ var sha256 = require('@noble/hashes/sha256');
12
+ var mobx = require('mobx');
13
+ var walletApiClient = require('@cef-ai/wallet-api-client');
14
+
15
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
16
+
17
+ var bs58__default = /*#__PURE__*/_interopDefault(bs58);
18
+
19
+ var __async = (__this, __arguments, generator) => {
20
+ return new Promise((resolve, reject) => {
21
+ var fulfilled = (value) => {
22
+ try {
23
+ step(generator.next(value));
24
+ } catch (e) {
25
+ reject(e);
26
+ }
27
+ };
28
+ var rejected = (value) => {
29
+ try {
30
+ step(generator.throw(value));
31
+ } catch (e) {
32
+ reject(e);
33
+ }
34
+ };
35
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
36
+ step((generator = generator.apply(__this, __arguments)).next());
37
+ });
38
+ };
39
+ var PRF_INPUT_LABEL = "cere-wallet-prf-v1";
40
+ var PRF_INPUT_SEED = new Uint8Array(crypto.createHash("sha256").update(PRF_INPUT_LABEL).digest());
41
+ var EVM_HKDF_INFO = "cere-wallet-evm-secp256k1-v1";
42
+
43
+ // src/browser.ts
44
+ function detectPrfSupport() {
45
+ return __async(this, null, function* () {
46
+ const PKC = globalThis.PublicKeyCredential;
47
+ const webAuthn = typeof PKC !== "undefined";
48
+ if (!webAuthn) {
49
+ return { webAuthn: false, platformAuthenticator: false, prfPotentiallySupported: false };
50
+ }
51
+ let platformAuthenticator = false;
52
+ try {
53
+ platformAuthenticator = typeof PKC.isUserVerifyingPlatformAuthenticatorAvailable === "function" && (yield PKC.isUserVerifyingPlatformAuthenticatorAvailable());
54
+ } catch (e) {
55
+ platformAuthenticator = false;
56
+ }
57
+ let prfPotentiallySupported = true;
58
+ try {
59
+ if (typeof PKC.getClientCapabilities === "function") {
60
+ const caps = yield PKC.getClientCapabilities();
61
+ if (caps && caps["extensions:prf"] === false) {
62
+ prfPotentiallySupported = false;
63
+ }
64
+ }
65
+ } catch (e) {
66
+ }
67
+ return { webAuthn, platformAuthenticator, prfPotentiallySupported };
68
+ });
69
+ }
70
+ function isPrfSupportedResult(extensions) {
71
+ var _a;
72
+ const prf = extensions == null ? void 0 : extensions.prf;
73
+ if (!prf) return false;
74
+ if (prf.enabled === true) return true;
75
+ if (((_a = prf.results) == null ? void 0 : _a.first) instanceof Uint8Array) return true;
76
+ return false;
77
+ }
78
+ var CERE_SS58_PREFIX = 54;
79
+ var SS58PRE = new TextEncoder().encode("SS58PRE");
80
+ function deriveCereAddress(edPubkey) {
81
+ if (edPubkey.length !== 32) {
82
+ throw new Error(`expected 32-byte Ed25519 pubkey, got ${edPubkey.length}`);
83
+ }
84
+ const prefix = new Uint8Array([CERE_SS58_PREFIX]);
85
+ const body = utils.concatBytes(prefix, edPubkey);
86
+ const checksum = blake2b.blake2b(utils.concatBytes(SS58PRE, body), { dkLen: 64 }).slice(0, 2);
87
+ const payload = utils.concatBytes(body, checksum);
88
+ return bs58__default.default.encode(payload);
89
+ }
90
+ function deriveSolanaAddress(edPubkey) {
91
+ if (edPubkey.length !== 32) {
92
+ throw new Error(`expected 32-byte Ed25519 pubkey, got ${edPubkey.length}`);
93
+ }
94
+ return bs58__default.default.encode(edPubkey);
95
+ }
96
+ function decodeEd25519Pubkey(solanaAddress) {
97
+ const pubkey = bs58__default.default.decode(solanaAddress);
98
+ if (pubkey.length !== 32) {
99
+ throw new Error(`expected a 32-byte Ed25519 pubkey from the Solana address, got ${pubkey.length}`);
100
+ }
101
+ return pubkey;
102
+ }
103
+ function deriveEvmAddress(secpPubkey) {
104
+ let xy;
105
+ if (secpPubkey.length === 65 && secpPubkey[0] === 4) {
106
+ xy = secpPubkey.slice(1);
107
+ } else if (secpPubkey.length === 64) {
108
+ xy = secpPubkey;
109
+ } else {
110
+ throw new Error(`expected 64-byte (X||Y) or 65-byte (0x04||X||Y) secp256k1 pubkey, got ${secpPubkey.length}`);
111
+ }
112
+ const hash = sha3.keccak_256(xy);
113
+ const last20 = hash.slice(12);
114
+ return "0x" + Buffer.from(last20).toString("hex");
115
+ }
116
+ function derivedKeys(prfOutput) {
117
+ if (prfOutput.length !== 32) {
118
+ throw new Error(`expected 32-byte PRF output, got ${prfOutput.length}`);
119
+ }
120
+ const edSeed = new Uint8Array(prfOutput);
121
+ const secpKey = hkdf.hkdf(sha256.sha256, prfOutput, new Uint8Array(0), EVM_HKDF_INFO, 32);
122
+ const edPubkey = ed25519.ed25519.getPublicKey(edSeed);
123
+ const secpPubkeyUncompressed = secp256k1.secp256k1.getPublicKey(secpKey, false);
124
+ return { edSeed, secpKey, edPubkey, secpPubkeyUncompressed };
125
+ }
126
+
127
+ // src/base64url.ts
128
+ function bytesToB64u(input) {
129
+ const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
130
+ let bin = "";
131
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
132
+ const b64 = btoa(bin);
133
+ return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
134
+ }
135
+ function b64uToBytes(s) {
136
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
137
+ const padded = b64 + "=".repeat((4 - b64.length % 4) % 4);
138
+ const bin = atob(padded);
139
+ const out = new Uint8Array(bin.length);
140
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
141
+ return out;
142
+ }
143
+
144
+ // src/ceremony.ts
145
+ var PRF_CAPABLE_HINTS = ["client-device", "hybrid", "security-key"];
146
+ var WebAuthnCeremonyAdapter = class {
147
+ constructor(opts = {}) {
148
+ var _a, _b;
149
+ this.credentials = (_b = opts.credentials) != null ? _b : (_a = globalThis.navigator) == null ? void 0 : _a.credentials;
150
+ }
151
+ register(opts) {
152
+ return __async(this, null, function* () {
153
+ var _a, _b, _c, _d;
154
+ if (!this.credentials) {
155
+ throw new Error("WebAuthnCeremonyAdapter: navigator.credentials is unavailable");
156
+ }
157
+ const cred = yield this.credentials.create({
158
+ publicKey: {
159
+ challenge: opts.challenge,
160
+ rp: { id: opts.rpId, name: opts.rpId },
161
+ user: {
162
+ id: opts.userHandle,
163
+ name: (_a = opts.label) != null ? _a : "scp-wallet",
164
+ displayName: (_b = opts.label) != null ? _b : "SCP Wallet"
165
+ },
166
+ pubKeyCredParams: [
167
+ { alg: -8, type: "public-key" },
168
+ // EdDSA (Ed25519 security keys) — preferred
169
+ { alg: -7, type: "public-key" }
170
+ // ES256 (Apple/Windows/Android platform authenticators)
171
+ ],
172
+ authenticatorSelection: {
173
+ // No `authenticatorAttachment`: allow platform authenticators
174
+ // (Touch ID / Windows Hello), roaming security keys, and phone
175
+ // passkeys via hybrid — PRF works across all of them, and the
176
+ // pubKeyCredParams above explicitly prefer Ed25519 security keys.
177
+ // The post-ceremony PRF result is the real capability gate.
178
+ userVerification: "required",
179
+ residentKey: "preferred"
180
+ },
181
+ // Bias the picker toward PRF-capable authenticators so the user
182
+ // doesn't default onto a password-manager passkey. See PRF_CAPABLE_HINTS.
183
+ hints: [...PRF_CAPABLE_HINTS],
184
+ timeout: 6e4,
185
+ extensions: { prf: { eval: { first: opts.prfInput } } }
186
+ }
187
+ });
188
+ if (!cred) {
189
+ throw new Error("WebAuthnCeremonyAdapter.register: credentials.create returned null");
190
+ }
191
+ const response = cred.response;
192
+ const transports = typeof response.getTransports === "function" ? response.getTransports() : [];
193
+ const ext = cred.getClientExtensionResults();
194
+ const prfFirst = (_d = (_c = ext == null ? void 0 : ext.prf) == null ? void 0 : _c.results) == null ? void 0 : _d.first;
195
+ return {
196
+ credentialId: cred.id,
197
+ clientDataJSON: bytesToB64u(response.clientDataJSON),
198
+ attestationObject: bytesToB64u(response.attestationObject),
199
+ transports,
200
+ prfOutput: prfFirst ? new Uint8Array(prfFirst) : void 0
201
+ };
202
+ });
203
+ }
204
+ login(opts) {
205
+ return __async(this, null, function* () {
206
+ var _a, _b;
207
+ if (!this.credentials) {
208
+ throw new Error("WebAuthnCeremonyAdapter: navigator.credentials is unavailable");
209
+ }
210
+ const cred = yield this.credentials.get({
211
+ publicKey: {
212
+ challenge: opts.challenge,
213
+ rpId: opts.rpId,
214
+ allowCredentials: opts.credentialId ? [{ id: b64uToBytes(opts.credentialId), type: "public-key" }] : void 0,
215
+ userVerification: "required",
216
+ // Bias first-login-on-device (no allowCredentials) toward PRF-capable
217
+ // authenticators, mirroring register. See PRF_CAPABLE_HINTS.
218
+ hints: [...PRF_CAPABLE_HINTS],
219
+ timeout: 6e4,
220
+ extensions: { prf: { eval: { first: opts.prfInput } } }
221
+ }
222
+ });
223
+ if (!cred) {
224
+ throw new Error("WebAuthnCeremonyAdapter.login: credentials.get returned null");
225
+ }
226
+ const response = cred.response;
227
+ const ext = cred.getClientExtensionResults();
228
+ const prfFirst = (_b = (_a = ext == null ? void 0 : ext.prf) == null ? void 0 : _a.results) == null ? void 0 : _b.first;
229
+ return {
230
+ credentialId: cred.id,
231
+ clientDataJSON: bytesToB64u(response.clientDataJSON),
232
+ authenticatorData: bytesToB64u(response.authenticatorData),
233
+ signature: bytesToB64u(response.signature),
234
+ prfOutput: prfFirst ? new Uint8Array(prfFirst) : void 0
235
+ };
236
+ });
237
+ }
238
+ };
239
+ function b64u(bytes) {
240
+ return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
241
+ }
242
+ var SoftAuthenticator = class {
243
+ constructor(opts = {}) {
244
+ this.creds = /* @__PURE__ */ new Map();
245
+ /** Monotonic counter mixed into the seed-derived material so successive
246
+ * register() calls on a seeded instance produce distinct credentials. */
247
+ this.regCounter = 0;
248
+ this.seedBytes = opts.seed != null ? new TextEncoder().encode(opts.seed) : null;
249
+ this.fallbackToLastRegistered = opts.fallbackToLastRegistered === true;
250
+ }
251
+ register(opts) {
252
+ return __async(this, null, function* () {
253
+ let edSeed;
254
+ let prfSecret;
255
+ let credentialIdBytes;
256
+ if (this.seedBytes) {
257
+ const salt = new TextEncoder().encode(`reg-${this.regCounter++}`);
258
+ edSeed = hkdf.hkdf(sha256.sha256, this.seedBytes, salt, "ed-seed", 32);
259
+ prfSecret = hkdf.hkdf(sha256.sha256, this.seedBytes, salt, "prf-secret", 32);
260
+ credentialIdBytes = hkdf.hkdf(sha256.sha256, this.seedBytes, salt, "credential-id", 16);
261
+ } else {
262
+ edSeed = ed25519.ed25519.utils.randomPrivateKey();
263
+ prfSecret = ed25519.ed25519.utils.randomPrivateKey();
264
+ credentialIdBytes = ed25519.ed25519.utils.randomPrivateKey().slice(0, 16);
265
+ }
266
+ const credentialId = b64u(credentialIdBytes);
267
+ this.creds.set(credentialId, { credentialId, edSeed, prfSecret });
268
+ const clientData = {
269
+ type: "webauthn.create",
270
+ challenge: b64u(opts.challenge),
271
+ origin: `https://${opts.rpId}`,
272
+ crossOrigin: false
273
+ };
274
+ const clientDataJSON = new TextEncoder().encode(JSON.stringify(clientData));
275
+ const attestationObject = new Uint8Array([170]);
276
+ const prfOutput = this.computePrf(prfSecret, opts.prfInput);
277
+ return {
278
+ credentialId,
279
+ clientDataJSON: b64u(clientDataJSON),
280
+ attestationObject: b64u(attestationObject),
281
+ transports: ["internal"],
282
+ prfOutput
283
+ };
284
+ });
285
+ }
286
+ login(opts) {
287
+ return __async(this, null, function* () {
288
+ let stored;
289
+ if (opts.credentialId) {
290
+ stored = this.creds.get(opts.credentialId);
291
+ if (!stored) {
292
+ throw new Error(`SoftAuthenticator.login: unknown credential ${opts.credentialId}`);
293
+ }
294
+ } else {
295
+ if (!this.fallbackToLastRegistered) {
296
+ throw new Error(
297
+ "SoftAuthenticator.login: credentialId is required (set `fallbackToLastRegistered: true` in the constructor to opt into the e2e convenience of picking the most-recently-registered credential)"
298
+ );
299
+ }
300
+ const all = Array.from(this.creds.values());
301
+ stored = all[all.length - 1];
302
+ if (!stored) {
303
+ throw new Error("SoftAuthenticator.login: no credentials registered");
304
+ }
305
+ }
306
+ const clientData = {
307
+ type: "webauthn.get",
308
+ challenge: b64u(opts.challenge),
309
+ origin: `https://${opts.rpId}`,
310
+ crossOrigin: false
311
+ };
312
+ const clientDataJSON = new TextEncoder().encode(JSON.stringify(clientData));
313
+ const authenticatorData = new Uint8Array(37);
314
+ authenticatorData[32] = 5;
315
+ const clientDataHash = sha256.sha256(clientDataJSON);
316
+ const toSign = utils.concatBytes(authenticatorData, clientDataHash);
317
+ const signature = ed25519.ed25519.sign(toSign, stored.edSeed);
318
+ const prfOutput = this.computePrf(stored.prfSecret, opts.prfInput);
319
+ return {
320
+ credentialId: stored.credentialId,
321
+ clientDataJSON: b64u(clientDataJSON),
322
+ authenticatorData: b64u(authenticatorData),
323
+ signature: b64u(signature),
324
+ prfOutput
325
+ };
326
+ });
327
+ }
328
+ /** Public-key bytes for a credential. Used by integration tests for cross-checks. */
329
+ getEd25519PublicKey(credentialId) {
330
+ const stored = this.creds.get(credentialId);
331
+ if (!stored) throw new Error(`unknown credential ${credentialId}`);
332
+ return ed25519.ed25519.getPublicKey(stored.edSeed);
333
+ }
334
+ computePrf(prfSecret, prfInput) {
335
+ return hkdf.hkdf(sha256.sha256, prfSecret, new Uint8Array(0), prfInput, 32);
336
+ }
337
+ };
338
+ function createSessionVault() {
339
+ let state = null;
340
+ return {
341
+ isOpen() {
342
+ return state !== null;
343
+ },
344
+ snapshot() {
345
+ if (!state) return null;
346
+ return { addresses: state.addresses, credentialId: state.credentialId };
347
+ },
348
+ set({ keys, addresses, credentialId }) {
349
+ state = {
350
+ edSeed: new Uint8Array(keys.edSeed),
351
+ secpKey: new Uint8Array(keys.secpKey),
352
+ addresses,
353
+ credentialId
354
+ };
355
+ },
356
+ clear() {
357
+ if (state) {
358
+ state.edSeed.fill(0);
359
+ state.secpKey.fill(0);
360
+ }
361
+ state = null;
362
+ },
363
+ sign(chain, payload) {
364
+ return __async(this, null, function* () {
365
+ if (!state) {
366
+ throw new Error("SessionVault: closed \u2014 no session keys available");
367
+ }
368
+ if (chain === "cere" || chain === "solana") {
369
+ return ed25519.ed25519.sign(payload, state.edSeed);
370
+ }
371
+ if (chain === "evm") {
372
+ const sig = secp256k1.secp256k1.sign(payload, state.secpKey);
373
+ const compact = sig.toCompactRawBytes();
374
+ const v = sig.recovery;
375
+ const out = new Uint8Array(65);
376
+ out.set(compact, 0);
377
+ out[64] = v;
378
+ return out;
379
+ }
380
+ throw new Error(`SessionVault.sign: unknown chain "${String(chain)}"`);
381
+ });
382
+ }
383
+ };
384
+ }
385
+ function signRegistrationProof(keys, challenge) {
386
+ const identitySig = ed25519.ed25519.sign(challenge, keys.edSeed);
387
+ const evmSig = secp256k1.secp256k1.sign(challenge, keys.secpKey).toCompactRawBytes();
388
+ return { identitySig, evmPubkey: keys.secpPubkeyUncompressed, evmSig };
389
+ }
390
+
391
+ // src/CrossTabSync.ts
392
+ var CHANNEL_NAME = "scp-wallet-v2";
393
+ var CrossTabSync = class {
394
+ constructor() {
395
+ this.listeners = /* @__PURE__ */ new Set();
396
+ if (typeof BroadcastChannel === "undefined") {
397
+ this.channel = null;
398
+ return;
399
+ }
400
+ this.channel = new BroadcastChannel(CHANNEL_NAME);
401
+ this.channel.onmessage = (ev) => {
402
+ const msg = ev.data;
403
+ if (!msg || typeof msg !== "object" || typeof msg.type !== "string") {
404
+ return;
405
+ }
406
+ for (const fn of this.listeners) {
407
+ try {
408
+ fn(msg);
409
+ } catch (e) {
410
+ }
411
+ }
412
+ };
413
+ }
414
+ /** Broadcast a message to all other tabs (and not to this tab — that's the
415
+ * BroadcastChannel spec). The originating tab is expected to have already
416
+ * applied the state change locally before broadcasting. */
417
+ broadcast(msg) {
418
+ var _a;
419
+ (_a = this.channel) == null ? void 0 : _a.postMessage(msg);
420
+ }
421
+ /** Subscribe to messages from other tabs. Returns an unsubscribe function. */
422
+ subscribe(fn) {
423
+ this.listeners.add(fn);
424
+ return () => {
425
+ this.listeners.delete(fn);
426
+ };
427
+ }
428
+ /**
429
+ * Close the underlying channel and drop all listeners. Idempotent.
430
+ *
431
+ * Sets `this.channel = null` so subsequent `broadcast()` calls become a
432
+ * no-op (the `?.postMessage` short-circuits) and we don't accidentally
433
+ * postMessage on a closed BroadcastChannel (which throws InvalidStateError
434
+ * in some implementations).
435
+ */
436
+ close() {
437
+ var _a;
438
+ (_a = this.channel) == null ? void 0 : _a.close();
439
+ this.channel = null;
440
+ this.listeners.clear();
441
+ }
442
+ };
443
+
444
+ // src/IdentityImpl.ts
445
+ var JWT_REFRESH_SLACK_MS = 6e4;
446
+ function decodeJwtExpMs(token) {
447
+ const parts = token.split(".");
448
+ if (parts.length < 2) {
449
+ throw new walletApiClient.WalletError("validation", "JWT does not have 3 parts");
450
+ }
451
+ const payloadB64 = parts[1];
452
+ const payload = JSON.parse(Buffer.from(payloadB64.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
453
+ if (typeof payload.exp !== "number") {
454
+ throw new walletApiClient.WalletError("validation", "JWT missing exp");
455
+ }
456
+ return payload.exp * 1e3;
457
+ }
458
+ function b64uDecode(s) {
459
+ return Uint8Array.from(Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64"));
460
+ }
461
+ var IdentityImpl = class {
462
+ constructor(opts) {
463
+ this.opts = opts;
464
+ this.vault = createSessionVault();
465
+ this.cachedJwt = null;
466
+ /** Server-returned credential ID (used as the public identity.credentialId). */
467
+ this.serverCredentialId = null;
468
+ // Public observable state — derived from vault snapshot + serverCredentialId.
469
+ // Kept in sync via syncPublicState() so React `observer()` wrappers around
470
+ // components reading isAuthenticated/addresses/credentialId actually re-render
471
+ // on register/login/logout. The vault itself stays non-observable (security:
472
+ // the closure-encapsulated keys must not be tracked by MobX).
473
+ this._isAuthenticated = mobx.observable.box(false);
474
+ // `deep: false` keeps the stored value a plain object instead of wrapping it
475
+ // in a MobX proxy. addresses is a wholesale-replaced snapshot (never mutated
476
+ // field-by-field), so deep observability buys nothing — and a proxy is not
477
+ // structured-cloneable, which breaks `postMessage` when the embed popup
478
+ // bridge sends addresses to the host page (wallet:login:ok).
479
+ this._addresses = mobx.observable.box(null, { deep: false });
480
+ this._credentialId = mobx.observable.box(null);
481
+ /** Cross-tab logout coordination via BroadcastChannel('scp-wallet-v2'). */
482
+ this.crossTabSync = new CrossTabSync();
483
+ /** Idempotency flag for dispose(). */
484
+ this.disposed = false;
485
+ this.syncPublicState();
486
+ this.crossTabSyncUnsubscribe = this.crossTabSync.subscribe((msg) => {
487
+ if (msg.type === "logout" && this.vault.isOpen()) {
488
+ this.vault.clear();
489
+ this.cachedJwt = null;
490
+ this.serverCredentialId = null;
491
+ this.syncPublicState();
492
+ }
493
+ });
494
+ }
495
+ get isAuthenticated() {
496
+ return this._isAuthenticated.get();
497
+ }
498
+ get addresses() {
499
+ return this._addresses.get();
500
+ }
501
+ /**
502
+ * Returns the server-returned credentialId from the last register/login ceremony.
503
+ * The vault internally tracks the authenticator-generated credential ID for login use.
504
+ */
505
+ get credentialId() {
506
+ return this._credentialId.get();
507
+ }
508
+ /**
509
+ * Read the (non-observable) vault snapshot + cached server credential ID and
510
+ * push the values into the three public observable boxes. Call after any
511
+ * mutation that could change the public derived state: register, login,
512
+ * logout, or constructor (hydration).
513
+ */
514
+ syncPublicState() {
515
+ const snap = this.vault.snapshot();
516
+ mobx.runInAction(() => {
517
+ var _a;
518
+ this._isAuthenticated.set(this.vault.isOpen());
519
+ this._addresses.set((_a = snap == null ? void 0 : snap.addresses) != null ? _a : null);
520
+ this._credentialId.set(this.serverCredentialId);
521
+ });
522
+ }
523
+ register() {
524
+ return __async(this, arguments, function* (opts = {}) {
525
+ const start = yield this.opts.apiClient.passkey.registerStart({
526
+ addresses: { cere: "5GplaceholderClientCannotKnowYet" },
527
+ label: opts.label
528
+ });
529
+ const cer = yield this.opts.ceremony.register({
530
+ rpId: start.rpId,
531
+ challenge: b64uDecode(start.challenge),
532
+ userHandle: b64uDecode(start.userHandle),
533
+ prfInput: PRF_INPUT_SEED,
534
+ label: opts.label
535
+ });
536
+ if (!cer.prfOutput) {
537
+ throw new walletApiClient.WalletError("prf-unsupported", "authenticator did not return a PRF output");
538
+ }
539
+ const keys = derivedKeys(cer.prfOutput);
540
+ const clientAddresses = {
541
+ cere: deriveCereAddress(keys.edPubkey),
542
+ solana: deriveSolanaAddress(keys.edPubkey),
543
+ evm: deriveEvmAddress(keys.secpPubkeyUncompressed)
544
+ };
545
+ const proof = signRegistrationProof(keys, b64uDecode(start.challenge));
546
+ const finish = yield this.opts.apiClient.passkey.registerFinish({
547
+ challengeId: start.challengeId,
548
+ clientDataJSON: cer.clientDataJSON,
549
+ attestationObject: cer.attestationObject,
550
+ addresses: clientAddresses,
551
+ transports: cer.transports,
552
+ identitySig: bytesToB64u(proof.identitySig),
553
+ evmPubkey: bytesToB64u(proof.evmPubkey),
554
+ evmSig: bytesToB64u(proof.evmSig)
555
+ });
556
+ this.crossCheckAddresses(clientAddresses, finish.addresses);
557
+ this.vault.set({
558
+ keys,
559
+ addresses: clientAddresses,
560
+ credentialId: cer.credentialId
561
+ });
562
+ this.serverCredentialId = finish.credentialId;
563
+ this.cachedJwt = { token: finish.token, expMs: decodeJwtExpMs(finish.token) };
564
+ this.syncPublicState();
565
+ });
566
+ }
567
+ login() {
568
+ return __async(this, null, function* () {
569
+ var _a;
570
+ const start = yield this.opts.apiClient.passkey.loginStart();
571
+ const cer = yield this.opts.ceremony.login({
572
+ rpId: start.rpId,
573
+ challenge: b64uDecode(start.challenge),
574
+ credentialId: (_a = this.vault.snapshot()) == null ? void 0 : _a.credentialId,
575
+ prfInput: PRF_INPUT_SEED
576
+ });
577
+ if (!cer.prfOutput) {
578
+ throw new walletApiClient.WalletError("prf-unsupported", "authenticator did not return a PRF output");
579
+ }
580
+ const keys = derivedKeys(cer.prfOutput);
581
+ const clientAddresses = {
582
+ cere: deriveCereAddress(keys.edPubkey),
583
+ solana: deriveSolanaAddress(keys.edPubkey),
584
+ evm: deriveEvmAddress(keys.secpPubkeyUncompressed)
585
+ };
586
+ const finish = yield this.opts.apiClient.passkey.loginFinish({
587
+ challengeId: start.challengeId,
588
+ credentialId: cer.credentialId,
589
+ clientDataJSON: cer.clientDataJSON,
590
+ authenticatorData: cer.authenticatorData,
591
+ signature: cer.signature
592
+ });
593
+ this.crossCheckAddresses(clientAddresses, finish.addresses);
594
+ this.vault.set({
595
+ keys,
596
+ addresses: clientAddresses,
597
+ credentialId: cer.credentialId
598
+ });
599
+ this.serverCredentialId = finish.credentialId;
600
+ this.cachedJwt = { token: finish.token, expMs: decodeJwtExpMs(finish.token) };
601
+ this.syncPublicState();
602
+ });
603
+ }
604
+ logout() {
605
+ this.vault.clear();
606
+ this.cachedJwt = null;
607
+ this.serverCredentialId = null;
608
+ this.syncPublicState();
609
+ this.crossTabSync.broadcast({ type: "logout" });
610
+ }
611
+ getJwt() {
612
+ return __async(this, null, function* () {
613
+ if (this.cachedJwt && Date.now() < this.cachedJwt.expMs - JWT_REFRESH_SLACK_MS) {
614
+ return this.cachedJwt.token;
615
+ }
616
+ if (!this.vault.isOpen()) {
617
+ throw new walletApiClient.WalletError("unauthorized", "session is closed; call register() or login() first");
618
+ }
619
+ yield this.login();
620
+ return this.cachedJwt.token;
621
+ });
622
+ }
623
+ /** Helper used by callers (e.g. ApiClient.getAuthToken). */
624
+ getCachedJwtForApiClient() {
625
+ var _a, _b;
626
+ return (_b = (_a = this.cachedJwt) == null ? void 0 : _a.token) != null ? _b : null;
627
+ }
628
+ /**
629
+ * Returns the session vault. Used by popup-side `wallet:sign` handlers
630
+ * which need direct access to `vault.sign(chain, payload)`. NOT for host-
631
+ * side use — keys never reach the host. Spec §3.6 invariant 1.
632
+ */
633
+ getVault() {
634
+ return this.vault;
635
+ }
636
+ /**
637
+ * Release the BroadcastChannel + cross-tab subscriber. Idempotent.
638
+ *
639
+ * Called by:
640
+ * - WalletProvider's useEffect cleanup (SPA unmount / apiBaseUrl change)
641
+ * - test afterEach hooks (prevent listener leakage across tests)
642
+ *
643
+ * After dispose(), the identity is unusable: register/login/logout/getJwt
644
+ * still execute their normal logic, but cross-tab broadcasts no longer
645
+ * propagate (channel is closed) and the local subscriber is detached.
646
+ * The vault remains cleared (logout() runs locally). Spec §9.4 lifecycle.
647
+ */
648
+ dispose() {
649
+ if (this.disposed) return;
650
+ this.disposed = true;
651
+ this.crossTabSyncUnsubscribe();
652
+ this.crossTabSync.close();
653
+ }
654
+ // ---- private --------------------------------------------------------------
655
+ crossCheckAddresses(client, server) {
656
+ if (!server) {
657
+ throw new walletApiClient.WalletError("derivation-mismatch", "API did not return addresses");
658
+ }
659
+ if (server.cere != null && client.cere !== server.cere) {
660
+ throw new walletApiClient.WalletError("derivation-mismatch", `Cere mismatch: client=${client.cere} api=${server.cere}`);
661
+ }
662
+ if (server.solana != null && client.solana !== server.solana) {
663
+ throw new walletApiClient.WalletError("derivation-mismatch", `Solana mismatch: client=${client.solana} api=${server.solana}`);
664
+ }
665
+ if (server.evm != null && client.evm.toLowerCase() !== server.evm.toLowerCase()) {
666
+ throw new walletApiClient.WalletError("derivation-mismatch", `EVM mismatch: client=${client.evm} api=${server.evm}`);
667
+ }
668
+ }
669
+ };
670
+
671
+ exports.CERE_SS58_PREFIX = CERE_SS58_PREFIX;
672
+ exports.CrossTabSync = CrossTabSync;
673
+ exports.EVM_HKDF_INFO = EVM_HKDF_INFO;
674
+ exports.IdentityImpl = IdentityImpl;
675
+ exports.PRF_INPUT_LABEL = PRF_INPUT_LABEL;
676
+ exports.PRF_INPUT_SEED = PRF_INPUT_SEED;
677
+ exports.SoftAuthenticator = SoftAuthenticator;
678
+ exports.WebAuthnCeremonyAdapter = WebAuthnCeremonyAdapter;
679
+ exports.b64uToBytes = b64uToBytes;
680
+ exports.bytesToB64u = bytesToB64u;
681
+ exports.createSessionVault = createSessionVault;
682
+ exports.decodeEd25519Pubkey = decodeEd25519Pubkey;
683
+ exports.deriveCereAddress = deriveCereAddress;
684
+ exports.deriveEvmAddress = deriveEvmAddress;
685
+ exports.deriveSolanaAddress = deriveSolanaAddress;
686
+ exports.derivedKeys = derivedKeys;
687
+ exports.detectPrfSupport = detectPrfSupport;
688
+ exports.isPrfSupportedResult = isPrfSupportedResult;
689
+ //# sourceMappingURL=index.cjs.map
690
+ //# sourceMappingURL=index.cjs.map