@dexterai/connect 0.13.0 → 0.15.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.
@@ -8,57 +8,124 @@ var ConnectError = class extends Error {
8
8
  }
9
9
  };
10
10
 
11
- // src/base64.ts
12
- function base64ToBytes(s) {
13
- const bin = atob(s);
14
- const out = new Uint8Array(bin.length);
15
- for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
16
- return out;
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
+ }
17
21
  }
18
- function base64urlToBytes(s) {
19
- const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
20
- const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
21
- return base64ToBytes(b64);
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
+ }
22
35
  }
23
- function bytesToBase64(bytes) {
24
- let bin = "";
25
- for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
26
- return btoa(bin);
36
+ function writeRoster(wallets) {
37
+ if (!hasStorage()) return;
38
+ try {
39
+ window.localStorage.setItem(ROSTER_KEY, JSON.stringify(wallets));
40
+ } catch {
41
+ }
27
42
  }
28
- function bytesToBase64url(bytes) {
29
- return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
43
+ function emit() {
44
+ for (const l of listeners) {
45
+ try {
46
+ l();
47
+ } catch {
48
+ }
49
+ }
30
50
  }
31
- function compactSignatureToDer(compact) {
32
- if (compact.length !== 64) {
33
- throw new Error(`expected 64-byte compact signature, got ${compact.length}`);
51
+ function getActiveHandle() {
52
+ if (!hasStorage()) return null;
53
+ try {
54
+ return window.localStorage.getItem(ACTIVE_HANDLE_KEY);
55
+ } catch {
56
+ return null;
34
57
  }
35
- const r = derInteger(compact.subarray(0, 32));
36
- const s = derInteger(compact.subarray(32, 64));
37
- const body = new Uint8Array(r.length + s.length);
38
- body.set(r, 0);
39
- body.set(s, r.length);
40
- const out = new Uint8Array(2 + body.length);
41
- out[0] = 48;
42
- out[1] = body.length;
43
- out.set(body, 2);
44
- return out;
45
58
  }
46
- function derInteger(component) {
47
- let start = 0;
48
- while (start < component.length - 1 && component[start] === 0) start += 1;
49
- let content = component.subarray(start);
50
- if (content[0] & 128) {
51
- const padded = new Uint8Array(content.length + 1);
52
- padded[0] = 0;
53
- padded.set(content, 1);
54
- content = padded;
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;
55
65
  }
56
- const out = new Uint8Array(2 + content.length);
57
- out[0] = 2;
58
- out[1] = content.length;
59
- out.set(content, 2);
60
- return out;
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));
61
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
+
127
+ // src/enroll.ts
128
+ import { startRegistration } from "@simplewebauthn/browser";
62
129
 
63
130
  // src/popup.ts
64
131
  var DEFAULT_CONNECT_HOST = "https://dexter.cash/connect";
@@ -145,8 +212,103 @@ function makeNonce() {
145
212
  return `rid-${new URL(location.href).searchParams.get("v") ?? ""}-${typeof performance !== "undefined" ? performance.now() : 0}`;
146
213
  }
147
214
 
148
- // src/relay.ts
215
+ // src/enroll.ts
149
216
  var DEFAULT_API_BASE = "https://api.dexter.cash";
217
+ var DEFAULT_RP_ID = "dexter.cash";
218
+ var DEFAULT_WALLET_NAME = "Dexter Wallet";
219
+ async function createWallet(config = {}) {
220
+ if (shouldUsePopup(config.transport)) {
221
+ return openCeremonyPopup("create", {
222
+ connectHost: config.connectHost,
223
+ name: config.name,
224
+ apiBase: config.apiBase
225
+ });
226
+ }
227
+ if (typeof navigator === "undefined" || !navigator.credentials) {
228
+ throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
229
+ }
230
+ const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
231
+ const rpId = config.rpId ?? DEFAULT_RP_ID;
232
+ const name = config.name && config.name.trim() || DEFAULT_WALLET_NAME;
233
+ config.onPhase?.("challenge");
234
+ const options = await fetchEnrollChallenge(apiBase);
235
+ config.onPhase?.("passkey");
236
+ const optionsJSON = {
237
+ ...options,
238
+ rp: { ...options.rp, id: options.rp.id ?? rpId, name: "Dexter" },
239
+ user: { ...options.user, name, displayName: name }
240
+ };
241
+ let regResponse;
242
+ try {
243
+ regResponse = await startRegistration({ optionsJSON });
244
+ } catch (err) {
245
+ throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
246
+ }
247
+ config.onPhase?.("verifying");
248
+ const enrolled = await submitEnrollComplete(apiBase, regResponse);
249
+ config.onPhase?.("finalizing");
250
+ const init = await initializeVault(apiBase, enrolled.userHandle, enrolled.credentialId);
251
+ setActiveHandle(enrolled.userHandle, name, enrolled.credentialId);
252
+ return {
253
+ handle: enrolled.userHandle,
254
+ credentialId: enrolled.credentialId,
255
+ vault: {
256
+ vaultPda: init.vaultPda,
257
+ swigAddress: init.swigStateAddress,
258
+ // FAIL SAFE: never invent a receive address — null until the server returns
259
+ // one (depositing to the config PDA would strand funds).
260
+ receiveAddress: init.receiveAddress ?? null,
261
+ usdcAta: null,
262
+ // swig not deployed yet (counterfactual pattern)
263
+ publicKey: enrolled.publicKey,
264
+ userHandle: enrolled.userHandle,
265
+ credentialId: enrolled.credentialId
266
+ }
267
+ };
268
+ }
269
+ async function fetchEnrollChallenge(apiBase) {
270
+ const res = await fetch(`${apiBase}/api/passkey-anon/enroll/challenge`, {
271
+ method: "POST",
272
+ headers: { "content-type": "application/json" },
273
+ body: "{}"
274
+ });
275
+ if (!res.ok) throw new ConnectError("enroll_challenge_failed", `enroll/challenge ${res.status}`);
276
+ const data = await res.json();
277
+ if (!data?.options?.challenge) {
278
+ throw new ConnectError("enroll_challenge_malformed", "no creation options in response");
279
+ }
280
+ return data.options;
281
+ }
282
+ async function submitEnrollComplete(apiBase, response) {
283
+ const res = await fetch(`${apiBase}/api/passkey-anon/enroll/complete`, {
284
+ method: "POST",
285
+ headers: { "content-type": "application/json" },
286
+ body: JSON.stringify({ credential: response })
287
+ });
288
+ if (!res.ok) throw new ConnectError(await readErrorCode(res), `enroll/complete ${res.status}`);
289
+ return await res.json();
290
+ }
291
+ async function initializeVault(apiBase, userHandle, credentialId) {
292
+ const res = await fetch(`${apiBase}/api/passkey-vault-anon/initialize`, {
293
+ method: "POST",
294
+ headers: { "content-type": "application/json" },
295
+ body: JSON.stringify({ userHandle, credentialId, coolingOffSeconds: 0 })
296
+ });
297
+ if (!res.ok) throw new ConnectError(await readErrorCode(res), `initialize ${res.status}`);
298
+ return await res.json();
299
+ }
300
+ async function readErrorCode(res) {
301
+ try {
302
+ const body = await res.json();
303
+ if (body?.error) return body.error;
304
+ } catch {
305
+ }
306
+ return `http_${res.status}`;
307
+ }
308
+
309
+ // src/relay.ts
310
+ import { startAuthentication, browserSupportsWebAuthn } from "@simplewebauthn/browser";
311
+ var DEFAULT_API_BASE2 = "https://api.dexter.cash";
150
312
  var ANON_SIGN_BASE = "/api/passkey-anon/sign";
151
313
  async function passkeyLogin(config = {}, onPhase) {
152
314
  if (shouldUsePopup(config.transport)) {
@@ -155,13 +317,36 @@ async function passkeyLogin(config = {}, onPhase) {
155
317
  apiBase: config.apiBase
156
318
  });
157
319
  }
158
- const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
320
+ if (!browserSupportsWebAuthn()) {
321
+ throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
322
+ }
323
+ const apiBase = (config.apiBase ?? DEFAULT_API_BASE2).replace(/\/$/, "");
159
324
  onPhase?.("challenge");
160
325
  const options = await fetchLoginChallenge(apiBase);
161
326
  onPhase?.("passkey");
162
- const credential = await getAssertion(options);
327
+ let response;
328
+ try {
329
+ response = await startAuthentication({ optionsJSON: options });
330
+ } catch (err) {
331
+ throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
332
+ }
163
333
  onPhase?.("verifying");
164
- return submitLogin(apiBase, credential);
334
+ return submitLogin(apiBase, response);
335
+ }
336
+ async function continueWithDexter(config = {}, onPhase) {
337
+ if (shouldUsePopup(config.transport)) {
338
+ return openCeremonyPopup("continue", {
339
+ connectHost: config.connectHost,
340
+ name: config.name,
341
+ apiBase: config.apiBase
342
+ });
343
+ }
344
+ if (getActiveHandle()) {
345
+ const result2 = await passkeyLogin(config, onPhase);
346
+ return { kind: "signin", ...result2 };
347
+ }
348
+ const result = await createWallet({ ...config, onPhase });
349
+ return { kind: "create", ...result };
165
350
  }
166
351
  async function fetchLoginChallenge(apiBase) {
167
352
  const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/login-challenge`, {
@@ -178,50 +363,14 @@ async function fetchLoginChallenge(apiBase) {
178
363
  }
179
364
  return data.options;
180
365
  }
181
- async function getAssertion(options) {
182
- if (typeof navigator === "undefined" || !navigator.credentials) {
183
- throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
184
- }
185
- let credential;
186
- try {
187
- credential = await navigator.credentials.get({
188
- publicKey: {
189
- challenge: base64urlToBytes(options.challenge).buffer.slice(0),
190
- rpId: options.rpId,
191
- timeout: options.timeout ?? 6e4,
192
- userVerification: options.userVerification ?? "required"
193
- // No allowCredentials — discoverable resident-key login.
194
- }
195
- });
196
- } catch (err) {
197
- throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
198
- }
199
- if (!credential || credential.type !== "public-key") {
200
- throw new ConnectError("no_credential", "WebAuthn returned no credential");
201
- }
202
- return credential;
203
- }
204
- async function submitLogin(apiBase, credential) {
205
- const assertion = credential.response;
206
- const credentialJson = {
207
- id: credential.id,
208
- rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
209
- type: credential.type,
210
- response: {
211
- clientDataJSON: bytesToBase64url(new Uint8Array(assertion.clientDataJSON)),
212
- authenticatorData: bytesToBase64url(new Uint8Array(assertion.authenticatorData)),
213
- signature: bytesToBase64url(new Uint8Array(assertion.signature)),
214
- userHandle: assertion.userHandle ? bytesToBase64url(new Uint8Array(assertion.userHandle)) : null
215
- },
216
- clientExtensionResults: credential.getClientExtensionResults?.() ?? {}
217
- };
366
+ async function submitLogin(apiBase, response) {
218
367
  const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/passkey-login`, {
219
368
  method: "POST",
220
369
  headers: { "content-type": "application/json" },
221
- body: JSON.stringify({ credential: credentialJson })
370
+ body: JSON.stringify({ credential: response })
222
371
  });
223
372
  if (!res.ok) {
224
- throw new ConnectError(await readErrorCode(res), `passkey-login ${res.status}`);
373
+ throw new ConnectError(await readErrorCode2(res), `passkey-login ${res.status}`);
225
374
  }
226
375
  const data = await res.json();
227
376
  const session = {
@@ -233,7 +382,7 @@ async function submitLogin(apiBase, credential) {
233
382
  };
234
383
  return data.vault ? { session, vault: data.vault } : { session };
235
384
  }
236
- async function readErrorCode(res) {
385
+ async function readErrorCode2(res) {
237
386
  try {
238
387
  const body = await res.json();
239
388
  if (body?.error) return body.error;
@@ -242,10 +391,62 @@ async function readErrorCode(res) {
242
391
  return `http_${res.status}`;
243
392
  }
244
393
 
394
+ // src/base64.ts
395
+ function base64ToBytes(s) {
396
+ const bin = atob(s);
397
+ const out = new Uint8Array(bin.length);
398
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
399
+ return out;
400
+ }
401
+ function base64urlToBytes(s) {
402
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
403
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
404
+ return base64ToBytes(b64);
405
+ }
406
+ function bytesToBase64(bytes) {
407
+ let bin = "";
408
+ for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
409
+ return btoa(bin);
410
+ }
411
+ function bytesToBase64url(bytes) {
412
+ return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
413
+ }
414
+ function compactSignatureToDer(compact) {
415
+ if (compact.length !== 64) {
416
+ throw new Error(`expected 64-byte compact signature, got ${compact.length}`);
417
+ }
418
+ const r = derInteger(compact.subarray(0, 32));
419
+ const s = derInteger(compact.subarray(32, 64));
420
+ const body = new Uint8Array(r.length + s.length);
421
+ body.set(r, 0);
422
+ body.set(s, r.length);
423
+ const out = new Uint8Array(2 + body.length);
424
+ out[0] = 48;
425
+ out[1] = body.length;
426
+ out.set(body, 2);
427
+ return out;
428
+ }
429
+ function derInteger(component) {
430
+ let start = 0;
431
+ while (start < component.length - 1 && component[start] === 0) start += 1;
432
+ let content = component.subarray(start);
433
+ if (content[0] & 128) {
434
+ const padded = new Uint8Array(content.length + 1);
435
+ padded[0] = 0;
436
+ padded.set(content, 1);
437
+ content = padded;
438
+ }
439
+ const out = new Uint8Array(2 + content.length);
440
+ out[0] = 2;
441
+ out[1] = content.length;
442
+ out.set(content, 2);
443
+ return out;
444
+ }
445
+
245
446
  // src/anon-policy.ts
246
- var DEFAULT_API_BASE2 = "https://api.dexter.cash";
447
+ var DEFAULT_API_BASE3 = "https://api.dexter.cash";
247
448
  var ANON_SIGN_BASE2 = "/api/passkey-anon/sign";
248
- function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
449
+ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE3) {
249
450
  const base = apiBase.replace(/\/$/, "");
250
451
  return {
251
452
  async issueChallenge({ userHandle, operationHash }) {
@@ -257,7 +458,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
257
458
  operationHash: bytesToBase64url(operationHash)
258
459
  })
259
460
  });
260
- if (!res.ok) throw new ConnectError(await readErrorCode2(res), `challenge ${res.status}`);
461
+ if (!res.ok) throw new ConnectError(await readErrorCode3(res), `challenge ${res.status}`);
261
462
  const data = await res.json();
262
463
  const options = data?.options;
263
464
  if (!options?.challenge) {
@@ -294,7 +495,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
294
495
  headers: { "content-type": "application/json" },
295
496
  body: JSON.stringify({ credential, userHandle: bytesToBase64url(userHandle) })
296
497
  });
297
- if (!res.ok) throw new ConnectError(await readErrorCode2(res), `verify ${res.status}`);
498
+ if (!res.ok) throw new ConnectError(await readErrorCode3(res), `verify ${res.status}`);
298
499
  const data = await res.json();
299
500
  if (data?.verified === false) {
300
501
  throw new ConnectError("verification_failed", "server returned verified=false");
@@ -302,7 +503,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
302
503
  }
303
504
  };
304
505
  }
