@dexterai/connect 0.16.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,107 @@
1
+ // src/DexterButton.tsx
2
+ import { useEffect } from "react";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ var STYLE_ID = "dexter-connect-button-styles";
5
+ var BUTTON_CSS = `
6
+ @keyframes dx-spin { to { transform: rotate(360deg); } }
7
+ @keyframes dx-pulse { 0%,100% { opacity: 1; } 50% { opacity: .6; } }
8
+ .dx-btn{
9
+ --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
10
+ position:relative; display:inline-flex; align-items:center; justify-content:center; gap:10px;
11
+ padding:11px 22px; border:1px solid color-mix(in srgb,var(--dx-ember) 55%,transparent);
12
+ border-radius:var(--dx-radius);
13
+ background:linear-gradient(135deg,var(--dx-ember),var(--dx-ember-2));
14
+ color:var(--dx-fg); font:inherit; font-weight:600; font-size:.78rem; letter-spacing:.12em;
15
+ text-transform:uppercase; cursor:pointer; -webkit-tap-highlight-color:transparent;
16
+ box-shadow:0 14px 26px color-mix(in srgb,var(--dx-ember) 24%,transparent);
17
+ transition:transform .16s ease, box-shadow .16s ease, filter .16s ease, background .16s ease;
18
+ }
19
+ .dx-btn:hover{ transform:translateY(-1px); filter:brightness(1.07); box-shadow:0 20px 34px color-mix(in srgb,var(--dx-ember) 32%,transparent); }
20
+ .dx-btn:active{ transform:translateY(0); filter:brightness(.97); box-shadow:0 8px 16px color-mix(in srgb,var(--dx-ember) 22%,transparent); }
21
+ .dx-btn:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 38%,transparent); }
22
+ .dx-btn:disabled{ cursor:default; filter:saturate(.85) brightness(.98); }
23
+ .dx-btn--secondary{ background:transparent; color:var(--dx-ember); box-shadow:none; border-color:color-mix(in srgb,var(--dx-ember) 45%,transparent); }
24
+ .dx-btn--secondary:hover{ background:color-mix(in srgb,var(--dx-ember) 9%,transparent); filter:none; box-shadow:none; }
25
+ .dx-btn--block{ width:100%; }
26
+ .dx-btn__mark{ flex-shrink:0; }
27
+ .dx-btn__spin{ width:15px; height:15px; flex-shrink:0; border-radius:50%;
28
+ border:2px solid color-mix(in srgb,currentColor 30%,transparent); border-top-color:currentColor;
29
+ animation:dx-spin .7s linear infinite; }
30
+ .dx-btn__doing{ animation:dx-pulse 1.4s ease-in-out infinite; }
31
+ .dx-chip{ display:inline-flex; align-items:center; gap:8px; padding:6px 10px; font:inherit;
32
+ font-variant-numeric:tabular-nums; border-radius:var(--dx-radius,0);
33
+ border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 35%,transparent); }
34
+ .dx-chip__dot{ width:7px; height:7px; border-radius:50%; background:var(--dx-ember,#f26c18); }
35
+ .dx-chip__bal{ font-weight:600; opacity:.85; }
36
+ .dx-chip__x{ margin-left:2px; border:none; background:transparent; color:inherit; cursor:pointer; font-size:16px; line-height:1; opacity:.6; }
37
+ .dx-chip__x:hover{ opacity:1; }
38
+ `;
39
+ function ensureDexterButtonStyles() {
40
+ if (typeof document === "undefined") return;
41
+ if (document.getElementById(STYLE_ID)) return;
42
+ const el = document.createElement("style");
43
+ el.id = STYLE_ID;
44
+ el.textContent = BUTTON_CSS;
45
+ document.head.appendChild(el);
46
+ }
47
+ ensureDexterButtonStyles();
48
+ function cx(...parts) {
49
+ return parts.filter(Boolean).join(" ");
50
+ }
51
+ function DexterMark() {
52
+ return /* @__PURE__ */ jsxs(
53
+ "svg",
54
+ {
55
+ className: "dx-btn__mark",
56
+ width: "18",
57
+ height: "18",
58
+ viewBox: "0 0 300 300",
59
+ fill: "currentColor",
60
+ "aria-hidden": "true",
61
+ children: [
62
+ /* @__PURE__ */ jsx("path", { d: "M143.18,22.65c35.41,7.66,68.19,23.6,94.89,48.28,5.22,4.86,11.17,10.45,15.18,16.1,1.38,1.93,1.94,3.6.99,5.23-1.08,1.92-4.22,3.41-6.56,4.17-28.39,9.43-61.55,8.26-88.62-4.69-13.81-7.66-17.02-5.76-31.67-3.48-21.89,2.38-46.67.37-65.06-12.31-6.07-4.99-9.33-12.71-8.8-20.52-.16-9.4,4.25-18.12,11.47-24.06,21.29-17.85,52.64-14.45,78-8.77l.18.04h0Z" }),
63
+ /* @__PURE__ */ jsx("path", { d: "M46.08,129.98c1.06-1.03,3.52-1.07,5.29-1.04,48.98-.05,98.1-.06,146.83-.1,17.53.14,35.01-.31,52.49.18,2.13.18,3.89.74,4.73,2.05,1.46,2.38.35,6.09-1.98,7.6-3.66,2.05-8.62,1.33-12.86,1.74-2.85.12-5.45.13-7.02,2.02-.91,1.07-1.28,2.56-1.56,3.95-.57,3.23-1.16,6.52-1.89,9.62-2.81,12.43-8.68,24.65-19.76,31.56-9.49,5.59-20.42,6.86-31.2,5.75-11.88-1.69-22.15-8.81-29.11-18.28-3.51-4.81-4.92-10.5-5.8-16.29-.47-2.56-.51-5.87-1.35-8-1.16-3.38-6.14-2.59-9.25-1.92-4.21.95-4.39,5.7-5.14,9.19-2.25,11.18-6.84,20.68-16.15,27.65-1.31,1.05-2.91,2.03-2.12,3.66,2.5,3.21,6.65,4.49,10.44,5.97,3.26,1.17,6.86,2.41,7.18,6.06.05,8.18-11.97,3.46-16.32,1.85-3.95-1.55-7.4-4.27-10.42-7.26-3.92-4.28-9.66-4.5-15.16-4.45-3.45-.07-6.99-.19-10.45-.82-21.29-4-31.08-21.3-30.9-42.01-.08-4.63.03-9.32.09-13.91.04-1.69.07-3.46,1.28-4.67l.1-.09h0Z" }),
64
+ /* @__PURE__ */ jsx("path", { d: "M173.06,203.11c9.24-.06,21.6,4.49,22.85,14.84.3,2.12-.67,4.34-2.92,4.73-1.38.29-2.88-.05-4.09-.75-1.64-.9-2.97-2.86-4.19-3.05-1.33-.2-1.99.81-2.93,1.94-10.27,13.34-28.04,20.92-44.83,20.42-15.41-.33-31.89-5.95-43.53-17.34-3.15-3.39-1.55-9.88,3.61-9.19,1.83.32,3.29,1.45,4.76,2.65,10.49,10.12,24.85,14.04,39.12,13.03,10.55-1.23,20.38-5.47,28.74-11.92,1.11-1.06,4.45-3.63,3.5-5.12-.76-.85-4.31-.47-5.92-2.01-2.25-1.92-1.39-6.22,1.16-7.36,1.36-.65,2.96-.81,4.47-.86h.2,0Z" })
65
+ ]
66
+ }
67
+ );
68
+ }
69
+ function DexterButton(props) {
70
+ const {
71
+ children,
72
+ loading = false,
73
+ loadingLabel = "Connecting\u2026",
74
+ variant = "primary",
75
+ block = false,
76
+ withMark = true,
77
+ onClick,
78
+ disabled = false,
79
+ className,
80
+ type = "button"
81
+ } = props;
82
+ useEffect(ensureDexterButtonStyles, []);
83
+ return /* @__PURE__ */ jsx(
84
+ "button",
85
+ {
86
+ type,
87
+ className: cx("dx-btn", variant === "secondary" && "dx-btn--secondary", block && "dx-btn--block", className),
88
+ onClick,
89
+ disabled: disabled || loading,
90
+ "aria-busy": loading,
91
+ children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
92
+ /* @__PURE__ */ jsx("span", { className: "dx-btn__spin", "aria-hidden": true }),
93
+ /* @__PURE__ */ jsx("span", { className: "dx-btn__doing", children: loadingLabel })
94
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
95
+ withMark && /* @__PURE__ */ jsx(DexterMark, {}),
96
+ children
97
+ ] })
98
+ }
99
+ );
100
+ }
101
+
102
+ export {
103
+ ensureDexterButtonStyles,
104
+ cx,
105
+ DexterMark,
106
+ DexterButton
107
+ };
package/dist/react.js CHANGED
@@ -15,6 +15,12 @@ import {
15
15
  subscribe,
16
16
  switchWallet
17
17
  } from "./chunk-Y22JFT53.js";
