@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 +119 -22
- package/dist/chunk-S7G2ZZOO.js +107 -0
- package/dist/react.js +49 -146
- package/dist/server.d.ts +72 -0
- package/dist/server.js +90 -0
- package/dist/worldid.d.ts +61 -0
- package/dist/worldid.js +108 -0
- package/package.json +18 -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
|
|
|
@@ -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 {
|
|
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
|
-
|
|
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__ */
|
|
260
|
-
/* @__PURE__ */
|
|
261
|
-
/* @__PURE__ */
|
|
262
|
-
c.usdcBalance !== null && /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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
|
|
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
|
-
|
|
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
|
|
230
|
+
import { useEffect as useEffect4 } from "react";
|
|
328
231
|
|
|
329
232
|
// src/walletKitStyles.ts
|
|
330
|
-
var
|
|
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(
|
|
272
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
370
273
|
const el = document.createElement("style");
|
|
371
|
-
el.id =
|
|
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
|
|
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
|
-
|
|
284
|
+
useEffect4(ensureWalletKitStyles, []);
|
|
382
285
|
const initial = (avatarInitial ?? label.trim().charAt(0) ?? "D").toUpperCase();
|
|
383
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
392
|
-
/* @__PURE__ */
|
|
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
|
|
400
|
-
import { jsx as
|
|
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
|
-
|
|
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__ */
|
|
408
|
-
/* @__PURE__ */
|
|
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__ */
|
|
317
|
+
children: /* @__PURE__ */ jsx3("span", { children: "\u2190 Back to wallet" })
|
|
415
318
|
}
|
|
416
319
|
),
|
|
417
|
-
/* @__PURE__ */
|
|
418
|
-
saveHint ? /* @__PURE__ */
|
|
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__ */
|
|
424
|
-
/* @__PURE__ */
|
|
425
|
-
/* @__PURE__ */
|
|
426
|
-
/* @__PURE__ */
|
|
427
|
-
/* @__PURE__ */
|
|
428
|
-
/* @__PURE__ */
|
|
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__ */
|
|
432
|
-
onManageWallet ? /* @__PURE__ */
|
|
433
|
-
/* @__PURE__ */
|
|
434
|
-
/* @__PURE__ */
|
|
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__ */
|
|
437
|
-
/* @__PURE__ */
|
|
438
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
448
|
-
/* @__PURE__ */
|
|
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
|
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
|
+
};
|
|
@@ -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 };
|
package/dist/worldid.js
ADDED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
}
|