@dexterai/connect 0.19.0 → 0.21.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 CHANGED
@@ -157,10 +157,42 @@ import { VerifyPersonhood } from '@dexterai/connect/worldid';
157
157
  `useVerifyPersonhood` is the headless version. Requires the optional
158
158
  `@worldcoin/idkit` peer.
159
159
 
160
+ ## Wallet-only sign-in (no account session)
161
+
162
+ `recoverWallet` re-points a browser at an existing Dexter Wallet — the wallet
163
+ IS the sign-in; nothing else is minted. Use it when your surface treats the
164
+ wallet as the identity (the dexter.cash header does exactly this):
165
+
166
+ ```ts
167
+ import { recoverWallet } from '@dexterai/connect';
168
+
169
+ const outcome = await recoverWallet({ preferImmediate: true }); // fire on TAP, never on mount
170
+ if (outcome.ok) {
171
+ // outcome.vault.swigAddress, outcome.vault.walletLabel — the store + every
172
+ // useIdentity/useDexterWallet surface is already updated.
173
+ } else if (outcome.reason === 'no_credential') {
174
+ // this device has no wallet passkey → offer your create flow
175
+ } else if (outcome.reason === 'cancelled') {
176
+ // the user dismissed the sheet → stay silent
177
+ }
178
+ ```
179
+
180
+ It returns a discriminated outcome instead of throwing: user cancel is a
181
+ normal result in WebAuthn, not an exception. `preferImmediate` uses Chrome
182
+ 149+'s immediate UI mode to fast-fail instantly when the device holds no
183
+ passkey (no empty account-picker sheet); everywhere else it falls back to the
184
+ normal modal. Works from any website — off dexter.cash the ceremony runs in
185
+ the hosted popup automatically.
186
+
187
+ React: `<SignInWithDexter mode="recover" preferImmediate onRecovered={…} />`
188
+ (after a successful recover the element renders null — show identity with
189
+ `DexterWalletChip` over `useIdentity`), or `useSignInWithDexter().recover()`.
190
+
160
191
  ## Wallet lifecycle
161
192
 
162
193
  - `createWallet` mints a brand-new named passkey + vault; `passkeyLogin` signs
163
- an existing one in; `continueWithDexter` resumes a known wallet.
194
+ an existing one in; `continueWithDexter` resumes a known wallet;
195
+ `recoverWallet` restores the wallet with no account session.
164
196
  - The **wallet store** (`getActiveHandle`, `listWallets`, `switchWallet`,
165
197
  `ejectActiveWallet`, `forgetWallet`, `subscribeWallet`) is the canonical
166
198
  owner of the active-wallet handle. Read and write through it rather than
@@ -6,22 +6,21 @@ var BUTTON_CSS = `
6
6
  @keyframes dx-spin { to { transform: rotate(360deg); } }
7
7
  @keyframes dx-pulse { 0%,100% { opacity: 1; } 50% { opacity: .6; } }
8
8
  .dx-btn{
9
- --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
10
9
  position:relative; display:inline-flex; align-items:center; justify-content:center; gap:10px;
11
- padding:11px 22px; border:1px solid color-mix(in srgb,var(--dx-ember) 55%,transparent);
12
- border-radius:var(--dx-radius);
13
- background:linear-gradient(135deg,var(--dx-ember),var(--dx-ember-2));
14
- color:var(--dx-fg); font:inherit; font-weight:600; font-size:.78rem; letter-spacing:.12em;
10
+ padding:11px 22px; border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 55%,transparent);
11
+ border-radius:var(--dx-radius,0px);
12
+ background:linear-gradient(135deg,var(--dx-ember,#f26c18),var(--dx-ember-2,#ba3a00));
13
+ color:var(--dx-fg,#fff4ea); font:inherit; font-weight:600; font-size:.78rem; letter-spacing:.12em;
15
14
  text-transform:uppercase; cursor:pointer; -webkit-tap-highlight-color:transparent;
16
- box-shadow:0 14px 26px color-mix(in srgb,var(--dx-ember) 24%,transparent);
15
+ box-shadow:0 14px 26px color-mix(in srgb,var(--dx-ember,#f26c18) 24%,transparent);
17
16
  transition:transform .16s ease, box-shadow .16s ease, filter .16s ease, background .16s ease;
18
17
  }
19
- .dx-btn:hover{ transform:translateY(-1px); filter:brightness(1.07); box-shadow:0 20px 34px color-mix(in srgb,var(--dx-ember) 32%,transparent); }
20
- .dx-btn:active{ transform:translateY(0); filter:brightness(.97); box-shadow:0 8px 16px color-mix(in srgb,var(--dx-ember) 22%,transparent); }
21
- .dx-btn:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 38%,transparent); }
18
+ .dx-btn:hover{ transform:translateY(-1px); filter:brightness(1.07); box-shadow:0 20px 34px color-mix(in srgb,var(--dx-ember,#f26c18) 32%,transparent); }
19
+ .dx-btn:active{ transform:translateY(0); filter:brightness(.97); box-shadow:0 8px 16px color-mix(in srgb,var(--dx-ember,#f26c18) 22%,transparent); }
20
+ .dx-btn:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember,#f26c18) 38%,transparent); }
22
21
  .dx-btn:disabled{ cursor:default; filter:saturate(.85) brightness(.98); }
23
- .dx-btn--secondary{ background:transparent; color:var(--dx-ember); box-shadow:none; border-color:color-mix(in srgb,var(--dx-ember) 45%,transparent); }
24
- .dx-btn--secondary:hover{ background:color-mix(in srgb,var(--dx-ember) 9%,transparent); filter:none; box-shadow:none; }
22
+ .dx-btn--secondary{ background:transparent; color:var(--dx-ember,#f26c18); box-shadow:none; border-color:color-mix(in srgb,var(--dx-ember,#f26c18) 45%,transparent); }
23
+ .dx-btn--secondary:hover{ background:color-mix(in srgb,var(--dx-ember,#f26c18) 9%,transparent); filter:none; box-shadow:none; }
25
24
  .dx-btn--block{ width:100%; }
26
25
  .dx-btn__mark{ flex-shrink:0; }
27
26
  .dx-btn__spin{ width:15px; height:15px; flex-shrink:0; border-radius:50%;
@@ -29,7 +28,7 @@ var BUTTON_CSS = `
29
28
  animation:dx-spin .7s linear infinite; }
30
29
  .dx-btn__doing{ animation:dx-pulse 1.4s ease-in-out infinite; }
31
30
  .dx-chip{ display:inline-flex; align-items:center; gap:8px; padding:6px 10px; font:inherit;
32
- font-variant-numeric:tabular-nums; border-radius:var(--dx-radius,0);
31
+ font-variant-numeric:tabular-nums; border-radius:var(--dx-radius,0px);
33
32
  border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 35%,transparent); }
