@dexterai/connect 0.17.0 → 0.18.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
@@ -5,7 +5,7 @@
5
5
  <h1 align="center">@dexterai/connect</h1>
6
6
 
7
7
  <p align="center">
8
- <strong>Sign in with Dexter passkey sign-in for any app. Tap your face, you're in: a non-custodial Dexter Wallet and its live USD balance, in one component.</strong>
8
+ <strong>Sign in with Dexter. One passkey tap gives any website a non-custodial Dexter Wallet, and gives any server an offline-verifiable session.</strong>
9
9
  </p>
10
10
 
11
11
  <p align="center">
@@ -18,23 +18,31 @@
18
18
 
19
19
  ## What this is
20
20
 
21
- A React connector that adds **"Sign in with Dexter"** to any app. One
22
- `<SignInWithDexter/>` button runs a discoverable passkey ceremony the user
23
- taps their face and you get back a session plus their non-custodial **Dexter
24
- Wallet** (address + live USD balance). No password, no seed phrase, no
25
- extension.
21
+ A `<SignInWithDexter/>` button runs a discoverable passkey ceremony. The user
22
+ taps their face, and your app gets back a session plus their **Dexter
23
+ Wallet**: address, live USD balance, and the rails for an agent to spend from
24
+ it under on-chain limits. The user holds their own keys; only their passkey
25
+ moves funds, enforced on-chain. Composes
26
+ [`@dexterai/vault`](https://www.npmjs.com/package/@dexterai/vault).
26
27
 
27
- The user holds their own keys. Nothing here is custodial — only the user's
28
- passkey moves funds, enforced on-chain. Composes
29
- [`@dexterai/vault`](https://www.npmjs.com/package/@dexterai/vault); the only
30
- peer is React.
28
+ Four entry points cover the whole flow:
29
+
30
+ | Entry point | Runs in | What it gives you |
31
+ |---|---|---|
32
+ | `@dexterai/connect` | browser | framework-free core: `passkeyLogin`, `createWallet`, `continueWithDexter`, the wallet store, agent-spend controls |
33
+ | `@dexterai/connect/react` | browser | `<SignInWithDexter/>`, the branded wallet kit, hooks |
34
+ | `@dexterai/connect/server` | Node 18+, Workers, Vercel edge | `verifyDexterSession`: offline session verification |
35
+ | `@dexterai/connect/worldid` | browser | `<VerifyPersonhood/>` World ID proof-of-personhood button |
31
36
 
32
37
  ## Install
33
38
 
34
39
  ```bash
35
- npm install @dexterai/connect react
40
+ npm install @dexterai/connect @dexterai/vault react
36
41
  ```
37
42
 
43
+ `@solana/web3.js` and `@worldcoin/idkit` are optional peers: the first for
44
+ agent-spend signing, the second only if you use the World ID button.
45
+
38
46
  ## Quick start
39
47
 
40
48
  ```tsx
@@ -52,39 +60,128 @@ function Header() {
52
60
  }
53
61
  ```
54
62
 
55
- Signed out, it renders a **Sign in with Dexter** button. On success it becomes
56
- a compact chip — the Dexter Wallet address + **"$X.XX available."**
63
+ Signed out, it renders a **Sign in with Dexter** button. Connected, it becomes
64
+ the wallet chip: address plus **"$X.XX available."**
57
65
 
58
- ## Hook (full control)
66
+ ## Works on any website
67
+
68
+ The ceremony is not limited to dexter.cash. On a foreign origin, `passkeyLogin`
69
+ opens a hosted popup on `dexter.cash/connect`, runs the ceremony there, and
70
+ posts the result back to your page (origin-checked and nonce-bound on both
71
+ sides). The default `transport: 'auto'` picks the right mode; `'popup'` and
72
+ `'inline'` force it.
73
+
74
+ ```ts
75
+ import { passkeyLogin } from '@dexterai/connect';
76
+
77
+ const { session, vault } = await passkeyLogin({ transport: 'auto' });
78
+ ```
59
79
 
60
- For your own UI, use the hook directly:
80
+ ## Verify the session on your server
81
+
82
+ ```ts
83
+ import { createDexterClient } from '@dexterai/connect/server';
84
+
85
+ const dexter = createDexterClient(); // parameterized on (iss, jwksUrl); defaults to Dexter's issuer
86
+
87
+ export async function handler(req: Request) {
88
+ const auth = await dexter.authenticateRequest(req);
89
+ if (!auth.isSignedIn) return new Response('unauthorized', { status: 401 });
90
+ auth.sub; // stable user id
91
+ auth.vaultAddress; // the Dexter Wallet address, from the signed dexter claim
92
+ auth.claims; // full verified JWT payload
93
+ }
94
+ ```
95
+
96
+ Verification is a local ES256 signature check against a cached JWKS. The first
97
+ call fetches the key set; every later call is pure local crypto with zero
98
+ network (measured at ~0.6ms). The algorithm list is pinned to ES256, and
99
+ issuer plus audience are always checked. `verifyDexterSession(token, options)`
100
+ does the same for a bare token string, and `jwtKey` accepts a public JWK for
101
+ fully networkless deployments.
102
+
103
+ ## Hook (full control)
61
104
 
62
105
  ```tsx
63
106
  import { useSignInWithDexter } from '@dexterai/connect/react';
64
107
 
65
108
  const c = useSignInWithDexter();
66
109
  await c.signIn(); // run the passkey ceremony
67
- c.status; // idle pending done error
110
+ c.status; // idle -> pending -> done -> error
68
111
  c.vaultAddress; // the Dexter Wallet address (base58)
69
112
  c.usdcBalance; // USD available (via Dexter's RPC), or null
70
113
  c.disconnect();
71
114
  ```
72
115
 
73
- ## What `useSignInWithDexter()` gives you
74
-
75
116
  | Field | What it is |
76
117
  |---|---|
77
118
  | `signIn()` / `disconnect()` | run the passkey ceremony / clear state |
78
- | `status` / `isVaultConnected` | `idlependingdoneerror` / connected flag |
119
+ | `status` / `isVaultConnected` | `idle->pending->done->error` / connected flag |
79
120
  | `session` | auth session tokens (camelCase) |
80
121
  | `vaultAddress` / `vaultPda` | the Dexter Wallet address / PDA |
81
122
  | `usdcBalance` / `refreshBalance()` | USD available, best-effort via Dexter's RPC |
82
123
  | `vault` / `credentialId` / `error` | raw vault payload / credential id / typed error |
83
124
 
84
- ## Exports
125
+ ## The wallet kit
126
+
127
+ Branded, presentational pieces that share one implementation across every
128
+ Dexter surface, themed with `--dx-*` CSS variables: `DexterButton` (and
129
+ `DexterMark`) for any action that should look like Dexter, `DexterWalletChip`
130
+ as the header trigger, `DexterWalletMenu` for manage / save / start-fresh, and
131
+ the `useDexterWallet` + `useIdentity` hooks to drive them.
132
+
133
+ ## Agent spend
134
+
135
+ The control surface for letting an agent spend from the connected wallet:
136
+
137
+ ```ts
138
+ import {
139
+ assembleAgentSpendStatus, // honest two-mode status read
140
+ enableAgentSpend, // the on switch
141
+ revokeAgentSpend, // the off switch
142
+ createPasskeySigner, // @dexterai/vault guest signer for x402 / tab flows
143
+ } from '@dexterai/connect';
144
+ ```
145
+
146
+ The verbs are framework-free and take `apiOrigin` as a parameter; the SDK
147
+ reads no environment variables.
148
+
149
+ ## World ID
150
+
151
+ ```tsx
152
+ import { VerifyPersonhood } from '@dexterai/connect/worldid';
153
+
154
+ <VerifyPersonhood onSuccess={(proof) => sendToYourVerifier(proof)} />
155
+ ```
85
156
 
86
- - `@dexterai/connect` framework-free: `passkeyLogin()`, `ConnectError`, types.
87
- - `@dexterai/connect/react` — `<SignInWithDexter/>`, `useSignInWithDexter()`.
157
+ `useVerifyPersonhood` is the headless version. Requires the optional
158
+ `@worldcoin/idkit` peer.
159
+
160
+ ## Wallet lifecycle
161
+
162
+ - `createWallet` mints a brand-new named passkey + vault; `passkeyLogin` signs
163
+ an existing one in; `continueWithDexter` resumes a known wallet.
164
+ - The **wallet store** (`getActiveHandle`, `listWallets`, `switchWallet`,
165
+ `ejectActiveWallet`, `forgetWallet`, `subscribeWallet`) is the canonical
166
+ owner of the active-wallet handle. Read and write through it rather than
167
+ touching localStorage.
168
+ - The **WebAuthn Signal API** helpers (`renamePasskey`, `prunePasskey`,
169
+ `syncAcceptedPasskeys`, `passkeySignalSupport`) keep the OS keychain in sync
170
+ where the browser supports it.
171
+ - `resolveIdentity` combines the wallet handle with whatever account token you
172
+ pass in; `ceremonyPhaseLabel` gives shared copy for connecting-step UI;
173
+ `createAnonServerPolicy` builds the anonymous server policy for the signer.
174
+
175
+ ## Peer dependencies
176
+
177
+ | Peer | Required | Why |
178
+ |---|---|---|
179
+ | `react` >=18 | yes | the `/react` and `/worldid` surfaces |
180
+ | `@dexterai/vault` >=0.30 | yes | signer + agent-spend message builders (0.30 matches the deployed program's account layout) |
181
+ | `@solana/web3.js` | optional | agent-spend signing paths |
182
+ | `@worldcoin/idkit` | optional | only for `/worldid` |
183
+
184
+ The `/server` entry has none of these peers; it depends only on `jose`.
88
185
 
89
186
  ## License
90
187
 
@@ -0,0 +1,72 @@
1
+ import { JWTPayload, JWK, JSONWebKeySet } from 'jose';
2
+
3
+ /**
4
+ * @dexterai/connect/server — offline Dexter session verification.
5
+ *
6
+ * The server half of Sign in with Dexter (CONTRACT-dexter-session-token.md):
7
+ * a local ES256 signature check against the published JWKS — no call to
8
+ * Supabase or dexter-api on the hot path, so it runs on Node and edge
9
+ * runtimes alike. Pre-hook tokens (no `dexter` claim) verify as signed-in
10
+ * with `vaultAddress: null`; once the access-token hook is enabled the
11
+ * claim appears with no code change here.
12
+ *
13
+ * Phase 1 issuer is Supabase (CONTRACT §3); everything is parameterized on
14
+ * (iss, jwksUrl) so the Phase-2 sovereign cutover is a config flip.
15
+ */
16
+
17
+ declare const DEFAULT_ISS = "https://qdgumpoqnthrjfmqziwm.supabase.co/auth/v1";
18
+ declare const DEFAULT_AUDIENCE = "authenticated";
19
+ /** The namespaced claim sealed into the token by the access-token hook. */
20
+ interface DexterClaim {
21
+ ver: number;
22
+ /** Swig state address (base58) — the canonical Dexter Wallet identity. */
23
+ vault: string;
24
+ /** 16-byte passkey handle, base64url; absent on rows without one. */
25
+ userHandle?: string;
26
+ agentGrant?: unknown;
27
+ }
28
+ type VerifyFailureReason = 'no_token' | 'invalid' | 'expired' | 'issuer_mismatch' | 'audience_mismatch';
29
+ type DexterSession = {
30
+ isSignedIn: true;
31
+ sub: string;
32
+ vaultAddress: string | null;
33
+ userHandle: string | null;
34
+ agentGrant: unknown;
35
+ sessionId: string | null;
36
+ aal: string | null;
37
+ claims: JWTPayload & {
38
+ dexter?: DexterClaim;
39
+ };
40
+ } | {
41
+ isSignedIn: false;
42
+ reason: VerifyFailureReason;
43
+ };
44
+ interface VerifyOptions {
45
+ /** Expected issuer. Phase 1 default: the Dexter Supabase project. */
46
+ iss?: string;
47
+ /** JWKS location; defaults to `${iss}/.well-known/jwks.json`. */
48
+ jwksUrl?: string;
49
+ /**
50
+ * Public key(s) for fully networkless verification (a JWK or a JWKS).
51
+ * When omitted, the JWKS is fetched once and cached in-instance.
52
+ */
53
+ jwtKey?: JWK | JSONWebKeySet;
54
+ /** Expected audience. Default: Supabase's `authenticated`. */
55
+ audience?: string;
56
+ }
57
+ /** A fetch-API Request or anything with a node-style headers bag. */
58
+ type RequestLike = Request | {
59
+ headers: Record<string, string | string[] | undefined>;
60
+ };
61
+ interface DexterClient {
62
+ verifyDexterSession(token: string): Promise<DexterSession>;
63
+ authenticateRequest(req: RequestLike): Promise<DexterSession>;
64
+ }
65
+ declare function createDexterClient(options?: VerifyOptions): DexterClient;
66
+ /**
67
+ * One-off verification. For servers verifying many requests against a
68
+ * remote JWKS, create a client once instead so the key set caches.
69
+ */
70
+ declare function verifyDexterSession(token: string, options?: VerifyOptions): Promise<DexterSession>;
71
+
72
+ export { DEFAULT_AUDIENCE, DEFAULT_ISS, type DexterClaim, type DexterClient, type DexterSession, type RequestLike, type VerifyFailureReason, type VerifyOptions, createDexterClient, verifyDexterSession };
package/dist/server.js ADDED
@@ -0,0 +1,90 @@
1
+ // src/server.ts
2
+ import {
3
+ createLocalJWKSet,
4
+ createRemoteJWKSet,
5
+ jwtVerify,
6
+ errors as joseErrors
7
+ } from "jose";
8
+ var DEFAULT_ISS = "https://qdgumpoqnthrjfmqziwm.supabase.co/auth/v1";
9
+ var DEFAULT_AUDIENCE = "authenticated";
10
+ function buildGetKey(opts) {
11
+ if (opts.jwtKey) {
12
+ const set = "keys" in opts.jwtKey ? opts.jwtKey : { keys: [opts.jwtKey] };
13
+ return createLocalJWKSet(set);
14
+ }
15
+ const iss = opts.iss ?? DEFAULT_ISS;
16
+ const url = new URL(opts.jwksUrl ?? `${iss}/.well-known/jwks.json`);
17
+ return createRemoteJWKSet(url, {
18
+ cacheMaxAge: 6e5,
19
+ cooldownDuration: 3e4,
20
+ timeoutDuration: 5e3
21
+ });
22
+ }
23
+ function failureReason(err) {
24
+ if (err instanceof joseErrors.JWTExpired) return "expired";
25
+ if (err instanceof joseErrors.JWTClaimValidationFailed) {
26
+ if (err.claim === "iss") return "issuer_mismatch";
27
+ if (err.claim === "aud") return "audience_mismatch";
28
+ if (err.claim === "exp") return "expired";
29
+ }
30
+ return "invalid";
31
+ }
32
+ function bearerFrom(req) {
33
+ let raw;
34
+ const headers = req.headers;
35
+ if (headers && typeof headers.get === "function") {
36
+ raw = headers.get("authorization");
37
+ } else {
38
+ const bag = headers;
39
+ raw = bag.authorization ?? bag.Authorization;
40
+ }
41
+ const value = Array.isArray(raw) ? raw[0] : raw;
42
+ if (!value) return null;
43
+ const match = /^Bearer\s+(.+)$/i.exec(value);
44
+ return match ? match[1] : null;
45
+ }
46
+ function createDexterClient(options = {}) {
47
+ const iss = options.iss ?? DEFAULT_ISS;
48
+ const audience = options.audience ?? DEFAULT_AUDIENCE;
49
+ const getKey = buildGetKey(options);
50
+ async function verify(token) {
51
+ try {
52
+ const { payload } = await jwtVerify(token, getKey, {
53
+ issuer: iss,
54
+ audience,
55
+ algorithms: ["ES256"]
56
+ // pinned — defeats alg-confusion / alg:none
57
+ });
58
+ const dexter = payload.dexter;
59
+ return {
60
+ isSignedIn: true,
61
+ sub: payload.sub ?? "",
62
+ vaultAddress: dexter?.vault ?? null,
63
+ userHandle: dexter?.userHandle ?? null,
64
+ agentGrant: dexter?.agentGrant ?? null,
65
+ sessionId: payload.session_id ?? null,
66
+ aal: payload.aal ?? null,
67
+ claims: payload
68
+ };
69
+ } catch (err) {
70
+ return { isSignedIn: false, reason: failureReason(err) };
71
+ }
72
+ }
73
+ return {
74
+ verifyDexterSession: verify,
75
+ async authenticateRequest(req) {
76
+ const token = bearerFrom(req);
77
+ if (!token) return { isSignedIn: false, reason: "no_token" };
78
+ return verify(token);
79
+ }
80
+ };
81
+ }
82
+ function verifyDexterSession(token, options = {}) {
83
+ return createDexterClient(options).verifyDexterSession(token);
84
+ }
85
+ export {
86
+ DEFAULT_AUDIENCE,
87
+ DEFAULT_ISS,
88
+ createDexterClient,
89
+ verifyDexterSession
90
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexterai/connect",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "Sign in with Dexter — passkey connector. Composes @dexterai/vault.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,6 +15,10 @@
15
15
  "./worldid": {
16
16
  "types": "./dist/worldid.d.ts",
17
17
  "import": "./dist/worldid.js"
18
+ },
19
+ "./server": {
20
+ "types": "./dist/server.d.ts",
21
+ "import": "./dist/server.js"
18
22
  }
19
23
  },
20
24
  "files": [
@@ -26,7 +30,7 @@
26
30
  "typecheck": "tsc --noEmit"
27
31
  },
28
32
  "peerDependencies": {
29
- "@dexterai/vault": ">=0.22",
33
+ "@dexterai/vault": ">=0.30.0",
30
34
  "@solana/web3.js": "^1.98.0",
31
35
  "@worldcoin/idkit": "^4.1.8",
32
36
  "react": ">=18"
@@ -40,7 +44,7 @@
40
44
  }
41
45
  },
42
46
  "devDependencies": {
43
- "@dexterai/vault": "^0.24.0",
47
+ "@dexterai/vault": "^0.30.0",
44
48
  "@solana/web3.js": "^1.98.4",
45
49
  "@types/react": "^19.1.12",
46
50
  "@worldcoin/idkit": "^4.1.8",
@@ -50,6 +54,7 @@
50
54
  "vitest": "^4.1.9"
51
55
  },
52
56
  "dependencies": {
53
- "@simplewebauthn/browser": "^13.3.0"
57
+ "@simplewebauthn/browser": "^13.3.0",
58
+ "jose": "^6.2.3"
54
59
  }
55
60
  }