@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/README.md +64 -0
- package/dist/index.cjs +690 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +443 -0
- package/dist/index.d.ts +443 -0
- package/dist/index.js +667 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
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
|