34
33
  .dx-chip__dot{ width:7px; height:7px; border-radius:50%; background:var(--dx-ember,#f26c18); }
35
34
  .dx-chip__bal{ font-weight:600; opacity:.85; }
@@ -166,6 +166,7 @@ function openCeremonyPopup(op, config = {}) {
166
166
  const params = new URLSearchParams({ v: "1", op, requestId, origin: openerOrigin });
167
167
  if (config.name) params.set("name", config.name);
168
168
  if (config.apiBase) params.set("apiBase", config.apiBase);
169
+ if (config.preferImmediate) params.set("preferImmediate", "1");
169
170
  const url = `${host}?${params.toString()}`;
170
171
  return new Promise((resolve, reject) => {
171
172
  const popup = window.open(url, "dexter-connect", popupFeatures());
@@ -230,6 +231,16 @@ function makeNonce() {
230
231
  return `rid-${new URL(location.href).searchParams.get("v") ?? ""}-${typeof performance !== "undefined" ? performance.now() : 0}`;
231
232
  }
232
233
 
234
+ // src/httpError.ts
235
+ async function readErrorCode(res) {
236
+ try {
237
+ const body = await res.json();
238
+ if (body?.error) return body.error;
239
+ } catch {
240
+ }
241
+ return `http_${res.status}`;
242
+ }
243
+
233
244
  // src/enroll.ts
234
245
  var DEFAULT_API_BASE = "https://api.dexter.cash";
235
246
  var DEFAULT_RP_ID = "dexter.cash";
@@ -327,14 +338,6 @@ async function initializeVault(apiBase, userHandle, credentialId, spendPolicy) {
327
338
  if (!res.ok) throw new ConnectError(await readErrorCode(res), `initialize ${res.status}`);
328
339
  return await res.json();
329
340
  }
330
- async function readErrorCode(res) {
331
- try {
332
- const body = await res.json();
333
- if (body?.error) return body.error;
334
- } catch {
335
- }
336
- return `http_${res.status}`;
337
- }
338
341
 
339
342
  // src/relay.ts
340
343
  import { startAuthentication, browserSupportsWebAuthn } from "@simplewebauthn/browser";
@@ -414,7 +417,7 @@ async function submitLogin(apiBase, response) {
414
417
  body: JSON.stringify({ credential: response })
415
418
  });
416
419
  if (!res.ok) {
417
- throw new ConnectError(await readErrorCode2(res), `passkey-login ${res.status}`);
420
+ throw new ConnectError(await readErrorCode(res), `passkey-login ${res.status}`);
418
421
  }
419
422
  const data = await res.json();
420
423
  const session = {
@@ -426,14 +429,6 @@ async function submitLogin(apiBase, response) {
426
429
  };
427
430
  return data.vault ? { session, vault: data.vault } : { session };
428
431
  }
429
- async function readErrorCode2(res) {
430
- try {
431
- const body = await res.json();
432
- if (body?.error) return body.error;
433
- } catch {
434
- }
435
- return `http_${res.status}`;
436
- }
437
432
 
438
433
  // src/base64.ts
439
434
  function base64ToBytes(s) {
@@ -506,7 +501,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE3) {
506
501
  operationHash: bytesToBase64url(operationHash)
507
502
  })
508
503
  });
509
- if (!res.ok) throw new ConnectError(await readErrorCode3(res), `challenge ${res.status}`);
504
+ if (!res.ok) throw new ConnectError(await readErrorCode(res), `challenge ${res.status}`);
510
505
  const data = await res.json();
511
506
  const options = data?.options;
512
507
  if (!options?.challenge) {
@@ -543,7 +538,7 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE3) {
543
538
  headers: { "content-type": "application/json" },
544
539
  body: JSON.stringify({ credential, userHandle: bytesToBase64url(userHandle) })
545
540
  });
546
- if (!res.ok) throw new ConnectError(await readErrorCode3(res), `verify ${res.status}`);
541
+ if (!res.ok) throw new ConnectError(await readErrorCode(res), `verify ${res.status}`);
547
542
  const data = await res.json();
548
543
  if (data?.verified === false) {
549
544
  throw new ConnectError("verification_failed", "server returned verified=false");
@@ -551,14 +546,6 @@ function createAnonServerPolicy(apiBase = DEFAULT_API_BASE3) {
551
546
  }
552
547
  };
553
548
  }
554
- async function readErrorCode3(res) {
555
- try {
556
- const body = await res.json();
557
- if (body?.error) return body.error;
558
- } catch {
559
- }
560
- return `http_${res.status}`;
561
- }
562
549
 
563
550
  // src/signer.ts
564
551
  import { DexterApiBrowserPasskeySigner } from "@dexterai/vault/signers/browser";
@@ -585,6 +572,181 @@ function resolveIdentity(input) {
585
572
  return { kind, userHandle, accountToken, hasPasskeyVault, hasAccount, hasWallet };
586
573
  }
587
574
 
