@thru/passkey 0.2.13 → 0.2.15
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 +73 -90
- package/dist/auth.cjs +672 -0
- package/dist/auth.cjs.map +1 -0
- package/dist/auth.d.cts +60 -0
- package/dist/auth.d.ts +60 -0
- package/dist/auth.js +422 -0
- package/dist/auth.js.map +1 -0
- package/dist/chunk-2JHC7OOH.js +250 -0
- package/dist/chunk-2JHC7OOH.js.map +1 -0
- package/dist/chunk-75G2FPYW.js +54 -0
- package/dist/chunk-75G2FPYW.js.map +1 -0
- package/dist/chunk-B5SN7AS7.js +586 -0
- package/dist/chunk-B5SN7AS7.js.map +1 -0
- package/dist/chunk-LNDWK3FA.js +163 -0
- package/dist/chunk-LNDWK3FA.js.map +1 -0
- package/dist/index.cjs +27 -94
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -187
- package/dist/index.d.ts +4 -187
- package/dist/index.js +47 -810
- package/dist/index.js.map +1 -1
- package/dist/mobile.cjs +301 -0
- package/dist/mobile.cjs.map +1 -0
- package/dist/mobile.d.cts +49 -0
- package/dist/mobile.d.ts +49 -0
- package/dist/mobile.js +41 -0
- package/dist/mobile.js.map +1 -0
- package/dist/popup.cjs +247 -0
- package/dist/popup.cjs.map +1 -0
- package/dist/popup.d.cts +22 -0
- package/dist/popup.d.ts +22 -0
- package/dist/popup.js +31 -0
- package/dist/popup.js.map +1 -0
- package/dist/server.cjs +351 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +119 -0
- package/dist/server.d.ts +119 -0
- package/dist/server.js +340 -0
- package/dist/server.js.map +1 -0
- package/dist/types-_HRzmn-j.d.cts +125 -0
- package/dist/types-_HRzmn-j.d.ts +125 -0
- package/dist/web.cjs +758 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.cts +32 -0
- package/dist/web.d.ts +32 -0
- package/dist/web.js +60 -0
- package/dist/web.js.map +1 -0
- package/package.json +47 -2
- package/src/auth/execute-tx.ts +87 -0
- package/src/auth/index.ts +18 -0
- package/src/auth/types.ts +56 -0
- package/src/auth/use-passkey-auth.ts +428 -0
- package/src/index.ts +37 -39
- package/src/mobile/errors.ts +31 -0
- package/src/mobile/index.ts +33 -0
- package/src/mobile/passkey.ts +154 -0
- package/src/mobile/storage.ts +115 -0
- package/src/mobile/types.ts +24 -0
- package/src/popup-entry.ts +33 -0
- package/src/popup-service.ts +0 -103
- package/src/server/challenge.ts +26 -0
- package/src/server/create-wallet.ts +149 -0
- package/src/server/handlers.ts +93 -0
- package/src/server/index.ts +13 -0
- package/src/server/submit.ts +47 -0
- package/src/server/types.ts +70 -0
- package/src/server/utils.ts +69 -0
- package/src/types.ts +1 -0
- package/src/web.ts +51 -0
- package/tsconfig.json +6 -1
- package/tsup.config.ts +9 -1
package/dist/auth.cjs
ADDED
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/auth/index.ts
|
|
31
|
+
var auth_exports = {};
|
|
32
|
+
__export(auth_exports, {
|
|
33
|
+
createPasskeyAuthStore: () => createPasskeyAuthStore,
|
|
34
|
+
executePasskeyTransaction: () => executePasskeyTransaction,
|
|
35
|
+
getPasskeyAuthStore: () => getPasskeyAuthStore,
|
|
36
|
+
usePasskeyAuth: () => usePasskeyAuth
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(auth_exports);
|
|
39
|
+
|
|
40
|
+
// src/auth/execute-tx.ts
|
|
41
|
+
var import_passkey_manager2 = require("@thru/passkey-manager");
|
|
42
|
+
|
|
43
|
+
// src/mobile/passkey.ts
|
|
44
|
+
var import_react_native_passkeys = require("react-native-passkeys");
|
|
45
|
+
var import_passkey_manager = require("@thru/passkey-manager");
|
|
46
|
+
function getDefaultConfig(config) {
|
|
47
|
+
const env = globalThis.process?.env ?? {};
|
|
48
|
+
return {
|
|
49
|
+
rpId: config?.rpId ?? env.EXPO_PUBLIC_PASSKEY_RP_ID ?? "wallet.thru.org",
|
|
50
|
+
rpName: config?.rpName ?? env.EXPO_PUBLIC_PASSKEY_RP_NAME ?? "Thru Wallet"
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
async function registerPasskey(alias, userId, config) {
|
|
54
|
+
const { rpId, rpName } = getDefaultConfig(config);
|
|
55
|
+
const challenge = (0, import_passkey_manager.bytesToBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
|
|
56
|
+
const userIdB64 = (0, import_passkey_manager.bytesToBase64Url)(new TextEncoder().encode(userId));
|
|
57
|
+
const result = await (0, import_react_native_passkeys.create)({
|
|
58
|
+
challenge,
|
|
59
|
+
rp: { id: rpId, name: rpName },
|
|
60
|
+
user: { id: userIdB64, name: alias, displayName: alias },
|
|
61
|
+
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
|
|
62
|
+
authenticatorSelection: {
|
|
63
|
+
authenticatorAttachment: "platform",
|
|
64
|
+
userVerification: "required",
|
|
65
|
+
residentKey: "required"
|
|
66
|
+
},
|
|
67
|
+
attestation: "none",
|
|
68
|
+
timeout: 6e4
|
|
69
|
+
});
|
|
70
|
+
if (!result) {
|
|
71
|
+
throw new Error("Passkey registration was cancelled");
|
|
72
|
+
}
|
|
73
|
+
const publicKeyB64 = result.response.getPublicKey?.();
|
|
74
|
+
if (!publicKeyB64) {
|
|
75
|
+
throw new Error("Failed to retrieve public key from registration");
|
|
76
|
+
}
|
|
77
|
+
const keyBytes = (0, import_passkey_manager.base64UrlToBytes)(publicKeyB64);
|
|
78
|
+
const { x, y } = extractP256Coordinates(keyBytes);
|
|
79
|
+
return {
|
|
80
|
+
credentialId: result.id,
|
|
81
|
+
publicKeyX: x,
|
|
82
|
+
publicKeyY: y,
|
|
83
|
+
rpId
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async function signWithPasskey(credentialId, challenge, rpId) {
|
|
87
|
+
const resolvedRpId = rpId ?? getDefaultConfig().rpId;
|
|
88
|
+
const challengeB64 = (0, import_passkey_manager.bytesToBase64Url)(challenge);
|
|
89
|
+
const result = await (0, import_react_native_passkeys.get)({
|
|
90
|
+
challenge: challengeB64,
|
|
91
|
+
rpId: resolvedRpId,
|
|
92
|
+
allowCredentials: [{ type: "public-key", id: credentialId }],
|
|
93
|
+
userVerification: "required",
|
|
94
|
+
timeout: 6e4
|
|
95
|
+
});
|
|
96
|
+
if (!result) {
|
|
97
|
+
throw new Error("Passkey authentication was cancelled");
|
|
98
|
+
}
|
|
99
|
+
const derSignature = (0, import_passkey_manager.base64UrlToBytes)(result.response.signature);
|
|
100
|
+
let { r, s } = (0, import_passkey_manager.parseDerSignature)(derSignature);
|
|
101
|
+
s = (0, import_passkey_manager.normalizeLowS)(s);
|
|
102
|
+
return {
|
|
103
|
+
signature: new Uint8Array([...r, ...s]),
|
|
104
|
+
authenticatorData: (0, import_passkey_manager.base64UrlToBytes)(result.response.authenticatorData),
|
|
105
|
+
clientDataJSON: (0, import_passkey_manager.base64UrlToBytes)(result.response.clientDataJSON),
|
|
106
|
+
signatureR: r,
|
|
107
|
+
signatureS: s
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function authenticateWithDiscoverablePasskey(config) {
|
|
111
|
+
const { rpId } = getDefaultConfig(config);
|
|
112
|
+
try {
|
|
113
|
+
const challenge = (0, import_passkey_manager.bytesToBase64Url)(crypto.getRandomValues(new Uint8Array(32)));
|
|
114
|
+
const result = await (0, import_react_native_passkeys.get)({
|
|
115
|
+
challenge,
|
|
116
|
+
rpId,
|
|
117
|
+
userVerification: "required",
|
|
118
|
+
timeout: 6e4
|
|
119
|
+
});
|
|
120
|
+
return result ? { credentialId: result.id, rpId } : null;
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function extractP256Coordinates(keyBytes) {
|
|
126
|
+
if (keyBytes.length === 64) {
|
|
127
|
+
return {
|
|
128
|
+
x: keyBytes.slice(0, 32),
|
|
129
|
+
y: keyBytes.slice(32, 64)
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (keyBytes.length === 65 && keyBytes[0] === 4) {
|
|
133
|
+
return {
|
|
134
|
+
x: keyBytes.slice(1, 33),
|
|
135
|
+
y: keyBytes.slice(33, 65)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const pointStart = keyBytes.length - 65;
|
|
139
|
+
if (pointStart > 0 && keyBytes[pointStart] === 4) {
|
|
140
|
+
return {
|
|
141
|
+
x: keyBytes.slice(pointStart + 1, pointStart + 33),
|
|
142
|
+
y: keyBytes.slice(pointStart + 33, pointStart + 65)
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Unsupported public key format (${keyBytes.length} bytes). Expected raw X||Y (64), uncompressed point (65), or SPKI DER (91).`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/mobile/storage.ts
|
|
151
|
+
var SecureStore = __toESM(require("expo-secure-store"), 1);
|
|
152
|
+
var SECURE_STORE_OPTS = {
|
|
153
|
+
keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY
|
|
154
|
+
};
|
|
155
|
+
var PASSKEY_CREDENTIAL_ID_KEY = "thru_passkey_credential_id";
|
|
156
|
+
var PASSKEY_PUBLIC_KEY_X_KEY = "thru_passkey_pubkey_x";
|
|
157
|
+
var PASSKEY_PUBLIC_KEY_Y_KEY = "thru_passkey_pubkey_y";
|
|
158
|
+
var PASSKEY_RP_ID_KEY = "thru_passkey_rp_id";
|
|
159
|
+
var PASSKEY_LABEL_KEY = "thru_passkey_label";
|
|
160
|
+
var PASSKEY_CREATED_AT_KEY = "thru_passkey_created_at";
|
|
161
|
+
var PASSKEY_LAST_USED_AT_KEY = "thru_passkey_last_used_at";
|
|
162
|
+
var ADDRESS_KEY = "thru_address";
|
|
163
|
+
var USER_ID_KEY = "thru_user_id";
|
|
164
|
+
var TOKEN_ACCOUNT_KEY = "thru_token_account";
|
|
165
|
+
async function storePasskeyMetadata(metadata) {
|
|
166
|
+
await Promise.all([
|
|
167
|
+
SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, metadata.credentialId, SECURE_STORE_OPTS),
|
|
168
|
+
SecureStore.setItemAsync(PASSKEY_PUBLIC_KEY_X_KEY, metadata.publicKeyX, SECURE_STORE_OPTS),
|
|
169
|
+
SecureStore.setItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY, metadata.publicKeyY, SECURE_STORE_OPTS),
|
|
170
|
+
SecureStore.setItemAsync(PASSKEY_RP_ID_KEY, metadata.rpId, SECURE_STORE_OPTS),
|
|
171
|
+
SecureStore.setItemAsync(PASSKEY_LABEL_KEY, metadata.label ?? "", SECURE_STORE_OPTS),
|
|
172
|
+
SecureStore.setItemAsync(PASSKEY_CREATED_AT_KEY, metadata.createdAt, SECURE_STORE_OPTS),
|
|
173
|
+
SecureStore.setItemAsync(PASSKEY_LAST_USED_AT_KEY, metadata.lastUsedAt, SECURE_STORE_OPTS)
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
async function getStoredPasskeyMetadata() {
|
|
177
|
+
const credentialId = await SecureStore.getItemAsync(PASSKEY_CREDENTIAL_ID_KEY);
|
|
178
|
+
if (!credentialId) return null;
|
|
179
|
+
const [publicKeyX, publicKeyY, rpId, label, createdAt, lastUsedAt] = await Promise.all([
|
|
180
|
+
SecureStore.getItemAsync(PASSKEY_PUBLIC_KEY_X_KEY),
|
|
181
|
+
SecureStore.getItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY),
|
|
182
|
+
SecureStore.getItemAsync(PASSKEY_RP_ID_KEY),
|
|
183
|
+
SecureStore.getItemAsync(PASSKEY_LABEL_KEY),
|
|
184
|
+
SecureStore.getItemAsync(PASSKEY_CREATED_AT_KEY),
|
|
185
|
+
SecureStore.getItemAsync(PASSKEY_LAST_USED_AT_KEY)
|
|
186
|
+
]);
|
|
187
|
+
if (!rpId || !createdAt) return null;
|
|
188
|
+
return {
|
|
189
|
+
credentialId,
|
|
190
|
+
publicKeyX: publicKeyX ?? "",
|
|
191
|
+
publicKeyY: publicKeyY ?? "",
|
|
192
|
+
rpId,
|
|
193
|
+
label: label || void 0,
|
|
194
|
+
createdAt,
|
|
195
|
+
lastUsedAt: lastUsedAt ?? createdAt
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function touchPasskeyLastUsedAt(lastUsedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
199
|
+
await SecureStore.setItemAsync(PASSKEY_LAST_USED_AT_KEY, lastUsedAt, SECURE_STORE_OPTS);
|
|
200
|
+
return lastUsedAt;
|
|
201
|
+
}
|
|
202
|
+
async function hasStoredPasskey() {
|
|
203
|
+
return await SecureStore.getItemAsync(PASSKEY_CREDENTIAL_ID_KEY) !== null;
|
|
204
|
+
}
|
|
205
|
+
async function clearPasskeyMetadata() {
|
|
206
|
+
await Promise.all([
|
|
207
|
+
SecureStore.deleteItemAsync(PASSKEY_CREDENTIAL_ID_KEY),
|
|
208
|
+
SecureStore.deleteItemAsync(PASSKEY_PUBLIC_KEY_X_KEY),
|
|
209
|
+
SecureStore.deleteItemAsync(PASSKEY_PUBLIC_KEY_Y_KEY),
|
|
210
|
+
SecureStore.deleteItemAsync(PASSKEY_RP_ID_KEY),
|
|
211
|
+
SecureStore.deleteItemAsync(PASSKEY_LABEL_KEY),
|
|
212
|
+
SecureStore.deleteItemAsync(PASSKEY_CREATED_AT_KEY),
|
|
213
|
+
SecureStore.deleteItemAsync(PASSKEY_LAST_USED_AT_KEY)
|
|
214
|
+
]);
|
|
215
|
+
}
|
|
216
|
+
async function storeWalletInfo(address, userId, tokenAccountAddress) {
|
|
217
|
+
await Promise.all([
|
|
218
|
+
SecureStore.setItemAsync(ADDRESS_KEY, address, SECURE_STORE_OPTS),
|
|
219
|
+
SecureStore.setItemAsync(USER_ID_KEY, userId, SECURE_STORE_OPTS),
|
|
220
|
+
tokenAccountAddress ? SecureStore.setItemAsync(TOKEN_ACCOUNT_KEY, tokenAccountAddress, SECURE_STORE_OPTS) : SecureStore.deleteItemAsync(TOKEN_ACCOUNT_KEY)
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
async function hasStoredWallet() {
|
|
224
|
+
return await SecureStore.getItemAsync(ADDRESS_KEY) !== null;
|
|
225
|
+
}
|
|
226
|
+
async function getStoredAddress() {
|
|
227
|
+
return SecureStore.getItemAsync(ADDRESS_KEY);
|
|
228
|
+
}
|
|
229
|
+
async function getStoredUserId() {
|
|
230
|
+
return SecureStore.getItemAsync(USER_ID_KEY);
|
|
231
|
+
}
|
|
232
|
+
async function clearSession() {
|
|
233
|
+
await Promise.all([
|
|
234
|
+
SecureStore.deleteItemAsync(ADDRESS_KEY),
|
|
235
|
+
SecureStore.deleteItemAsync(USER_ID_KEY),
|
|
236
|
+
SecureStore.deleteItemAsync(TOKEN_ACCOUNT_KEY)
|
|
237
|
+
]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/auth/execute-tx.ts
|
|
241
|
+
async function readJson(response) {
|
|
242
|
+
try {
|
|
243
|
+
return await response.json();
|
|
244
|
+
} catch {
|
|
245
|
+
throw new Error(`Non-JSON response (HTTP ${response.status})`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function executePasskeyTransaction(opts) {
|
|
249
|
+
let challengeRes;
|
|
250
|
+
try {
|
|
251
|
+
challengeRes = await fetch(opts.challengeUrl, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "Content-Type": "application/json" },
|
|
254
|
+
body: JSON.stringify(opts.params)
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
throw new Error("Network request failed (challenge)");
|
|
258
|
+
}
|
|
259
|
+
const challengeData = await readJson(challengeRes);
|
|
260
|
+
if (!challengeRes.ok || challengeData.success !== true) {
|
|
261
|
+
throw new Error(
|
|
262
|
+
typeof challengeData.error === "string" ? challengeData.error : "Failed to get challenge"
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (typeof challengeData.challenge !== "string") {
|
|
266
|
+
throw new Error("Challenge response did not include a challenge");
|
|
267
|
+
}
|
|
268
|
+
const challengeBytes = (0, import_passkey_manager2.base64UrlToBytes)(challengeData.challenge);
|
|
269
|
+
const signature = await signWithPasskey(
|
|
270
|
+
opts.credentialId,
|
|
271
|
+
challengeBytes,
|
|
272
|
+
opts.rpId
|
|
273
|
+
);
|
|
274
|
+
await touchPasskeyLastUsedAt().catch((error) => {
|
|
275
|
+
console.warn("Failed to update passkey last-used timestamp:", error);
|
|
276
|
+
});
|
|
277
|
+
const { success: _success, error: _error, ...challengeFields } = challengeData;
|
|
278
|
+
let submitRes;
|
|
279
|
+
try {
|
|
280
|
+
submitRes = await fetch(opts.submitUrl, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers: { "Content-Type": "application/json" },
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
...opts.params,
|
|
285
|
+
...challengeFields,
|
|
286
|
+
signatureR: (0, import_passkey_manager2.bytesToHex)(signature.signatureR),
|
|
287
|
+
signatureS: (0, import_passkey_manager2.bytesToHex)(signature.signatureS),
|
|
288
|
+
authenticatorData: (0, import_passkey_manager2.bytesToBase64)(signature.authenticatorData),
|
|
289
|
+
clientDataJSON: (0, import_passkey_manager2.bytesToBase64)(signature.clientDataJSON)
|
|
290
|
+
})
|
|
291
|
+
});
|
|
292
|
+
} catch {
|
|
293
|
+
throw new Error("Network request failed (submit)");
|
|
294
|
+
}
|
|
295
|
+
const submitData = await readJson(submitRes);
|
|
296
|
+
if (!submitRes.ok || submitData.success !== true) {
|
|
297
|
+
throw new Error(
|
|
298
|
+
typeof submitData.error === "string" ? submitData.error : "Failed to submit transaction"
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
return submitData;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/auth/use-passkey-auth.ts
|
|
305
|
+
var import_passkey_manager3 = require("@thru/passkey-manager");
|
|
306
|
+
var import_zustand = require("zustand");
|
|
307
|
+
|
|
308
|
+
// src/mobile/errors.ts
|
|
309
|
+
var PASSKEY_ERRORS = {
|
|
310
|
+
USER_CANCELLED: [
|
|
311
|
+
"error 1001",
|
|
312
|
+
"UserCancelled",
|
|
313
|
+
"Passkey authentication was cancelled",
|
|
314
|
+
"Passkey registration was cancelled"
|
|
315
|
+
],
|
|
316
|
+
NOT_FOUND: [
|
|
317
|
+
"not found",
|
|
318
|
+
"No credentials available",
|
|
319
|
+
"no passkey",
|
|
320
|
+
"NoCredentials"
|
|
321
|
+
]
|
|
322
|
+
};
|
|
323
|
+
function classifyPasskeyError(error) {
|
|
324
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : null;
|
|
325
|
+
if (!message) return null;
|
|
326
|
+
for (const [kind, patterns] of Object.entries(PASSKEY_ERRORS)) {
|
|
327
|
+
if (patterns.some((pattern) => message.includes(pattern))) {
|
|
328
|
+
return kind;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// src/auth/use-passkey-auth.ts
|
|
335
|
+
var storeCache = /* @__PURE__ */ new Map();
|
|
336
|
+
function createStoreKey(config) {
|
|
337
|
+
return [config.apiUrl, config.alias ?? "", config.rpId ?? "", config.rpName ?? ""].join("::");
|
|
338
|
+
}
|
|
339
|
+
function buildDisplayName(address) {
|
|
340
|
+
return `${address.slice(0, 8)}...${address.slice(-4)}`;
|
|
341
|
+
}
|
|
342
|
+
function toPasskeyUser(user) {
|
|
343
|
+
return {
|
|
344
|
+
id: user.id,
|
|
345
|
+
displayName: buildDisplayName(user.publicKey),
|
|
346
|
+
tokenAccountAddress: user.tokenAccountAddress ?? null,
|
|
347
|
+
extras: user.extras
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function readJson2(response) {
|
|
351
|
+
return await response.json();
|
|
352
|
+
}
|
|
353
|
+
async function postJson(url, body) {
|
|
354
|
+
const response = await fetch(url, {
|
|
355
|
+
method: "POST",
|
|
356
|
+
headers: { "Content-Type": "application/json" },
|
|
357
|
+
body: JSON.stringify(body)
|
|
358
|
+
});
|
|
359
|
+
const data = await readJson2(response);
|
|
360
|
+
if (!response.ok || data.success !== true) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
typeof data.error === "string" ? data.error : "Request failed"
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
return data;
|
|
366
|
+
}
|
|
367
|
+
async function getCurrentUser(apiUrl, address) {
|
|
368
|
+
const response = await fetch(`${apiUrl}/auth/me`, {
|
|
369
|
+
headers: {
|
|
370
|
+
"Content-Type": "application/json",
|
|
371
|
+
"x-wallet-address": address
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
if (response.status === 404) return null;
|
|
375
|
+
const data = await readJson2(response);
|
|
376
|
+
if (!response.ok || data.success !== true) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
typeof data.error === "string" ? data.error : "Failed to fetch current user"
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
return data;
|
|
382
|
+
}
|
|
383
|
+
function createPasskeyAuthStore(config) {
|
|
384
|
+
const resolvedAlias = config.alias ?? "Thru Wallet";
|
|
385
|
+
return (0, import_zustand.create)((set, get) => ({
|
|
386
|
+
isAuthenticated: false,
|
|
387
|
+
isLoading: false,
|
|
388
|
+
isInitialized: false,
|
|
389
|
+
hasExistingPasskey: false,
|
|
390
|
+
needsNewPasskey: false,
|
|
391
|
+
error: null,
|
|
392
|
+
user: null,
|
|
393
|
+
address: null,
|
|
394
|
+
activeCredentialId: null,
|
|
395
|
+
initialize: async () => {
|
|
396
|
+
try {
|
|
397
|
+
const hasPasskey = await hasStoredPasskey();
|
|
398
|
+
if (hasPasskey) {
|
|
399
|
+
const storedAddress = await getStoredAddress();
|
|
400
|
+
if (storedAddress) {
|
|
401
|
+
const user = await Promise.race([
|
|
402
|
+
getCurrentUser(config.apiUrl, storedAddress),
|
|
403
|
+
new Promise(
|
|
404
|
+
(_, reject) => setTimeout(() => reject(new Error("timeout")), 2e3)
|
|
405
|
+
)
|
|
406
|
+
]).catch(() => void 0);
|
|
407
|
+
if (user === null) {
|
|
408
|
+
await clearSession();
|
|
409
|
+
await clearPasskeyMetadata();
|
|
410
|
+
set({ isInitialized: true, hasExistingPasskey: false });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
} else {
|
|
414
|
+
const metadata = await getStoredPasskeyMetadata();
|
|
415
|
+
if (metadata && !metadata.publicKeyX && !metadata.publicKeyY) {
|
|
416
|
+
await clearPasskeyMetadata();
|
|
417
|
+
set({ isInitialized: true, hasExistingPasskey: false });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
set({ isInitialized: true, hasExistingPasskey: hasPasskey });
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error("Failed to initialize passkey auth:", error);
|
|
425
|
+
set({ isInitialized: true, error: "Failed to initialize wallet" });
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
createWallet: async () => {
|
|
429
|
+
set({ isLoading: true, error: null, needsNewPasskey: false });
|
|
430
|
+
try {
|
|
431
|
+
const tempId = `user-${Date.now()}`;
|
|
432
|
+
const { credentialId, publicKeyX, publicKeyY, rpId } = await registerPasskey(resolvedAlias, tempId, {
|
|
433
|
+
rpId: config.rpId,
|
|
434
|
+
rpName: config.rpName
|
|
435
|
+
});
|
|
436
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
437
|
+
const pubkeyXHex = (0, import_passkey_manager3.bytesToHex)(publicKeyX);
|
|
438
|
+
const pubkeyYHex = (0, import_passkey_manager3.bytesToHex)(publicKeyY);
|
|
439
|
+
await storePasskeyMetadata({
|
|
440
|
+
credentialId,
|
|
441
|
+
publicKeyX: pubkeyXHex,
|
|
442
|
+
publicKeyY: pubkeyYHex,
|
|
443
|
+
rpId,
|
|
444
|
+
createdAt: now,
|
|
445
|
+
lastUsedAt: now
|
|
446
|
+
});
|
|
447
|
+
const response = await postJson(
|
|
448
|
+
`${config.apiUrl}/auth/register-passkey-wallet`,
|
|
449
|
+
{
|
|
450
|
+
pubkeyX: pubkeyXHex,
|
|
451
|
+
pubkeyY: pubkeyYHex,
|
|
452
|
+
credentialId
|
|
453
|
+
}
|
|
454
|
+
);
|
|
455
|
+
const walletAddress = response.user.publicKey;
|
|
456
|
+
await storeWalletInfo(
|
|
457
|
+
walletAddress,
|
|
458
|
+
response.user.id,
|
|
459
|
+
response.user.tokenAccountAddress ?? void 0
|
|
460
|
+
);
|
|
461
|
+
set({
|
|
462
|
+
isLoading: false,
|
|
463
|
+
isAuthenticated: true,
|
|
464
|
+
hasExistingPasskey: true,
|
|
465
|
+
activeCredentialId: credentialId,
|
|
466
|
+
address: walletAddress,
|
|
467
|
+
user: toPasskeyUser(response.user)
|
|
468
|
+
});
|
|
469
|
+
return true;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (classifyPasskeyError(error) === "USER_CANCELLED") {
|
|
472
|
+
set({ isLoading: false });
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
console.error("Failed to create passkey wallet:", error);
|
|
476
|
+
set({
|
|
477
|
+
isLoading: false,
|
|
478
|
+
error: error instanceof Error ? error.message : "Failed to create wallet"
|
|
479
|
+
});
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
},
|
|
483
|
+
unlockWithPasskey: async () => {
|
|
484
|
+
set({ isLoading: true, error: null, needsNewPasskey: false });
|
|
485
|
+
try {
|
|
486
|
+
const metadata = await getStoredPasskeyMetadata();
|
|
487
|
+
if (!metadata) throw new Error("No stored passkey found");
|
|
488
|
+
await signWithPasskey(
|
|
489
|
+
metadata.credentialId,
|
|
490
|
+
crypto.getRandomValues(new Uint8Array(32)),
|
|
491
|
+
metadata.rpId
|
|
492
|
+
);
|
|
493
|
+
await touchPasskeyLastUsedAt().catch((error) => {
|
|
494
|
+
console.warn("Failed to update passkey last-used timestamp:", error);
|
|
495
|
+
});
|
|
496
|
+
const hasWallet = await hasStoredWallet();
|
|
497
|
+
let walletAddress;
|
|
498
|
+
let userId;
|
|
499
|
+
let tokenAccountAddress;
|
|
500
|
+
let response = null;
|
|
501
|
+
if (hasWallet) {
|
|
502
|
+
const storedAddress = await getStoredAddress();
|
|
503
|
+
const storedUserId = await getStoredUserId();
|
|
504
|
+
if (!storedAddress || !storedUserId) {
|
|
505
|
+
throw new Error("Incomplete wallet data");
|
|
506
|
+
}
|
|
507
|
+
walletAddress = storedAddress;
|
|
508
|
+
userId = storedUserId;
|
|
509
|
+
const current = await getCurrentUser(config.apiUrl, walletAddress);
|
|
510
|
+
if (current) {
|
|
511
|
+
response = current;
|
|
512
|
+
tokenAccountAddress = current.user.tokenAccountAddress ?? void 0;
|
|
513
|
+
} else if (metadata.publicKeyX && metadata.publicKeyY) {
|
|
514
|
+
response = await postJson(
|
|
515
|
+
`${config.apiUrl}/auth/register-passkey-wallet`,
|
|
516
|
+
{
|
|
517
|
+
pubkeyX: metadata.publicKeyX,
|
|
518
|
+
pubkeyY: metadata.publicKeyY,
|
|
519
|
+
credentialId: metadata.credentialId
|
|
520
|
+
}
|
|
521
|
+
);
|
|
522
|
+
walletAddress = response.user.publicKey;
|
|
523
|
+
userId = response.user.id;
|
|
524
|
+
tokenAccountAddress = response.user.tokenAccountAddress ?? void 0;
|
|
525
|
+
await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
|
|
526
|
+
} else {
|
|
527
|
+
await clearSession();
|
|
528
|
+
await clearPasskeyMetadata();
|
|
529
|
+
set({ isLoading: false, hasExistingPasskey: false, error: null });
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
} else if (metadata.publicKeyX && metadata.publicKeyY) {
|
|
533
|
+
response = await postJson(
|
|
534
|
+
`${config.apiUrl}/auth/register-passkey-wallet`,
|
|
535
|
+
{
|
|
536
|
+
pubkeyX: metadata.publicKeyX,
|
|
537
|
+
pubkeyY: metadata.publicKeyY,
|
|
538
|
+
credentialId: metadata.credentialId
|
|
539
|
+
}
|
|
540
|
+
);
|
|
541
|
+
walletAddress = response.user.publicKey;
|
|
542
|
+
userId = response.user.id;
|
|
543
|
+
tokenAccountAddress = response.user.tokenAccountAddress ?? void 0;
|
|
544
|
+
await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
|
|
545
|
+
} else {
|
|
546
|
+
const recovered = await postJson(
|
|
547
|
+
`${config.apiUrl}/auth/recover-passkey-wallet`,
|
|
548
|
+
{ credentialId: metadata.credentialId }
|
|
549
|
+
).catch(() => null);
|
|
550
|
+
if (!recovered) {
|
|
551
|
+
await clearSession();
|
|
552
|
+
await clearPasskeyMetadata();
|
|
553
|
+
set({ isLoading: false, hasExistingPasskey: false, error: null });
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
response = recovered;
|
|
557
|
+
walletAddress = recovered.user.publicKey;
|
|
558
|
+
userId = recovered.user.id;
|
|
559
|
+
tokenAccountAddress = recovered.user.tokenAccountAddress ?? void 0;
|
|
560
|
+
await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
|
|
561
|
+
}
|
|
562
|
+
set({
|
|
563
|
+
isLoading: false,
|
|
564
|
+
isAuthenticated: true,
|
|
565
|
+
activeCredentialId: metadata.credentialId,
|
|
566
|
+
address: walletAddress,
|
|
567
|
+
user: response ? toPasskeyUser(response.user) : get().user
|
|
568
|
+
});
|
|
569
|
+
return true;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error("Failed to unlock with passkey:", error);
|
|
572
|
+
const kind = classifyPasskeyError(error);
|
|
573
|
+
if (kind === "USER_CANCELLED") {
|
|
574
|
+
set({ isLoading: false });
|
|
575
|
+
} else if (kind === "NOT_FOUND") {
|
|
576
|
+
await clearPasskeyMetadata();
|
|
577
|
+
set({ isLoading: false, hasExistingPasskey: false, error: null });
|
|
578
|
+
} else {
|
|
579
|
+
set({
|
|
580
|
+
isLoading: false,
|
|
581
|
+
error: error instanceof Error ? error.message : "Failed to unlock"
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
return false;
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
recoverWithDiscoverablePasskey: async () => {
|
|
588
|
+
set({ isLoading: true, error: null, needsNewPasskey: false });
|
|
589
|
+
try {
|
|
590
|
+
const discovered = await authenticateWithDiscoverablePasskey({
|
|
591
|
+
rpId: config.rpId
|
|
592
|
+
});
|
|
593
|
+
if (!discovered) {
|
|
594
|
+
set({ isLoading: false });
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const response = await postJson(
|
|
598
|
+
`${config.apiUrl}/auth/recover-passkey-wallet`,
|
|
599
|
+
{ credentialId: discovered.credentialId }
|
|
600
|
+
).catch(() => null);
|
|
601
|
+
if (!response) {
|
|
602
|
+
set({ isLoading: false, needsNewPasskey: true });
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
const walletAddress = response.user.publicKey;
|
|
606
|
+
const userId = response.user.id;
|
|
607
|
+
const tokenAccountAddress = response.user.tokenAccountAddress ?? void 0;
|
|
608
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
|
+
await storePasskeyMetadata({
|
|
610
|
+
credentialId: discovered.credentialId,
|
|
611
|
+
publicKeyX: "",
|
|
612
|
+
publicKeyY: "",
|
|
613
|
+
rpId: discovered.rpId,
|
|
614
|
+
createdAt: now,
|
|
615
|
+
lastUsedAt: now
|
|
616
|
+
});
|
|
617
|
+
await storeWalletInfo(walletAddress, userId, tokenAccountAddress);
|
|
618
|
+
set({
|
|
619
|
+
isLoading: false,
|
|
620
|
+
isAuthenticated: true,
|
|
621
|
+
hasExistingPasskey: true,
|
|
622
|
+
activeCredentialId: discovered.credentialId,
|
|
623
|
+
address: walletAddress,
|
|
624
|
+
user: toPasskeyUser(response.user)
|
|
625
|
+
});
|
|
626
|
+
return true;
|
|
627
|
+
} catch (error) {
|
|
628
|
+
console.error("Failed to recover with discoverable passkey:", error);
|
|
629
|
+
set({
|
|
630
|
+
isLoading: false,
|
|
631
|
+
error: error instanceof Error ? error.message : "Failed to recover wallet"
|
|
632
|
+
});
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
logout: async () => {
|
|
637
|
+
try {
|
|
638
|
+
await clearSession();
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error("Failed to clear passkey session:", error);
|
|
641
|
+
}
|
|
642
|
+
set({
|
|
643
|
+
isAuthenticated: false,
|
|
644
|
+
needsNewPasskey: false,
|
|
645
|
+
user: null,
|
|
646
|
+
address: null,
|
|
647
|
+
activeCredentialId: null
|
|
648
|
+
});
|
|
649
|
+
},
|
|
650
|
+
clearError: () => set({ error: null }),
|
|
651
|
+
dismissNewPasskey: () => set({ needsNewPasskey: false })
|
|
652
|
+
}));
|
|
653
|
+
}
|
|
654
|
+
function getPasskeyAuthStore(config) {
|
|
655
|
+
const key = createStoreKey(config);
|
|
656
|
+
const cached = storeCache.get(key);
|
|
657
|
+
if (cached) return cached;
|
|
658
|
+
const store = createPasskeyAuthStore(config);
|
|
659
|
+
storeCache.set(key, store);
|
|
660
|
+
return store;
|
|
661
|
+
}
|
|
662
|
+
function usePasskeyAuth(config) {
|
|
663
|
+
return getPasskeyAuthStore(config)();
|
|
664
|
+
}
|
|
665
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
666
|
+
0 && (module.exports = {
|
|
667
|
+
createPasskeyAuthStore,
|
|
668
|
+
executePasskeyTransaction,
|
|
669
|
+
getPasskeyAuthStore,
|
|
670
|
+
usePasskeyAuth
|
|
671
|
+
});
|
|
672
|
+
//# sourceMappingURL=auth.cjs.map
|