@dexterai/connect 0.13.0 → 0.14.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/{chunk-6NIGGPVI.js → chunk-IUZBHWTP.js} +283 -133
- package/dist/index.d.ts +35 -29
- package/dist/index.js +4 -142
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{signals-DM7IQdq6.d.ts → signals-BuP-7inZ.d.ts} +1 -1
- package/package.json +1 -1
|
@@ -8,6 +8,122 @@ var ConnectError = class extends Error {
|
|
|
8
8
|
}
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
+
// src/walletStore.ts
|
|
12
|
+
var ACTIVE_HANDLE_KEY = "dexter:passkey:userHandle";
|
|
13
|
+
var ROSTER_KEY = "dexter:passkey:wallets";
|
|
14
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
15
|
+
function hasStorage() {
|
|
16
|
+
try {
|
|
17
|
+
return typeof window !== "undefined" && !!window.localStorage;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function readRoster() {
|
|
23
|
+
if (!hasStorage()) return [];
|
|
24
|
+
try {
|
|
25
|
+
const raw = window.localStorage.getItem(ROSTER_KEY);
|
|
26
|
+
if (!raw) return [];
|
|
27
|
+
const parsed = JSON.parse(raw);
|
|
28
|
+
if (!Array.isArray(parsed)) return [];
|
|
29
|
+
return parsed.filter(
|
|
30
|
+
(w) => !!w && typeof w === "object" && typeof w.handle === "string"
|
|
31
|
+
);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function writeRoster(wallets) {
|
|
37
|
+
if (!hasStorage()) return;
|
|
38
|
+
try {
|
|
39
|
+
window.localStorage.setItem(ROSTER_KEY, JSON.stringify(wallets));
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function emit() {
|
|
44
|
+
for (const l of listeners) {
|
|
45
|
+
try {
|
|
46
|
+
l();
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function getActiveHandle() {
|
|
52
|
+
if (!hasStorage()) return null;
|
|
53
|
+
try {
|
|
54
|
+
return window.localStorage.getItem(ACTIVE_HANDLE_KEY);
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function setActiveHandle(handle, label, credentialId) {
|
|
60
|
+
if (!hasStorage() || !handle) return;
|
|
61
|
+
try {
|
|
62
|
+
window.localStorage.setItem(ACTIVE_HANDLE_KEY, handle);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const roster = readRoster();
|
|
67
|
+
const existing = roster.find((w) => w.handle === handle);
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (existing) {
|
|
70
|
+
existing.lastUsedAt = now;
|
|
71
|
+
if (label !== void 0) existing.label = label;
|
|
72
|
+
if (credentialId !== void 0) existing.credentialId = credentialId;
|
|
73
|
+
} else {
|
|
74
|
+
roster.push({ handle, label, credentialId, lastUsedAt: now });
|
|
75
|
+
}
|
|
76
|
+
writeRoster(roster);
|
|
77
|
+
emit();
|
|
78
|
+
}
|
|
79
|
+
function getCredentialId(handle) {
|
|
80
|
+
return readRoster().find((w) => w.handle === handle)?.credentialId;
|
|
81
|
+
}
|
|
82
|
+
function ejectActiveWallet(opts) {
|
|
83
|
+
if (!hasStorage()) return;
|
|
84
|
+
const current = getActiveHandle();
|
|
85
|
+
try {
|
|
86
|
+
window.localStorage.removeItem(ACTIVE_HANDLE_KEY);
|
|
87
|
+
} catch {
|
|
88
|
+
}
|
|
89
|
+
if (opts?.forget && current) {
|
|
90
|
+
writeRoster(readRoster().filter((w) => w.handle !== current));
|
|
91
|
+
}
|
|
92
|
+
emit();
|
|
93
|
+
}
|
|
94
|
+
function listWallets() {
|
|
95
|
+
return readRoster().sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0));
|
|
96
|
+
}
|
|
97
|
+
function switchWallet(handle) {
|
|
98
|
+
if (!readRoster().some((w) => w.handle === handle)) return false;
|
|
99
|
+
setActiveHandle(handle);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function forgetWallet(handle) {
|
|
103
|
+
if (getActiveHandle() === handle) {
|
|
104
|
+
ejectActiveWallet({ forget: true });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
writeRoster(readRoster().filter((w) => w.handle !== handle));
|
|
108
|
+
emit();
|
|
109
|
+
}
|
|
110
|
+
function subscribe(listener) {
|
|
111
|
+
listeners.add(listener);
|
|
112
|
+
if (hasStorage() && listeners.size === 1) {
|
|
113
|
+
window.addEventListener("storage", onStorageEvent);
|
|
114
|
+
}
|
|
115
|
+
return () => {
|
|
116
|
+
listeners.delete(listener);
|
|
117
|
+
if (hasStorage() && listeners.size === 0) {
|
|
118
|
+
window.removeEventListener("storage", onStorageEvent);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function onStorageEvent(e) {
|
|
123
|
+
if (e.key === ACTIVE_HANDLE_KEY || e.key === ROSTER_KEY || e.key === null) emit();
|
|
124
|
+
}
|
|
125
|
+
var ACTIVE_WALLET_STORAGE_KEY = ACTIVE_HANDLE_KEY;
|
|
126
|
+
|
|
11
127
|
// src/base64.ts
|
|
12
128
|
function base64ToBytes(s) {
|
|
13
129
|
const bin = atob(s);
|
|
@@ -145,8 +261,145 @@ function makeNonce() {
|
|
|
145
261
|
return `rid-${new URL(location.href).searchParams.get("v") ?? ""}-${typeof performance !== "undefined" ? performance.now() : 0}`;
|
|
146
262
|
}
|
|
147
263
|
|
|
148
|
-
// src/
|
|
264
|
+
// src/enroll.ts
|
|
149
265
|
var DEFAULT_API_BASE = "https://api.dexter.cash";
|
|
266
|
+
var DEFAULT_RP_ID = "dexter.cash";
|
|
267
|
+
var DEFAULT_WALLET_NAME = "Dexter Wallet";
|
|
268
|
+
async function createWallet(config = {}) {
|
|
269
|
+
if (shouldUsePopup(config.transport)) {
|
|
270
|
+
return openCeremonyPopup("create", {
|
|
271
|
+
connectHost: config.connectHost,
|
|
272
|
+
name: config.name,
|
|
273
|
+
apiBase: config.apiBase
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (typeof navigator === "undefined" || !navigator.credentials) {
|
|
277
|
+
throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
|
|
278
|
+
}
|
|
279
|
+
const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
280
|
+
const rpId = config.rpId ?? DEFAULT_RP_ID;
|
|
281
|
+
const name = config.name && config.name.trim() || DEFAULT_WALLET_NAME;
|
|
282
|
+
config.onPhase?.("challenge");
|
|
283
|
+
const options = await fetchEnrollChallenge(apiBase);
|
|
284
|
+
config.onPhase?.("passkey");
|
|
285
|
+
const credential = await createCredential(options, name, rpId);
|
|
286
|
+
config.onPhase?.("verifying");
|
|
287
|
+
const enrolled = await submitEnrollComplete(apiBase, credential);
|
|
288
|
+
config.onPhase?.("finalizing");
|
|
289
|
+
const init = await initializeVault(apiBase, enrolled.userHandle, enrolled.credentialId);
|
|
290
|
+
setActiveHandle(enrolled.userHandle, name, enrolled.credentialId);
|
|
291
|
+
return {
|
|
292
|
+
handle: enrolled.userHandle,
|
|
293
|
+
credentialId: enrolled.credentialId,
|
|
294
|
+
vault: {
|
|
295
|
+
vaultPda: init.vaultPda,
|
|
296
|
+
swigAddress: init.swigStateAddress,
|
|
297
|
+
// FAIL SAFE: never invent a receive address — null until the server returns
|
|
298
|
+
// one (depositing to the config PDA would strand funds).
|
|
299
|
+
receiveAddress: init.receiveAddress ?? null,
|
|
300
|
+
usdcAta: null,
|
|
301
|
+
// swig not deployed yet (counterfactual pattern)
|
|
302
|
+
publicKey: enrolled.publicKey,
|
|
303
|
+
userHandle: enrolled.userHandle,
|
|
304
|
+
credentialId: enrolled.credentialId
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
async function fetchEnrollChallenge(apiBase) {
|
|
309
|
+
const res = await fetch(`${apiBase}/api/passkey-anon/enroll/challenge`, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "content-type": "application/json" },
|
|
312
|
+
body: "{}"
|
|
313
|
+
});
|
|
314
|
+
if (!res.ok) throw new ConnectError("enroll_challenge_failed", `enroll/challenge ${res.status}`);
|
|
315
|
+
const data = await res.json();
|
|
316
|
+
if (!data?.options?.challenge) {
|
|
317
|
+
throw new ConnectError("enroll_challenge_malformed", "no creation options in response");
|
|
318
|
+
}
|
|
319
|
+
return data.options;
|
|
320
|
+
}
|
|
321
|
+
async function createCredential(options, name, rpId) {
|
|
322
|
+
let credential;
|
|
323
|
+
try {
|
|
324
|
+
credential = await navigator.credentials.create({
|
|
325
|
+
publicKey: buildCreationOptions(options, name, rpId)
|
|
326
|
+
});
|
|
327
|
+
} catch (err) {
|
|
328
|
+
throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
|
|
329
|
+
}
|
|
330
|
+
if (!credential || credential.type !== "public-key") {
|
|
331
|
+
throw new ConnectError("no_credential", "authenticator returned no credential");
|
|
332
|
+
}
|
|
333
|
+
return credential;
|
|
334
|
+
}
|
|
335
|
+
async function submitEnrollComplete(apiBase, credential) {
|
|
336
|
+
const attestation = credential.response;
|
|
337
|
+
const credentialJson = {
|
|
338
|
+
id: credential.id,
|
|
339
|
+
rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
|
|
340
|
+
type: credential.type,
|
|
341
|
+
response: {
|
|
342
|
+
attestationObject: bytesToBase64url(new Uint8Array(attestation.attestationObject)),
|
|
343
|
+
clientDataJSON: bytesToBase64url(new Uint8Array(attestation.clientDataJSON)),
|
|
344
|
+
transports: typeof attestation.getTransports === "function" ? attestation.getTransports() : []
|
|
345
|
+
},
|
|
346
|
+
clientExtensionResults: credential.getClientExtensionResults?.() ?? {},
|
|
347
|
+
authenticatorAttachment: credential.authenticatorAttachment ?? null
|
|
348
|
+
};
|
|
349
|
+
const res = await fetch(`${apiBase}/api/passkey-anon/enroll/complete`, {
|
|
350
|
+
method: "POST",
|
|
351
|
+
headers: { "content-type": "application/json" },
|
|
352
|
+
body: JSON.stringify({ credential: credentialJson })
|
|
353
|
+
});
|
|
354
|
+
if (!res.ok) throw new ConnectError(await readErrorCode(res), `enroll/complete ${res.status}`);
|
|
355
|
+
return await res.json();
|
|
356
|
+
}
|
|
357
|
+
async function initializeVault(apiBase, userHandle, credentialId) {
|
|
358
|
+
const res = await fetch(`${apiBase}/api/passkey-vault-anon/initialize`, {
|
|
359
|
+
method: "POST",
|
|
360
|
+
headers: { "content-type": "application/json" },
|
|
361
|
+
body: JSON.stringify({ userHandle, credentialId, coolingOffSeconds: 0 })
|
|
362
|
+
});
|
|
363
|
+
if (!res.ok) throw new ConnectError(await readErrorCode(res), `initialize ${res.status}`);
|
|
364
|
+
return await res.json();
|
|
365
|
+
}
|
|
366
|
+
function toBuf(b64url) {
|
|
367
|
+
return base64urlToBytes(b64url).buffer.slice(0);
|
|
368
|
+
}
|
|
369
|
+
function buildCreationOptions(o, name, rpId) {
|
|
370
|
+
return {
|
|
371
|
+
// rp.name = the site shown in the keychain; user.name/displayName = the
|
|
372
|
+
// wallet label the user sees. We override the server's user.name (a raw,
|
|
373
|
+
// unreadable handle) with the chosen wallet name.
|
|
374
|
+
rp: { id: o.rp.id ?? rpId, name: "Dexter" },
|
|
375
|
+
user: {
|
|
376
|
+
id: toBuf(o.user.id),
|
|
377
|
+
name,
|
|
378
|
+
displayName: name
|
|
379
|
+
},
|
|
380
|
+
challenge: toBuf(o.challenge),
|
|
381
|
+
pubKeyCredParams: o.pubKeyCredParams,
|
|
382
|
+
timeout: o.timeout,
|
|
383
|
+
excludeCredentials: o.excludeCredentials?.map((c) => ({
|
|
384
|
+
id: toBuf(c.id),
|
|
385
|
+
type: c.type,
|
|
386
|
+
transports: c.transports
|
|
387
|
+
})),
|
|
388
|
+
authenticatorSelection: o.authenticatorSelection,
|
|
389
|
+
attestation: o.attestation
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
async function readErrorCode(res) {
|
|
393
|
+
try {
|
|
394
|
+
const body = await res.json();
|
|
395
|
+
if (body?.error) return body.error;
|
|
396
|
+
} catch {
|
|
397
|
+
}
|
|
398
|
+
return `http_${res.status}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/relay.ts
|
|
402
|
+
var DEFAULT_API_BASE2 = "https://api.dexter.cash";
|
|
150
403
|
var ANON_SIGN_BASE = "/api/passkey-anon/sign";
|
|
151
404
|
async function passkeyLogin(config = {}, onPhase) {
|
|
152
405
|
if (shouldUsePopup(config.transport)) {
|
|
@@ -155,7 +408,7 @@ async function passkeyLogin(config = {}, onPhase) {
|
|
|
155
408
|
apiBase: config.apiBase
|
|
156
409
|
});
|
|
157
410
|
}
|
|
158
|
-
const apiBase = (config.apiBase ??
|
|
411
|
+
const apiBase = (config.apiBase ?? DEFAULT_API_BASE2).replace(/\/$/, "");
|
|
159
412
|
onPhase?.("challenge");
|
|
160
413
|
const options = await fetchLoginChallenge(apiBase);
|
|
161
414
|
onPhase?.("passkey");
|
|
@@ -163,6 +416,21 @@ async function passkeyLogin(config = {}, onPhase) {
|
|
|
163
416
|
onPhase?.("verifying");
|
|
164
417
|
return submitLogin(apiBase, credential);
|
|
165
418
|
}
|
|
419
|
+
async function continueWithDexter(config = {}, onPhase) {
|
|
420
|
+
if (shouldUsePopup(config.transport)) {
|
|
421
|
+
return openCeremonyPopup("continue", {
|
|
422
|
+
connectHost: config.connectHost,
|
|
423
|
+
name: config.name,
|
|
424
|
+
apiBase: config.apiBase
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
if (getActiveHandle()) {
|
|
428
|
+
const result2 = await passkeyLogin(config, onPhase);
|
|
429
|
+
return { kind: "signin", ...result2 };
|
|
430
|
+
}
|
|
431
|
+
const result = await createWallet({ ...config, onPhase });
|
|
432
|
+
return { kind: "create", ...result };
|
|
433
|
+
}
|
|
166
434
|
async function fetchLoginChallenge(apiBase) {
|
|
167
435
|
const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/login-challenge`, {
|
|
168
436
|
method: "POST",
|
|
@@ -221,7 +489,7 @@ async function submitLogin(apiBase, credential) {
|
|
|
221
489
|
body: JSON.stringify({ credential: credentialJson })
|
|
222
490
|
});
|
|
223
491
|
if (!res.ok) {
|
|
224
|
-
throw new ConnectError(await
|
|
492
|
+
throw new ConnectError(await readErrorCode2(res), `passkey-login ${res.status}`);
|
|
225
493
|
}
|
|
226
494
|
const data = await res.json();
|
|
227
495
|
const session = {
|
|
@@ -233,7 +501,7 @@ async function submitLogin(apiBase, credential) {
|
|
|
233
501
|
};
|
|
234
502
|
return data.vault ? { session, vault: data.vault } : { session };
|
|
235
503
|
}
|
|
236
|
-
async function
|
|
504
|
+
async function readErrorCode2(res) {
|
|
237
505
|
try {
|
|
238
506
|
const body = await res.json();
|
|
239
507
|
if (body?.error) return body.error;
|
|
@@ -243,9 +511,9 @@ async function readErrorCode(res) {
|
|
|
243
511
|
}
|
|
244
512
|
|
|
245
513
|
// src/anon-policy.ts
|
|
246
|
-
var
|
|
514
|
+
var DEFAULT_API_BASE3 = "https://api.dexter.cash";
|
|
247
515
|
var ANON_SIGN_BASE2 = "/api/passkey-anon/sign";
|
|
248
|
-
function createAnonServerPolicy(apiBase =
|
|
516
|
+
function createAnonServerPolicy(apiBase = DEFAULT_API_BASE3) {
|
|
249
517
|
const base = apiBase.replace(/\/$/, "");
|
|
250
518
|
return {
|
|
251
519
|
async issueChallenge({ userHandle, operationHash }) {
|
|
@@ -257,7 +525,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
|
|
|
257
525
|
operationHash: bytesToBase64url(operationHash)
|
|
258
526
|
})
|
|
259
527
|
});
|
|
260
|
-
if (!res.ok) throw new ConnectError(await
|
|
528
|
+
if (!res.ok) throw new ConnectError(await readErrorCode3(res), `challenge ${res.status}`);
|
|
261
529
|
const data = await res.json();
|
|
262
530
|
const options = data?.options;
|
|
263
531
|
if (!options?.challenge) {
|
|
@@ -294,7 +562,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
|
|
|
294
562
|
headers: { "content-type": "application/json" },
|
|
295
563
|
body: JSON.stringify({ credential, userHandle: bytesToBase64url(userHandle) })
|
|
296
564
|
});
|
|
297
|
-
if (!res.ok) throw new ConnectError(await
|
|
565
|
+
if (!res.ok) throw new ConnectError(await readErrorCode3(res), `verify ${res.status}`);
|
|
298
566
|
const data = await res.json();
|
|
299
567
|
if (data?.verified === false) {
|
|
300
568
|
throw new ConnectError("verification_failed", "server returned verified=false");
|
|
@@ -302,7 +570,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
|
|
|
302
570
|
}
|
|
303
571
|
};
|
|
304
572
|
}
|
|
305
|
-
async function
|
|
573
|
+
async function readErrorCode3(res) {
|
|
306
574
|
try {
|
|
307
575
|
const body = await res.json();
|
|
308
576
|
if (body?.error) return body.error;
|
|
@@ -336,122 +604,6 @@ function resolveIdentity(input) {
|
|
|
336
604
|
return { kind, userHandle, accountToken, hasPasskeyVault, hasAccount, hasWallet };
|
|
337
605
|
}
|
|
338
606
|
|
|
339
|
-
// src/walletStore.ts
|
|
340
|
-
var ACTIVE_HANDLE_KEY = "dexter:passkey:userHandle";
|
|
341
|
-
var ROSTER_KEY = "dexter:passkey:wallets";
|
|
342
|
-
var listeners = /* @__PURE__ */ new Set();
|
|
343
|
-
function hasStorage() {
|
|
344
|
-
try {
|
|
345
|
-
return typeof window !== "undefined" && !!window.localStorage;
|
|
346
|
-
} catch {
|
|
347
|
-
return false;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
function readRoster() {
|
|
351
|
-
if (!hasStorage()) return [];
|
|
352
|
-
try {
|
|
353
|
-
const raw = window.localStorage.getItem(ROSTER_KEY);
|
|
354
|
-
if (!raw) return [];
|
|
355
|
-
const parsed = JSON.parse(raw);
|
|
356
|
-
if (!Array.isArray(parsed)) return [];
|
|
357
|
-
return parsed.filter(
|
|
358
|
-
(w) => !!w && typeof w === "object" && typeof w.handle === "string"
|
|
359
|
-
);
|
|
360
|
-
} catch {
|
|
361
|
-
return [];
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
function writeRoster(wallets) {
|
|
365
|
-
if (!hasStorage()) return;
|
|
366
|
-
try {
|
|
367
|
-
window.localStorage.setItem(ROSTER_KEY, JSON.stringify(wallets));
|
|
368
|
-
} catch {
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function emit() {
|
|
372
|
-
for (const l of listeners) {
|
|
373
|
-
try {
|
|
374
|
-
l();
|
|
375
|
-
} catch {
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
function getActiveHandle() {
|
|
380
|
-
if (!hasStorage()) return null;
|
|
381
|
-
try {
|
|
382
|
-
return window.localStorage.getItem(ACTIVE_HANDLE_KEY);
|
|
383
|
-
} catch {
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
function setActiveHandle(handle, label, credentialId) {
|
|
388
|
-
if (!hasStorage() || !handle) return;
|
|
389
|
-
try {
|
|
390
|
-
window.localStorage.setItem(ACTIVE_HANDLE_KEY, handle);
|
|
391
|
-
} catch {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
const roster = readRoster();
|
|
395
|
-
const existing = roster.find((w) => w.handle === handle);
|
|
396
|
-
const now = Date.now();
|
|
397
|
-
if (existing) {
|
|
398
|
-
existing.lastUsedAt = now;
|
|
399
|
-
if (label !== void 0) existing.label = label;
|
|
400
|
-
if (credentialId !== void 0) existing.credentialId = credentialId;
|
|
401
|
-
} else {
|
|
402
|
-
roster.push({ handle, label, credentialId, lastUsedAt: now });
|
|
403
|
-
}
|
|
404
|
-
writeRoster(roster);
|
|
405
|
-
emit();
|
|
406
|
-
}
|
|
407
|
-
function getCredentialId(handle) {
|
|
408
|
-
return readRoster().find((w) => w.handle === handle)?.credentialId;
|
|
409
|
-
}
|
|
410
|
-
function ejectActiveWallet(opts) {
|
|
411
|
-
if (!hasStorage()) return;
|
|
412
|
-
const current = getActiveHandle();
|
|
413
|
-
try {
|
|
414
|
-
window.localStorage.removeItem(ACTIVE_HANDLE_KEY);
|
|
415
|
-
} catch {
|
|
416
|
-
}
|
|
417
|
-
if (opts?.forget && current) {
|
|
418
|
-
writeRoster(readRoster().filter((w) => w.handle !== current));
|
|
419
|
-
}
|
|
420
|
-
emit();
|
|
421
|
-
}
|
|
422
|
-
function listWallets() {
|
|
423
|
-
return readRoster().sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0));
|
|
424
|
-
}
|
|
425
|
-
function switchWallet(handle) {
|
|
426
|
-
if (!readRoster().some((w) => w.handle === handle)) return false;
|
|
427
|
-
setActiveHandle(handle);
|
|
428
|
-
return true;
|
|
429
|
-
}
|
|
430
|
-
function forgetWallet(handle) {
|
|
431
|
-
if (getActiveHandle() === handle) {
|
|
432
|
-
ejectActiveWallet({ forget: true });
|
|
433
|
-
return;
|
|
434
|
-
}
|
|
435
|
-
writeRoster(readRoster().filter((w) => w.handle !== handle));
|
|
436
|
-
emit();
|
|
437
|
-
}
|
|
438
|
-
function subscribe(listener) {
|
|
439
|
-
listeners.add(listener);
|
|
440
|
-
if (hasStorage() && listeners.size === 1) {
|
|
441
|
-
window.addEventListener("storage", onStorageEvent);
|
|
442
|
-
}
|
|
443
|
-
return () => {
|
|
444
|
-
listeners.delete(listener);
|
|
445
|
-
if (hasStorage() && listeners.size === 0) {
|
|
446
|
-
window.removeEventListener("storage", onStorageEvent);
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
function onStorageEvent(e) {
|
|
451
|
-
if (e.key === ACTIVE_HANDLE_KEY || e.key === ROSTER_KEY || e.key === null) emit();
|
|
452
|
-
}
|
|
453
|
-
var ACTIVE_WALLET_STORAGE_KEY = ACTIVE_HANDLE_KEY;
|
|
454
|
-
|
|
455
607
|
// src/phase.ts
|
|
456
608
|
var PHASE_LABEL = {
|
|
457
609
|
challenge: "Preparing\u2026",
|
|
@@ -525,14 +677,6 @@ async function syncAcceptedPasskeys(args) {
|
|
|
525
677
|
|
|
526
678
|
export {
|
|
527
679
|
ConnectError,
|
|
528
|
-
base64urlToBytes,
|
|
529
|
-
bytesToBase64url,
|
|
530
|
-
shouldUsePopup,
|
|
531
|
-
openCeremonyPopup,
|
|
532
|
-
passkeyLogin,
|
|
533
|
-
createAnonServerPolicy,
|
|
534
|
-
createPasskeySigner,
|
|
535
|
-
resolveIdentity,
|
|
536
680
|
getActiveHandle,
|
|
537
681
|
setActiveHandle,
|
|
538
682
|
getCredentialId,
|
|
@@ -542,6 +686,12 @@ export {
|
|
|
542
686
|
forgetWallet,
|
|
543
687
|
subscribe,
|
|
544
688
|
ACTIVE_WALLET_STORAGE_KEY,
|
|
689
|
+
createWallet,
|
|
690
|
+
passkeyLogin,
|
|
691
|
+
continueWithDexter,
|
|
692
|
+
createAnonServerPolicy,
|
|
693
|
+
createPasskeySigner,
|
|
694
|
+
resolveIdentity,
|
|
545
695
|
ceremonyPhaseLabel,
|
|
546
696
|
passkeySignalSupport,
|
|
547
697
|
renamePasskey,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
|
-
import { D as DexterConnectConfig,
|
|
2
|
-
export { A as ACTIVE_WALLET_STORAGE_KEY, b as ConnectError, I as IdentityInput, c as IdentityKind, P as PasskeyLoginTokens, d as PasskeySignalSupport, R as ResolvedIdentity, e as StoredWallet, f as ejectActiveWallet, g as forgetWallet, h as getActiveHandle, i as getCredentialId, l as listWallets, p as passkeySignalSupport, j as prunePasskey, r as renamePasskey, k as resolveIdentity, s as setActiveHandle, m as subscribeWallet, n as switchWallet, o as syncAcceptedPasskeys } from './signals-
|
|
1
|
+
import { C as ConnectVault, D as DexterConnectConfig, a as CeremonyPhase, S as SignInResult } from './signals-BuP-7inZ.js';
|
|
2
|
+
export { A as ACTIVE_WALLET_STORAGE_KEY, b as ConnectError, I as IdentityInput, c as IdentityKind, P as PasskeyLoginTokens, d as PasskeySignalSupport, R as ResolvedIdentity, e as StoredWallet, f as ejectActiveWallet, g as forgetWallet, h as getActiveHandle, i as getCredentialId, l as listWallets, p as passkeySignalSupport, j as prunePasskey, r as renamePasskey, k as resolveIdentity, s as setActiveHandle, m as subscribeWallet, n as switchWallet, o as syncAcceptedPasskeys } from './signals-BuP-7inZ.js';
|
|
3
3
|
import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
|
|
4
4
|
|
|
5
|
+
interface CreateWalletConfig extends DexterConnectConfig {
|
|
6
|
+
/** Label for the passkey in the OS keychain AND the wallet roster. Set at
|
|
7
|
+
* creation — the only moment naming is guaranteed to stick. Default "Dexter Wallet". */
|
|
8
|
+
name?: string;
|
|
9
|
+
/** RP id for the new credential. Default "dexter.cash". */
|
|
10
|
+
rpId?: string;
|
|
11
|
+
/** Called as the ceremony progresses, for live "connecting steps" UI:
|
|
12
|
+
* challenge → passkey → verifying → finalizing. */
|
|
13
|
+
onPhase?: (phase: CeremonyPhase) => void;
|
|
14
|
+
}
|
|
15
|
+
interface CreateWalletResult {
|
|
16
|
+
/** Server-minted 16-byte user handle, base64url — the vault identity. */
|
|
17
|
+
handle: string;
|
|
18
|
+
/** base64url credential id of the new passkey. */
|
|
19
|
+
credentialId: string;
|
|
20
|
+
/** The freshly initialized vault (swig not yet deployed; deploys lazily). */
|
|
21
|
+
vault: ConnectVault;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Mint a brand-new Dexter wallet (passkey + vault) and make it the active wallet.
|
|
25
|
+
*
|
|
26
|
+
* One passkey approval. Throws ConnectError on any failed leg (the `code` is the
|
|
27
|
+
* server's error string, or webauthn_failed / no_credential for the ceremony).
|
|
28
|
+
*/
|
|
29
|
+
declare function createWallet(config?: CreateWalletConfig): Promise<CreateWalletResult>;
|
|
30
|
+
|
|
5
31
|
/**
|
|
6
32
|
* "Sign in with Dexter" — the discoverable-credential login ceremony.
|
|
7
33
|
*
|
|
@@ -16,6 +42,12 @@ import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
|
|
|
16
42
|
* Supabase session, so the Supabase-gated router would 401.
|
|
17
43
|
*/
|
|
18
44
|
declare function passkeyLogin(config?: DexterConnectConfig, onPhase?: (phase: CeremonyPhase) => void): Promise<SignInResult>;
|
|
45
|
+
type ContinueResult = ({
|
|
46
|
+
kind: 'signin';
|
|
47
|
+
} & SignInResult) | ({
|
|
48
|
+
kind: 'create';
|
|
49
|
+
} & CreateWalletResult);
|
|
50
|
+
declare function continueWithDexter(config?: CreateWalletConfig, onPhase?: (phase: CeremonyPhase) => void): Promise<ContinueResult>;
|
|
19
51
|
|
|
20
52
|
/** What `issueChallenge` returns to the SDK signer. */
|
|
21
53
|
interface AnonChallengeResult {
|
|
@@ -76,32 +108,6 @@ declare function createPasskeySigner(vault: ConnectVault, apiBase?: string, opts
|
|
|
76
108
|
__assertion?: AssertionLike;
|
|
77
109
|
}): DexterApiBrowserPasskeySigner;
|
|
78
110
|
|
|
79
|
-
interface CreateWalletConfig extends DexterConnectConfig {
|
|
80
|
-
/** Label for the passkey in the OS keychain AND the wallet roster. Set at
|
|
81
|
-
* creation — the only moment naming is guaranteed to stick. Default "Dexter Wallet". */
|
|
82
|
-
name?: string;
|
|
83
|
-
/** RP id for the new credential. Default "dexter.cash". */
|
|
84
|
-
rpId?: string;
|
|
85
|
-
/** Called as the ceremony progresses, for live "connecting steps" UI:
|
|
86
|
-
* challenge → passkey → verifying → finalizing. */
|
|
87
|
-
onPhase?: (phase: CeremonyPhase) => void;
|
|
88
|
-
}
|
|
89
|
-
interface CreateWalletResult {
|
|
90
|
-
/** Server-minted 16-byte user handle, base64url — the vault identity. */
|
|
91
|
-
handle: string;
|
|
92
|
-
/** base64url credential id of the new passkey. */
|
|
93
|
-
credentialId: string;
|
|
94
|
-
/** The freshly initialized vault (swig not yet deployed; deploys lazily). */
|
|
95
|
-
vault: ConnectVault;
|
|
96
|
-
}
|
|
97
|
-
/**
|
|
98
|
-
* Mint a brand-new Dexter wallet (passkey + vault) and make it the active wallet.
|
|
99
|
-
*
|
|
100
|
-
* One passkey approval. Throws ConnectError on any failed leg (the `code` is the
|
|
101
|
-
* server's error string, or webauthn_failed / no_credential for the ceremony).
|
|
102
|
-
*/
|
|
103
|
-
declare function createWallet(config?: CreateWalletConfig): Promise<CreateWalletResult>;
|
|
104
|
-
|
|
105
111
|
/**
|
|
106
112
|
* Human-readable label for a ceremony phase — the live "connecting step" copy.
|
|
107
113
|
* ONE source of truth so sign-in (SignInWithDexter) and create (consumer setup
|
|
@@ -110,4 +116,4 @@ declare function createWallet(config?: CreateWalletConfig): Promise<CreateWallet
|
|
|
110
116
|
*/
|
|
111
117
|
declare function ceremonyPhaseLabel(phase: CeremonyPhase): string;
|
|
112
118
|
|
|
113
|
-
export { type AnonChallengeResult, type AnonServerPolicy, CeremonyPhase, ConnectVault, type CreateWalletConfig, type CreateWalletResult, DexterConnectConfig, SignInResult, ceremonyPhaseLabel, createAnonServerPolicy, createPasskeySigner, createWallet, passkeyLogin };
|
|
119
|
+
export { type AnonChallengeResult, type AnonServerPolicy, CeremonyPhase, ConnectVault, type ContinueResult, type CreateWalletConfig, type CreateWalletResult, DexterConnectConfig, SignInResult, ceremonyPhaseLabel, continueWithDexter, createAnonServerPolicy, createPasskeySigner, createWallet, passkeyLogin };
|
package/dist/index.js
CHANGED
|
@@ -1,169 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ACTIVE_WALLET_STORAGE_KEY,
|
|
3
3
|
ConnectError,
|
|
4
|
-
base64urlToBytes,
|
|
5
|
-
bytesToBase64url,
|
|
6
4
|
ceremonyPhaseLabel,
|
|
5
|
+
continueWithDexter,
|
|
7
6
|
createAnonServerPolicy,
|
|
8
7
|
createPasskeySigner,
|
|
8
|
+
createWallet,
|
|
9
9
|
ejectActiveWallet,
|
|
10
10
|
forgetWallet,
|
|
11
11
|
getActiveHandle,
|
|
12
12
|
getCredentialId,
|
|
13
13
|
listWallets,
|
|
14
|
-
openCeremonyPopup,
|
|
15
14
|
passkeyLogin,
|
|
16
15
|
passkeySignalSupport,
|
|
17
16
|
prunePasskey,
|
|
18
17
|
renamePasskey,
|
|
19
18
|
resolveIdentity,
|
|
20
19
|
setActiveHandle,
|
|
21
|
-
shouldUsePopup,
|
|
22
20
|
subscribe,
|
|
23
21
|
switchWallet,
|
|
24
22
|
syncAcceptedPasskeys
|
|
25
|
-
} from "./chunk-
|
|
26
|
-
|
|
27
|
-
// src/enroll.ts
|
|
28
|
-
var DEFAULT_API_BASE = "https://api.dexter.cash";
|
|
29
|
-
var DEFAULT_RP_ID = "dexter.cash";
|
|
30
|
-
var DEFAULT_WALLET_NAME = "Dexter Wallet";
|
|
31
|
-
async function createWallet(config = {}) {
|
|
32
|
-
if (shouldUsePopup(config.transport)) {
|
|
33
|
-
return openCeremonyPopup("create", {
|
|
34
|
-
connectHost: config.connectHost,
|
|
35
|
-
name: config.name,
|
|
36
|
-
apiBase: config.apiBase
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
if (typeof navigator === "undefined" || !navigator.credentials) {
|
|
40
|
-
throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
|
|
41
|
-
}
|
|
42
|
-
const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
43
|
-
const rpId = config.rpId ?? DEFAULT_RP_ID;
|
|
44
|
-
const name = config.name && config.name.trim() || DEFAULT_WALLET_NAME;
|
|
45
|
-
config.onPhase?.("challenge");
|
|
46
|
-
const options = await fetchEnrollChallenge(apiBase);
|
|
47
|
-
config.onPhase?.("passkey");
|
|
48
|
-
const credential = await createCredential(options, name, rpId);
|
|
49
|
-
config.onPhase?.("verifying");
|
|
50
|
-
const enrolled = await submitEnrollComplete(apiBase, credential);
|
|
51
|
-
config.onPhase?.("finalizing");
|
|
52
|
-
const init = await initializeVault(apiBase, enrolled.userHandle, enrolled.credentialId);
|
|
53
|
-
setActiveHandle(enrolled.userHandle, name, enrolled.credentialId);
|
|
54
|
-
return {
|
|
55
|
-
handle: enrolled.userHandle,
|
|
56
|
-
credentialId: enrolled.credentialId,
|
|
57
|
-
vault: {
|
|
58
|
-
vaultPda: init.vaultPda,
|
|
59
|
-
swigAddress: init.swigStateAddress,
|
|
60
|
-
// FAIL SAFE: never invent a receive address — null until the server returns
|
|
61
|
-
// one (depositing to the config PDA would strand funds).
|
|
62
|
-
receiveAddress: init.receiveAddress ?? null,
|
|
63
|
-
usdcAta: null,
|
|
64
|
-
// swig not deployed yet (counterfactual pattern)
|
|
65
|
-
publicKey: enrolled.publicKey,
|
|
66
|
-
userHandle: enrolled.userHandle,
|
|
67
|
-
credentialId: enrolled.credentialId
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
async function fetchEnrollChallenge(apiBase) {
|
|
72
|
-
const res = await fetch(`${apiBase}/api/passkey-anon/enroll/challenge`, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: { "content-type": "application/json" },
|
|
75
|
-
body: "{}"
|
|
76
|
-
});
|
|
77
|
-
if (!res.ok) throw new ConnectError("enroll_challenge_failed", `enroll/challenge ${res.status}`);
|
|
78
|
-
const data = await res.json();
|
|
79
|
-
if (!data?.options?.challenge) {
|
|
80
|
-
throw new ConnectError("enroll_challenge_malformed", "no creation options in response");
|
|
81
|
-
}
|
|
82
|
-
return data.options;
|
|
83
|
-
}
|
|
84
|
-
async function createCredential(options, name, rpId) {
|
|
85
|
-
let credential;
|
|
86
|
-
try {
|
|
87
|
-
credential = await navigator.credentials.create({
|
|
88
|
-
publicKey: buildCreationOptions(options, name, rpId)
|
|
89
|
-
});
|
|
90
|
-
} catch (err) {
|
|
91
|
-
throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
|
|
92
|
-
}
|
|
93
|
-
if (!credential || credential.type !== "public-key") {
|
|
94
|
-
throw new ConnectError("no_credential", "authenticator returned no credential");
|
|
95
|
-
}
|
|
96
|
-
return credential;
|
|
97
|
-
}
|
|
98
|
-
async function submitEnrollComplete(apiBase, credential) {
|
|
99
|
-
const attestation = credential.response;
|
|
100
|
-
const credentialJson = {
|
|
101
|
-
id: credential.id,
|
|
102
|
-
rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
|
|
103
|
-
type: credential.type,
|
|
104
|
-
response: {
|
|
105
|
-
attestationObject: bytesToBase64url(new Uint8Array(attestation.attestationObject)),
|
|
106
|
-
clientDataJSON: bytesToBase64url(new Uint8Array(attestation.clientDataJSON)),
|
|
107
|
-
transports: typeof attestation.getTransports === "function" ? attestation.getTransports() : []
|
|
108
|
-
},
|
|
109
|
-
clientExtensionResults: credential.getClientExtensionResults?.() ?? {},
|
|
110
|
-
authenticatorAttachment: credential.authenticatorAttachment ?? null
|
|
111
|
-
};
|
|
112
|
-
const res = await fetch(`${apiBase}/api/passkey-anon/enroll/complete`, {
|
|
113
|
-
method: "POST",
|
|
114
|
-
headers: { "content-type": "application/json" },
|
|
115
|
-
body: JSON.stringify({ credential: credentialJson })
|
|
116
|
-
});
|
|
117
|
-
if (!res.ok) throw new ConnectError(await readErrorCode(res), `enroll/complete ${res.status}`);
|
|
118
|
-
return await res.json();
|
|
119
|
-
}
|
|
120
|
-
async function initializeVault(apiBase, userHandle, credentialId) {
|
|
121
|
-
const res = await fetch(`${apiBase}/api/passkey-vault-anon/initialize`, {
|
|
122
|
-
method: "POST",
|
|
123
|
-
headers: { "content-type": "application/json" },
|
|
124
|
-
body: JSON.stringify({ userHandle, credentialId, coolingOffSeconds: 0 })
|
|
125
|
-
});
|
|
126
|
-
if (!res.ok) throw new ConnectError(await readErrorCode(res), `initialize ${res.status}`);
|
|
127
|
-
return await res.json();
|
|
128
|
-
}
|
|
129
|
-
function toBuf(b64url) {
|
|
130
|
-
return base64urlToBytes(b64url).buffer.slice(0);
|
|
131
|
-
}
|
|
132
|
-
function buildCreationOptions(o, name, rpId) {
|
|
133
|
-
return {
|
|
134
|
-
// rp.name = the site shown in the keychain; user.name/displayName = the
|
|
135
|
-
// wallet label the user sees. We override the server's user.name (a raw,
|
|
136
|
-
// unreadable handle) with the chosen wallet name.
|
|
137
|
-
rp: { id: o.rp.id ?? rpId, name: "Dexter" },
|
|
138
|
-
user: {
|
|
139
|
-
id: toBuf(o.user.id),
|
|
140
|
-
name,
|
|
141
|
-
displayName: name
|
|
142
|
-
},
|
|
143
|
-
challenge: toBuf(o.challenge),
|
|
144
|
-
pubKeyCredParams: o.pubKeyCredParams,
|
|
145
|
-
timeout: o.timeout,
|
|
146
|
-
excludeCredentials: o.excludeCredentials?.map((c) => ({
|
|
147
|
-
id: toBuf(c.id),
|
|
148
|
-
type: c.type,
|
|
149
|
-
transports: c.transports
|
|
150
|
-
})),
|
|
151
|
-
authenticatorSelection: o.authenticatorSelection,
|
|
152
|
-
attestation: o.attestation
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
async function readErrorCode(res) {
|
|
156
|
-
try {
|
|
157
|
-
const body = await res.json();
|
|
158
|
-
if (body?.error) return body.error;
|
|
159
|
-
} catch {
|
|
160
|
-
}
|
|
161
|
-
return `http_${res.status}`;
|
|
162
|
-
}
|
|
23
|
+
} from "./chunk-IUZBHWTP.js";
|
|
163
24
|
export {
|
|
164
25
|
ACTIVE_WALLET_STORAGE_KEY,
|
|
165
26
|
ConnectError,
|
|
166
27
|
ceremonyPhaseLabel,
|
|
28
|
+
continueWithDexter,
|
|
167
29
|
createAnonServerPolicy,
|
|
168
30
|
createPasskeySigner,
|
|
169
31
|
createWallet,
|
package/dist/react.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
|
|
2
|
-
import {
|
|
2
|
+
import { a as CeremonyPhase, S as SignInResult, P as PasskeyLoginTokens, C as ConnectVault, b as ConnectError, e as StoredWallet, d as PasskeySignalSupport, R as ResolvedIdentity } from './signals-BuP-7inZ.js';
|
|
3
3
|
import { ReactElement, ReactNode } from 'react';
|
|
4
4
|
|
|
5
5
|
type ConnectStatus = 'idle' | 'pending' | 'done' | 'error';
|
package/dist/react.js
CHANGED
|
@@ -180,4 +180,4 @@ declare function syncAcceptedPasskeys(args: {
|
|
|
180
180
|
rpId?: string;
|
|
181
181
|
}): Promise<boolean>;
|
|
182
182
|
|
|
183
|
-
export { ACTIVE_WALLET_STORAGE_KEY as A, type
|
|
183
|
+
export { ACTIVE_WALLET_STORAGE_KEY as A, type ConnectVault as C, type DexterConnectConfig as D, type IdentityInput as I, type PasskeyLoginTokens as P, type ResolvedIdentity as R, type SignInResult as S, type CeremonyPhase as a, ConnectError as b, type IdentityKind as c, type PasskeySignalSupport as d, type StoredWallet as e, ejectActiveWallet as f, forgetWallet as g, getActiveHandle as h, getCredentialId as i, prunePasskey as j, resolveIdentity as k, listWallets as l, subscribe as m, switchWallet as n, syncAcceptedPasskeys as o, passkeySignalSupport as p, renamePasskey as r, setActiveHandle as s };
|