18
+ import {
19
+ DexterButton,
20
+ DexterMark,
21
+ cx,
22
+ ensureDexterButtonStyles
23
+ } from "./chunk-S7G2ZZOO.js";
18
24
 
19
25
  // src/useSignInWithDexter.ts
20
26
  import { useCallback, useEffect, useMemo, useState } from "react";
@@ -123,111 +129,8 @@ function useSignInWithDexter(config = {}) {
123
129
  }
124
130
 
125
131
  // src/SignInWithDexter.tsx
126
- import { useEffect as useEffect3 } from "react";
127
-
128
- // src/DexterButton.tsx
129
132
  import { useEffect as useEffect2 } from "react";
130
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
131
- var STYLE_ID = "dexter-connect-button-styles";
132
- var BUTTON_CSS = `
133
- @keyframes dx-spin { to { transform: rotate(360deg); } }
134
- @keyframes dx-pulse { 0%,100% { opacity: 1; } 50% { opacity: .6; } }
135
- .dx-btn{
136
- --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
137
- position:relative; display:inline-flex; align-items:center; justify-content:center; gap:10px;
138
- padding:11px 22px; border:1px solid color-mix(in srgb,var(--dx-ember) 55%,transparent);
139
- border-radius:var(--dx-radius);
140
- background:linear-gradient(135deg,var(--dx-ember),var(--dx-ember-2));
141
- color:var(--dx-fg); font:inherit; font-weight:600; font-size:.78rem; letter-spacing:.12em;
142
- text-transform:uppercase; cursor:pointer; -webkit-tap-highlight-color:transparent;
143
- box-shadow:0 14px 26px color-mix(in srgb,var(--dx-ember) 24%,transparent);
144
- transition:transform .16s ease, box-shadow .16s ease, filter .16s ease, background .16s ease;
145
- }
146
- .dx-btn:hover{ transform:translateY(-1px); filter:brightness(1.07); box-shadow:0 20px 34px color-mix(in srgb,var(--dx-ember) 32%,transparent); }
147
- .dx-btn:active{ transform:translateY(0); filter:brightness(.97); box-shadow:0 8px 16px color-mix(in srgb,var(--dx-ember) 22%,transparent); }
148
- .dx-btn:focus-visible{ outline:none; box-shadow:0 0 0 3px color-mix(in srgb,var(--dx-ember) 38%,transparent); }
149
- .dx-btn:disabled{ cursor:default; filter:saturate(.85) brightness(.98); }
150
- .dx-btn--secondary{ background:transparent; color:var(--dx-ember); box-shadow:none; border-color:color-mix(in srgb,var(--dx-ember) 45%,transparent); }
151
- .dx-btn--secondary:hover{ background:color-mix(in srgb,var(--dx-ember) 9%,transparent); filter:none; box-shadow:none; }
152
- .dx-btn--block{ width:100%; }
153
- .dx-btn__mark{ flex-shrink:0; }
154
- .dx-btn__spin{ width:15px; height:15px; flex-shrink:0; border-radius:50%;
155
- border:2px solid color-mix(in srgb,currentColor 30%,transparent); border-top-color:currentColor;
156
- animation:dx-spin .7s linear infinite; }
157
- .dx-btn__doing{ animation:dx-pulse 1.4s ease-in-out infinite; }
158
- .dx-chip{ display:inline-flex; align-items:center; gap:8px; padding:6px 10px; font:inherit;
159
- font-variant-numeric:tabular-nums; border-radius:var(--dx-radius,0);
160
- border:1px solid color-mix(in srgb,var(--dx-ember,#f26c18) 35%,transparent); }
161
- .dx-chip__dot{ width:7px; height:7px; border-radius:50%; background:var(--dx-ember,#f26c18); }
162
- .dx-chip__bal{ font-weight:600; opacity:.85; }
163
- .dx-chip__x{ margin-left:2px; border:none; background:transparent; color:inherit; cursor:pointer; font-size:16px; line-height:1; opacity:.6; }
164
- .dx-chip__x:hover{ opacity:1; }
165
- `;
166
- function ensureDexterButtonStyles() {
167
- if (typeof document === "undefined") return;
168
- if (document.getElementById(STYLE_ID)) return;
169
- const el = document.createElement("style");
170
- el.id = STYLE_ID;
171
- el.textContent = BUTTON_CSS;
172
- document.head.appendChild(el);
173
- }
174
- ensureDexterButtonStyles();
175
- function cx(...parts) {
176
- return parts.filter(Boolean).join(" ");
177
- }
178
- function DexterMark() {
179
- return /* @__PURE__ */ jsxs(
180
- "svg",
181
- {
182
- className: "dx-btn__mark",
183
- width: "18",
184
- height: "18",
185
- viewBox: "0 0 300 300",
186
- fill: "currentColor",
187
- "aria-hidden": "true",
188
- children: [
189
- /* @__PURE__ */ jsx("path", { d: "M143.18,22.65c35.41,7.66,68.19,23.6,94.89,48.28,5.22,4.86,11.17,10.45,15.18,16.1,1.38,1.93,1.94,3.6.99,5.23-1.08,1.92-4.22,3.41-6.56,4.17-28.39,9.43-61.55,8.26-88.62-4.69-13.81-7.66-17.02-5.76-31.67-3.48-21.89,2.38-46.67.37-65.06-12.31-6.07-4.99-9.33-12.71-8.8-20.52-.16-9.4,4.25-18.12,11.47-24.06,21.29-17.85,52.64-14.45,78-8.77l.18.04h0Z" }),
190
- /* @__PURE__ */ jsx("path", { d: "M46.08,129.98c1.06-1.03,3.52-1.07,5.29-1.04,48.98-.05,98.1-.06,146.83-.1,17.53.14,35.01-.31,52.49.18,2.13.18,3.89.74,4.73,2.05,1.46,2.38.35,6.09-1.98,7.6-3.66,2.05-8.62,1.33-12.86,1.74-2.85.12-5.45.13-7.02,2.02-.91,1.07-1.28,2.56-1.56,3.95-.57,3.23-1.16,6.52-1.89,9.62-2.81,12.43-8.68,24.65-19.76,31.56-9.49,5.59-20.42,6.86-31.2,5.75-11.88-1.69-22.15-8.81-29.11-18.28-3.51-4.81-4.92-10.5-5.8-16.29-.47-2.56-.51-5.87-1.35-8-1.16-3.38-6.14-2.59-9.25-1.92-4.21.95-4.39,5.7-5.14,9.19-2.25,11.18-6.84,20.68-16.15,27.65-1.31,1.05-2.91,2.03-2.12,3.66,2.5,3.21,6.65,4.49,10.44,5.97,3.26,1.17,6.86,2.41,7.18,6.06.05,8.18-11.97,3.46-16.32,1.85-3.95-1.55-7.4-4.27-10.42-7.26-3.92-4.28-9.66-4.5-15.16-4.45-3.45-.07-6.99-.19-10.45-.82-21.29-4-31.08-21.3-30.9-42.01-.08-4.63.03-9.32.09-13.91.04-1.69.07-3.46,1.28-4.67l.1-.09h0Z" }),
191
- /* @__PURE__ */ jsx("path", { d: "M173.06,203.11c9.24-.06,21.6,4.49,22.85,14.84.3,2.12-.67,4.34-2.92,4.73-1.38.29-2.88-.05-4.09-.75-1.64-.9-2.97-2.86-4.19-3.05-1.33-.2-1.99.81-2.93,1.94-10.27,13.34-28.04,20.92-44.83,20.42-15.41-.33-31.89-5.95-43.53-17.34-3.15-3.39-1.55-9.88,3.61-9.19,1.83.32,3.29,1.45,4.76,2.65,10.49,10.12,24.85,14.04,39.12,13.03,10.55-1.23,20.38-5.47,28.74-11.92,1.11-1.06,4.45-3.63,3.5-5.12-.76-.85-4.31-.47-5.92-2.01-2.25-1.92-1.39-6.22,1.16-7.36,1.36-.65,2.96-.81,4.47-.86h.2,0Z" })
192
- ]
193
- }
194
- );
195
- }
196
- function DexterButton(props) {
197
- const {
198
- children,
199
- loading = false,
200
- loadingLabel = "Connecting\u2026",
201
- variant = "primary",
202
- block = false,
203
- withMark = true,
204
- onClick,
205
- disabled = false,
206
- className,
207
- type = "button"
208
- } = props;
209
- useEffect2(ensureDexterButtonStyles, []);
210
- return /* @__PURE__ */ jsx(
211
- "button",
212
- {
213
- type,
214
- className: cx("dx-btn", variant === "secondary" && "dx-btn--secondary", block && "dx-btn--block", className),
215
- onClick,
216
- disabled: disabled || loading,
217
- "aria-busy": loading,
218
- children: loading ? /* @__PURE__ */ jsxs(Fragment, { children: [
219
- /* @__PURE__ */ jsx("span", { className: "dx-btn__spin", "aria-hidden": true }),
220
- /* @__PURE__ */ jsx("span", { className: "dx-btn__doing", children: loadingLabel })
221
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
222
- withMark && /* @__PURE__ */ jsx(DexterMark, {}),
223
- children
224
- ] })
225
- }
226
- );
227
- }
228
-
229
- // src/SignInWithDexter.tsx
230
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
133
+ import { jsx, jsxs } from "react/jsx-runtime";
231
134
  function shortAddress(addr) {
232
135
  return addr.length > 10 ? `${addr.slice(0, 4)}\u2026${addr.slice(-4)}` : addr;
233
136
  }