305
- async function readErrorCode2(res) {
506
+ async function readErrorCode3(res) {
306
507
  try {
307
508
  const body = await res.json();
308
509
  if (body?.error) return body.error;
@@ -336,122 +537,6 @@ function resolveIdentity(input) {
336
537
  return { kind, userHandle, accountToken, hasPasskeyVault, hasAccount, hasWallet };
337
538
  }
338
539
 
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
540
  // src/phase.ts
456
541
  var PHASE_LABEL = {
457
542
  challenge: "Preparing\u2026",
@@ -525,14 +610,6 @@ async function syncAcceptedPasskeys(args) {
525
610
 
526
611
  export {
527
612
  ConnectError,
528
- base64urlToBytes,
529
- bytesToBase64url,
530
- shouldUsePopup,
531
- openCeremonyPopup,
532
- passkeyLogin,
533
- createAnonServerPolicy,
534
- createPasskeySigner,
535
- resolveIdentity,
536
613
  getActiveHandle,
537
614
  setActiveHandle,
538
615
  getCredentialId,
@@ -542,6 +619,12 @@ export {
542
619
  forgetWallet,
543
620
  subscribe,
544
621
  ACTIVE_WALLET_STORAGE_KEY,
622
+ createWallet,
623
+ passkeyLogin,
624
+ continueWithDexter,
625
+ createAnonServerPolicy,
626
+ createPasskeySigner,
627
+ resolveIdentity,
545
628
  ceremonyPhaseLabel,
546
629
  passkeySignalSupport,
547
630
  renamePasskey,
package/dist/index.d.ts CHANGED
@@ -1,7 +1,33 @@
1
- import { D as DexterConnectConfig, C as CeremonyPhase, S as SignInResult, a as ConnectVault } from './signals-DM7IQdq6.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-DM7IQdq6.js';
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-6NIGGPVI.js";
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-RLVMYLHC.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 { C as CeremonyPhase, S as SignInResult, P as PasskeyLoginTokens, a as ConnectVault, b as ConnectError, e as StoredWallet, d as PasskeySignalSupport, R as ResolvedIdentity } from './signals-DM7IQdq6.js';
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
@@ -14,7 +14,7 @@ import {
14
14
  setActiveHandle,
15
15
  subscribe,
16
16
  switchWallet
17
- } from "./chunk-6NIGGPVI.js";
17
+ } from "./chunk-RLVMYLHC.js";
18
18
 
19
19
  // src/useSignInWithDexter.ts
20
20
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -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 CeremonyPhase as C, type DexterConnectConfig as D, type IdentityInput as I, type PasskeyLoginTokens as P, type ResolvedIdentity as R, type SignInResult as S, type ConnectVault 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 };
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexterai/connect",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Sign in with Dexter — passkey connector. Composes @dexterai/vault.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -32,5 +32,8 @@
32
32
  "tsup": "^8.5.0",
33
33
  "typescript": "^5.6.2",
34
34
  "vitest": "^4.1.9"
35
+ },
36
+ "dependencies": {
37
+ "@simplewebauthn/browser": "^13.3.0"
35
38
  }
36
39
  }