@dexterai/connect 0.1.0 → 0.2.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-46P7XAAI.js +236 -0
- package/dist/index.d.ts +63 -3
- package/dist/index.js +5 -1
- package/dist/react.d.ts +5 -1
- package/dist/react.js +8 -2
- package/dist/{types-DT3KPBOE.d.ts → types---RcNI2Y.d.ts} +1 -1
- package/package.json +3 -4
- package/dist/chunk-65VT7AU2.js +0 -118
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,236 @@
|
|
|
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
|
+
export {
|
|
232
|
+
ConnectError,
|
|
233
|
+
passkeyLogin,
|
|
234
|
+
createAnonServerPolicy,
|
|
235
|
+
createPasskeySigner
|
|
236
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { D as DexterConnectConfig, S as SignInResult } from './types
|
|
2
|
-
export {
|
|
1
|
+
import { D as DexterConnectConfig, S as SignInResult, C as ConnectVault } from './types---RcNI2Y.js';
|
|
2
|
+
export { a as ConnectError, P as PasskeyLoginTokens } from './types---RcNI2Y.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
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 } from './types---RcNI2Y.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>;
|
package/dist/react.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ConnectError,
|
|
3
|
+
createPasskeySigner,
|
|
3
4
|
passkeyLogin
|
|
4
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-46P7XAAI.js";
|
|
5
6
|
|
|
6
7
|
// src/useSignInWithDexter.ts
|
|
7
|
-
import { useCallback, useEffect, useState } from "react";
|
|
8
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
8
9
|
|
|
9
10
|
// src/balance.ts
|
|
10
11
|
async function fetchUsdcBalance(rpcUrl, usdcAta) {
|
|
@@ -80,6 +81,10 @@ function useSignInWithDexter(config = {}) {
|
|
|
80
81
|
setError(null);
|
|
81
82
|
setStatus("idle");
|
|
82
83
|
}, []);
|
|
84
|
+
const passkeySigner = useMemo(
|
|
85
|
+
() => vault ? createPasskeySigner(vault, apiBase) : null,
|
|
86
|
+
[vault, apiBase]
|
|
87
|
+
);
|
|
83
88
|
useEffect(() => {
|
|
84
89
|
void refreshBalance();
|
|
85
90
|
}, [refreshBalance]);
|
|
@@ -93,6 +98,7 @@ function useSignInWithDexter(config = {}) {
|
|
|
93
98
|
vaultAddress: vault?.swigAddress ?? null,
|
|
94
99
|
vaultPda: vault?.vaultPda ?? null,
|
|
95
100
|
credentialId: vault?.credentialId ?? null,
|
|
101
|
+
passkeySigner,
|
|
96
102
|
usdcBalance,
|
|
97
103
|
refreshBalance,
|
|
98
104
|
error
|
|
@@ -42,4 +42,4 @@ declare class ConnectError extends Error {
|
|
|
42
42
|
constructor(code: string, message?: string);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export {
|
|
45
|
+
export { type ConnectVault as C, type DexterConnectConfig as D, type PasskeyLoginTokens as P, type SignInResult as S, ConnectError as a };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexterai/connect",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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
|
-
};
|