@@ -245,7 +148,7 @@ function SignInWithDexter(props) {
245
148
  showConnectedChip = true,
246
149
  ...config
247
150
  } = props;
248
- useEffect3(ensureDexterButtonStyles, []);
151
+ useEffect2(ensureDexterButtonStyles, []);
249
152
  const c = useSignInWithDexter(config);
250
153
  const handleClick = async () => {
251
154
  try {
@@ -256,17 +159,17 @@ function SignInWithDexter(props) {
256
159
  };
257
160
  if (c.isVaultConnected) {
258
161
  if (!showConnectedChip) return null;
259
- return /* @__PURE__ */ jsxs2("span", { className: cx("dx-chip", className), children: [
260
- /* @__PURE__ */ jsx2("span", { className: "dx-chip__dot", "aria-hidden": true }),
261
- /* @__PURE__ */ jsx2("span", { children: c.vaultAddress ? shortAddress(c.vaultAddress) : "Connected" }),
262
- c.usdcBalance !== null && /* @__PURE__ */ jsxs2("span", { className: "dx-chip__bal", children: [
162
+ return /* @__PURE__ */ jsxs("span", { className: cx("dx-chip", className), children: [
163
+ /* @__PURE__ */ jsx("span", { className: "dx-chip__dot", "aria-hidden": true }),
164
+ /* @__PURE__ */ jsx("span", { children: c.vaultAddress ? shortAddress(c.vaultAddress) : "Connected" }),
165
+ c.usdcBalance !== null && /* @__PURE__ */ jsxs("span", { className: "dx-chip__bal", children: [
263
166
  formatUsd(c.usdcBalance),
264
167
  " available"
265
168
  ] }),
266
- /* @__PURE__ */ jsx2("button", { type: "button", className: "dx-chip__x", onClick: c.disconnect, "aria-label": "Disconnect", children: "\xD7" })
169
+ /* @__PURE__ */ jsx("button", { type: "button", className: "dx-chip__x", onClick: c.disconnect, "aria-label": "Disconnect", children: "\xD7" })
267
170
  ] });
268
171
  }
269
- return /* @__PURE__ */ jsx2(
172
+ return /* @__PURE__ */ jsx(
270
173
  DexterButton,
271
174
  {
272
175
  loading: c.status === "pending",
@@ -281,13 +184,13 @@ function SignInWithDexter(props) {
281
184
  }
282
185
 
283
186
  // src/useDexterWallet.ts
284
- import { useCallback as useCallback2, useEffect as useEffect4, useState as useState2 } from "react";
187
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
285
188
  var NO_SUPPORT = { rename: false, prune: false, syncAccepted: false };
286
189
  function useDexterWallet() {
287
190
  const [activeHandle, setHandle] = useState2(() => getActiveHandle());
288
191
  const [wallets, setWallets] = useState2(() => listWallets());
289
192
  const [support, setSupport] = useState2(NO_SUPPORT);
290
- useEffect4(() => {
193
+ useEffect3(() => {
291
194
  const sync = () => {
292
195
  setHandle(getActiveHandle());
293
196
  setWallets(listWallets());
@@ -324,10 +227,10 @@ function useDexterWallet() {
324
227
  }
325
228
 
326
229
  // src/DexterWalletChip.tsx
327
- import { useEffect as useEffect5 } from "react";
230
+ import { useEffect as useEffect4 } from "react";
328
231
 
329
232
  // src/walletKitStyles.ts
330
- var STYLE_ID2 = "dexter-connect-wallet-kit-styles";
233
+ var STYLE_ID = "dexter-connect-wallet-kit-styles";
331
234
  var WALLET_KIT_CSS = `
332
235
  .dx-wchip{
333
236
  --dx-ember:#f26c18; --dx-ember-2:#ba3a00; --dx-fg:#fff4ea; --dx-radius:0px;
@@ -366,21 +269,21 @@ var WALLET_KIT_CSS = `
366
269
  `;
367
270
  function ensureWalletKitStyles() {
368
271
  if (typeof document === "undefined") return;
369
- if (document.getElementById(STYLE_ID2)) return;
272
+ if (document.getElementById(STYLE_ID)) return;
370
273
  const el = document.createElement("style");
371
- el.id = STYLE_ID2;
274
+ el.id = STYLE_ID;
372
275
  el.textContent = WALLET_KIT_CSS;
373
276
  document.head.appendChild(el);
374
277
  }
375
278
  ensureWalletKitStyles();
376
279
 
377
280
  // src/DexterWalletChip.tsx
378
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
281
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
379
282
  function DexterWalletChip(props) {
380
283
  const { connected, label, avatarUrl, avatarInitial, onClick, className, ariaExpanded } = props;
381
- useEffect5(ensureWalletKitStyles, []);
284
+ useEffect4(ensureWalletKitStyles, []);
382
285
  const initial = (avatarInitial ?? label.trim().charAt(0) ?? "D").toUpperCase();
383
- return /* @__PURE__ */ jsxs3(
286
+ return /* @__PURE__ */ jsxs2(
384
287
  "button",
385
288
  {
386
289
  type: "button",
@@ -388,64 +291,64 @@ function DexterWalletChip(props) {
388
291
  onClick,
389
292
  "aria-expanded": ariaExpanded,
390
293
  children: [
391
- connected ? /* @__PURE__ */ jsx3("span", { className: "dx-wchip__avatar", "aria-hidden": "true", children: avatarUrl ? /* @__PURE__ */ jsx3("img", { src: avatarUrl, alt: "" }) : initial }) : null,
392
- /* @__PURE__ */ jsx3("span", { className: "dx-wchip__label", children: label })
294
+ connected ? /* @__PURE__ */ jsx2("span", { className: "dx-wchip__avatar", "aria-hidden": "true", children: avatarUrl ? /* @__PURE__ */ jsx2("img", { src: avatarUrl, alt: "" }) : initial }) : null,
295
+ /* @__PURE__ */ jsx2("span", { className: "dx-wchip__label", children: label })
393
296
  ]
394
297
  }
395
298
  );
396
299
  }
397
300
 
398
301
  // src/DexterWalletMenu.tsx
399
- import { useEffect as useEffect6, useState as useState3 } from "react";
400
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
302
+ import { useEffect as useEffect5, useState as useState3 } from "react";
303
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
401
304
  function DexterWalletMenu(props) {
402
305
  const { walletLabel, avatarInitial, onManageWallet, onStartFresh, saveSlot, saveHint, className } = props;
403
- useEffect6(ensureWalletKitStyles, []);
306
+ useEffect5(ensureWalletKitStyles, []);
404
307
  const [showSave, setShowSave] = useState3(false);
405
308
  const initial = (avatarInitial ?? walletLabel.trim().charAt(0) ?? "D").toUpperCase();
406
309
  if (showSave && saveSlot) {
407
- return /* @__PURE__ */ jsxs4("div", { className: cx("dx-wmenu", className), children: [
408
- /* @__PURE__ */ jsx4(
310
+ return /* @__PURE__ */ jsxs3("div", { className: cx("dx-wmenu", className), children: [
311
+ /* @__PURE__ */ jsx3(
409
312
  "button",
410
313
  {
411
314
  type: "button",
412
315
  className: "dx-wmenu__item dx-wmenu__back",
413
316
  onClick: () => setShowSave(false),
414
- children: /* @__PURE__ */ jsx4("span", { children: "\u2190 Back to wallet" })
317
+ children: /* @__PURE__ */ jsx3("span", { children: "\u2190 Back to wallet" })
415
318
  }
416
319
  ),
417
- /* @__PURE__ */ jsxs4("div", { className: "dx-wmenu__save", children: [
418
- saveHint ? /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__savehint", children: saveHint }) : null,
320
+ /* @__PURE__ */ jsxs3("div", { className: "dx-wmenu__save", children: [
321
+ saveHint ? /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__savehint", children: saveHint }) : null,
419
322
  saveSlot
420
323
  ] })
421
324
  ] });
422
325
  }
423
- return /* @__PURE__ */ jsxs4("div", { className: cx("dx-wmenu", className), children: [
424
- /* @__PURE__ */ jsxs4("div", { className: "dx-wmenu__id", children: [
425
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__avatar", "aria-hidden": "true", children: initial }),
426
- /* @__PURE__ */ jsxs4("span", { className: "dx-wmenu__meta", children: [
427
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__name", children: walletLabel }),
428
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__sub", children: "Your Dexter Wallet" })
326
+ return /* @__PURE__ */ jsxs3("div", { className: cx("dx-wmenu", className), children: [
327
+ /* @__PURE__ */ jsxs3("div", { className: "dx-wmenu__id", children: [
328
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__avatar", "aria-hidden": "true", children: initial }),
329
+ /* @__PURE__ */ jsxs3("span", { className: "dx-wmenu__meta", children: [
330
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__name", children: walletLabel }),
331
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__sub", children: "Your Dexter Wallet" })
429
332
  ] })
430
333
  ] }),
431
- /* @__PURE__ */ jsxs4("div", { className: "dx-wmenu__list", children: [
432
- onManageWallet ? /* @__PURE__ */ jsxs4("button", { type: "button", className: "dx-wmenu__item", onClick: onManageWallet, children: [
433
- /* @__PURE__ */ jsx4("span", { children: "Manage wallet" }),
434
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u2197" })
334
+ /* @__PURE__ */ jsxs3("div", { className: "dx-wmenu__list", children: [
335
+ onManageWallet ? /* @__PURE__ */ jsxs3("button", { type: "button", className: "dx-wmenu__item", onClick: onManageWallet, children: [
336
+ /* @__PURE__ */ jsx3("span", { children: "Manage wallet" }),
337
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u2197" })
435
338
  ] }) : null,
436
- saveSlot ? /* @__PURE__ */ jsxs4("button", { type: "button", className: "dx-wmenu__item", onClick: () => setShowSave(true), children: [
437
- /* @__PURE__ */ jsx4("span", { children: "Save your wallet" }),
438
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u2197" })
339
+ saveSlot ? /* @__PURE__ */ jsxs3("button", { type: "button", className: "dx-wmenu__item", onClick: () => setShowSave(true), children: [
340
+ /* @__PURE__ */ jsx3("span", { children: "Save your wallet" }),
341
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u2197" })
439
342
  ] }) : null,
440
- onStartFresh ? /* @__PURE__ */ jsxs4(
343
+ onStartFresh ? /* @__PURE__ */ jsxs3(
441
344
  "button",
442
345
  {
443
346
  type: "button",
444
347
  className: "dx-wmenu__item dx-wmenu__item--danger",
445
348
  onClick: onStartFresh,
446
349
  children: [
447
- /* @__PURE__ */ jsx4("span", { children: "Start fresh" }),
448
- /* @__PURE__ */ jsx4("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u27F5" })
350
+ /* @__PURE__ */ jsx3("span", { children: "Start fresh" }),
351
+ /* @__PURE__ */ jsx3("span", { className: "dx-wmenu__icon", "aria-hidden": "true", children: "\u27F5" })
449
352
  ]
450
353
  }
451
354
  ) : null
@@ -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
+ };
@@ -0,0 +1,61 @@
1
+ import { ReactElement } from 'react';
2
+ import { IDKitResult } from '@worldcoin/idkit';
3
+
4
+ type VerifyPersonhoodPhase = 'loading' | 'ready' | 'rp_error' | 'verified';
5
+ interface VerifyPersonhoodConfig {
6
+ /** World ID app id (`app_…`), from the developer portal. */
7
+ appId: `app_${string}`;
8
+ /** The registered action (e.g. `dexter-credit-root-v2`) — pins the nullifier namespace. */
9
+ action: string;
10
+ /** `production` | `staging`. Default `production`. */
11
+ environment?: 'production' | 'staging';
12
+ /**
13
+ * URL that returns the SERVER-SIGNED RP context as `{ rp_context: RpContext }`.
14
+ * The RP signing key is server-side only (never the browser), so the consumer
15
+ * exposes an endpoint that signs the request and returns just the signed
16
+ * context. Mirrors the capture rig's `/api/rp-context`.
17
+ */
18
+ rpContextUrl: string;
19
+ /** Optional `fetch` init for the RP-context request (auth headers, etc.). */
20
+ rpContextInit?: RequestInit;
21
+ }
22
+ interface UseVerifyPersonhood {
23
+ phase: VerifyPersonhoodPhase;
24
+ error: string | null;
25
+ result: IDKitResult | null;
26
+ /** Open the World App verification sheet. No-op until phase is 'ready'/'verified'. */
27
+ open: () => void;
28
+ /** The IDKitRequestWidget element to render (null until the RP context loads). */
29
+ widget: ReactElement | null;
30
+ }
31
+ /**
32
+ * Headless hook: manages the RP-context fetch + IDKit lifecycle and hands back
33
+ * the raw proof. Render `widget` and call `open()` from your own UI, or use the
34
+ * turnkey <VerifyPersonhood> below.
35
+ */
36
+ declare function useVerifyPersonhood(config: VerifyPersonhoodConfig, onProof?: (result: IDKitResult) => void, onError?: (error: Error) => void): UseVerifyPersonhood;
37
+ interface VerifyPersonhoodProps extends VerifyPersonhoodConfig {
38
+ /** Fired with the raw World ID v4 proof the moment verification completes. */
39
+ onProof?: (result: IDKitResult) => void;
40
+ /** Fired with the typed error if RP signing or verification fails. */
41
+ onError?: (error: Error) => void;
42
+ /** Button label when armed. Default "Verify your personhood". */
43
+ label?: string;
44
+ /** Label after a proof is captured. Default "Verify again". */
45
+ verifiedLabel?: string;
46
+ /** 'primary' = filled ember (default), 'secondary' = outline. */
47
+ variant?: 'primary' | 'secondary';
48
+ /** Full-width button. */
49
+ block?: boolean;
50
+ /** Extra className composed after the brand classes. */
51
+ className?: string;
52
+ }
53
+ /**
54
+ * Turnkey "Verify your personhood" element — the personhood sibling of
55
+ * <SignInWithDexter>. Renders the branded DexterButton (loading while the RP
56
+ * context signs server-side) that opens World App; on success hands the raw
57
+ * IDKitResult to `onProof`. Brand voice: no emojis. Themeable via --dx-* vars.
58
+ */
59
+ declare function VerifyPersonhood(props: VerifyPersonhoodProps): ReactElement;
60
+
61
+ export { type UseVerifyPersonhood, VerifyPersonhood, type VerifyPersonhoodConfig, type VerifyPersonhoodPhase, type VerifyPersonhoodProps, useVerifyPersonhood };
@@ -0,0 +1,108 @@
1
+ import {
2
+ DexterButton,
3
+ ensureDexterButtonStyles
4
+ } from "./chunk-S7G2ZZOO.js";
5
+
6
+ // src/worldid.tsx
7
+ import { useEffect, useState } from "react";
8
+ import {
9
+ IDKitRequestWidget,
10
+ proofOfHuman
11
+ } from "@worldcoin/idkit";
12
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
13
+ function useVerifyPersonhood(config, onProof, onError) {
14
+ const { appId, action, environment = "production", rpContextUrl, rpContextInit } = config;
15
+ const [phase, setPhase] = useState("loading");
16
+ const [error, setError] = useState(null);
17
+ const [rpContext, setRpContext] = useState(null);
18
+ const [isOpen, setIsOpen] = useState(false);
19
+ const [result, setResult] = useState(null);
20
+ useEffect(() => {
21
+ let cancelled = false;
22
+ void (async () => {
23
+ try {
24
+ const res = await fetch(rpContextUrl, { cache: "no-store", ...rpContextInit });
25
+ const data = await res.json();
26
+ if (!res.ok) throw new Error(data?.message ?? `RP context fetch failed (HTTP ${res.status})`);
27
+ if (cancelled) return;
28
+ setRpContext(data.rp_context);
29
+ setPhase("ready");
30
+ } catch (e) {
31
+ if (cancelled) return;
32
+ const err = e instanceof Error ? e : new Error(String(e));
33
+ setError(err.message);
34
+ setPhase("rp_error");
35
+ onError?.(err);
36
+ }
37
+ })();
38
+ return () => {
39
+ cancelled = true;
40
+ };
41
+ }, [rpContextUrl]);
42
+ const widget = rpContext ? /* @__PURE__ */ jsx(
43
+ IDKitRequestWidget,
44
+ {
45
+ app_id: appId,
46
+ action,
47
+ rp_context: rpContext,
48
+ preset: proofOfHuman(),
49
+ allow_legacy_proofs: false,
50
+ environment,
51
+ open: isOpen,
52
+ onOpenChange: setIsOpen,
53
+ onSuccess: (res) => {
54
+ setResult(res);
55
+ setPhase("verified");
56
+ onProof?.(res);
57
+ },
58
+ onError: (code) => {
59
+ const err = new Error(`World App error: ${code}`);
60
+ setError(err.message);
61
+ onError?.(err);
62
+ }
63
+ }
64
+ ) : null;
65
+ return {
66
+ phase,
67
+ error,
68
+ result,
69
+ open: () => {
70
+ if (phase === "ready" || phase === "verified") setIsOpen(true);
71
+ },
72
+ widget
73
+ };
74
+ }
75
+ function VerifyPersonhood(props) {
76
+ const {
77
+ onProof,
78
+ onError,
79
+ label = "Verify your personhood",
80
+ verifiedLabel = "Verify again",
81
+ variant = "primary",
82
+ block = false,
83
+ className,
84
+ ...config
85
+ } = props;
86
+ useEffect(ensureDexterButtonStyles, []);
87
+ const v = useVerifyPersonhood(config, onProof, onError);
88
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
89
+ /* @__PURE__ */ jsx(
90
+ DexterButton,
91
+ {
92
+ onClick: v.open,
93
+ loading: v.phase === "loading",
94
+ loadingLabel: "Preparing\u2026",
95
+ variant,
96
+ block,
97
+ className,
98
+ disabled: v.phase === "rp_error",
99
+ children: v.phase === "verified" ? verifiedLabel : label
100
+ }
101
+ ),
102
+ v.widget
103
+ ] });
104
+ }
105
+ export {
106
+ VerifyPersonhood,
107
+ useVerifyPersonhood
108
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexterai/connect",
3
- "version": "0.16.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": {
@@ -11,6 +11,14 @@
11
11
  "./react": {
12
12
  "types": "./dist/react.d.ts",
13
13
  "import": "./dist/react.js"
14
+ },
15
+ "./worldid": {
16
+ "types": "./dist/worldid.d.ts",
17
+ "import": "./dist/worldid.js"
18
+ },
19
+ "./server": {
20
+ "types": "./dist/server.d.ts",
21
+ "import": "./dist/server.js"
14
22
  }
15
23
  },
16
24
  "files": [
@@ -22,25 +30,31 @@
22
30
  "typecheck": "tsc --noEmit"
23
31
  },
24
32
  "peerDependencies": {
25
- "@dexterai/vault": ">=0.22",
33
+ "@dexterai/vault": ">=0.30.0",
26
34
  "@solana/web3.js": "^1.98.0",
35
+ "@worldcoin/idkit": "^4.1.8",
27
36
  "react": ">=18"
28
37
  },
29
38
  "peerDependenciesMeta": {
30
39
  "@solana/web3.js": {
31
40
  "optional": true
41
+ },
42
+ "@worldcoin/idkit": {
43
+ "optional": true
32
44
  }
33
45
  },
34
46
  "devDependencies": {
35
- "@dexterai/vault": "^0.24.0",
47
+ "@dexterai/vault": "^0.30.0",
36
48
  "@solana/web3.js": "^1.98.4",
37
49
  "@types/react": "^19.1.12",
50
+ "@worldcoin/idkit": "^4.1.8",
38
51
  "react": "^19.2.5",
39
52
  "tsup": "^8.5.0",
40
53
  "typescript": "^5.6.2",
41
54
  "vitest": "^4.1.9"
42
55
  },
43
56
  "dependencies": {
44
- "@simplewebauthn/browser": "^13.3.0"
57
+ "@simplewebauthn/browser": "^13.3.0",
58
+ "jose": "^6.2.3"
45
59
  }
46
60
  }