@dexterai/connect 0.20.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 +33 -1
- package/dist/{chunk-2QZ5QLGI.js → chunk-MOC5AVY7.js} +190 -27
- package/dist/index.d.ts +5 -3
- package/dist/index.js +3 -1
- package/dist/react.d.ts +31 -2
- package/dist/react.js +46 -4
- package/dist/{signals-qc4rDyQ6.d.ts → signals-pjh3z6N8.d.ts} +42 -1
- package/package.json +2 -2
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
|
|
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
|
|
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
|
|
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-
|
|
2
|
-
export { A as ACTIVE_WALLET_STORAGE_KEY,
|
|
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-
|
|
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,
|
|
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". */
|
|
@@ -200,4 +229,4 @@ interface CreateWalletPanelProps {
|
|
|
200
229
|
/** The turnkey consent-at-birth create panel. */
|
|
201
230
|
declare function CreateWalletPanel(props: CreateWalletPanelProps): ReactElement;
|
|
202
231
|
|
|
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 };
|
|
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,12 +11,13 @@ 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-
|
|
20
|
+
} from "./chunk-MOC5AVY7.js";
|
|
20
21
|
import {
|
|
21
22
|
DexterButton,
|
|
22
23
|
DexterMark,
|
|
@@ -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(
|
|
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
|
|
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexterai/connect",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Sign in with Dexter
|
|
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
|
".": {
|