575
+ // src/recover.ts
576
+ import { startAuthentication as startAuthentication2, browserSupportsWebAuthn as browserSupportsWebAuthn2 } from "@simplewebauthn/browser";
577
+
578
+ // src/immediate.ts
579
+ var immediateCapPromise = null;
580
+ function immediateGetSupported() {
581
+ if (!immediateCapPromise) {
582
+ immediateCapPromise = (async () => {
583
+ try {
584
+ if (typeof window === "undefined") return false;
585
+ const pkc2 = window.PublicKeyCredential;
586
+ if (!pkc2 || typeof pkc2.getClientCapabilities !== "function") return false;
587
+ const caps = await pkc2.getClientCapabilities.call(pkc2);
588
+ return caps?.immediateGet === true;
589
+ } catch {
590
+ return false;
591
+ }
592
+ })();
593
+ }
594
+ return immediateCapPromise;
595
+ }
596
+ function primeImmediateSupport() {
597
+ void immediateGetSupported();
598
+ }
599
+ async function immediateAuthentication(options) {
600
+ const getOptions = {
601
+ publicKey: {
602
+ challenge: base64urlToBytes(options.challenge),
603
+ rpId: options.rpId,
604
+ timeout: options.timeout,
605
+ userVerification: options.userVerification,
606
+ allowCredentials: options.allowCredentials?.map((c) => ({
607
+ id: base64urlToBytes(c.id),
608
+ type: c.type,
609
+ transports: c.transports
610
+ }))
611
+ }
612
+ };
613
+ getOptions.uiMode = "immediate";
614
+ const cred = await navigator.credentials.get(getOptions);
615
+ if (!cred) throw new DOMException("no credential", "NotAllowedError");
616
+ const a = cred.response;
617
+ return {
618
+ id: cred.id,
619
+ rawId: bytesToBase64url(new Uint8Array(cred.rawId)),
620
+ response: {
621
+ clientDataJSON: bytesToBase64url(new Uint8Array(a.clientDataJSON)),
622
+ authenticatorData: bytesToBase64url(new Uint8Array(a.authenticatorData)),
623
+ signature: bytesToBase64url(new Uint8Array(a.signature)),
624
+ userHandle: a.userHandle ? bytesToBase64url(new Uint8Array(a.userHandle)) : void 0
625
+ },
626
+ clientExtensionResults: cred.getClientExtensionResults?.() ?? {},
627
+ type: "public-key",
628
+ authenticatorAttachment: cred.authenticatorAttachment ?? void 0
629
+ };
630
+ }
631
+ function classifyWebAuthnRejection(err) {
632
+ const e = err;
633
+ if (!e) return false;
634
+ const blob = [e.name, e.cause?.name, typeof e.code === "string" ? e.code : "", e.message].filter(Boolean).join(" ").toLowerCase();
635
+ return /notallowed|not ?allowed|abort|cancel|timed out|timeout|denied|ceremony/.test(blob);
636
+ }
637
+
638
+ // src/recover.ts
639
+ var DEFAULT_API_BASE4 = "https://api.dexter.cash";
640
+ var ANON_SIGN_BASE3 = "/api/passkey-anon/sign";
641
+ primeImmediateSupport();
642
+ async function recoverWallet(config = {}) {
643
+ if (shouldUsePopup(config.transport)) {
644
+ let outcome;
645
+ try {
646
+ outcome = await openCeremonyPopup("recover", {
647
+ connectHost: config.connectHost,
648
+ apiBase: config.apiBase,
649
+ preferImmediate: config.preferImmediate
650
+ });
651
+ } catch (err) {
652
+ const ce = err instanceof ConnectError ? err : new ConnectError("popup_failed", err instanceof Error ? err.message : String(err));
653
+ if (ce.code === "popup_closed") return { ok: false, reason: "cancelled" };
654
+ return { ok: false, reason: "error", error: ce };
655
+ }
656
+ if (outcome.ok) {
657
+ setActiveHandle(outcome.userHandle, outcome.vault.walletLabel ?? void 0, outcome.credentialId);
658
+ }
659
+ return outcome;
660
+ }
661
+ if (typeof navigator === "undefined" || !browserSupportsWebAuthn2()) {
662
+ return {
663
+ ok: false,
664
+ reason: "error",
665
+ error: new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment")
666
+ };
667
+ }
668
+ const apiBase = (config.apiBase ?? DEFAULT_API_BASE4).replace(/\/$/, "");
669
+ const onPhase = config.onPhase;
670
+ onPhase?.("challenge");
671
+ let options;
672
+ try {
673
+ const res = await fetch(`${apiBase}${ANON_SIGN_BASE3}/recover-challenge`, {
674
+ method: "POST",
675
+ headers: { "content-type": "application/json" },
676
+ body: "{}"
677
+ });
678
+ if (!res.ok) return { ok: false, reason: "error", error: new ConnectError(await readErrorCode(res)) };
679
+ options = (await res.json()).options;
680
+ } catch (err) {
681
+ return netError("recover_challenge_failed", err);
682
+ }
683
+ onPhase?.("passkey");
684
+ const useImmediate = Boolean(config.preferImmediate) && await immediateGetSupported();
685
+ let credential;
686
+ try {
687
+ credential = useImmediate ? await immediateAuthentication(options) : await startAuthentication2({ optionsJSON: options });
688
+ } catch (err) {
689
+ if (classifyWebAuthnRejection(err)) {
690
+ return { ok: false, reason: useImmediate ? "no_credential" : "cancelled" };
691
+ }
692
+ return {
693
+ ok: false,
694
+ reason: "error",
695
+ error: new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err))
696
+ };
697
+ }
698
+ onPhase?.("verifying");
699
+ let userHandle;
700
+ let credentialId;
701
+ try {
702
+ const res = await fetch(`${apiBase}${ANON_SIGN_BASE3}/recover-verify`, {
703
+ method: "POST",
704
+ headers: { "content-type": "application/json" },
705
+ body: JSON.stringify({ credential })
706
+ });
707
+ if (res.status === 404) return { ok: false, reason: "no_credential" };
708
+ if (!res.ok) return { ok: false, reason: "error", error: new ConnectError(await readErrorCode(res)) };
709
+ const data = await res.json();
710
+ userHandle = data.userHandle;
711
+ credentialId = data.credentialId;
712
+ } catch (err) {
713
+ return netError("recover_verify_failed", err);
714
+ }
715
+ let vault;
716
+ try {
717
+ const res = await fetch(
718
+ `${apiBase}/api/passkey-vault-anon/status?user_handle=${encodeURIComponent(userHandle)}`
719
+ );
720
+ if (!res.ok) return { ok: false, reason: "error", error: new ConnectError(await readErrorCode(res)) };
721
+ const data = await res.json();
722
+ if (!data.hasVault || !data.vault) {
723
+ return {
724
+ ok: false,
725
+ reason: "error",
726
+ error: new ConnectError("vault_not_found", "this sign-in key has no wallet attached")
727
+ };
728
+ }
729
+ vault = {
730
+ vaultPda: data.vault.vaultPda,
731
+ swigAddress: data.vault.swigAddress,
732
+ receiveAddress: data.vault.receiveAddress ?? null,
733
+ isActivated: Boolean(data.vault.isActivated),
734
+ walletLabel: data.vault.walletLabel ?? null
735
+ };
736
+ } catch (err) {
737
+ return netError("vault_status_failed", err);
738
+ }
739
+ setActiveHandle(userHandle, vault.walletLabel ?? void 0, credentialId);
740
+ return { ok: true, userHandle, credentialId, vault };
741
+ }
742
+ function netError(code, err) {
743
+ return {
744
+ ok: false,
745
+ reason: "error",
746
+ error: new ConnectError(code, err instanceof Error ? err.message : String(err))
747
+ };
748
+ }
749
+
588
750
  // src/phase.ts
