@dexterai/connect 0.20.0 → 0.22.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
@@ -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,
@@ -21,6 +21,9 @@ var BUTTON_CSS = `
21
21
  .dx-btn:disabled{ cursor:default; filter:saturate(.85) brightness(.98); }
22
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
23
  .dx-btn--secondary:hover{ background:color-mix(in srgb,var(--dx-ember,#f26c18) 9%,transparent); filter:none; box-shadow:none; }
24
+ .dx-btn--danger{ background:transparent; color:var(--dx-danger,#b3261e); box-shadow:none; border-color:color-mix(in srgb,var(--dx-danger,#b3261e) 45%,transparent); }
25
+ .dx-btn--danger:hover{ background:color-mix(in srgb,var(--dx-danger,#b3261e) 9%,transparent); filter:none; box-shadow:none; transform:none; }
26
+ .dx-btn--danger:focus-visible{ box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-danger,#b3261e) 38%,transparent); }
24
27
  .dx-btn--block{ width:100%; }
25
28
  .dx-btn__mark{ flex-shrink:0; }
26
29
  .dx-btn__spin{ width:15px; height:15px; flex-shrink:0; border-radius:50%;
@@ -76,17 +79,25 @@ function DexterButton(props) {
76
79
  onClick,
77
80
  disabled = false,
78
81
  className,
79
- type = "button"
82
+ type = "button",
83
+ ...rest
80
84
  } = props;
81
85
  useEffect(ensureDexterButtonStyles, []);
82
86
  return /* @__PURE__ */ jsx(
83
87
  "button",
84
88
  {
85
89
  type,
86
- className: cx("dx-btn", variant === "secondary" && "dx-btn--secondary", block && "dx-btn--block", className),
90
+ className: cx(
91
+ "dx-btn",
92
+ variant === "secondary" && "dx-btn--secondary",
93
+ variant === "danger" && "dx-btn--danger",
94
+ block && "dx-btn--block",
95
+ className
96
+ ),
87
97
  onClick,
88
98
  disabled: disabled || loading,
89
99
  "aria-busy": loading,
100
+ ...rest,
90
101
  children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
91
102
  /* @__PURE__ */ jsx("span", { className: "dx-btn__spin", "aria-hidden": true }),
92
103
  /* @__PURE__ */ jsx("span", { className: "dx-btn__doing", children: loadingLabel })
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,6 +1,7 @@
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';
3
- import { ReactElement, ReactNode } from 'react';
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';
4
+ import { ReactElement, ButtonHTMLAttributes, ReactNode } from 'react';
4
5
 
5
6
  type ConnectStatus = 'idle' | 'pending' | 'done' | 'error';
6
7
  interface UseSignInWithDexterConfig {
@@ -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". */
@@ -72,15 +101,16 @@ declare function SignInWithDexter(props: SignInWithDexterProps): ReactElement |
72
101
 
73
102
  /** The Dexter passkey mark — the brand glyph carried on the branded button. */
74
103
  declare function DexterMark(): ReactElement;
75
- interface DexterButtonProps {
104
+ interface DexterButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onClick' | 'disabled' | 'className' | 'type'> {
76
105
  /** Button content (e.g. "Sign in with Dexter", "Create your Dexter Wallet"). */
77
106
  children?: ReactNode;
78
107
  /** Loading state: shows the animated spinner + `loadingLabel`, disables click. */
79
108
  loading?: boolean;
80
109
  /** Label shown next to the spinner while loading. Default "Connecting…". */
81
110
  loadingLabel?: string;
82
- /** 'primary' = filled ember (default), 'secondary' = outline. */
83
- variant?: 'primary' | 'secondary';
111
+ /** 'primary' = filled ember (default), 'secondary' = outline,
112
+ * 'danger' = outline in the danger color for destructive actions. */
113
+ variant?: 'primary' | 'secondary' | 'danger';
84
114
  /** Full-width (fills its container). */
85
115
  block?: boolean;
86
116
  /** Render the Dexter mark before the children. Default true. */
@@ -200,4 +230,4 @@ interface CreateWalletPanelProps {
200
230
  /** The turnkey consent-at-birth create panel. */
201
231
  declare function CreateWalletPanel(props: CreateWalletPanelProps): ReactElement;
202
232
 
203
- 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 };
233
+ 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-2WETR5DJ.js";
26
+ } from "./chunk-O3KYGAKT.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: [
@@ -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-2WETR5DJ.js";
4
+ } from "./chunk-O3KYGAKT.js";
5
5
 
6
6
  // src/worldid.tsx
7
7
  import { useEffect, useState } from "react";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexterai/connect",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Sign in with Dexter — passkey connector. Composes @dexterai/vault.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -44,7 +44,7 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@dexterai/vault": "^0.30.0",
47
+ "@dexterai/vault": "^0.37.0",
48
48
  "@solana/web3.js": "^1.98.4",
49
49
  "@types/react": "^19.1.12",
50
50
  "@types/react-dom": "^19.2.3",