@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 +119 -22
- package/dist/server.d.ts +72 -0
- package/dist/server.js +90 -0
- package/package.json +9 -4
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
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
56
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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` | `idle
|
|
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
|
-
##
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|
package/dist/server.d.ts
ADDED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
}
|