589
751
  var PHASE_LABEL = {
590
752
  challenge: "Preparing\u2026",
@@ -678,6 +840,7 @@ export {
678
840
  createAnonServerPolicy,
679
841
  createPasskeySigner,
680
842
  resolveIdentity,
843
+ recoverWallet,
681
844
  ceremonyPhaseLabel,
682
845
  passkeySignalSupport,
683
846
  renamePasskey,
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { S as SignInResult, C as CreateWalletResult, a as CreateWalletConfig, b as CeremonyPhase, D as DexterConnectConfig, c as ConnectVault, I as IdentityKind } from './signals-qc4rDyQ6.js';
2
- export { A as ACTIVE_WALLET_STORAGE_KEY, d as ConnectError, e as IdentityInput, P as PasskeyLoginTokens, f as PasskeySignalSupport, R as ResolvedIdentity, g as SESSION_TTL_30D, h as SpendPolicy, i as StoredWallet, j as authoredPolicy, k as createWallet, l as ejectActiveWallet, m as forgetWallet, n as getActiveHandle, o as getCredentialId, p as listWallets, q as passkeySignalSupport, r as prunePasskey, s as renamePasskey, t as resolveIdentity, u as setActiveHandle, v as subscribeWallet, w as switchWallet, x as syncAcceptedPasskeys, y as usdToAtomic } from './signals-qc4rDyQ6.js';
1
+ import { S as SignInResult, C as CreateWalletResult, a as CreateWalletConfig, b as CeremonyPhase, D as DexterConnectConfig, c as ConnectVault, R as RecoverWalletConfig, d as RecoverOutcome, I as IdentityKind } from './signals-pjh3z6N8.js';
2
+ export { A as ACTIVE_WALLET_STORAGE_KEY, e as ConnectError, f as IdentityInput, P as PasskeyLoginTokens, g as PasskeySignalSupport, h as RecoverVault, i as ResolvedIdentity, j as SESSION_TTL_30D, k as SpendPolicy, l as StoredWallet, m as authoredPolicy, n as createWallet, o as ejectActiveWallet, p as forgetWallet, q as getActiveHandle, r as getCredentialId, s as listWallets, t as passkeySignalSupport, u as prunePasskey, v as renamePasskey, w as resolveIdentity, x as setActiveHandle, y as subscribeWallet, z as switchWallet, B as syncAcceptedPasskeys, E as usdToAtomic } from './signals-pjh3z6N8.js';
3
3
  import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
4
4
 
5
5
  /**
@@ -82,6 +82,8 @@ declare function createPasskeySigner(vault: ConnectVault, apiBase?: string, opts
82
82
  __assertion?: AssertionLike;
83
83
  }): DexterApiBrowserPasskeySigner;
84
84
 
85
+ declare function recoverWallet(config?: RecoverWalletConfig): Promise<RecoverOutcome>;
86
+
85
87
  /**
86
88
  * Human-readable label for a ceremony phase — the live "connecting step" copy.
87
89
  * ONE source of truth so sign-in (SignInWithDexter) and create (consumer setup
@@ -221,4 +223,4 @@ interface EnableAgentSpendResult {
221
223
  */
222
224
  declare function enableAgentSpend(id: AgentSpendIdentity, vaultPda: string, apiOrigin: string, credentialId?: string | null): Promise<EnableAgentSpendResult>;
223
225
 
224
- export { AgentSpendError, type AgentSpendIdentity, type AgentSpendStatus, type AgentSpendTab, type AnonChallengeResult, type AnonServerPolicy, type AutomaticAgentSpend, CeremonyPhase, ConnectVault, type ContinueResult, CreateWalletConfig, CreateWalletResult, DexterConnectConfig, type EnableAgentSpendResult, IdentityKind, type RawAgentSpendSession, type RawAgentSpendStatus, type RevokeAgentSpendResult, SignInResult, assembleAgentSpendStatus, ceremonyPhaseLabel, continueWithDexter, createAnonServerPolicy, createPasskeySigner, describeAgentSpendError, enableAgentSpend, passkeyLogin, revokeAgentSpend };
226
+ export { AgentSpendError, type AgentSpendIdentity, type AgentSpendStatus, type AgentSpendTab, type AnonChallengeResult, type AnonServerPolicy, type AutomaticAgentSpend, CeremonyPhase, ConnectVault, type ContinueResult, CreateWalletConfig, CreateWalletResult, DexterConnectConfig, type EnableAgentSpendResult, IdentityKind, type RawAgentSpendSession, type RawAgentSpendStatus, RecoverOutcome, RecoverWalletConfig, type RevokeAgentSpendResult, SignInResult, assembleAgentSpendStatus, ceremonyPhaseLabel, continueWithDexter, createAnonServerPolicy, createPasskeySigner, describeAgentSpendError, enableAgentSpend, passkeyLogin, recoverWallet, revokeAgentSpend };
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  passkeyLogin,
19
19
  passkeySignalSupport,
20
20
  prunePasskey,
21
+ recoverWallet,
21
22
  renamePasskey,
22
23
  resolveIdentity,
23
24
  setActiveHandle,
@@ -25,7 +26,7 @@ import {
25
26
  switchWallet,
26
27
  syncAcceptedPasskeys,
27
28
  usdToAtomic
28
- } from "./chunk-2QZ5QLGI.js";
29
+ } from "./chunk-MOC5AVY7.js";
29
30
 
30
31
  // src/agentSpend.ts
31
32
  import { PublicKey } from "@solana/web3.js";
@@ -196,6 +197,7 @@ export {
196
197
  passkeyLogin,
197
198
  passkeySignalSupport,
198
199
  prunePasskey,
200
+ recoverWallet,
199
201
  renamePasskey,
200
202
  resolveIdentity,
201
203
  revokeAgentSpend,
package/dist/react.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
2
- import { b as CeremonyPhase, S as SignInResult, P as PasskeyLoginTokens, c as ConnectVault, d as ConnectError, i as StoredWallet, f as PasskeySignalSupport, R as ResolvedIdentity, C as CreateWalletResult } from './signals-qc4rDyQ6.js';
2
+ import { b as CeremonyPhase, S as SignInResult, d as RecoverOutcome, P as PasskeyLoginTokens, c as ConnectVault, e as ConnectError, l as StoredWallet, g as PasskeySignalSupport, i as ResolvedIdentity, C as CreateWalletResult } from './signals-pjh3z6N8.js';
3
+ export { h as RecoverVault } from './signals-pjh3z6N8.js';
3
4
  import { ReactElement, ReactNode } from 'react';
4
5
 
5
6
  type ConnectStatus = 'idle' | 'pending' | 'done' | 'error';
@@ -8,6 +9,16 @@ interface UseSignInWithDexterConfig {
8
9
  apiBase?: string;
9
10
  /** RPC for the connected-chip balance read. Default: Dexter's Helius proxy. */
10
11
  rpcUrl?: string;
12
+ /** Ceremony transport (both verbs). 'auto' (default) = inline on dexter.cash,
13
+ * popup anywhere else. Exposed so tests/staging can force a leg —
14
+ * CreateWalletPanel already had this knob; the sign-in surface was the odd
15
+ * one out. */
16
+ transport?: 'auto' | 'popup' | 'inline';
17
+ /** Hosted ceremony page for the popup transport. Default dexter.cash/connect. */
18
+ connectHost?: string;
19
+ /** Chrome-149+ immediate UI mode for the wallet-only verb: instant fast-fail
20
+ * when this device holds no passkey. Ignored by signIn(). */
21
+ preferImmediate?: boolean;
11
22
  }
12
23
  interface UseSignInWithDexter {
13
24
  status: ConnectStatus;
@@ -18,6 +29,14 @@ interface UseSignInWithDexter {
18
29
  /** Run the ceremony. Resolves with the result; throws ConnectError on failure
19
30
  * (error is also captured in `error` + `status==='error'` for declarative UI). */
20
31
  signIn: () => Promise<SignInResult>;
32
+ /** Wallet-only sign-in (P0c): re-points this browser at an existing wallet,
33
+ * mints NO session. Returns a discriminated outcome — cancel is a normal
34
+ * result, never a throw. Identity surfaces (useIdentity/useDexterWallet)
35
+ * light up via the wallet store; `session`/`vault` here stay null. Fire on
36
+ * tap only — never on mount (iOS gesture rule). */
37
+ recover: () => Promise<RecoverOutcome>;
38
+ /** Last recover outcome; null until recover() settles. */
39
+ recovered: RecoverOutcome | null;
21
40
  disconnect: () => void;
22
41
  session: PasskeyLoginTokens | null;
23
42
  vault: ConnectVault | null;
@@ -45,8 +64,18 @@ interface UseSignInWithDexter {
45
64
  declare function useSignInWithDexter(config?: UseSignInWithDexterConfig): UseSignInWithDexter;
46
65
 
47
66
  interface SignInWithDexterProps extends UseSignInWithDexterConfig {
67
+ /** 'signin' (default) = full sign-in minting the account session.
68
+ * 'recover' = wallet-only: re-points this browser at an existing wallet and
69
+ * mints NOTHING else (the wallet IS the sign-in). After a successful
70
+ * recover the element renders null — identity display belongs to
71
+ * DexterWalletChip/useIdentity, which light up via the wallet store. */
72
+ mode?: 'signin' | 'recover';
48
73
  /** Fired with the result the moment sign-in completes. */
49
74
  onSuccess?: (result: SignInResult) => void;
75
+ /** Recover mode: fired with EVERY settled outcome (ok, no_credential,
76
+ * cancelled, error) — the single channel; onError is not used in this mode.
77
+ * Branch on `reason`: no_credential → offer create; cancelled → stay silent. */
78
+ onRecovered?: (outcome: RecoverOutcome) => void;
50
79
  /** Fired with the typed error if the ceremony fails. */
51
80
  onError?: (error: ConnectError) => void;
52
81
  /** Button label when signed out. Default "Sign in with Dexter". */
@@ -149,8 +178,10 @@ interface DexterWalletMenuProps {
149
178
  avatarInitial?: string;
150
179
  /** "Manage wallet" → the consumer's wallet page. Row hidden if omitted. */
151
180
  onManageWallet?: () => void;
152
- /** "Start fresh" → the consumer ejects/resets (guard + perform on its side). Row hidden if omitted. */
181
+ /** Eject row → the consumer ejects/resets (guard + perform on its side). Row hidden if omitted. */
153
182
  onStartFresh?: () => void;
183
+ /** Label for the eject/reset row. Default "Eject wallet". */
184
+ startFreshLabel?: string;
154
185
  /** Sign-in/save UI revealed when "Save your wallet" is tapped (e.g. <SignInWithDexter/>). Row hidden if omitted. */
155
186
  saveSlot?: ReactNode;
156
187
  /** Short hint shown above the save UI. */
@@ -198,4 +229,4 @@ interface CreateWalletPanelProps {
198
229
  /** The turnkey consent-at-birth create panel. */
199
230
  declare function CreateWalletPanel(props: CreateWalletPanelProps): ReactElement;
200
231
 
201
- export { AllowanceChips, type AllowanceChipsProps, type ConnectStatus, CreateWalletPanel, type CreateWalletPanelProps, DexterButton, type DexterButtonProps, DexterMark, DexterWalletChip, type DexterWalletChipProps, DexterWalletMenu, type DexterWalletMenuProps, SignInWithDexter, type SignInWithDexterProps, type UseDexterWallet, type UseIdentityConfig, type UseSignInWithDexter, type UseSignInWithDexterConfig, useDexterWallet, useIdentity, useSignInWithDexter };
232
+ export { AllowanceChips, type AllowanceChipsProps, type ConnectStatus, CreateWalletPanel, type CreateWalletPanelProps, DexterButton, type DexterButtonProps, DexterMark, DexterWalletChip, type DexterWalletChipProps, DexterWalletMenu, type DexterWalletMenuProps, RecoverOutcome, SignInWithDexter, type SignInWithDexterProps, type UseDexterWallet, type UseIdentityConfig, type UseSignInWithDexter, type UseSignInWithDexterConfig, useDexterWallet, useIdentity, useSignInWithDexter };
package/dist/react.js CHANGED
@@ -11,18 +11,19 @@ import {
11
11
  passkeyLogin,
12
12
  passkeySignalSupport,
13
13
  prunePasskey,
14
+ recoverWallet,
14
15
  renamePasskey,
15
16
  resolveIdentity,
16
17
  setActiveHandle,
17
18
  subscribe,
18
19
  switchWallet
19
- } from "./chunk-2QZ5QLGI.js";
20
+ } from "./chunk-MOC5AVY7.js";
20
21
  import {
21
22
  DexterButton,
22
23
  DexterMark,
23
24
  cx,
24
25
  ensureDexterButtonStyles
25
- } from "./chunk-S7G2ZZOO.js";
26
+ } from "./chunk-2WETR5DJ.js";
26
27
 
27
28
  // src/useSignInWithDexter.ts
28
29
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -67,13 +68,14 @@ function base64ToBytes(s) {
67
68
  // src/useSignInWithDexter.ts
68
69
  var DEFAULT_RPC = "https://api.dexter.cash/proxy/helius/rpc";
69
70
  function useSignInWithDexter(config = {}) {
70
- const { apiBase, rpcUrl = DEFAULT_RPC } = config;
71
+ const { apiBase, rpcUrl = DEFAULT_RPC, transport, connectHost, preferImmediate } = config;
71
72
  const [status, setStatus] = useState("idle");
72
73
  const [phase, setPhase] = useState(null);
73
74
  const [session, setSession] = useState(null);
74
75
  const [vault, setVault] = useState(null);
75
76
  const [usdcBalance, setUsdcBalance] = useState(null);
76
77
  const [error, setError] = useState(null);
78
+ const [recovered, setRecovered] = useState(null);
77
79
  const refreshBalance = useCallback(async () => {
78
80
  const ata = vault?.usdcAta;
79
81
  if (!ata) return;
@@ -84,7 +86,14 @@ function useSignInWithDexter(config = {}) {
84
86
  setPhase(null);
85
87
  setStatus("pending");
86
88
  try {
87
- const result = await passkeyLogin(apiBase ? { apiBase } : {}, setPhase);
89
+ const result = await passkeyLogin(
90
+ {
91
+ ...apiBase ? { apiBase } : {},
92
+ ...transport ? { transport } : {},
93
+ ...connectHost ? { connectHost } : {}
94
+ },
95
+ setPhase
96
+ );
88
97
  setSession(result.session);
89
98
  setVault(result.vault ?? null);
90
99
  setStatus("done");
@@ -97,12 +106,36 @@ function useSignInWithDexter(config = {}) {
97
106
  setPhase(null);
98
107
  throw e;
99
108
  }
100
- }, [apiBase]);
109
+ }, [apiBase, transport, connectHost]);
110
+ const recover = useCallback(async () => {
111
+ setError(null);
112
+ setPhase(null);
113
+ setStatus("pending");
114
+ const outcome = await recoverWallet({
115
+ ...apiBase ? { apiBase } : {},
116
+ ...transport ? { transport } : {},
117
+ ...connectHost ? { connectHost } : {},
118
+ ...preferImmediate ? { preferImmediate } : {},
119
+ onPhase: setPhase
120
+ });
121
+ setPhase(null);
122
+ setRecovered(outcome);
123
+ if (outcome.ok) {
124
+ setStatus("done");
125
+ } else if (outcome.reason === "error") {
126
+ setError(outcome.error ?? new ConnectError("recover_failed"));
127
+ setStatus("error");
128
+ } else {
129
+ setStatus("idle");
130
+ }
131
+ return outcome;
132
+ }, [apiBase, transport, connectHost, preferImmediate]);
101
133
  const disconnect = useCallback(() => {
102
134
  setSession(null);
103
135
  setVault(null);
104
136
  setUsdcBalance(null);
105
137
  setError(null);
138
+ setRecovered(null);
106
139
  setStatus("idle");
107
140
  }, []);
108
141
  const passkeySigner = useMemo(
@@ -117,6 +150,8 @@ function useSignInWithDexter(config = {}) {
117
150
  phase,
118
151
  isVaultConnected: status === "done" && vault !== null,
119
152
  signIn,
153
+ recover,
154
+ recovered,
120
155
  disconnect,
121
156
  session,
122
157
  vault,
@@ -141,8 +176,10 @@ function formatUsd(n) {
141
176
  }
142
177
  function SignInWithDexter(props) {
143
178
  const {
179
+ mode = "signin",
144
180
  onSuccess,
145
181
  onError,
182
+ onRecovered,
146
183
  label = "Sign in with Dexter",
147
184
  variant = "primary",
148
185
  block = false,
@@ -153,12 +190,17 @@ function SignInWithDexter(props) {
153
190
  useEffect2(ensureDexterButtonStyles, []);
154
191
  const c = useSignInWithDexter(config);
155
192
  const handleClick = async () => {
193
+ if (mode === "recover") {
194
+ onRecovered?.(await c.recover());
195
+ return;
196
+ }
156
197
  try {
157
198
  onSuccess?.(await c.signIn());
158
199
  } catch (err) {
159
200
  onError?.(err);
160
201
  }
161
202
  };
203
+ if (mode === "recover" && c.recovered?.ok) return null;
162
204
  if (c.isVaultConnected) {
163
205
  if (!showConnectedChip) return null;
164
206
  return /* @__PURE__ */ jsxs("span", { className: cx("dx-chip", className), children: [
@@ -235,35 +277,34 @@ import { useEffect as useEffect4 } from "react";
235
277
  var STYLE_ID = "dexter-connect-wallet-kit-styles";
236
278
  var WALLET_KIT_CSS = `
237
279
  .dx-wchip{
238
- --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
239
280
  display:inline-flex; align-items:center; gap:9px; max-width:240px;
240
281
  padding:6px 12px 6px 7px; font:inherit; font-weight:600; font-size:.74rem;
241
282
  letter-spacing:.1em; text-transform:uppercase; cursor:pointer; background:transparent;
242
- color:inherit; border:1px solid color-mix(in srgb,var(--dx-ember) 42%,transparent);
243
- border-radius:var(--dx-radius); -webkit-tap-highlight-color:transparent;
283
+ color:inherit; border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 42%,transparent);
284
+ border-radius:var(--dx-radius,0px); -webkit-tap-highlight-color:transparent;
244
285
  transition:filter .16s ease, box-shadow .16s ease, border-color .16s ease;
245
286
  }
246
- .dx-wchip:hover{ filter:brightness(1.08); box-shadow:0 8px 18px color-mix(in srgb,var(--dx-ember) 18%,transparent); }
247
- .dx-wchip:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 34%,transparent); }
287
+ .dx-wchip:hover{ filter:brightness(1.08); box-shadow:0 8px 18px color-mix(in srgb,var(--dx-ember,#f26c18) 18%,transparent); }
288
+ .dx-wchip:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember,#f26c18) 34%,transparent); }
248
289
  .dx-wchip--signedout{ border-color:color-mix(in srgb,currentColor 22%,transparent); padding-left:12px; }
249
290
  .dx-wchip__avatar{
250
291
  width:22px; height:22px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center;
251
292
  border-radius:50%; overflow:hidden; font-size:.72rem; font-weight:700; letter-spacing:0; line-height:1;
252
- background:linear-gradient(135deg,var(--dx-ember),var(--dx-ember-2)); color:var(--dx-fg);
293
+ background:linear-gradient(135deg,var(--dx-ember,#f26c18),var(--dx-ember-2,#ba3a00)); color:var(--dx-fg,#fff4ea);
253
294
  }
254
295
  .dx-wchip__avatar img{ width:100%; height:100%; object-fit:cover; }
255
296
  .dx-wchip__label{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
256
297
 
257
- .dx-wmenu{ --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; display:flex; flex-direction:column; min-width:248px; }
298
+ .dx-wmenu{ display:flex; flex-direction:column; min-width:248px; }
258
299
  .dx-wmenu__id{ display:flex; align-items:center; gap:10px; padding:13px 15px; border-bottom:1px solid color-mix(in srgb,currentColor 12%,transparent); }
259
- .dx-wmenu__avatar{ width:32px; height:32px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; border-radius:50%; font-weight:700; font-size:.85rem; line-height:1; background:linear-gradient(135deg,var(--dx-ember),var(--dx-ember-2)); color:var(--dx-fg); }
300
+ .dx-wmenu__avatar{ width:32px; height:32px; flex-shrink:0; display:inline-flex; align-items:center; justify-content:center; border-radius:50%; font-weight:700; font-size:.85rem; line-height:1; background:linear-gradient(135deg,var(--dx-ember,#f26c18),var(--dx-ember-2,#ba3a00)); color:var(--dx-fg,#fff4ea); }
260
301
  .dx-wmenu__meta{ display:flex; flex-direction:column; gap:2px; min-width:0; }
261
302
  .dx-wmenu__name{ font-weight:700; font-size:.9rem; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
262
303
  .dx-wmenu__sub{ font-size:.66rem; letter-spacing:.12em; text-transform:uppercase; opacity:.55; }
263
304
  .dx-wmenu__list{ display:flex; flex-direction:column; padding:6px; }
264
305
  .dx-wmenu__item{ display:flex; align-items:center; justify-content:space-between; gap:10px; width:100%; padding:10px 12px; font:inherit; font-size:.82rem; font-weight:600; text-align:left; background:transparent; border:none; color:inherit; cursor:pointer; transition:background .12s ease; }
265
- .dx-wmenu__item:hover{ background:color-mix(in srgb,var(--dx-ember) 10%,transparent); }
266
- .dx-wmenu__item--danger{ color:#e5552e; }
306
+ .dx-wmenu__item:hover{ background:color-mix(in srgb,var(--dx-ember,#f26c18) 10%,transparent); }
307
+ .dx-wmenu__item--danger{ color:var(--dx-danger,#e5552e); }
267
308
  .dx-wmenu__icon{ opacity:.45; font-size:.9rem; }
268
309
  .dx-wmenu__back{ border-bottom:1px solid color-mix(in srgb,currentColor 10%,transparent); opacity:.85; }
269
310
  .dx-wmenu__save{ display:flex; flex-direction:column; gap:9px; padding:13px 15px; }
@@ -304,7 +345,16 @@ function DexterWalletChip(props) {
304
345
  import { useEffect as useEffect5, useState as useState3 } from "react";
305
346
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
306
347
  function DexterWalletMenu(props) {
307
- const { walletLabel, avatarInitial, onManageWallet, onStartFresh, saveSlot, saveHint, className } = props;
348
+ const {
349
+ walletLabel,
350
+ avatarInitial,
351
+ onManageWallet,
352
+ onStartFresh,
353
+ startFreshLabel = "Eject wallet",
354
+ saveSlot,
355
+ saveHint,
356
+ className
357
+ } = props;
308
358
  useEffect5(ensureWalletKitStyles, []);
309
359
  const [showSave, setShowSave] = useState3(false);
310
360
  const initial = (avatarInitial ?? walletLabel.trim().charAt(0) ?? "D").toUpperCase();
@@ -349,7 +399,7 @@ function DexterWalletMenu(props) {
349
399
  className: "dx-wmenu__item dx-wmenu__item--danger",
350
400
  onClick: onStartFresh,
351
401
  children: [
352
- /* @__PURE__ */ jsx3("span", { children: "Start fresh" }),
402
+ /* @__PURE__ */ jsx3("span", { children: startFreshLabel }),
353
403
  /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u27F5" })
354
404
  ]
355
405
  }
@@ -378,39 +428,37 @@ import { useEffect as useEffect6, useState as useState4 } from "react";
378
428
  var STYLE_ID2 = "dexter-connect-consent-styles";
379
429
  var CONSENT_CSS = `
380
430
  .dx-allow{
381
- --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
382
431
  display:flex; flex-wrap:wrap; gap:8px; align-items:center;
383
432
  }
384
433
  .dx-allow__chip{
385
434
  display:inline-flex; align-items:center; justify-content:center; padding:9px 15px;
386
435
  font:inherit; font-weight:600; font-size:.78rem; letter-spacing:.12em; text-transform:uppercase;
387
436
  font-variant-numeric:tabular-nums; cursor:pointer; background:transparent; color:inherit;
388
- border:1px solid color-mix(in srgb,var(--dx-ember) 45%,transparent);
437
+ border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 45%,transparent);
389
438
  border-radius:var(--dx-radius,0px); -webkit-tap-highlight-color:transparent;
390
439
  transition:filter .16s ease, box-shadow .16s ease, border-color .16s ease, background .16s ease, color .16s ease;
391
440
  }
392
- .dx-allow__chip:hover{ border-color:color-mix(in srgb,var(--dx-ember) 70%,transparent); filter:brightness(1.05); }
393
- .dx-allow__chip:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 34%,transparent); }
441
+ .dx-allow__chip:hover{ border-color:color-mix(in srgb,var(--dx-ember,#f26c18) 70%,transparent); filter:brightness(1.05); }
442
+ .dx-allow__chip:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember,#f26c18) 34%,transparent); }
394
443
  .dx-allow__chip--active{
395
444
  background:linear-gradient(135deg,var(--dx-ember,#f26c18),var(--dx-ember-2,#ba3a00));
396
- color:var(--dx-fg,#fff4ea); border-color:color-mix(in srgb,var(--dx-ember) 55%,transparent);
397
- box-shadow:0 10px 22px color-mix(in srgb,var(--dx-ember) 22%,transparent);
445
+ color:var(--dx-fg,#fff4ea); border-color:color-mix(in srgb,var(--dx-ember,#f26c18) 55%,transparent);
446
+ box-shadow:0 10px 22px color-mix(in srgb,var(--dx-ember,#f26c18) 22%,transparent);
398
447
  }
399
448
  .dx-allow__input{
400
449
  flex:1 1 130px; min-width:120px; padding:9px 13px; font:inherit; font-size:.9rem;
401
450
  font-variant-numeric:tabular-nums; color:inherit; background:transparent;
402
- border:1px solid color-mix(in srgb,var(--dx-ember) 45%,transparent);
451
+ border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 45%,transparent);
403
452
  border-radius:var(--dx-radius,0px); -webkit-tap-highlight-color:transparent;
404
453
  transition:border-color .16s ease, box-shadow .16s ease;
405
454
  }
406
455
  .dx-allow__input:focus, .dx-allow__input:focus-visible{
407
- outline:none; border-color:color-mix(in srgb,var(--dx-ember) 70%,transparent);
408
- box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 30%,transparent);
456
+ outline:none; border-color:color-mix(in srgb,var(--dx-ember,#f26c18) 70%,transparent);
457
+ box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember,#f26c18) 30%,transparent);
409
458
  }
410
459
  .dx-allow__input::placeholder{ color:currentColor; opacity:.5; letter-spacing:.02em; text-transform:none; }
411
460
 
412
461
  .dx-cwp{
413
- --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
414
462
  display:flex; flex-direction:column; gap:16px; font:inherit;
415
463
  }
416
464
  .dx-cwp__field{ display:flex; flex-direction:column; gap:8px; }
@@ -421,12 +469,12 @@ var CONSENT_CSS = `
421
469
  -webkit-tap-highlight-color:transparent; transition:border-color .16s ease, box-shadow .16s ease;
422
470
  }
423
471
  .dx-cwp__name:focus, .dx-cwp__name:focus-visible{
424
- outline:none; border-color:color-mix(in srgb,var(--dx-ember) 60%,transparent);
425
- box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 26%,transparent);
472
+ outline:none; border-color:color-mix(in srgb,var(--dx-ember,#f26c18) 60%,transparent);
473
+ box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember,#f26c18) 26%,transparent);
426
474
  }
427
475
  .dx-cwp__name::placeholder{ color:currentColor; opacity:.42; }
428
476
  .dx-cwp__fine{ font-size:.76rem; line-height:1.5; opacity:.66; }
429
- .dx-cwp__err{ font-size:.78rem; line-height:1.45; color:#e5552e; }
477
+ .dx-cwp__err{ font-size:.78rem; line-height:1.45; color:var(--dx-danger,#e5552e); }
430
478
  `;
431
479
  function ensureConsentStyles() {
432
480
  if (typeof document === "undefined") return;
@@ -38,6 +38,47 @@ interface SignInResult {
38
38
  * challenge → passkey (the OS prompt) → verifying → finalizing (create only).
39
39
  */
40
40
  type CeremonyPhase = 'challenge' | 'passkey' | 'verifying' | 'finalizing';
41
+ /**
42
+ * Vault identity on the wallet-only recover leg, as reported by
43
+ * /api/passkey-vault-anon/status. Narrower than ConnectVault on purpose —
44
+ * the status endpoint carries no publicKey/usdcAta, so a recover cannot
45
+ * construct a passkey signer; it re-points this browser at the wallet and
46
+ * lets useIdentity/useDexterWallet light the UI.
47
+ */
48
+ interface RecoverVault {
49
+ vaultPda: string;
50
+ /** Swig state address, base58 — the user-facing Dexter Wallet address. */
51
+ swigAddress: string;
52
+ /** Deposit address; null until the swig is deployed. */
53
+ receiveAddress: string | null;
54
+ isActivated: boolean;
55
+ walletLabel: string | null;
56
+ }
57
+ /**
58
+ * Result of a wallet-only recover ceremony. A discriminated RESULT, not a
59
+ * throw: user-cancel is a normal outcome in WebAuthn, and consumers branch on
60
+ * `reason` (no_credential → offer create; cancelled → stay silent; error →
61
+ * retry copy). `no_credential` covers both the immediate-mode instant
62
+ * rejection (no passkey on this device) and a verify 404 (a passkey the
63
+ * server has no row for).
64
+ */
65
+ type RecoverOutcome = {
66
+ ok: true;
67
+ userHandle: string;
68
+ credentialId: string;
69
+ vault: RecoverVault;
70
+ } | {
71
+ ok: false;
72
+ reason: 'no_credential' | 'cancelled' | 'error';
73
+ error?: ConnectError;
74
+ };
75
+ interface RecoverWalletConfig extends DexterConnectConfig {
76
+ /** Chrome-149+ immediate UI mode: instant fast-fail when this device has no
77
+ * discoverable passkey (no empty account-picker sheet). Falls back to the
78
+ * normal modal wherever unsupported. */
79
+ preferImmediate?: boolean;
80
+ onPhase?: (phase: CeremonyPhase) => void;
81
+ }
41
82
  interface DexterConnectConfig {
42
83
  /** dexter-api base. Default https://api.dexter.cash. */
43
84
  apiBase?: string;
@@ -229,4 +270,4 @@ declare function syncAcceptedPasskeys(args: {
229
270
  rpId?: string;
230
271
  }): Promise<boolean>;
231
272
 
232
- export { ACTIVE_WALLET_STORAGE_KEY as A, type CreateWalletResult as C, type DexterConnectConfig as D, type IdentityKind as I, type PasskeyLoginTokens as P, type ResolvedIdentity as R, type SignInResult as S, type CreateWalletConfig as a, type CeremonyPhase as b, type ConnectVault as c, ConnectError as d, type IdentityInput as e, type PasskeySignalSupport as f, SESSION_TTL_30D as g, type SpendPolicy as h, type StoredWallet as i, authoredPolicy as j, createWallet as k, ejectActiveWallet as l, forgetWallet as m, getActiveHandle as n, getCredentialId as o, listWallets as p, passkeySignalSupport as q, prunePasskey as r, renamePasskey as s, resolveIdentity as t, setActiveHandle as u, subscribe as v, switchWallet as w, syncAcceptedPasskeys as x, usdToAtomic as y };
273
+ export { ACTIVE_WALLET_STORAGE_KEY as A, syncAcceptedPasskeys as B, type CreateWalletResult as C, type DexterConnectConfig as D, usdToAtomic as E, type IdentityKind as I, type PasskeyLoginTokens as P, type RecoverWalletConfig as R, type SignInResult as S, type CreateWalletConfig as a, type CeremonyPhase as b, type ConnectVault as c, type RecoverOutcome as d, ConnectError as e, type IdentityInput as f, type PasskeySignalSupport as g, type RecoverVault as h, type ResolvedIdentity as i, SESSION_TTL_30D as j, type SpendPolicy as k, type StoredWallet as l, authoredPolicy as m, createWallet as n, ejectActiveWallet as o, forgetWallet as p, getActiveHandle as q, getCredentialId as r, listWallets as s, passkeySignalSupport as t, prunePasskey as u, renamePasskey as v, resolveIdentity as w, setActiveHandle as x, subscribe as y, switchWallet as z };
package/dist/worldid.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DexterButton,
3
3
  ensureDexterButtonStyles
4
- } from "./chunk-S7G2ZZOO.js";
4
+ } from "./chunk-2WETR5DJ.js";
5
5
 
6
6
  // src/worldid.tsx
7
7
  import { useEffect, useState } from "react";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dexterai/connect",
3
- "version": "0.19.0",
4
- "description": "Sign in with Dexter passkey connector. Composes @dexterai/vault.",
3
+ "version": "0.21.0",
4
+ "description": "Sign in with Dexter \u2014 passkey connector. Composes @dexterai/vault.",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {