@dexterai/connect 0.1.0 → 0.3.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 +85 -10
- package/dist/chunk-HFG2GUFX.js +356 -0
- package/dist/index.d.ts +63 -3
- package/dist/index.js +23 -3
- package/dist/react.d.ts +27 -2
- package/dist/react.js +38 -3
- package/dist/walletStore-CWXhXuzK.d.ts +92 -0
- package/package.json +3 -4
- package/dist/chunk-65VT7AU2.js +0 -118
- package/dist/types-DT3KPBOE.d.ts +0 -45
package/README.md
CHANGED
|
@@ -1,16 +1,91 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Dexter-DAO/dexter-x402-sdk/main/assets/dexter-wordmark.svg" alt="Dexter" width="360">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">@dexterai/connect</h1>
|
|
4
6
|
|
|
5
|
-
|
|
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>
|
|
9
|
+
</p>
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://www.npmjs.com/package/@dexterai/connect"><img src="https://img.shields.io/npm/v/@dexterai/connect.svg" alt="npm"></a>
|
|
13
|
+
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/react-%3E=18-brightgreen.svg" alt="React"></a>
|
|
14
|
+
<a href="https://www.w3.org/TR/webauthn-2/"><img src="https://img.shields.io/badge/auth-passkey-00FF88" alt="Passkey"></a>
|
|
15
|
+
</p>
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
sign-in button must not inherit a payment graph.
|
|
17
|
+
---
|
|
12
18
|
|
|
13
|
-
##
|
|
19
|
+
## What this is
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
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.
|
|
26
|
+
|
|
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.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @dexterai/connect react
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import { SignInWithDexter } from '@dexterai/connect/react';
|
|
42
|
+
|
|
43
|
+
function Header() {
|
|
44
|
+
return (
|
|
45
|
+
<SignInWithDexter
|
|
46
|
+
onSuccess={({ session, vault }) => {
|
|
47
|
+
// session = auth tokens (camelCase); vault = the Dexter Wallet
|
|
48
|
+
seatYourSession(session);
|
|
49
|
+
}}
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
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."**
|
|
57
|
+
|
|
58
|
+
## Hook (full control)
|
|
59
|
+
|
|
60
|
+
For your own UI, use the hook directly:
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { useSignInWithDexter } from '@dexterai/connect/react';
|
|
64
|
+
|
|
65
|
+
const c = useSignInWithDexter();
|
|
66
|
+
await c.signIn(); // run the passkey ceremony
|
|
67
|
+
c.status; // idle → pending → done → error
|
|
68
|
+
c.vaultAddress; // the Dexter Wallet address (base58)
|
|
69
|
+
c.usdcBalance; // USD available (via Dexter's RPC), or null
|
|
70
|
+
c.disconnect();
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## What `useSignInWithDexter()` gives you
|
|
74
|
+
|
|
75
|
+
| Field | What it is |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `signIn()` / `disconnect()` | run the passkey ceremony / clear state |
|
|
78
|
+
| `status` / `isVaultConnected` | `idle→pending→done→error` / connected flag |
|
|
79
|
+
| `session` | auth session tokens (camelCase) |
|
|
80
|
+
| `vaultAddress` / `vaultPda` | the Dexter Wallet address / PDA |
|
|
81
|
+
| `usdcBalance` / `refreshBalance()` | USD available, best-effort via Dexter's RPC |
|
|
82
|
+
| `vault` / `credentialId` / `error` | raw vault payload / credential id / typed error |
|
|
83
|
+
|
|
84
|
+
## Exports
|
|
85
|
+
|
|
86
|
+
- `@dexterai/connect` — framework-free: `passkeyLogin()`, `ConnectError`, types.
|
|
87
|
+
- `@dexterai/connect/react` — `<SignInWithDexter/>`, `useSignInWithDexter()`.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var ConnectError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code, message) {
|
|
5
|
+
super(message ?? code);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = "ConnectError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/base64.ts
|
|
12
|
+
function base64ToBytes(s) {
|
|
13
|
+
const bin = atob(s);
|
|
14
|
+
const out = new Uint8Array(bin.length);
|
|
15
|
+
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function base64urlToBytes(s) {
|
|
19
|
+
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
20
|
+
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
21
|
+
return base64ToBytes(b64);
|
|
22
|
+
}
|
|
23
|
+
function bytesToBase64(bytes) {
|
|
24
|
+
let bin = "";
|
|
25
|
+
for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
|
|
26
|
+
return btoa(bin);
|
|
27
|
+
}
|
|
28
|
+
function bytesToBase64url(bytes) {
|
|
29
|
+
return bytesToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
30
|
+
}
|
|
31
|
+
function compactSignatureToDer(compact) {
|
|
32
|
+
if (compact.length !== 64) {
|
|
33
|
+
throw new Error(`expected 64-byte compact signature, got ${compact.length}`);
|
|
34
|
+
}
|
|
35
|
+
const r = derInteger(compact.subarray(0, 32));
|
|
36
|
+
const s = derInteger(compact.subarray(32, 64));
|
|
37
|
+
const body = new Uint8Array(r.length + s.length);
|
|
38
|
+
body.set(r, 0);
|
|
39
|
+
body.set(s, r.length);
|
|
40
|
+
const out = new Uint8Array(2 + body.length);
|
|
41
|
+
out[0] = 48;
|
|
42
|
+
out[1] = body.length;
|
|
43
|
+
out.set(body, 2);
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function derInteger(component) {
|
|
47
|
+
let start = 0;
|
|
48
|
+
while (start < component.length - 1 && component[start] === 0) start += 1;
|
|
49
|
+
let content = component.subarray(start);
|
|
50
|
+
if (content[0] & 128) {
|
|
51
|
+
const padded = new Uint8Array(content.length + 1);
|
|
52
|
+
padded[0] = 0;
|
|
53
|
+
padded.set(content, 1);
|
|
54
|
+
content = padded;
|
|
55
|
+
}
|
|
56
|
+
const out = new Uint8Array(2 + content.length);
|
|
57
|
+
out[0] = 2;
|
|
58
|
+
out[1] = content.length;
|
|
59
|
+
out.set(content, 2);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/relay.ts
|
|
64
|
+
var DEFAULT_API_BASE = "https://api.dexter.cash";
|
|
65
|
+
var ANON_SIGN_BASE = "/api/passkey-anon/sign";
|
|
66
|
+
async function passkeyLogin(config = {}) {
|
|
67
|
+
const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
68
|
+
const options = await fetchLoginChallenge(apiBase);
|
|
69
|
+
const credential = await getAssertion(options);
|
|
70
|
+
return submitLogin(apiBase, credential);
|
|
71
|
+
}
|
|
72
|
+
async function fetchLoginChallenge(apiBase) {
|
|
73
|
+
const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/login-challenge`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
body: "{}"
|
|
77
|
+
});
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new ConnectError("login_challenge_failed", `login-challenge ${res.status}`);
|
|
80
|
+
}
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
if (!data?.options?.challenge) {
|
|
83
|
+
throw new ConnectError("login_challenge_malformed", "no challenge in response");
|
|
84
|
+
}
|
|
85
|
+
return data.options;
|
|
86
|
+
}
|
|
87
|
+
async function getAssertion(options) {
|
|
88
|
+
if (typeof navigator === "undefined" || !navigator.credentials) {
|
|
89
|
+
throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
|
|
90
|
+
}
|
|
91
|
+
let credential;
|
|
92
|
+
try {
|
|
93
|
+
credential = await navigator.credentials.get({
|
|
94
|
+
publicKey: {
|
|
95
|
+
challenge: base64urlToBytes(options.challenge).buffer.slice(0),
|
|
96
|
+
rpId: options.rpId,
|
|
97
|
+
timeout: options.timeout ?? 6e4,
|
|
98
|
+
userVerification: options.userVerification ?? "required"
|
|
99
|
+
// No allowCredentials — discoverable resident-key login.
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} catch (err) {
|
|
103
|
+
throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
|
|
104
|
+
}
|
|
105
|
+
if (!credential || credential.type !== "public-key") {
|
|
106
|
+
throw new ConnectError("no_credential", "WebAuthn returned no credential");
|
|
107
|
+
}
|
|
108
|
+
return credential;
|
|
109
|
+
}
|
|
110
|
+
async function submitLogin(apiBase, credential) {
|
|
111
|
+
const assertion = credential.response;
|
|
112
|
+
const credentialJson = {
|
|
113
|
+
id: credential.id,
|
|
114
|
+
rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
|
|
115
|
+
type: credential.type,
|
|
116
|
+
response: {
|
|
117
|
+
clientDataJSON: bytesToBase64url(new Uint8Array(assertion.clientDataJSON)),
|
|
118
|
+
authenticatorData: bytesToBase64url(new Uint8Array(assertion.authenticatorData)),
|
|
119
|
+
signature: bytesToBase64url(new Uint8Array(assertion.signature)),
|
|
120
|
+
userHandle: assertion.userHandle ? bytesToBase64url(new Uint8Array(assertion.userHandle)) : null
|
|
121
|
+
},
|
|
122
|
+
clientExtensionResults: credential.getClientExtensionResults?.() ?? {}
|
|
123
|
+
};
|
|
124
|
+
const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/passkey-login`, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers: { "content-type": "application/json" },
|
|
127
|
+
body: JSON.stringify({ credential: credentialJson })
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
throw new ConnectError(await readErrorCode(res), `passkey-login ${res.status}`);
|
|
131
|
+
}
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
const session = {
|
|
134
|
+
accessToken: data.accessToken,
|
|
135
|
+
refreshToken: data.refreshToken,
|
|
136
|
+
expiresAt: data.expiresAt,
|
|
137
|
+
expiresIn: data.expiresIn,
|
|
138
|
+
tokenType: data.tokenType
|
|
139
|
+
};
|
|
140
|
+
return data.vault ? { session, vault: data.vault } : { session };
|
|
141
|
+
}
|
|
142
|
+
async function readErrorCode(res) {
|
|
143
|
+
try {
|
|
144
|
+
const body = await res.json();
|
|
145
|
+
if (body?.error) return body.error;
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
return `http_${res.status}`;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/anon-policy.ts
|
|
152
|
+
var DEFAULT_API_BASE2 = "https://api.dexter.cash";
|
|
153
|
+
var ANON_SIGN_BASE2 = "/api/passkey-anon/sign";
|
|
154
|
+
function createAnonServerPolicy(apiBase = DEFAULT_API_BASE2) {
|
|
155
|
+
const base = apiBase.replace(/\/$/, "");
|
|
156
|
+
return {
|
|
157
|
+
async issueChallenge({ userHandle, operationHash }) {
|
|
158
|
+
const res = await fetch(`${base}${ANON_SIGN_BASE2}/challenge`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "content-type": "application/json" },
|
|
161
|
+
body: JSON.stringify({
|
|
162
|
+
userHandle: bytesToBase64url(userHandle),
|
|
163
|
+
operationHash: bytesToBase64url(operationHash)
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
if (!res.ok) throw new ConnectError(await readErrorCode2(res), `challenge ${res.status}`);
|
|
167
|
+
const data = await res.json();
|
|
168
|
+
const options = data?.options;
|
|
169
|
+
if (!options?.challenge) {
|
|
170
|
+
throw new ConnectError("challenge_malformed", "no challenge in response");
|
|
171
|
+
}
|
|
172
|
+
const cred = options.allowCredentials?.[0];
|
|
173
|
+
if (!cred?.id) {
|
|
174
|
+
throw new ConnectError("no_credential", "no allow-listed credential for this userHandle");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
challenge: base64urlToBytes(options.challenge),
|
|
178
|
+
credentialId: base64urlToBytes(cred.id),
|
|
179
|
+
rpId: options.rpId,
|
|
180
|
+
transports: cred.transports
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
async verify({ userHandle, credentialId, signature, clientDataJSON, authenticatorData }) {
|
|
184
|
+
const credential = {
|
|
185
|
+
id: bytesToBase64url(credentialId),
|
|
186
|
+
rawId: bytesToBase64url(credentialId),
|
|
187
|
+
type: "public-key",
|
|
188
|
+
response: {
|
|
189
|
+
clientDataJSON: bytesToBase64url(clientDataJSON),
|
|
190
|
+
authenticatorData: bytesToBase64url(authenticatorData),
|
|
191
|
+
// SDK 0.19 passes the COMPACT sig; dexter-api's verifier wants DER.
|
|
192
|
+
signature: bytesToBase64url(compactSignatureToDer(signature)),
|
|
193
|
+
userHandle: null
|
|
194
|
+
},
|
|
195
|
+
clientExtensionResults: {},
|
|
196
|
+
authenticatorAttachment: null
|
|
197
|
+
};
|
|
198
|
+
const res = await fetch(`${base}${ANON_SIGN_BASE2}/verify`, {
|
|
199
|
+
method: "POST",
|
|
200
|
+
headers: { "content-type": "application/json" },
|
|
201
|
+
body: JSON.stringify({ credential, userHandle: bytesToBase64url(userHandle) })
|
|
202
|
+
});
|
|
203
|
+
if (!res.ok) throw new ConnectError(await readErrorCode2(res), `verify ${res.status}`);
|
|
204
|
+
const data = await res.json();
|
|
205
|
+
if (data?.verified === false) {
|
|
206
|
+
throw new ConnectError("verification_failed", "server returned verified=false");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
async function readErrorCode2(res) {
|
|
212
|
+
try {
|
|
213
|
+
const body = await res.json();
|
|
214
|
+
if (body?.error) return body.error;
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
return `http_${res.status}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/signer.ts
|
|
221
|
+
import { DexterApiBrowserPasskeySigner } from "@dexterai/vault/signers/browser";
|
|
222
|
+
function createPasskeySigner(vault, apiBase, opts = {}) {
|
|
223
|
+
return new DexterApiBrowserPasskeySigner({
|
|
224
|
+
identity: { kind: "guest", userHandle: base64urlToBytes(vault.userHandle) },
|
|
225
|
+
publicKey: base64ToBytes(vault.publicKey),
|
|
226
|
+
anonPolicy: createAnonServerPolicy(apiBase),
|
|
227
|
+
...opts.__assertion ? { __assertion: opts.__assertion } : {}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/walletStore.ts
|
|
232
|
+
var ACTIVE_HANDLE_KEY = "dexter:passkey:userHandle";
|
|
233
|
+
var ROSTER_KEY = "dexter:passkey:wallets";
|
|
234
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
235
|
+
function hasStorage() {
|
|
236
|
+
try {
|
|
237
|
+
return typeof window !== "undefined" && !!window.localStorage;
|
|
238
|
+
} catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function readRoster() {
|
|
243
|
+
if (!hasStorage()) return [];
|
|
244
|
+
try {
|
|
245
|
+
const raw = window.localStorage.getItem(ROSTER_KEY);
|
|
246
|
+
if (!raw) return [];
|
|
247
|
+
const parsed = JSON.parse(raw);
|
|
248
|
+
if (!Array.isArray(parsed)) return [];
|
|
249
|
+
return parsed.filter(
|
|
250
|
+
(w) => !!w && typeof w === "object" && typeof w.handle === "string"
|
|
251
|
+
);
|
|
252
|
+
} catch {
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function writeRoster(wallets) {
|
|
257
|
+
if (!hasStorage()) return;
|
|
258
|
+
try {
|
|
259
|
+
window.localStorage.setItem(ROSTER_KEY, JSON.stringify(wallets));
|
|
260
|
+
} catch {
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function emit() {
|
|
264
|
+
for (const l of listeners) {
|
|
265
|
+
try {
|
|
266
|
+
l();
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function getActiveHandle() {
|
|
272
|
+
if (!hasStorage()) return null;
|
|
273
|
+
try {
|
|
274
|
+
return window.localStorage.getItem(ACTIVE_HANDLE_KEY);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function setActiveHandle(handle, label) {
|
|
280
|
+
if (!hasStorage() || !handle) return;
|
|
281
|
+
try {
|
|
282
|
+
window.localStorage.setItem(ACTIVE_HANDLE_KEY, handle);
|
|
283
|
+
} catch {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const roster = readRoster();
|
|
287
|
+
const existing = roster.find((w) => w.handle === handle);
|
|
288
|
+
const now = Date.now();
|
|
289
|
+
if (existing) {
|
|
290
|
+
existing.lastUsedAt = now;
|
|
291
|
+
if (label !== void 0) existing.label = label;
|
|
292
|
+
} else {
|
|
293
|
+
roster.push({ handle, label, lastUsedAt: now });
|
|
294
|
+
}
|
|
295
|
+
writeRoster(roster);
|
|
296
|
+
emit();
|
|
297
|
+
}
|
|
298
|
+
function ejectActiveWallet(opts) {
|
|
299
|
+
if (!hasStorage()) return;
|
|
300
|
+
const current = getActiveHandle();
|
|
301
|
+
try {
|
|
302
|
+
window.localStorage.removeItem(ACTIVE_HANDLE_KEY);
|
|
303
|
+
} catch {
|
|
304
|
+
}
|
|
305
|
+
if (opts?.forget && current) {
|
|
306
|
+
writeRoster(readRoster().filter((w) => w.handle !== current));
|
|
307
|
+
}
|
|
308
|
+
emit();
|
|
309
|
+
}
|
|
310
|
+
function listWallets() {
|
|
311
|
+
return readRoster().sort((a, b) => (b.lastUsedAt ?? 0) - (a.lastUsedAt ?? 0));
|
|
312
|
+
}
|
|
313
|
+
function switchWallet(handle) {
|
|
314
|
+
if (!readRoster().some((w) => w.handle === handle)) return false;
|
|
315
|
+
setActiveHandle(handle);
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
function forgetWallet(handle) {
|
|
319
|
+
if (getActiveHandle() === handle) {
|
|
320
|
+
ejectActiveWallet({ forget: true });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
writeRoster(readRoster().filter((w) => w.handle !== handle));
|
|
324
|
+
emit();
|
|
325
|
+
}
|
|
326
|
+
function subscribe(listener) {
|
|
327
|
+
listeners.add(listener);
|
|
328
|
+
if (hasStorage() && listeners.size === 1) {
|
|
329
|
+
window.addEventListener("storage", onStorageEvent);
|
|
330
|
+
}
|
|
331
|
+
return () => {
|
|
332
|
+
listeners.delete(listener);
|
|
333
|
+
if (hasStorage() && listeners.size === 0) {
|
|
334
|
+
window.removeEventListener("storage", onStorageEvent);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
function onStorageEvent(e) {
|
|
339
|
+
if (e.key === ACTIVE_HANDLE_KEY || e.key === ROSTER_KEY || e.key === null) emit();
|
|
340
|
+
}
|
|
341
|
+
var ACTIVE_WALLET_STORAGE_KEY = ACTIVE_HANDLE_KEY;
|
|
342
|
+
|
|
343
|
+
export {
|
|
344
|
+
ConnectError,
|
|
345
|
+
passkeyLogin,
|
|
346
|
+
createAnonServerPolicy,
|
|
347
|
+
createPasskeySigner,
|
|
348
|
+
getActiveHandle,
|
|
349
|
+
setActiveHandle,
|
|
350
|
+
ejectActiveWallet,
|
|
351
|
+
listWallets,
|
|
352
|
+
switchWallet,
|
|
353
|
+
forgetWallet,
|
|
354
|
+
subscribe,
|
|
355
|
+
ACTIVE_WALLET_STORAGE_KEY
|
|
356
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { D as DexterConnectConfig, S as SignInResult } from './
|
|
2
|
-
export {
|
|
1
|
+
import { D as DexterConnectConfig, S as SignInResult, C as ConnectVault } from './walletStore-CWXhXuzK.js';
|
|
2
|
+
export { A as ACTIVE_WALLET_STORAGE_KEY, a as ConnectError, P as PasskeyLoginTokens, b as StoredWallet, e as ejectActiveWallet, f as forgetWallet, g as getActiveHandle, l as listWallets, s as setActiveHandle, c as subscribeWallet, d as switchWallet } from './walletStore-CWXhXuzK.js';
|
|
3
|
+
import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* "Sign in with Dexter" — the discoverable-credential login ceremony.
|
|
@@ -16,4 +17,63 @@ export { C as ConnectError, a as ConnectVault, P as PasskeyLoginTokens } from '.
|
|
|
16
17
|
*/
|
|
17
18
|
declare function passkeyLogin(config?: DexterConnectConfig): Promise<SignInResult>;
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
/** What `issueChallenge` returns to the SDK signer. */
|
|
21
|
+
interface AnonChallengeResult {
|
|
22
|
+
/** Server-issued WebAuthn challenge (=== the supplied operationHash). */
|
|
23
|
+
challenge: Uint8Array;
|
|
24
|
+
/** Credential id the server resolved from the userHandle (allowCredentials[0]). */
|
|
25
|
+
credentialId: Uint8Array;
|
|
26
|
+
rpId?: string;
|
|
27
|
+
transports?: AuthenticatorTransport[];
|
|
28
|
+
}
|
|
29
|
+
/** The policy the SDK's guest `DexterApiBrowserPasskeySigner` consumes. */
|
|
30
|
+
interface AnonServerPolicy {
|
|
31
|
+
issueChallenge(args: {
|
|
32
|
+
userHandle: Uint8Array;
|
|
33
|
+
operationHash: Uint8Array;
|
|
34
|
+
}): Promise<AnonChallengeResult>;
|
|
35
|
+
verify(args: {
|
|
36
|
+
userHandle: Uint8Array;
|
|
37
|
+
credentialId: Uint8Array;
|
|
38
|
+
signature: Uint8Array;
|
|
39
|
+
clientDataJSON: Uint8Array;
|
|
40
|
+
authenticatorData: Uint8Array;
|
|
41
|
+
}): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Build the anon ServerPolicy for a given dexter-api base.
|
|
45
|
+
*
|
|
46
|
+
* `issueChallenge` → POST /challenge { userHandle, operationHash } (both base64url).
|
|
47
|
+
* The server uses operationHash AS the WebAuthn challenge (replay binding +
|
|
48
|
+
* the on-chain webauthn.rs law: clientDataJSON.challenge === sha256(op)) and
|
|
49
|
+
* resolves the credential into options.allowCredentials[0].
|
|
50
|
+
* `verify` → POST /verify { credential, userHandle }. NOTE: the SDK hands us the
|
|
51
|
+
* COMPACT 64-byte signature; dexter-api's WebAuthn verifier wants DER, so we
|
|
52
|
+
* re-encode compact → DER here (compactSignatureToDer).
|
|
53
|
+
*/
|
|
54
|
+
declare function createAnonServerPolicy(apiBase?: string): AnonServerPolicy;
|
|
55
|
+
|
|
56
|
+
/** Test seam mirroring the SDK's injected-assertion shape (production omits it). */
|
|
57
|
+
type AssertionLike = {
|
|
58
|
+
credentialId: Uint8Array;
|
|
59
|
+
assertOver(challenge: Uint8Array): Promise<{
|
|
60
|
+
signature: Uint8Array;
|
|
61
|
+
clientDataJSON: Uint8Array;
|
|
62
|
+
authenticatorData: Uint8Array;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Construct the guest signer from a connected `ConnectVault`.
|
|
67
|
+
*
|
|
68
|
+
* `vault.publicKey` is base64 (33-byte SEC1 compressed P-256); `vault.userHandle`
|
|
69
|
+
* is base64url (server-minted). Both are decoded to the Uint8Arrays the SDK wants.
|
|
70
|
+
*
|
|
71
|
+
* @param vault the connected vault from useSignInWithDexter()
|
|
72
|
+
* @param apiBase dexter-api base (defaults to https://api.dexter.cash via the policy)
|
|
73
|
+
* @param opts.__assertion test-only injected assertion (skips real WebAuthn)
|
|
74
|
+
*/
|
|
75
|
+
declare function createPasskeySigner(vault: ConnectVault, apiBase?: string, opts?: {
|
|
76
|
+
__assertion?: AssertionLike;
|
|
77
|
+
}): DexterApiBrowserPasskeySigner;
|
|
78
|
+
|
|
79
|
+
export { type AnonChallengeResult, type AnonServerPolicy, ConnectVault, DexterConnectConfig, SignInResult, createAnonServerPolicy, createPasskeySigner, passkeyLogin };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ACTIVE_WALLET_STORAGE_KEY,
|
|
2
3
|
ConnectError,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
createAnonServerPolicy,
|
|
5
|
+
createPasskeySigner,
|
|
6
|
+
ejectActiveWallet,
|
|
7
|
+
forgetWallet,
|
|
8
|
+
getActiveHandle,
|
|
9
|
+
listWallets,
|
|
10
|
+
passkeyLogin,
|
|
11
|
+
setActiveHandle,
|
|
12
|
+
subscribe,
|
|
13
|
+
switchWallet
|
|
14
|
+
} from "./chunk-HFG2GUFX.js";
|
|
5
15
|
export {
|
|
16
|
+
ACTIVE_WALLET_STORAGE_KEY,
|
|
6
17
|
ConnectError,
|
|
7
|
-
|
|
18
|
+
createAnonServerPolicy,
|
|
19
|
+
createPasskeySigner,
|
|
20
|
+
ejectActiveWallet,
|
|
21
|
+
forgetWallet,
|
|
22
|
+
getActiveHandle,
|
|
23
|
+
listWallets,
|
|
24
|
+
passkeyLogin,
|
|
25
|
+
setActiveHandle,
|
|
26
|
+
subscribe as subscribeWallet,
|
|
27
|
+
switchWallet
|
|
8
28
|
};
|
package/dist/react.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { DexterApiBrowserPasskeySigner } from '@dexterai/vault/signers/browser';
|
|
2
|
+
import { S as SignInResult, P as PasskeyLoginTokens, C as ConnectVault, a as ConnectError, b as StoredWallet } from './walletStore-CWXhXuzK.js';
|
|
2
3
|
import { ReactElement } from 'react';
|
|
3
4
|
|
|
4
5
|
type ConnectStatus = 'idle' | 'pending' | 'done' | 'error';
|
|
@@ -21,6 +22,9 @@ interface UseSignInWithDexter {
|
|
|
21
22
|
vaultAddress: string | null;
|
|
22
23
|
vaultPda: string | null;
|
|
23
24
|
credentialId: string | null;
|
|
25
|
+
/** Guest passkey signer for authorizing spends / opening x402 tabs. null until
|
|
26
|
+
* a vault is connected. Drive it via `passkeySigner.signOperation(op)`. */
|
|
27
|
+
passkeySigner: DexterApiBrowserPasskeySigner | null;
|
|
24
28
|
/** USD available. number once read; null = unknown → chip shows wallet only. */
|
|
25
29
|
usdcBalance: number | null;
|
|
26
30
|
refreshBalance: () => Promise<void>;
|
|
@@ -60,4 +64,25 @@ interface SignInWithDexterProps extends UseSignInWithDexterConfig {
|
|
|
60
64
|
*/
|
|
61
65
|
declare function SignInWithDexter(props: SignInWithDexterProps): ReactElement | null;
|
|
62
66
|
|
|
63
|
-
|
|
67
|
+
interface UseDexterWallet {
|
|
68
|
+
/** Active wallet handle, or null if this browser has no active wallet. */
|
|
69
|
+
activeHandle: string | null;
|
|
70
|
+
/** Known wallets on this browser, most-recently-used first. */
|
|
71
|
+
wallets: StoredWallet[];
|
|
72
|
+
/**
|
|
73
|
+
* Eject the active wallet — "switch / start fresh / sign out of this wallet".
|
|
74
|
+
* The browser is no longer bound to it; the next enroll/recover starts clean.
|
|
75
|
+
* Pass `{ forget: true }` to also drop it from the roster.
|
|
76
|
+
*/
|
|
77
|
+
eject: (opts?: {
|
|
78
|
+
forget?: boolean;
|
|
79
|
+
}) => void;
|
|
80
|
+
/** Switch the active wallet to a known handle. No-op if unknown. */
|
|
81
|
+
switchTo: (handle: string) => boolean;
|
|
82
|
+
/** Record/activate a handle (after enroll or recover). Prefer this over
|
|
83
|
+
* hand-writing localStorage so the roster + subscribers stay correct. */
|
|
84
|
+
setActive: (handle: string, label?: string) => void;
|
|
85
|
+
}
|
|
86
|
+
declare function useDexterWallet(): UseDexterWallet;
|
|
87
|
+
|
|
88
|
+
export { type ConnectStatus, SignInWithDexter, type SignInWithDexterProps, type UseDexterWallet, type UseSignInWithDexter, type UseSignInWithDexterConfig, useDexterWallet, useSignInWithDexter };
|
package/dist/react.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConnectError,
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
createPasskeySigner,
|
|
4
|
+
ejectActiveWallet,
|
|
5
|
+
getActiveHandle,
|
|
6
|
+
listWallets,
|
|
7
|
+
passkeyLogin,
|
|
8
|
+
setActiveHandle,
|
|
9
|
+
subscribe,
|
|
10
|
+
switchWallet
|
|
11
|
+
} from "./chunk-HFG2GUFX.js";
|
|
5
12
|
|
|
6
13
|
// src/useSignInWithDexter.ts
|
|
7
|
-
import { useCallback, useEffect, useState } from "react";
|
|
14
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
8
15
|
|
|
9
16
|
// src/balance.ts
|
|
10
17
|
async function fetchUsdcBalance(rpcUrl, usdcAta) {
|
|
@@ -80,6 +87,10 @@ function useSignInWithDexter(config = {}) {
|
|
|
80
87
|
setError(null);
|
|
81
88
|
setStatus("idle");
|
|
82
89
|
}, []);
|
|
90
|
+
const passkeySigner = useMemo(
|
|
91
|
+
() => vault ? createPasskeySigner(vault, apiBase) : null,
|
|
92
|
+
[vault, apiBase]
|
|
93
|
+
);
|
|
83
94
|
useEffect(() => {
|
|
84
95
|
void refreshBalance();
|
|
85
96
|
}, [refreshBalance]);
|
|
@@ -93,6 +104,7 @@ function useSignInWithDexter(config = {}) {
|
|
|
93
104
|
vaultAddress: vault?.swigAddress ?? null,
|
|
94
105
|
vaultPda: vault?.vaultPda ?? null,
|
|
95
106
|
credentialId: vault?.credentialId ?? null,
|
|
107
|
+
passkeySigner,
|
|
96
108
|
usdcBalance,
|
|
97
109
|
refreshBalance,
|
|
98
110
|
error
|
|
@@ -192,7 +204,30 @@ var DISCONNECT = {
|
|
|
192
204
|
lineHeight: 1,
|
|
193
205
|
opacity: 0.6
|
|
194
206
|
};
|
|
207
|
+
|
|
208
|
+
// src/useDexterWallet.ts
|
|
209
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
|
|
210
|
+
function useDexterWallet() {
|
|
211
|
+
const [activeHandle, setHandle] = useState2(() => getActiveHandle());
|
|
212
|
+
const [wallets, setWallets] = useState2(() => listWallets());
|
|
213
|
+
useEffect2(() => {
|
|
214
|
+
const sync = () => {
|
|
215
|
+
setHandle(getActiveHandle());
|
|
216
|
+
setWallets(listWallets());
|
|
217
|
+
};
|
|
218
|
+
sync();
|
|
219
|
+
return subscribe(sync);
|
|
220
|
+
}, []);
|
|
221
|
+
return {
|
|
222
|
+
activeHandle,
|
|
223
|
+
wallets,
|
|
224
|
+
eject: useCallback2((opts) => ejectActiveWallet(opts), []),
|
|
225
|
+
switchTo: useCallback2((handle) => switchWallet(handle), []),
|
|
226
|
+
setActive: useCallback2((handle, label) => setActiveHandle(handle, label), [])
|
|
227
|
+
};
|
|
228
|
+
}
|
|
195
229
|
export {
|
|
196
230
|
SignInWithDexter,
|
|
231
|
+
useDexterWallet,
|
|
197
232
|
useSignInWithDexter
|
|
198
233
|
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** Supabase session tokens returned by dexter-api's passkey-login (camelCase). */
|
|
2
|
+
interface PasskeyLoginTokens {
|
|
3
|
+
accessToken: string;
|
|
4
|
+
refreshToken: string;
|
|
5
|
+
expiresAt: number;
|
|
6
|
+
expiresIn: number;
|
|
7
|
+
tokenType: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Vault identity, returned ALONGSIDE the session by passkey-login once
|
|
11
|
+
* vault-review ships the dexter-api change (ASK 1). Optional until then —
|
|
12
|
+
* the connector degrades to session-only. Consumers that open x402 tabs
|
|
13
|
+
* (dexter-agents) need `vaultPda` + `publicKey` to build a passkey signer.
|
|
14
|
+
*/
|
|
15
|
+
interface ConnectVault {
|
|
16
|
+
vaultPda: string;
|
|
17
|
+
/** Swig state address, base58 — the user-facing Dexter Wallet address. */
|
|
18
|
+
swigAddress: string;
|
|
19
|
+
/** v2 swig wallet PDA (deposit address); null until the swig is deployed. */
|
|
20
|
+
receiveAddress: string | null;
|
|
21
|
+
/** Swig wallet's USDC ATA, base58 (for the connected-chip balance read);
|
|
22
|
+
* null until the swig is deployed. Server-resolved (off-curve-safe). */
|
|
23
|
+
usdcAta: string | null;
|
|
24
|
+
/** base64 33-byte SEC1 compressed P-256 authority pubkey (for the signer). */
|
|
25
|
+
publicKey: string;
|
|
26
|
+
userHandle: string;
|
|
27
|
+
credentialId: string;
|
|
28
|
+
}
|
|
29
|
+
/** Result of a completed "Sign in with Dexter" ceremony. */
|
|
30
|
+
interface SignInResult {
|
|
31
|
+
session: PasskeyLoginTokens;
|
|
32
|
+
/** Present once vault-review ships the vault-in-login change. */
|
|
33
|
+
vault?: ConnectVault;
|
|
34
|
+
}
|
|
35
|
+
interface DexterConnectConfig {
|
|
36
|
+
/** dexter-api base. Default https://api.dexter.cash. */
|
|
37
|
+
apiBase?: string;
|
|
38
|
+
}
|
|
39
|
+
/** Typed error whose `code` is the server's snake_case error string. */
|
|
40
|
+
declare class ConnectError extends Error {
|
|
41
|
+
readonly code: string;
|
|
42
|
+
constructor(code: string, message?: string);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A wallet this browser knows about. `handle` is the identity; the rest is UX. */
|
|
46
|
+
interface StoredWallet {
|
|
47
|
+
/** base64url 16-byte user handle — the vault identity. */
|
|
48
|
+
handle: string;
|
|
49
|
+
/** Human label for switch UIs (e.g. an email, or "Dexter Wallet"). */
|
|
50
|
+
label?: string;
|
|
51
|
+
/** Epoch ms of last activation — for ordering the switcher. */
|
|
52
|
+
lastUsedAt?: number;
|
|
53
|
+
}
|
|
54
|
+
type Listener = () => void;
|
|
55
|
+
/** The active wallet handle, or null if this browser has no active wallet. */
|
|
56
|
+
declare function getActiveHandle(): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Set the active wallet handle (e.g. after enroll or recover), upserting it into
|
|
59
|
+
* the roster with a fresh `lastUsedAt`. Idempotent. Fires subscribers.
|
|
60
|
+
*/
|
|
61
|
+
declare function setActiveHandle(handle: string, label?: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* EJECT — clear the active wallet so the browser is no longer bound to it. This
|
|
64
|
+
* is "switch / start fresh / sign out of this wallet". The wallet stays in the
|
|
65
|
+
* roster (so the user can switch back) unless `forget` is true. After eject,
|
|
66
|
+
* `getActiveHandle()` is null and the next enroll/recover starts clean. Fires
|
|
67
|
+
* subscribers. This is the function whose absence WAS the welded-wallet bug.
|
|
68
|
+
*/
|
|
69
|
+
declare function ejectActiveWallet(opts?: {
|
|
70
|
+
forget?: boolean;
|
|
71
|
+
}): void;
|
|
72
|
+
/** Every wallet this browser knows about, most-recently-used first. */
|
|
73
|
+
declare function listWallets(): StoredWallet[];
|
|
74
|
+
/**
|
|
75
|
+
* Switch the active wallet to a handle ALREADY in the roster. Returns false (and
|
|
76
|
+
* does nothing) if the handle is unknown — switching is only ever to a wallet
|
|
77
|
+
* this browser has seen, never to an arbitrary string.
|
|
78
|
+
*/
|
|
79
|
+
declare function switchWallet(handle: string): boolean;
|
|
80
|
+
/** Remove a wallet from the roster entirely; clears active if it was active. */
|
|
81
|
+
declare function forgetWallet(handle: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to active-wallet/roster changes. Returns an unsubscribe fn. Also
|
|
84
|
+
* wires the cross-tab `storage` event once, so ejecting in one tab updates the
|
|
85
|
+
* others. The React hook (`useDexterWallet`) is a thin wrapper over this.
|
|
86
|
+
*/
|
|
87
|
+
declare function subscribe(listener: Listener): () => void;
|
|
88
|
+
/** Exposed for consumers that must reference the canonical key (migrations,
|
|
89
|
+
* tests). Prefer the accessors above — do NOT read localStorage by hand. */
|
|
90
|
+
declare const ACTIVE_WALLET_STORAGE_KEY = "dexter:passkey:userHandle";
|
|
91
|
+
|
|
92
|
+
export { ACTIVE_WALLET_STORAGE_KEY as A, type ConnectVault as C, type DexterConnectConfig as D, type PasskeyLoginTokens as P, type SignInResult as S, ConnectError as a, type StoredWallet as b, subscribe as c, switchWallet as d, ejectActiveWallet as e, forgetWallet as f, getActiveHandle as g, listWallets as l, setActiveHandle as s };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexterai/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Sign in with Dexter — passkey connector. Composes @dexterai/vault.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -21,13 +21,12 @@
|
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"typecheck": "tsc --noEmit"
|
|
23
23
|
},
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"@dexterai/vault": "^0.14.1"
|
|
26
|
-
},
|
|
27
24
|
"peerDependencies": {
|
|
25
|
+
"@dexterai/vault": ">=0.19",
|
|
28
26
|
"react": ">=18"
|
|
29
27
|
},
|
|
30
28
|
"devDependencies": {
|
|
29
|
+
"@dexterai/vault": "^0.19.0",
|
|
31
30
|
"@types/react": "^19.1.12",
|
|
32
31
|
"react": "^19.2.5",
|
|
33
32
|
"tsup": "^8.5.0",
|
package/dist/chunk-65VT7AU2.js
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
var ConnectError = class extends Error {
|
|
3
|
-
code;
|
|
4
|
-
constructor(code, message) {
|
|
5
|
-
super(message ?? code);
|
|
6
|
-
this.code = code;
|
|
7
|
-
this.name = "ConnectError";
|
|
8
|
-
}
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
// src/relay.ts
|
|
12
|
-
var DEFAULT_API_BASE = "https://api.dexter.cash";
|
|
13
|
-
var ANON_SIGN_BASE = "/api/passkey-anon/sign";
|
|
14
|
-
async function passkeyLogin(config = {}) {
|
|
15
|
-
const apiBase = (config.apiBase ?? DEFAULT_API_BASE).replace(/\/$/, "");
|
|
16
|
-
const options = await fetchLoginChallenge(apiBase);
|
|
17
|
-
const credential = await getAssertion(options);
|
|
18
|
-
return submitLogin(apiBase, credential);
|
|
19
|
-
}
|
|
20
|
-
async function fetchLoginChallenge(apiBase) {
|
|
21
|
-
const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/login-challenge`, {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "content-type": "application/json" },
|
|
24
|
-
body: "{}"
|
|
25
|
-
});
|
|
26
|
-
if (!res.ok) {
|
|
27
|
-
throw new ConnectError("login_challenge_failed", `login-challenge ${res.status}`);
|
|
28
|
-
}
|
|
29
|
-
const data = await res.json();
|
|
30
|
-
if (!data?.options?.challenge) {
|
|
31
|
-
throw new ConnectError("login_challenge_malformed", "no challenge in response");
|
|
32
|
-
}
|
|
33
|
-
return data.options;
|
|
34
|
-
}
|
|
35
|
-
async function getAssertion(options) {
|
|
36
|
-
if (typeof navigator === "undefined" || !navigator.credentials) {
|
|
37
|
-
throw new ConnectError("webauthn_unsupported", "WebAuthn unavailable in this environment");
|
|
38
|
-
}
|
|
39
|
-
let credential;
|
|
40
|
-
try {
|
|
41
|
-
credential = await navigator.credentials.get({
|
|
42
|
-
publicKey: {
|
|
43
|
-
challenge: base64urlToBytes(options.challenge).buffer.slice(0),
|
|
44
|
-
rpId: options.rpId,
|
|
45
|
-
timeout: options.timeout ?? 6e4,
|
|
46
|
-
userVerification: options.userVerification ?? "required"
|
|
47
|
-
// No allowCredentials — discoverable resident-key login.
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
} catch (err) {
|
|
51
|
-
throw new ConnectError("webauthn_failed", err instanceof Error ? err.message : String(err));
|
|
52
|
-
}
|
|
53
|
-
if (!credential || credential.type !== "public-key") {
|
|
54
|
-
throw new ConnectError("no_credential", "WebAuthn returned no credential");
|
|
55
|
-
}
|
|
56
|
-
return credential;
|
|
57
|
-
}
|
|
58
|
-
async function submitLogin(apiBase, credential) {
|
|
59
|
-
const assertion = credential.response;
|
|
60
|
-
const credentialJson = {
|
|
61
|
-
id: credential.id,
|
|
62
|
-
rawId: bytesToBase64url(new Uint8Array(credential.rawId)),
|
|
63
|
-
type: credential.type,
|
|
64
|
-
response: {
|
|
65
|
-
clientDataJSON: bytesToBase64url(new Uint8Array(assertion.clientDataJSON)),
|
|
66
|
-
authenticatorData: bytesToBase64url(new Uint8Array(assertion.authenticatorData)),
|
|
67
|
-
signature: bytesToBase64url(new Uint8Array(assertion.signature)),
|
|
68
|
-
userHandle: assertion.userHandle ? bytesToBase64url(new Uint8Array(assertion.userHandle)) : null
|
|
69
|
-
},
|
|
70
|
-
clientExtensionResults: credential.getClientExtensionResults?.() ?? {}
|
|
71
|
-
};
|
|
72
|
-
const res = await fetch(`${apiBase}${ANON_SIGN_BASE}/passkey-login`, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: { "content-type": "application/json" },
|
|
75
|
-
body: JSON.stringify({ credential: credentialJson })
|
|
76
|
-
});
|
|
77
|
-
if (!res.ok) {
|
|
78
|
-
throw new ConnectError(await readErrorCode(res), `passkey-login ${res.status}`);
|
|
79
|
-
}
|
|
80
|
-
const data = await res.json();
|
|
81
|
-
const session = {
|
|
82
|
-
accessToken: data.accessToken,
|
|
83
|
-
refreshToken: data.refreshToken,
|
|
84
|
-
expiresAt: data.expiresAt,
|
|
85
|
-
expiresIn: data.expiresIn,
|
|
86
|
-
tokenType: data.tokenType
|
|
87
|
-
};
|
|
88
|
-
return data.vault ? { session, vault: data.vault } : { session };
|
|
89
|
-
}
|
|
90
|
-
async function readErrorCode(res) {
|
|
91
|
-
try {
|
|
92
|
-
const body = await res.json();
|
|
93
|
-
if (body?.error) return body.error;
|
|
94
|
-
} catch {
|
|
95
|
-
}
|
|
96
|
-
return `http_${res.status}`;
|
|
97
|
-
}
|
|
98
|
-
function base64ToBytes(s) {
|
|
99
|
-
const bin = atob(s);
|
|
100
|
-
const out = new Uint8Array(bin.length);
|
|
101
|
-
for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
|
|
102
|
-
return out;
|
|
103
|
-
}
|
|
104
|
-
function base64urlToBytes(s) {
|
|
105
|
-
const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - s.length % 4);
|
|
106
|
-
const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + pad;
|
|
107
|
-
return base64ToBytes(b64);
|
|
108
|
-
}
|
|
109
|
-
function bytesToBase64url(bytes) {
|
|
110
|
-
let bin = "";
|
|
111
|
-
for (let i = 0; i < bytes.length; i += 1) bin += String.fromCharCode(bytes[i]);
|
|
112
|
-
return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export {
|
|
116
|
-
ConnectError,
|
|
117
|
-
passkeyLogin
|
|
118
|
-
};
|
package/dist/types-DT3KPBOE.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/** Supabase session tokens returned by dexter-api's passkey-login (camelCase). */
|
|
2
|
-
interface PasskeyLoginTokens {
|
|
3
|
-
accessToken: string;
|
|
4
|
-
refreshToken: string;
|
|
5
|
-
expiresAt: number;
|
|
6
|
-
expiresIn: number;
|
|
7
|
-
tokenType: string;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Vault identity, returned ALONGSIDE the session by passkey-login once
|
|
11
|
-
* vault-review ships the dexter-api change (ASK 1). Optional until then —
|
|
12
|
-
* the connector degrades to session-only. Consumers that open x402 tabs
|
|
13
|
-
* (dexter-agents) need `vaultPda` + `publicKey` to build a passkey signer.
|
|
14
|
-
*/
|
|
15
|
-
interface ConnectVault {
|
|
16
|
-
vaultPda: string;
|
|
17
|
-
/** Swig state address, base58 — the user-facing Dexter Wallet address. */
|
|
18
|
-
swigAddress: string;
|
|
19
|
-
/** v2 swig wallet PDA (deposit address); null until the swig is deployed. */
|
|
20
|
-
receiveAddress: string | null;
|
|
21
|
-
/** Swig wallet's USDC ATA, base58 (for the connected-chip balance read);
|
|
22
|
-
* null until the swig is deployed. Server-resolved (off-curve-safe). */
|
|
23
|
-
usdcAta: string | null;
|
|
24
|
-
/** base64 33-byte SEC1 compressed P-256 authority pubkey (for the signer). */
|
|
25
|
-
publicKey: string;
|
|
26
|
-
userHandle: string;
|
|
27
|
-
credentialId: string;
|
|
28
|
-
}
|
|
29
|
-
/** Result of a completed "Sign in with Dexter" ceremony. */
|
|
30
|
-
interface SignInResult {
|
|
31
|
-
session: PasskeyLoginTokens;
|
|
32
|
-
/** Present once vault-review ships the vault-in-login change. */
|
|
33
|
-
vault?: ConnectVault;
|
|
34
|
-
}
|
|
35
|
-
interface DexterConnectConfig {
|
|
36
|
-
/** dexter-api base. Default https://api.dexter.cash. */
|
|
37
|
-
apiBase?: string;
|
|
38
|
-
}
|
|
39
|
-
/** Typed error whose `code` is the server's snake_case error string. */
|
|
40
|
-
declare class ConnectError extends Error {
|
|
41
|
-
readonly code: string;
|
|
42
|
-
constructor(code: string, message?: string);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export { ConnectError as C, type DexterConnectConfig as D, type PasskeyLoginTokens as P, type SignInResult as S, type ConnectVault as a };
|