@dexterai/connect 0.1.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 ADDED
@@ -0,0 +1,16 @@
1
+ # @dexterai/connect
2
+
3
+ **Sign in with Dexter** — a passkey connector. Tap your face, you're in.
4
+
5
+ Composes [`@dexterai/vault`](https://www.npmjs.com/package/@dexterai/vault). Two entry points:
6
+
7
+ - `@dexterai/connect` — framework-free relay client + types
8
+ - `@dexterai/connect/react` — `<SignInWithDexter/>` + `useSignInWithDexter()`
9
+
10
+ Dependencies: `@dexterai/vault` + `react` (peer) **only**. No payment-layer peers — a
11
+ sign-in button must not inherit a payment graph.
12
+
13
+ ## Status
14
+
15
+ Scaffolding — Milestone 1. Build plan + frozen contracts:
16
+ `dexter-fe/docs/superpowers/plans/2026-06-20-signin-with-dexter-connect.md`.
@@ -0,0 +1,118 @@
1
+ // src/types.ts
2
+ var ConnectError = class extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message ?? code);
6
+ this.code = code;
7
+ this.name = "ConnectError";
8
+ }
9
+ };
10
+
11
+ // src/relay.ts
12
+ var DEFAULT_API_BASE = "https://api.dexter.cash";
13
+ var ANON_SIGN_BASE = "/api/passkey-anon/sign";
14
+ async function passkeyLogin(config = {}) {
15
+ const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
16
+ const options = await fetchLoginChallenge(apiBase);
17
+ const credential = await getAssertion(options);
18
+ return submitLogin(apiBase, credential);
19
+ }
20
+ async function fetchLoginChallenge(apiBase) {
21
+ const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/login-challenge`, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json" },
24
+ body: "{}"
25
+ });
26
+ if (!res.ok) {
27
+ throw new ConnectError("login_challenge_failed", `login-challenge ${res.status}`);
28
+ }
29
+ const data = await res.json();
30
+ if (!data?.options?.challenge) {
31
+ throw new ConnectError("login_challenge_malformed", "no challenge in response");
32
+ }
33
+ return data.options;
34
+ }
35
+ async function getAssertion(options) {
36
+ if (typeof navigator === "undefined" || !navigator.credentials) {
37
+ throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
38
+ }
39
+ let credential;
40
+ try {
41
+ credential = await navigator.credentials.get({
42
+ publicKey: {
43
+ challenge: base64urlToBytes(options.challenge).buffer.slice(0),
44
+ rpId: options.rpId,
45
+ timeout: options.timeout ?? 6e4,
46
+ userVerification: options.userVerification ?? "required"
47
+ // No allowCredentials — discoverable resident-key login.
48
+ }
49
+ });
50
+ } catch (err) {
51
+ throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
52
+ }
53
+ if (!credential || credential.type !== "public-key") {
54
+ throw new ConnectError("no_credential", "WebAuthn returned no credential");
55
+ }
56
+ return credential;
57
+ }
58
+ async function submitLogin(apiBase, credential) {
59
+ const assertion = credential.response;
60
+ const credentialJson = {
61
+ id: credential.id,
62
+ rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
63
+ type: credential.type,
64
+ response: {
65
+ clientDataJSON: bytesToBase64url(new Uint8Array(assertion.clientDataJSON)),
66
+ authenticatorData: bytesToBase64url(new Uint8Array(assertion.authenticatorData)),
67
+ signature: bytesToBase64url(new Uint8Array(assertion.signature)),
68
+ userHandle: assertion.userHandle ? bytesToBase64url(new Uint8Array(assertion.userHandle)) : null
69
+ },
70
+ clientExtensionResults: credential.getClientExtensionResults?.() ?? {}
71
+ };
72
+ const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/passkey-login`, {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({ credential: credentialJson })
76
+ });
77
+ if (!res.ok) {
78
+ throw new ConnectError(await readErrorCode(res), `passkey-login ${res.status}`);
79
+ }
80
+ const data = await res.json();
81
+ const session = {
82
+ accessToken: data.accessToken,
83
+ refreshToken: data.refreshToken,
84
+ expiresAt: data.expiresAt,
85
+ expiresIn: data.expiresIn,
86
+ tokenType: data.tokenType
87
+ };
88
+ return data.vault ? { session, vault: data.vault } : { session };
89
+ }
90
+ async function readErrorCode(res) {
91
+ try {
92
+ const body = await res.json();
93
+ if (body?.error) return body.error;
94
+ } catch {
95
+ }
96
+ return `http_${res.status}`;
97
+ }
98
+ function base64ToBytes(s) {
99
+ const bin = atob(s);
100
+ const out = new Uint8Array(bin.length);
101
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
102
+ return out;
103
+ }
104
+ function base64urlToBytes(s) {
105
+ const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
106
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
107
+ return base64ToBytes(b64);
108
+ }
109
+ function bytesToBase64url(bytes) {
110
+ let bin = "";
111
+ for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
112
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
113
+ }
114
+
115
+ export {
116
+ ConnectError,
117
+ passkeyLogin
118
+ };
@@ -0,0 +1,19 @@
1
+ import { D as DexterConnectConfig, S as SignInResult } from './types-DT3KPBOE.js';
2
+ export { C as ConnectError, a as ConnectVault, P as PasskeyLoginTokens } from './types-DT3KPBOE.js';
3
+
4
+ /**
5
+ * "Sign in with Dexter" — the discoverable-credential login ceremony.
6
+ *
7
+ * 1. POST /login-challenge → a server-issued challenge (no allow-list:
8
+ * the resident passkey itself identifies the user)
9
+ * 2. navigator.credentials.get over that challenge (no allowCredentials)
10
+ * 3. POST /passkey-login → the server resolves the credential + vault,
11
+ * verifies the assertion, and returns a Supabase session (+ the vault
12
+ * payload once vault-review ships the dexter-api change — ASK 1)
13
+ *
14
+ * Relays to dexter-api's ANON router — a first-time third-party user has no
15
+ * Supabase session, so the Supabase-gated router would 401.
16
+ */
17
+ declare function passkeyLogin(config?: DexterConnectConfig): Promise<SignInResult>;
18
+
19
+ export { DexterConnectConfig, SignInResult, passkeyLogin };
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ import {
2
+ ConnectError,
3
+ passkeyLogin
4
+ } from "./chunk-65VT7AU2.js";
5
+ export {
6
+ ConnectError,
7
+ passkeyLogin
8
+ };
@@ -0,0 +1,63 @@
1
+ import { S as SignInResult, P as PasskeyLoginTokens, a as ConnectVault, C as ConnectError } from './types-DT3KPBOE.js';
2
+ import { ReactElement } from 'react';
3
+
4
+ type ConnectStatus = 'idle' | 'pending' | 'done' | 'error';
5
+ interface UseSignInWithDexterConfig {
6
+ /** dexter-api base. Default https://api.dexter.cash. */
7
+ apiBase?: string;
8
+ /** RPC for the connected-chip balance read. Default: Dexter's Helius proxy. */
9
+ rpcUrl?: string;
10
+ }
11
+ interface UseSignInWithDexter {
12
+ status: ConnectStatus;
13
+ isVaultConnected: boolean;
14
+ /** Run the ceremony. Resolves with the result; throws ConnectError on failure
15
+ * (error is also captured in `error` + `status==='error'` for declarative UI). */
16
+ signIn: () => Promise<SignInResult>;
17
+ disconnect: () => void;
18
+ session: PasskeyLoginTokens | null;
19
+ vault: ConnectVault | null;
20
+ /** Dexter Wallet address (swigAddress, base58). */
21
+ vaultAddress: string | null;
22
+ vaultPda: string | null;
23
+ credentialId: string | null;
24
+ /** USD available. number once read; null = unknown → chip shows wallet only. */
25
+ usdcBalance: number | null;
26
+ refreshBalance: () => Promise<void>;
27
+ error: ConnectError | null;
28
+ }
29
+ /**
30
+ * "Sign in with Dexter" — React surface over the login ceremony.
31
+ *
32
+ * Returns the Supabase session (always) plus the vault identity + USD balance
33
+ * (vault-review's login payload is live). dexter.cash login needs only
34
+ * `session`; the vault fields + balance drive the connected chip. The
35
+ * passkeySigner (for opening x402 tabs — dexter-agents) lands next, on the
36
+ * anon ServerPolicy bridge over the now-live publicKey/credentialId.
37
+ */
38
+ declare function useSignInWithDexter(config?: UseSignInWithDexterConfig): UseSignInWithDexter;
39
+
40
+ interface SignInWithDexterProps extends UseSignInWithDexterConfig {
41
+ /** Fired with the result the moment sign-in completes. */
42
+ onSuccess?: (result: SignInResult) => void;
43
+ /** Fired with the typed error if the ceremony fails. */
44
+ onError?: (error: ConnectError) => void;
45
+ /** Button label when signed out. Default "Sign in with Dexter". */
46
+ label?: string;
47
+ /** className on the root (button when signed out, chip when connected) — for
48
+ * full restyling. Brand it from the consumer; the inline defaults are a base. */
49
+ className?: string;
50
+ /** Render the built-in connected chip (wallet + balance). Default true.
51
+ * Set false to render nothing once connected (consumer renders its own UI). */
52
+ showConnectedChip?: boolean;
53
+ }
54
+ /**
55
+ * Turnkey "Sign in with Dexter" element. Signed out → an ember button; signed
56
+ * in → a compact chip with the Dexter Wallet address + USD available. Wraps
57
+ * useSignInWithDexter; consumers who need the raw vault/passkey data should use
58
+ * that hook directly. Inline styles are a sensible default — restyle via
59
+ * className (Dexter Ember, no emojis, "unlock" banned per brand voice).
60
+ */
61
+ declare function SignInWithDexter(props: SignInWithDexterProps): ReactElement | null;
62
+
63
+ export { type ConnectStatus, SignInWithDexter, type SignInWithDexterProps, type UseSignInWithDexter, type UseSignInWithDexterConfig, useSignInWithDexter };
package/dist/react.js ADDED
@@ -0,0 +1,198 @@
1
+ import {
2
+ ConnectError,
3
+ passkeyLogin
4
+ } from "./chunk-65VT7AU2.js";
5
+
6
+ // src/useSignInWithDexter.ts
7
+ import { useCallback, useEffect, useState } from "react";
8
+
9
+ // src/balance.ts
10
+ async function fetchUsdcBalance(rpcUrl, usdcAta) {
11
+ try {
12
+ const res = await fetch(rpcUrl, {
13
+ method: "POST",
14
+ headers: { "content-type": "application/json" },
15
+ body: JSON.stringify({
16
+ jsonrpc: "2.0",
17
+ id: 1,
18
+ method: "getAccountInfo",
19
+ params: [usdcAta, { encoding: "base64", commitment: "confirmed" }]
20
+ })
21
+ });
22
+ if (!res.ok) return null;
23
+ const json = await res.json();
24
+ const value = json?.result?.value;
25
+ if (!value || !value.data) return 0;
26
+ const b64 = Array.isArray(value.data) ? value.data[0] : value.data;
27
+ const bytes = base64ToBytes(b64);
28
+ if (bytes.length < 72) return 0;
29
+ return Number(readU64LE(bytes, 64)) / 1e6;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ function readU64LE(bytes, offset) {
35
+ let v = 0n;
36
+ for (let i = 7; i >= 0; i -= 1) v = v << 8n | BigInt(bytes[offset + i]);
37
+ return v;
38
+ }
39
+ function base64ToBytes(s) {
40
+ const bin = atob(s);
41
+ const out = new Uint8Array(bin.length);
42
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
43
+ return out;
44
+ }
45
+
46
+ // src/useSignInWithDexter.ts
47
+ var DEFAULT_RPC = "https://api.dexter.cash/proxy/helius/rpc";
48
+ function useSignInWithDexter(config = {}) {
49
+ const { apiBase, rpcUrl = DEFAULT_RPC } = config;
50
+ const [status, setStatus] = useState("idle");
51
+ const [session, setSession] = useState(null);
52
+ const [vault, setVault] = useState(null);
53
+ const [usdcBalance, setUsdcBalance] = useState(null);
54
+ const [error, setError] = useState(null);
55
+ const refreshBalance = useCallback(async () => {
56
+ const ata = vault?.usdcAta;
57
+ if (!ata) return;
58
+ setUsdcBalance(await fetchUsdcBalance(rpcUrl, ata));
59
+ }, [vault, rpcUrl]);
60
+ const signIn = useCallback(async () => {
61
+ setError(null);
62
+ setStatus("pending");
63
+ try {
64
+ const result = await passkeyLogin(apiBase ? { apiBase } : {});
65
+ setSession(result.session);
66
+ setVault(result.vault ?? null);
67
+ setStatus("done");
68
+ return result;
69
+ } catch (err) {
70
+ const e = err instanceof ConnectError ? err : new ConnectError("sign_in_failed", String(err));
71
+ setError(e);
72
+ setStatus("error");
73
+ throw e;
74
+ }
75
+ }, [apiBase]);
76
+ const disconnect = useCallback(() => {
77
+ setSession(null);
78
+ setVault(null);
79
+ setUsdcBalance(null);
80
+ setError(null);
81
+ setStatus("idle");
82
+ }, []);
83
+ useEffect(() => {
84
+ void refreshBalance();
85
+ }, [refreshBalance]);
86
+ return {
87
+ status,
88
+ isVaultConnected: status === "done" && vault !== null,
89
+ signIn,
90
+ disconnect,
91
+ session,
92
+ vault,
93
+ vaultAddress: vault?.swigAddress ?? null,
94
+ vaultPda: vault?.vaultPda ?? null,
95
+ credentialId: vault?.credentialId ?? null,
96
+ usdcBalance,
97
+ refreshBalance,
98
+ error
99
+ };
100
+ }
101
+
102
+ // src/SignInWithDexter.tsx
103
+ import { jsx, jsxs } from "react/jsx-runtime";
104
+ function shortAddress(addr) {
105
+ return addr.length > 10 ? `${addr.slice(0, 4)}\u2026${addr.slice(-4)}` : addr;
106
+ }
107
+ function formatUsd(n) {
108
+ return n.toLocaleString("en-US", { style: "currency", currency: "USD" });
109
+ }
110
+ function SignInWithDexter(props) {
111
+ const {
112
+ onSuccess,
113
+ onError,
114
+ label = "Sign in with Dexter",
115
+ className,
116
+ showConnectedChip = true,
117
+ ...config
118
+ } = props;
119
+ const c = useSignInWithDexter(config);
120
+ const handleClick = async () => {
121
+ try {
122
+ onSuccess?.(await c.signIn());
123
+ } catch (err) {
124
+ onError?.(err);
125
+ }
126
+ };
127
+ if (c.isVaultConnected) {
128
+ if (!showConnectedChip) return null;
129
+ return /* @__PURE__ */ jsxs("span", { className, style: CHIP, children: [
130
+ /* @__PURE__ */ jsx("span", { style: DOT, "aria-hidden": true }),
131
+ /* @__PURE__ */ jsx("span", { children: c.vaultAddress ? shortAddress(c.vaultAddress) : "Connected" }),
132
+ c.usdcBalance !== null && /* @__PURE__ */ jsxs("span", { style: BALANCE, children: [
133
+ formatUsd(c.usdcBalance),
134
+ " available"
135
+ ] }),
136
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: c.disconnect, style: DISCONNECT, "aria-label": "Disconnect", children: "\xD7" })
137
+ ] });
138
+ }
139
+ return /* @__PURE__ */ jsx(
140
+ "button",
141
+ {
142
+ type: "button",
143
+ className,
144
+ onClick: handleClick,
145
+ disabled: c.status === "pending",
146
+ style: BUTTON,
147
+ children: c.status === "pending" ? "Signing in\u2026" : label
148
+ }
149
+ );
150
+ }
151
+ var EMBER = "#ef6820";
152
+ var BUTTON = {
153
+ display: "inline-flex",
154
+ alignItems: "center",
155
+ gap: 8,
156
+ padding: "10px 16px",
157
+ border: "none",
158
+ borderRadius: 6,
159
+ background: EMBER,
160
+ color: "#fff",
161
+ font: "inherit",
162
+ fontWeight: 600,
163
+ cursor: "pointer"
164
+ };
165
+ var CHIP = {
166
+ display: "inline-flex",
167
+ alignItems: "center",
168
+ gap: 8,
169
+ padding: "6px 10px",
170
+ borderRadius: 6,
171
+ border: "1px solid rgba(239,104,32,0.35)",
172
+ font: "inherit",
173
+ fontVariantNumeric: "tabular-nums"
174
+ };
175
+ var DOT = {
176
+ width: 7,
177
+ height: 7,
178
+ borderRadius: "50%",
179
+ background: EMBER
180
+ };
181
+ var BALANCE = {
182
+ fontWeight: 600,
183
+ opacity: 0.85
184
+ };
185
+ var DISCONNECT = {
186
+ marginLeft: 2,
187
+ border: "none",
188
+ background: "transparent",
189
+ color: "inherit",
190
+ cursor: "pointer",
191
+ fontSize: 16,
192
+ lineHeight: 1,
193
+ opacity: 0.6
194
+ };
195
+ export {
196
+ SignInWithDexter,
197
+ useSignInWithDexter
198
+ };
@@ -0,0 +1,45 @@
1
+ /** Supabase session tokens returned by dexter-api's passkey-login (camelCase). */
2
+ interface PasskeyLoginTokens {
3
+ accessToken: string;
4
+ refreshToken: string;
5
+ expiresAt: number;
6
+ expiresIn: number;
7
+ tokenType: string;
8
+ }
9
+ /**
10
+ * Vault identity, returned ALONGSIDE the session by passkey-login once
11
+ * vault-review ships the dexter-api change (ASK 1). Optional until then —
12
+ * the connector degrades to session-only. Consumers that open x402 tabs
13
+ * (dexter-agents) need `vaultPda` + `publicKey` to build a passkey signer.
14
+ */
15
+ interface ConnectVault {
16
+ vaultPda: string;
17
+ /** Swig state address, base58 — the user-facing Dexter Wallet address. */
18
+ swigAddress: string;
19
+ /** v2 swig wallet PDA (deposit address); null until the swig is deployed. */
20
+ receiveAddress: string | null;
21
+ /** Swig wallet's USDC ATA, base58 (for the connected-chip balance read);
22
+ * null until the swig is deployed. Server-resolved (off-curve-safe). */
23
+ usdcAta: string | null;
24
+ /** base64 33-byte SEC1 compressed P-256 authority pubkey (for the signer). */
25
+ publicKey: string;
26
+ userHandle: string;
27
+ credentialId: string;
28
+ }
29
+ /** Result of a completed "Sign in with Dexter" ceremony. */
30
+ interface SignInResult {
31
+ session: PasskeyLoginTokens;
32
+ /** Present once vault-review ships the vault-in-login change. */
33
+ vault?: ConnectVault;
34
+ }
35
+ interface DexterConnectConfig {
36
+ /** dexter-api base. Default https://api.dexter.cash. */
37
+ apiBase?: string;
38
+ }
39
+ /** Typed error whose `code` is the server's snake_case error string. */
40
+ declare class ConnectError extends Error {
41
+ readonly code: string;
42
+ constructor(code: string, message?: string);
43
+ }
44
+
45
+ export { ConnectError as C, type DexterConnectConfig as D, type PasskeyLoginTokens as P, type SignInResult as S, type ConnectVault as a };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@dexterai/connect",
3
+ "version": "0.1.0",
4
+ "description": "Sign in with Dexter — passkey connector. Composes @dexterai/vault.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "import": "./dist/index.js"
10
+ },
11
+ "./react": {
12
+ "types": "./dist/react.d.ts",
13
+ "import": "./dist/react.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "test": "vitest run",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "dependencies": {
25
+ "@dexterai/vault": "^0.14.1"
26
+ },
27
+ "peerDependencies": {
28
+ "react": ">=18"
29
+ },
30
+ "devDependencies": {
31
+ "@types/react": "^19.1.12",
32
+ "react": "^19.2.5",
33
+ "tsup": "^8.5.0",
34
+ "typescript": "^5.6.2",
35
+ "vitest": "^4.1.9"
36
+ }
37
+ }