@bandeira-tech/b3nd-web 0.2.3 → 0.2.4

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.
@@ -0,0 +1,1775 @@
1
+ import {
2
+ createAuthenticatedMessage,
3
+ createSignedEncryptedMessage,
4
+ decodeHex,
5
+ decrypt,
6
+ encodeHex,
7
+ encrypt,
8
+ verifyPayload
9
+ } from "./chunk-JN75UL5C.js";
10
+ import {
11
+ HttpClient
12
+ } from "./chunk-LFUC4ETD.js";
13
+
14
+ // wallet-server/interfaces.ts
15
+ var defaultLogger = {
16
+ log: (...args) => console.log(...args),
17
+ warn: (...args) => console.warn(...args),
18
+ error: (...args) => console.error(...args)
19
+ };
20
+ var MemoryFileStorage = class {
21
+ constructor() {
22
+ this.storage = /* @__PURE__ */ new Map();
23
+ }
24
+ async readTextFile(path) {
25
+ const content = this.storage.get(path);
26
+ if (content === void 0) {
27
+ throw new Error(`File not found: ${path}`);
28
+ }
29
+ return content;
30
+ }
31
+ async writeTextFile(path, content) {
32
+ this.storage.set(path, content);
33
+ }
34
+ async exists(path) {
35
+ return this.storage.has(path);
36
+ }
37
+ };
38
+ var ConfigEnvironment = class {
39
+ constructor(config = {}) {
40
+ this.config = config;
41
+ }
42
+ get(key) {
43
+ return this.config[key];
44
+ }
45
+ };
46
+
47
+ // wallet-server/jwt.ts
48
+ function base64urlEncode(data) {
49
+ const base64 = btoa(data);
50
+ return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
51
+ }
52
+ function base64urlDecode(encoded) {
53
+ const padded = encoded.replace(/-/g, "+").replace(/_/g, "/").padEnd(encoded.length + (4 - encoded.length % 4) % 4, "=");
54
+ return atob(padded);
55
+ }
56
+ async function createJwt(username, secret, expirationSeconds) {
57
+ const now = Math.floor(Date.now() / 1e3);
58
+ const header = {
59
+ alg: "HS256",
60
+ typ: "JWT"
61
+ };
62
+ const payload = {
63
+ username,
64
+ iat: now,
65
+ exp: now + expirationSeconds,
66
+ type: "access"
67
+ };
68
+ const headerEncoded = base64urlEncode(JSON.stringify(header));
69
+ const payloadEncoded = base64urlEncode(JSON.stringify(payload));
70
+ const message = `${headerEncoded}.${payloadEncoded}`;
71
+ const encoder = new TextEncoder();
72
+ const key = await crypto.subtle.importKey(
73
+ "raw",
74
+ encoder.encode(secret),
75
+ { name: "HMAC", hash: "SHA-256" },
76
+ false,
77
+ ["sign"]
78
+ );
79
+ const signature = await crypto.subtle.sign(
80
+ "HMAC",
81
+ key,
82
+ encoder.encode(message)
83
+ );
84
+ const signatureEncoded = base64urlEncode(
85
+ String.fromCharCode(...new Uint8Array(signature))
86
+ );
87
+ return `${message}.${signatureEncoded}`;
88
+ }
89
+ async function verifyJwt(token, secret) {
90
+ const parts = token.split(".");
91
+ if (parts.length !== 3) {
92
+ throw new Error("Invalid JWT format");
93
+ }
94
+ const [headerEncoded, payloadEncoded, signatureEncoded] = parts;
95
+ const message = `${headerEncoded}.${payloadEncoded}`;
96
+ const encoder = new TextEncoder();
97
+ const key = await crypto.subtle.importKey(
98
+ "raw",
99
+ encoder.encode(secret),
100
+ { name: "HMAC", hash: "SHA-256" },
101
+ false,
102
+ ["verify"]
103
+ );
104
+ const signaturePadded = signatureEncoded.replace(/-/g, "+").replace(/_/g, "/").padEnd(signatureEncoded.length + (4 - signatureEncoded.length % 4) % 4, "=");
105
+ const signatureBytes = new Uint8Array(
106
+ atob(signaturePadded).split("").map((c) => c.charCodeAt(0))
107
+ );
108
+ const isValid = await crypto.subtle.verify(
109
+ "HMAC",
110
+ key,
111
+ signatureBytes,
112
+ encoder.encode(message)
113
+ );
114
+ if (!isValid) {
115
+ throw new Error("Invalid JWT signature");
116
+ }
117
+ const payloadJson = base64urlDecode(payloadEncoded);
118
+ const payload = JSON.parse(payloadJson);
119
+ const now = Math.floor(Date.now() / 1e3);
120
+ if (payload.exp < now) {
121
+ throw new Error("JWT token has expired");
122
+ }
123
+ if (payload.type !== "access") {
124
+ throw new Error("Invalid token type");
125
+ }
126
+ return payload;
127
+ }
128
+ function extractUsernameFromJwt(token) {
129
+ try {
130
+ const parts = token.split(".");
131
+ if (parts.length !== 3) return null;
132
+ const payloadJson = base64urlDecode(parts[1]);
133
+ const payload = JSON.parse(payloadJson);
134
+ return payload.username;
135
+ } catch {
136
+ return null;
137
+ }
138
+ }
139
+
140
+ // wallet-server/obfuscation.ts
141
+ async function deriveObfuscatedPath(serverPublicKey, username, operationType, ...params) {
142
+ const encoder = new TextEncoder();
143
+ const parts = [username, operationType, serverPublicKey, ...params];
144
+ const input = parts.join("|");
145
+ const key = await crypto.subtle.importKey(
146
+ "raw",
147
+ encoder.encode(serverPublicKey),
148
+ { name: "HMAC", hash: "SHA-256" },
149
+ false,
150
+ ["sign"]
151
+ );
152
+ const signature = await crypto.subtle.sign(
153
+ "HMAC",
154
+ key,
155
+ encoder.encode(input)
156
+ );
157
+ return encodeHex(new Uint8Array(signature)).substring(0, 32);
158
+ }
159
+ async function pemToCryptoKey(pem, algorithm = "Ed25519") {
160
+ const base64 = pem.split("\n").filter((line) => !line.startsWith("-----")).join("");
161
+ const binaryString = atob(base64);
162
+ const bytes = new Uint8Array(binaryString.length);
163
+ for (let i = 0; i < binaryString.length; i++) {
164
+ bytes[i] = binaryString.charCodeAt(i);
165
+ }
166
+ const buffer = bytes.buffer.slice(
167
+ bytes.byteOffset,
168
+ bytes.byteOffset + bytes.byteLength
169
+ );
170
+ if (algorithm === "Ed25519") {
171
+ return await crypto.subtle.importKey(
172
+ "pkcs8",
173
+ buffer,
174
+ { name: "Ed25519", namedCurve: "Ed25519" },
175
+ false,
176
+ ["sign"]
177
+ );
178
+ } else {
179
+ return await crypto.subtle.importKey(
180
+ "pkcs8",
181
+ buffer,
182
+ { name: "X25519", namedCurve: "X25519" },
183
+ false,
184
+ ["deriveBits"]
185
+ );
186
+ }
187
+ }
188
+ async function createSignedEncryptedPayload(data, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex) {
189
+ const identityPrivateKey = await pemToCryptoKey(
190
+ serverIdentityPrivateKeyPem,
191
+ "Ed25519"
192
+ );
193
+ return await createSignedEncryptedMessage(
194
+ data,
195
+ [{ privateKey: identityPrivateKey, publicKeyHex: serverIdentityPublicKeyHex }],
196
+ serverEncryptionPublicKeyHex
197
+ );
198
+ }
199
+ async function decryptSignedEncryptedPayload(signedMessage, serverEncryptionPrivateKeyPem) {
200
+ const encryptionPrivateKey = await pemToCryptoKey(
201
+ serverEncryptionPrivateKeyPem,
202
+ "X25519"
203
+ );
204
+ const { verified, signers } = await verifyPayload({
205
+ payload: signedMessage.payload,
206
+ auth: signedMessage.auth
207
+ });
208
+ const data = await decrypt(signedMessage.payload, encryptionPrivateKey);
209
+ return { data, verified, signers };
210
+ }
211
+ async function encryptForBackend(data, serverEncryptionPublicKeyHex) {
212
+ return await encrypt(data, serverEncryptionPublicKeyHex);
213
+ }
214
+ async function decryptFromBackend(encryptedPayload, serverEncryptionPrivateKeyPem) {
215
+ const privateKey = await pemToCryptoKey(
216
+ serverEncryptionPrivateKeyPem,
217
+ "X25519"
218
+ );
219
+ return await decrypt(encryptedPayload, privateKey);
220
+ }
221
+
222
+ // wallet-server/keys.ts
223
+ function bytesToBase64(bytes) {
224
+ return btoa(String.fromCharCode(...bytes));
225
+ }
226
+ async function generateAccountKeyPair() {
227
+ const keyPair = await crypto.subtle.generateKey("Ed25519", true, [
228
+ "sign",
229
+ "verify"
230
+ ]);
231
+ const privateKeyBuffer = await crypto.subtle.exportKey(
232
+ "pkcs8",
233
+ keyPair.privateKey
234
+ );
235
+ const publicKeyBuffer = await crypto.subtle.exportKey(
236
+ "raw",
237
+ keyPair.publicKey
238
+ );
239
+ const privateKeyBase64 = bytesToBase64(new Uint8Array(privateKeyBuffer));
240
+ const privateKeyPem = `-----BEGIN PRIVATE KEY-----
241
+ ${privateKeyBase64.match(/.{1,64}/g)?.join("\n")}
242
+ -----END PRIVATE KEY-----`;
243
+ const publicKeyHex = encodeHex(new Uint8Array(publicKeyBuffer));
244
+ return { privateKeyPem, publicKeyHex };
245
+ }
246
+ async function generateEncryptionKeyPair() {
247
+ const keyPair = await crypto.subtle.generateKey(
248
+ {
249
+ name: "X25519",
250
+ namedCurve: "X25519"
251
+ },
252
+ true,
253
+ ["deriveBits"]
254
+ );
255
+ const privateKeyBuffer = await crypto.subtle.exportKey(
256
+ "pkcs8",
257
+ keyPair.privateKey
258
+ );
259
+ const publicKeyBuffer = await crypto.subtle.exportKey(
260
+ "raw",
261
+ keyPair.publicKey
262
+ );
263
+ const privateKeyBase64 = bytesToBase64(new Uint8Array(privateKeyBuffer));
264
+ const privateKeyPem = `-----BEGIN PRIVATE KEY-----
265
+ ${privateKeyBase64.match(/.{1,64}/g)?.join("\n")}
266
+ -----END PRIVATE KEY-----`;
267
+ const publicKeyHex = encodeHex(new Uint8Array(publicKeyBuffer));
268
+ return { privateKeyPem, publicKeyHex };
269
+ }
270
+ async function generateUserKeys(client, serverPublicKey, username, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex) {
271
+ const [accountKey, encryptionKey] = await Promise.all([
272
+ generateAccountKeyPair(),
273
+ generateEncryptionKeyPair()
274
+ ]);
275
+ await Promise.all([
276
+ (async () => {
277
+ const path = await deriveObfuscatedPath(
278
+ serverPublicKey,
279
+ username,
280
+ "account-key"
281
+ );
282
+ const signed = await createSignedEncryptedPayload(
283
+ accountKey,
284
+ serverIdentityPrivateKeyPem,
285
+ serverIdentityPublicKeyHex,
286
+ serverEncryptionPublicKeyHex
287
+ );
288
+ await client.write(
289
+ `mutable://accounts/${serverPublicKey}/${path}`,
290
+ signed
291
+ );
292
+ })(),
293
+ (async () => {
294
+ const path = await deriveObfuscatedPath(
295
+ serverPublicKey,
296
+ username,
297
+ "encryption-key"
298
+ );
299
+ const signed = await createSignedEncryptedPayload(
300
+ encryptionKey,
301
+ serverIdentityPrivateKeyPem,
302
+ serverIdentityPublicKeyHex,
303
+ serverEncryptionPublicKeyHex
304
+ );
305
+ await client.write(
306
+ `mutable://accounts/${serverPublicKey}/${path}`,
307
+ signed
308
+ );
309
+ })()
310
+ ]);
311
+ return { accountKey, encryptionKey };
312
+ }
313
+ async function loadUserAccountKey(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
314
+ const path = await deriveObfuscatedPath(
315
+ serverPublicKey,
316
+ username,
317
+ "account-key"
318
+ );
319
+ const result = await client.read(
320
+ `mutable://accounts/${serverPublicKey}/${path}`
321
+ );
322
+ if (!result.success || !result.record?.data) {
323
+ throw new Error("Account key not found");
324
+ }
325
+ const { data, verified } = await decryptSignedEncryptedPayload(
326
+ result.record.data,
327
+ serverEncryptionPrivateKeyPem
328
+ );
329
+ if (!verified) {
330
+ logger?.warn(
331
+ "Account key signature verification failed for user:",
332
+ username
333
+ );
334
+ }
335
+ return data;
336
+ }
337
+ async function loadUserEncryptionKey(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
338
+ const path = await deriveObfuscatedPath(
339
+ serverPublicKey,
340
+ username,
341
+ "encryption-key"
342
+ );
343
+ const result = await client.read(
344
+ `mutable://accounts/${serverPublicKey}/${path}`
345
+ );
346
+ if (!result.success || !result.record?.data) {
347
+ throw new Error("Encryption key not found");
348
+ }
349
+ const { data, verified } = await decryptSignedEncryptedPayload(
350
+ result.record.data,
351
+ serverEncryptionPrivateKeyPem
352
+ );
353
+ if (!verified) {
354
+ logger?.warn(
355
+ "Encryption key signature verification failed for user:",
356
+ username
357
+ );
358
+ }
359
+ return data;
360
+ }
361
+ async function loadUserKeys(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
362
+ const [accountKey, encryptionKey] = await Promise.all([
363
+ loadUserAccountKey(
364
+ client,
365
+ serverPublicKey,
366
+ username,
367
+ serverEncryptionPrivateKeyPem,
368
+ logger
369
+ ),
370
+ loadUserEncryptionKey(
371
+ client,
372
+ serverPublicKey,
373
+ username,
374
+ serverEncryptionPrivateKeyPem,
375
+ logger
376
+ )
377
+ ]);
378
+ return { accountKey, encryptionKey };
379
+ }
380
+ async function getUserPublicKeys(client, serverPublicKey, username, serverEncryptionPrivateKeyPem, logger) {
381
+ const { accountKey, encryptionKey } = await loadUserKeys(
382
+ client,
383
+ serverPublicKey,
384
+ username,
385
+ serverEncryptionPrivateKeyPem,
386
+ logger
387
+ );
388
+ return {
389
+ accountPublicKeyHex: accountKey.publicKeyHex,
390
+ encryptionPublicKeyHex: encryptionKey.publicKeyHex
391
+ };
392
+ }
393
+
394
+ // wallet-server/proxy.ts
395
+ async function proxyWrite(proxyClient, credentialClient, serverPublicKey, username, serverEncryptionPrivateKeyPem, request) {
396
+ try {
397
+ const [accountKey, encryptionKey] = await Promise.all([
398
+ loadUserAccountKey(
399
+ credentialClient,
400
+ serverPublicKey,
401
+ username,
402
+ serverEncryptionPrivateKeyPem
403
+ ),
404
+ loadUserEncryptionKey(
405
+ credentialClient,
406
+ serverPublicKey,
407
+ username,
408
+ serverEncryptionPrivateKeyPem
409
+ )
410
+ ]);
411
+ const resolvedUri = request.uri.replace(/:key/g, accountKey.publicKeyHex);
412
+ const userPrivateKey = await pemToCryptoKey(
413
+ accountKey.privateKeyPem,
414
+ "Ed25519"
415
+ );
416
+ const signer = {
417
+ privateKey: userPrivateKey,
418
+ publicKeyHex: accountKey.publicKeyHex
419
+ };
420
+ let signedMessage;
421
+ if (request.encrypt) {
422
+ signedMessage = await createSignedEncryptedMessage(
423
+ request.data,
424
+ [signer],
425
+ encryptionKey.publicKeyHex
426
+ );
427
+ } else {
428
+ signedMessage = await createAuthenticatedMessage(request.data, [signer]);
429
+ }
430
+ const result = await proxyClient.write(resolvedUri, signedMessage);
431
+ if (!result.success) {
432
+ return {
433
+ success: false,
434
+ error: result.error || "Write failed"
435
+ };
436
+ }
437
+ return {
438
+ success: true,
439
+ resolvedUri,
440
+ record: result.record
441
+ };
442
+ } catch (error) {
443
+ return {
444
+ success: false,
445
+ error: `Proxy write failed: ${error instanceof Error ? error.message : String(error)}`
446
+ };
447
+ }
448
+ }
449
+ async function proxyRead(proxyClient, uri, serverEncryptionPrivateKeyPem) {
450
+ try {
451
+ const result = await proxyClient.read(uri);
452
+ if (!result.success) {
453
+ return {
454
+ success: false,
455
+ error: result.error || "Read failed"
456
+ };
457
+ }
458
+ const response = {
459
+ success: true,
460
+ record: result.record
461
+ };
462
+ if (serverEncryptionPrivateKeyPem && result.record?.data) {
463
+ try {
464
+ const data = result.record.data;
465
+ if (typeof data === "object" && data.payload && typeof data.payload === "object") {
466
+ const payload = data.payload;
467
+ if (payload.data && payload.nonce && payload.ephemeralPublicKey) {
468
+ const privateKey = await pemToCryptoKey(
469
+ serverEncryptionPrivateKeyPem,
470
+ "X25519"
471
+ );
472
+ const encryptedPayload = {
473
+ data: payload.data,
474
+ nonce: payload.nonce,
475
+ ephemeralPublicKey: payload.ephemeralPublicKey
476
+ };
477
+ const decrypted = await decrypt(
478
+ encryptedPayload,
479
+ privateKey
480
+ );
481
+ return {
482
+ ...response,
483
+ decrypted
484
+ };
485
+ }
486
+ }
487
+ } catch {
488
+ }
489
+ }
490
+ return response;
491
+ } catch (error) {
492
+ return {
493
+ success: false,
494
+ error: `Proxy read failed: ${error instanceof Error ? error.message : String(error)}`
495
+ };
496
+ }
497
+ }
498
+
499
+ // wallet-server/auth.ts
500
+ function generateSalt() {
501
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
502
+ return encodeHex(bytes);
503
+ }
504
+ async function hashPassword(password, salt) {
505
+ const encoder = new TextEncoder();
506
+ const data = encoder.encode(password);
507
+ const saltBytes = new Uint8Array(decodeHex(salt));
508
+ const key = await crypto.subtle.importKey("raw", data, "PBKDF2", false, [
509
+ "deriveBits"
510
+ ]);
511
+ const derived = await crypto.subtle.deriveBits(
512
+ {
513
+ name: "PBKDF2",
514
+ salt: saltBytes.buffer,
515
+ iterations: 1e5,
516
+ hash: "SHA-256"
517
+ },
518
+ key,
519
+ 256
520
+ );
521
+ return encodeHex(new Uint8Array(derived));
522
+ }
523
+ async function verifyPassword(password, salt, hash) {
524
+ const computedHash = await hashPassword(password, salt);
525
+ return computedHash === hash;
526
+ }
527
+ async function userExists(client, serverPublicKey, username, appScope) {
528
+ const path = await deriveObfuscatedPath(
529
+ serverPublicKey,
530
+ username,
531
+ "profile",
532
+ ...appScope ? [appScope] : []
533
+ );
534
+ const result = await client.read(
535
+ `mutable://accounts/${serverPublicKey}/${path}`
536
+ );
537
+ return result.success;
538
+ }
539
+ async function createUser(client, serverPublicKey, username, password, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
540
+ if (await userExists(client, serverPublicKey, username, appScope)) {
541
+ throw new Error("User already exists");
542
+ }
543
+ const salt = generateSalt();
544
+ const hash = await hashPassword(password, salt);
545
+ const profilePath = await deriveObfuscatedPath(
546
+ serverPublicKey,
547
+ username,
548
+ "profile",
549
+ ...appScope ? [appScope] : []
550
+ );
551
+ const profileData = {
552
+ username,
553
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
554
+ };
555
+ const profileSigned = await createSignedEncryptedPayload(
556
+ profileData,
557
+ serverIdentityPrivateKeyPem,
558
+ serverIdentityPublicKeyHex,
559
+ serverEncryptionPublicKeyHex
560
+ );
561
+ await client.write(
562
+ `mutable://accounts/${serverPublicKey}/${profilePath}`,
563
+ profileSigned
564
+ );
565
+ const passwordPath = await deriveObfuscatedPath(
566
+ serverPublicKey,
567
+ username,
568
+ "password",
569
+ ...appScope ? [appScope] : []
570
+ );
571
+ const passwordData = { hash, salt };
572
+ const passwordSigned = await createSignedEncryptedPayload(
573
+ passwordData,
574
+ serverIdentityPrivateKeyPem,
575
+ serverIdentityPublicKeyHex,
576
+ serverEncryptionPublicKeyHex
577
+ );
578
+ await client.write(
579
+ `mutable://accounts/${serverPublicKey}/${passwordPath}`,
580
+ passwordSigned
581
+ );
582
+ return { salt, hash };
583
+ }
584
+ async function authenticateUser(client, serverPublicKey, username, password, serverIdentityPublicKeyHex, serverEncryptionPrivateKeyPem, appScope, logger) {
585
+ const passwordPath = await deriveObfuscatedPath(
586
+ serverPublicKey,
587
+ username,
588
+ "password",
589
+ ...appScope ? [appScope] : []
590
+ );
591
+ const result = await client.read(
592
+ `mutable://accounts/${serverPublicKey}/${passwordPath}`
593
+ );
594
+ if (!result.success || !result.record?.data) {
595
+ return false;
596
+ }
597
+ const { data, verified } = await decryptSignedEncryptedPayload(
598
+ result.record.data,
599
+ serverEncryptionPrivateKeyPem
600
+ );
601
+ if (!verified) {
602
+ logger?.warn(
603
+ "Password credential signature verification failed for user:",
604
+ username
605
+ );
606
+ }
607
+ const { salt, hash } = data;
608
+ return await verifyPassword(password, salt, hash);
609
+ }
610
+ async function changePassword(client, serverPublicKey, username, oldPassword, newPassword, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, serverEncryptionPrivateKeyPem, appScope) {
611
+ const isValid = await authenticateUser(
612
+ client,
613
+ serverPublicKey,
614
+ username,
615
+ oldPassword,
616
+ serverIdentityPublicKeyHex,
617
+ serverEncryptionPrivateKeyPem,
618
+ appScope
619
+ );
620
+ if (!isValid) {
621
+ throw new Error("Current password is incorrect");
622
+ }
623
+ const salt = generateSalt();
624
+ const hash = await hashPassword(newPassword, salt);
625
+ const passwordPath = await deriveObfuscatedPath(
626
+ serverPublicKey,
627
+ username,
628
+ "password",
629
+ ...appScope ? [appScope] : []
630
+ );
631
+ const passwordData = { hash, salt };
632
+ const passwordSigned = await createSignedEncryptedPayload(
633
+ passwordData,
634
+ serverIdentityPrivateKeyPem,
635
+ serverIdentityPublicKeyHex,
636
+ serverEncryptionPublicKeyHex
637
+ );
638
+ await client.write(
639
+ `mutable://accounts/${serverPublicKey}/${passwordPath}`,
640
+ passwordSigned
641
+ );
642
+ }
643
+ async function createPasswordResetToken(client, serverPublicKey, username, ttlSeconds, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
644
+ if (!await userExists(client, serverPublicKey, username)) {
645
+ throw new Error("User not found");
646
+ }
647
+ const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
648
+ const token = encodeHex(tokenBytes);
649
+ const now = /* @__PURE__ */ new Date();
650
+ const expiresAt = new Date(now.getTime() + ttlSeconds * 1e3);
651
+ const tokenPath = await deriveObfuscatedPath(
652
+ serverPublicKey,
653
+ username,
654
+ "reset-tokens",
655
+ token,
656
+ ...appScope ? [appScope] : []
657
+ );
658
+ const tokenData = {
659
+ username,
660
+ createdAt: now.toISOString(),
661
+ expiresAt: expiresAt.toISOString()
662
+ };
663
+ const tokenSigned = await createSignedEncryptedPayload(
664
+ tokenData,
665
+ serverIdentityPrivateKeyPem,
666
+ serverIdentityPublicKeyHex,
667
+ serverEncryptionPublicKeyHex
668
+ );
669
+ await client.write(
670
+ `mutable://accounts/${serverPublicKey}/${tokenPath}`,
671
+ tokenSigned
672
+ );
673
+ return token;
674
+ }
675
+ async function resetPasswordWithToken(client, serverPublicKey, token, newPassword, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, serverEncryptionPrivateKeyPem, username, appScope, logger) {
676
+ const tokenPath = await deriveObfuscatedPath(
677
+ serverPublicKey,
678
+ username,
679
+ "reset-tokens",
680
+ token,
681
+ ...appScope ? [appScope] : []
682
+ );
683
+ const result = await client.read(
684
+ `mutable://accounts/${serverPublicKey}/${tokenPath}`
685
+ );
686
+ if (!result.success || !result.record?.data) {
687
+ throw new Error("Invalid or expired reset token");
688
+ }
689
+ const { data: tokenData, verified } = await decryptSignedEncryptedPayload(
690
+ result.record.data,
691
+ serverEncryptionPrivateKeyPem
692
+ );
693
+ if (!verified) {
694
+ logger?.warn(
695
+ "Reset token signature verification failed for user:",
696
+ username
697
+ );
698
+ }
699
+ const { username: tokenUsername, expiresAt } = tokenData;
700
+ if (tokenUsername !== username) {
701
+ throw new Error("Invalid reset token");
702
+ }
703
+ if (new Date(expiresAt) < /* @__PURE__ */ new Date()) {
704
+ throw new Error("Reset token has expired");
705
+ }
706
+ const salt = generateSalt();
707
+ const hash = await hashPassword(newPassword, salt);
708
+ const passwordPath = await deriveObfuscatedPath(
709
+ serverPublicKey,
710
+ username,
711
+ "password",
712
+ ...appScope ? [appScope] : []
713
+ );
714
+ const passwordData = { hash, salt };
715
+ const passwordSigned = await createSignedEncryptedPayload(
716
+ passwordData,
717
+ serverIdentityPrivateKeyPem,
718
+ serverIdentityPublicKeyHex,
719
+ serverEncryptionPublicKeyHex
720
+ );
721
+ await client.write(
722
+ `mutable://accounts/${serverPublicKey}/${passwordPath}`,
723
+ passwordSigned
724
+ );
725
+ await client.delete(`mutable://accounts/${serverPublicKey}/${tokenPath}`);
726
+ return tokenUsername;
727
+ }
728
+ async function googleUserExists(client, serverPublicKey, googleSub, appScope) {
729
+ const path = await deriveObfuscatedPath(
730
+ serverPublicKey,
731
+ googleSub,
732
+ "google-profile",
733
+ ...appScope ? [appScope] : []
734
+ );
735
+ const result = await client.read(
736
+ `mutable://accounts/${serverPublicKey}/${path}`
737
+ );
738
+ return result.success;
739
+ }
740
+ async function createGoogleUser(client, serverPublicKey, username, googlePayload, serverIdentityPrivateKeyPem, serverIdentityPublicKeyHex, serverEncryptionPublicKeyHex, appScope) {
741
+ if (await googleUserExists(client, serverPublicKey, googlePayload.sub, appScope)) {
742
+ throw new Error("Google account already registered");
743
+ }
744
+ if (await userExists(client, serverPublicKey, username, appScope)) {
745
+ throw new Error("Username already exists");
746
+ }
747
+ const profilePath = await deriveObfuscatedPath(
748
+ serverPublicKey,
749
+ username,
750
+ "profile",
751
+ ...appScope ? [appScope] : []
752
+ );
753
+ const profileData = {
754
+ username,
755
+ authProvider: "google",
756
+ googleSub: googlePayload.sub,
757
+ email: googlePayload.email,
758
+ name: googlePayload.name,
759
+ picture: googlePayload.picture,
760
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
761
+ };
762
+ const profileSigned = await createSignedEncryptedPayload(
763
+ profileData,
764
+ serverIdentityPrivateKeyPem,
765
+ serverIdentityPublicKeyHex,
766
+ serverEncryptionPublicKeyHex
767
+ );
768
+ await client.write(
769
+ `mutable://accounts/${serverPublicKey}/${profilePath}`,
770
+ profileSigned
771
+ );
772
+ const googleProfilePath = await deriveObfuscatedPath(
773
+ serverPublicKey,
774
+ googlePayload.sub,
775
+ "google-profile",
776
+ ...appScope ? [appScope] : []
777
+ );
778
+ const googleProfileData = {
779
+ googleSub: googlePayload.sub,
780
+ username,
781
+ email: googlePayload.email,
782
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
783
+ };
784
+ const googleProfileSigned = await createSignedEncryptedPayload(
785
+ googleProfileData,
786
+ serverIdentityPrivateKeyPem,
787
+ serverIdentityPublicKeyHex,
788
+ serverEncryptionPublicKeyHex
789
+ );
790
+ await client.write(
791
+ `mutable://accounts/${serverPublicKey}/${googleProfilePath}`,
792
+ googleProfileSigned
793
+ );
794
+ return { username, googleSub: googlePayload.sub };
795
+ }
796
+ async function authenticateGoogleUser(client, serverPublicKey, googleSub, serverEncryptionPrivateKeyPem, appScope, logger) {
797
+ const googleProfilePath = await deriveObfuscatedPath(
798
+ serverPublicKey,
799
+ googleSub,
800
+ "google-profile",
801
+ ...appScope ? [appScope] : []
802
+ );
803
+ const result = await client.read(
804
+ `mutable://accounts/${serverPublicKey}/${googleProfilePath}`
805
+ );
806
+ if (!result.success || !result.record?.data) {
807
+ return null;
808
+ }
809
+ const { data, verified } = await decryptSignedEncryptedPayload(
810
+ result.record.data,
811
+ serverEncryptionPrivateKeyPem
812
+ );
813
+ if (!verified) {
814
+ logger?.warn(
815
+ "Google profile signature verification failed for sub:",
816
+ googleSub
817
+ );
818
+ }
819
+ const { username } = data;
820
+ return username;
821
+ }
822
+
823
+ // wallet-server/google-oauth.ts
824
+ var cachedKeys = [];
825
+ var cacheExpiry = 0;
826
+ async function getGooglePublicKeys(fetchImpl = fetch) {
827
+ const now = Date.now();
828
+ if (cachedKeys.length > 0 && now < cacheExpiry) {
829
+ return cachedKeys;
830
+ }
831
+ const response = await fetchImpl(
832
+ "https://www.googleapis.com/oauth2/v3/certs"
833
+ );
834
+ if (!response.ok) {
835
+ throw new Error("Failed to fetch Google public keys");
836
+ }
837
+ const cacheControl = response.headers.get("cache-control");
838
+ let maxAge = 3600;
839
+ if (cacheControl) {
840
+ const match = cacheControl.match(/max-age=(\d+)/);
841
+ if (match) {
842
+ maxAge = parseInt(match[1], 10);
843
+ }
844
+ }
845
+ const data = await response.json();
846
+ cachedKeys = data.keys;
847
+ cacheExpiry = now + maxAge * 1e3;
848
+ return cachedKeys;
849
+ }
850
+ function clearGooglePublicKeyCache() {
851
+ cachedKeys = [];
852
+ cacheExpiry = 0;
853
+ }
854
+ function base64UrlDecode(str) {
855
+ let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
856
+ while (base64.length % 4) {
857
+ base64 += "=";
858
+ }
859
+ const binary = atob(base64);
860
+ const bytes = new Uint8Array(binary.length);
861
+ for (let i = 0; i < binary.length; i++) {
862
+ bytes[i] = binary.charCodeAt(i);
863
+ }
864
+ return bytes;
865
+ }
866
+ async function importRsaPublicKey(key) {
867
+ const jwk = {
868
+ kty: key.kty,
869
+ n: key.n,
870
+ e: key.e,
871
+ alg: key.alg,
872
+ use: key.use
873
+ };
874
+ return await crypto.subtle.importKey(
875
+ "jwk",
876
+ jwk,
877
+ {
878
+ name: "RSASSA-PKCS1-v1_5",
879
+ hash: { name: "SHA-256" }
880
+ },
881
+ false,
882
+ ["verify"]
883
+ );
884
+ }
885
+ async function verifyGoogleIdToken(idToken, clientId, fetchImpl = fetch) {
886
+ const parts = idToken.split(".");
887
+ if (parts.length !== 3) {
888
+ throw new Error("Invalid Google ID token format");
889
+ }
890
+ const [headerB64, payloadB64, signatureB64] = parts;
891
+ const headerJson = new TextDecoder().decode(base64UrlDecode(headerB64));
892
+ const header = JSON.parse(headerJson);
893
+ const kid = header.kid;
894
+ if (!kid) {
895
+ throw new Error("Google ID token missing key ID");
896
+ }
897
+ let keys = await getGooglePublicKeys(fetchImpl);
898
+ let key = keys.find((k) => k.kid === kid);
899
+ if (!key) {
900
+ clearGooglePublicKeyCache();
901
+ keys = await getGooglePublicKeys(fetchImpl);
902
+ key = keys.find((k) => k.kid === kid);
903
+ if (!key) {
904
+ throw new Error("Google public key not found for token");
905
+ }
906
+ }
907
+ const publicKey = await importRsaPublicKey(key);
908
+ const signatureData = base64UrlDecode(signatureB64);
909
+ const signedData = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
910
+ const isValid = await crypto.subtle.verify(
911
+ "RSASSA-PKCS1-v1_5",
912
+ publicKey,
913
+ signatureData.buffer,
914
+ signedData
915
+ );
916
+ if (!isValid) {
917
+ throw new Error("Google ID token signature verification failed");
918
+ }
919
+ const payloadJson = new TextDecoder().decode(base64UrlDecode(payloadB64));
920
+ const payload = JSON.parse(payloadJson);
921
+ if (payload.iss !== "accounts.google.com" && payload.iss !== "https://accounts.google.com") {
922
+ throw new Error("Invalid Google ID token issuer");
923
+ }
924
+ if (payload.aud !== clientId) {
925
+ throw new Error("Google ID token audience mismatch");
926
+ }
927
+ const now = Math.floor(Date.now() / 1e3);
928
+ if (payload.exp < now) {
929
+ throw new Error("Google ID token has expired");
930
+ }
931
+ if (!payload.email_verified) {
932
+ throw new Error("Google email not verified");
933
+ }
934
+ return payload;
935
+ }
936
+ async function generateGoogleUsername(googleSub) {
937
+ const encoder = new TextEncoder();
938
+ const data = encoder.encode(`google:${googleSub}`);
939
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
940
+ const hashHex = encodeHex(new Uint8Array(hashBuffer));
941
+ return `g_${hashHex.substring(0, 12)}`;
942
+ }
943
+
944
+ // wallet-server/credentials.ts
945
+ var PasswordCredentialHandler = class {
946
+ async signup(payload, context) {
947
+ const { username, password } = payload;
948
+ if (!username || typeof username !== "string") {
949
+ throw new Error("username is required");
950
+ }
951
+ if (!password || typeof password !== "string" || password.length < 8) {
952
+ throw new Error(
953
+ "password is required and must be at least 8 characters"
954
+ );
955
+ }
956
+ context.logger?.log(`Creating password user: ${username}`);
957
+ await createUser(
958
+ context.client,
959
+ context.serverPublicKey,
960
+ username,
961
+ password,
962
+ context.serverIdentityPrivateKeyPem,
963
+ context.serverIdentityPublicKeyHex,
964
+ context.serverEncryptionPublicKeyHex,
965
+ context.appKey
966
+ );
967
+ context.logger?.log(`Password user created: ${username}`);
968
+ context.logger?.log(`Generating keys for user: ${username}`);
969
+ await generateUserKeys(
970
+ context.client,
971
+ context.serverPublicKey,
972
+ username,
973
+ context.serverIdentityPrivateKeyPem,
974
+ context.serverIdentityPublicKeyHex,
975
+ context.serverEncryptionPublicKeyHex
976
+ );
977
+ context.logger?.log(`Keys generated for user: ${username}`);
978
+ return { username };
979
+ }
980
+ async login(payload, context) {
981
+ const { username, password } = payload;
982
+ if (!username || !password) {
983
+ throw new Error("username and password are required");
984
+ }
985
+ const isValid = await authenticateUser(
986
+ context.client,
987
+ context.serverPublicKey,
988
+ username,
989
+ password,
990
+ context.serverIdentityPublicKeyHex,
991
+ context.serverEncryptionPrivateKeyPem,
992
+ context.appKey,
993
+ context.logger
994
+ );
995
+ if (!isValid) {
996
+ throw new Error("Invalid username or password");
997
+ }
998
+ return { username };
999
+ }
1000
+ };
1001
+ var GoogleCredentialHandler = class {
1002
+ async signup(payload, context) {
1003
+ if (!context.googleClientId) {
1004
+ throw new Error("Google OAuth is not configured");
1005
+ }
1006
+ const { googleIdToken } = payload;
1007
+ if (!googleIdToken || typeof googleIdToken !== "string") {
1008
+ throw new Error("googleIdToken is required");
1009
+ }
1010
+ context.logger?.log("Verifying Google ID token...");
1011
+ const googlePayload = await verifyGoogleIdToken(
1012
+ googleIdToken,
1013
+ context.googleClientId,
1014
+ context.fetch
1015
+ );
1016
+ context.logger?.log(`Google token verified for: ${googlePayload.email}`);
1017
+ const username = await generateGoogleUsername(googlePayload.sub);
1018
+ context.logger?.log(`Creating Google user: ${username}`);
1019
+ await createGoogleUser(
1020
+ context.client,
1021
+ context.serverPublicKey,
1022
+ username,
1023
+ googlePayload,
1024
+ context.serverIdentityPrivateKeyPem,
1025
+ context.serverIdentityPublicKeyHex,
1026
+ context.serverEncryptionPublicKeyHex,
1027
+ context.appKey
1028
+ );
1029
+ context.logger?.log(`Google user created: ${username}`);
1030
+ context.logger?.log(`Generating keys for Google user: ${username}`);
1031
+ await generateUserKeys(
1032
+ context.client,
1033
+ context.serverPublicKey,
1034
+ username,
1035
+ context.serverIdentityPrivateKeyPem,
1036
+ context.serverIdentityPublicKeyHex,
1037
+ context.serverEncryptionPublicKeyHex
1038
+ );
1039
+ context.logger?.log(`Keys generated for Google user: ${username}`);
1040
+ return {
1041
+ username,
1042
+ metadata: {
1043
+ email: googlePayload.email,
1044
+ name: googlePayload.name,
1045
+ picture: googlePayload.picture
1046
+ }
1047
+ };
1048
+ }
1049
+ async login(payload, context) {
1050
+ if (!context.googleClientId) {
1051
+ throw new Error("Google OAuth is not configured");
1052
+ }
1053
+ const { googleIdToken } = payload;
1054
+ if (!googleIdToken || typeof googleIdToken !== "string") {
1055
+ throw new Error("googleIdToken is required");
1056
+ }
1057
+ context.logger?.log("Verifying Google ID token for login...");
1058
+ const googlePayload = await verifyGoogleIdToken(
1059
+ googleIdToken,
1060
+ context.googleClientId,
1061
+ context.fetch
1062
+ );
1063
+ context.logger?.log(`Google token verified for: ${googlePayload.email}`);
1064
+ const username = await authenticateGoogleUser(
1065
+ context.client,
1066
+ context.serverPublicKey,
1067
+ googlePayload.sub,
1068
+ context.serverEncryptionPrivateKeyPem,
1069
+ context.appKey,
1070
+ context.logger
1071
+ );
1072
+ if (!username) {
1073
+ throw new Error("Google account not registered. Please sign up first.");
1074
+ }
1075
+ context.logger?.log(`Google login successful for user: ${username}`);
1076
+ return {
1077
+ username,
1078
+ metadata: {
1079
+ email: googlePayload.email,
1080
+ name: googlePayload.name,
1081
+ picture: googlePayload.picture
1082
+ }
1083
+ };
1084
+ }
1085
+ };
1086
+ var credentialHandlers = /* @__PURE__ */ new Map([
1087
+ ["password", new PasswordCredentialHandler()],
1088
+ ["google", new GoogleCredentialHandler()]
1089
+ ]);
1090
+ function getCredentialHandler(type) {
1091
+ const handler = credentialHandlers.get(type);
1092
+ if (!handler) {
1093
+ throw new Error(`Unknown credential type: ${type}`);
1094
+ }
1095
+ return handler;
1096
+ }
1097
+ function registerCredentialHandler(type, handler) {
1098
+ credentialHandlers.set(type, handler);
1099
+ }
1100
+ function getSupportedCredentialTypes() {
1101
+ return Array.from(credentialHandlers.keys());
1102
+ }
1103
+
1104
+ // wallet-server/core.ts
1105
+ import { Hono } from "hono";
1106
+ import { cors } from "hono/cors";
1107
+ var WalletServerCore = class {
1108
+ constructor(userConfig) {
1109
+ this.validateConfig(userConfig);
1110
+ this.serverKeys = userConfig.serverKeys;
1111
+ this.config = this.applyDefaults(userConfig);
1112
+ this.logger = userConfig.deps?.logger ?? defaultLogger;
1113
+ this.storage = userConfig.deps?.storage ?? new MemoryFileStorage();
1114
+ this.fetchImpl = userConfig.deps?.fetch ?? fetch;
1115
+ this.credentialClient = userConfig.deps?.credentialClient ?? new HttpClient({ url: userConfig.credentialNodeUrl });
1116
+ this.proxyClient = userConfig.deps?.proxyClient ?? new HttpClient({ url: userConfig.proxyNodeUrl });
1117
+ this.app = this.createApp();
1118
+ }
1119
+ validateConfig(config) {
1120
+ if (!config.serverKeys?.identityKey?.privateKeyPem) {
1121
+ throw new Error("serverKeys.identityKey.privateKeyPem is required");
1122
+ }
1123
+ if (!config.serverKeys?.identityKey?.publicKeyHex) {
1124
+ throw new Error("serverKeys.identityKey.publicKeyHex is required");
1125
+ }
1126
+ if (!config.serverKeys?.encryptionKey?.privateKeyPem) {
1127
+ throw new Error("serverKeys.encryptionKey.privateKeyPem is required");
1128
+ }
1129
+ if (!config.serverKeys?.encryptionKey?.publicKeyHex) {
1130
+ throw new Error("serverKeys.encryptionKey.publicKeyHex is required");
1131
+ }
1132
+ if (!config.jwtSecret || config.jwtSecret.length < 32) {
1133
+ throw new Error("jwtSecret is required and must be at least 32 characters");
1134
+ }
1135
+ if (!config.credentialNodeUrl && !config.deps?.credentialClient) {
1136
+ throw new Error(
1137
+ "Either credentialNodeUrl or deps.credentialClient is required"
1138
+ );
1139
+ }
1140
+ if (!config.proxyNodeUrl && !config.deps?.proxyClient) {
1141
+ throw new Error("Either proxyNodeUrl or deps.proxyClient is required");
1142
+ }
1143
+ }
1144
+ applyDefaults(config) {
1145
+ return {
1146
+ serverKeys: config.serverKeys,
1147
+ jwtSecret: config.jwtSecret,
1148
+ jwtExpirationSeconds: config.jwtExpirationSeconds ?? 86400,
1149
+ allowedOrigins: config.allowedOrigins ?? ["*"],
1150
+ passwordResetTokenTtlSeconds: config.passwordResetTokenTtlSeconds ?? 3600,
1151
+ googleClientId: config.googleClientId ?? null,
1152
+ bootstrapStatePath: config.bootstrapStatePath ?? null,
1153
+ appBackend: config.appBackend ?? null
1154
+ };
1155
+ }
1156
+ /**
1157
+ * Get the Hono app instance
1158
+ */
1159
+ getApp() {
1160
+ return this.app;
1161
+ }
1162
+ /**
1163
+ * Get the fetch handler for use with various runtimes
1164
+ */
1165
+ getFetchHandler() {
1166
+ return this.app.fetch.bind(this.app);
1167
+ }
1168
+ /**
1169
+ * Get server public keys
1170
+ */
1171
+ getServerKeys() {
1172
+ return {
1173
+ identityPublicKeyHex: this.serverKeys.identityKey.publicKeyHex,
1174
+ encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
1175
+ };
1176
+ }
1177
+ /**
1178
+ * Bootstrap wallet app registration (optional)
1179
+ */
1180
+ async bootstrap() {
1181
+ if (!this.config.appBackend) {
1182
+ this.logger.warn(
1183
+ "App backend not configured; skipping wallet app bootstrap"
1184
+ );
1185
+ return null;
1186
+ }
1187
+ const appKey = this.serverKeys.identityKey.publicKeyHex;
1188
+ const apiBasePath = this.normalizeApiBasePath(
1189
+ this.config.appBackend.apiBasePath ?? "/api/v1"
1190
+ );
1191
+ const appServerUrl = this.config.appBackend.url.replace(/\/$/, "");
1192
+ if (this.config.bootstrapStatePath) {
1193
+ try {
1194
+ const existingState = await this.readBootstrapState(
1195
+ this.config.bootstrapStatePath
1196
+ );
1197
+ if (existingState) {
1198
+ if (existingState.appKey !== appKey) {
1199
+ throw new Error(
1200
+ `Bootstrap state belongs to ${existingState.appKey}, expected ${appKey}`
1201
+ );
1202
+ }
1203
+ return existingState;
1204
+ }
1205
+ } catch {
1206
+ }
1207
+ }
1208
+ const bootstrapJwt = await createJwt(
1209
+ "__wallet_bootstrap__",
1210
+ this.config.jwtSecret,
1211
+ this.config.jwtExpirationSeconds
1212
+ );
1213
+ const signMessage = async (payload) => {
1214
+ const privateKey = await pemToCryptoKey(
1215
+ this.serverKeys.identityKey.privateKeyPem,
1216
+ "Ed25519"
1217
+ );
1218
+ return await createAuthenticatedMessage(payload, [
1219
+ { privateKey, publicKeyHex: appKey }
1220
+ ]);
1221
+ };
1222
+ const originsMessage = await signMessage({
1223
+ allowedOrigins: this.config.allowedOrigins,
1224
+ encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
1225
+ });
1226
+ const originsRes = await this.fetchImpl(
1227
+ `${appServerUrl}${apiBasePath}/apps/origins/${appKey}`,
1228
+ {
1229
+ method: "POST",
1230
+ headers: {
1231
+ "Content-Type": "application/json",
1232
+ Authorization: `Bearer ${bootstrapJwt}`
1233
+ },
1234
+ body: JSON.stringify(originsMessage)
1235
+ }
1236
+ );
1237
+ const originsBody = await originsRes.json().catch(() => ({}));
1238
+ if (!originsRes.ok || !originsBody?.success) {
1239
+ throw new Error(
1240
+ `Wallet app origins bootstrap failed: ${originsBody?.error || originsRes.statusText}`
1241
+ );
1242
+ }
1243
+ const schemaMessage = await signMessage({
1244
+ actions: [],
1245
+ encryptionPublicKeyHex: this.serverKeys.encryptionKey.publicKeyHex
1246
+ });
1247
+ const schemaRes = await this.fetchImpl(
1248
+ `${appServerUrl}${apiBasePath}/apps/schema/${appKey}`,
1249
+ {
1250
+ method: "POST",
1251
+ headers: {
1252
+ "Content-Type": "application/json",
1253
+ Authorization: `Bearer ${bootstrapJwt}`
1254
+ },
1255
+ body: JSON.stringify(schemaMessage)
1256
+ }
1257
+ );
1258
+ const schemaBody = await schemaRes.json().catch(() => ({}));
1259
+ if (!schemaRes.ok || !schemaBody?.success) {
1260
+ throw new Error(
1261
+ `Wallet app schema bootstrap failed: ${schemaBody?.error || schemaRes.statusText}`
1262
+ );
1263
+ }
1264
+ const state = {
1265
+ appKey,
1266
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1267
+ appServerUrl,
1268
+ apiBasePath
1269
+ };
1270
+ if (this.config.bootstrapStatePath) {
1271
+ await this.writeBootstrapState(this.config.bootstrapStatePath, state);
1272
+ }
1273
+ return state;
1274
+ }
1275
+ normalizeApiBasePath(path) {
1276
+ if (!path || typeof path !== "string") {
1277
+ return "/api/v1";
1278
+ }
1279
+ const base = path.startsWith("/") ? path : `/${path}`;
1280
+ return base.replace(/\/$/, "");
1281
+ }
1282
+ async readBootstrapState(path) {
1283
+ try {
1284
+ const text = await this.storage.readTextFile(path);
1285
+ const parsed = JSON.parse(text);
1286
+ if (!parsed.appKey || !parsed.createdAt || !parsed.appServerUrl || !parsed.apiBasePath) {
1287
+ throw new Error(`Invalid bootstrap state file at ${path}`);
1288
+ }
1289
+ return parsed;
1290
+ } catch {
1291
+ return null;
1292
+ }
1293
+ }
1294
+ async writeBootstrapState(path, state) {
1295
+ await this.storage.writeTextFile(path, JSON.stringify(state, null, 2));
1296
+ }
1297
+ async sessionExists(appKey, sessionKey) {
1298
+ const input = new TextEncoder().encode(sessionKey);
1299
+ const digest = await crypto.subtle.digest("SHA-256", input);
1300
+ const sigHex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 32);
1301
+ const uri = `mutable://accounts/${appKey}/sessions/${sigHex}`;
1302
+ const res = await this.proxyClient.read(uri);
1303
+ return res.success;
1304
+ }
1305
+ createApp() {
1306
+ const app = new Hono();
1307
+ const serverPublicKey = this.serverKeys.identityKey.publicKeyHex;
1308
+ const serverIdentityPrivateKeyPem = this.serverKeys.identityKey.privateKeyPem;
1309
+ const serverIdentityPublicKeyHex = this.serverKeys.identityKey.publicKeyHex;
1310
+ const serverEncryptionPublicKeyHex = this.serverKeys.encryptionKey.publicKeyHex;
1311
+ const serverEncryptionPrivateKeyPem = this.serverKeys.encryptionKey.privateKeyPem;
1312
+ app.use(
1313
+ "/*",
1314
+ cors({
1315
+ origin: (origin) => this.config.allowedOrigins[0] === "*" ? origin : this.config.allowedOrigins.join(","),
1316
+ allowMethods: ["GET", "POST", "OPTIONS"],
1317
+ allowHeaders: ["Content-Type", "Authorization"]
1318
+ })
1319
+ );
1320
+ app.use(async (c, next) => {
1321
+ const start = Date.now();
1322
+ await next();
1323
+ const duration = Date.now() - start;
1324
+ this.logger.log(
1325
+ `[${(/* @__PURE__ */ new Date()).toISOString()}] ${c.req.method} ${c.req.path} ${c.res.status} - ${duration}ms`
1326
+ );
1327
+ });
1328
+ app.get("/api/v1/health", (c) => {
1329
+ return c.json({
1330
+ success: true,
1331
+ status: "ok",
1332
+ server: "b3nd-wallet-server",
1333
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1334
+ });
1335
+ });
1336
+ app.get("/api/v1/server-keys", (c) => {
1337
+ return c.json({
1338
+ success: true,
1339
+ identityPublicKeyHex: serverIdentityPublicKeyHex,
1340
+ encryptionPublicKeyHex: serverEncryptionPublicKeyHex
1341
+ });
1342
+ });
1343
+ app.get("/api/v1/auth/public-keys/:appKey", async (c) => {
1344
+ try {
1345
+ const appKey = c.req.param("appKey");
1346
+ const authHeader = c.req.header("Authorization");
1347
+ if (!appKey) {
1348
+ return c.json({ success: false, error: "appKey is required" }, 400);
1349
+ }
1350
+ if (!authHeader?.startsWith("Bearer ")) {
1351
+ return c.json({ success: false, error: "Authorization required" }, 401);
1352
+ }
1353
+ const token = authHeader.substring(7);
1354
+ const payload = await verifyJwt(token, this.config.jwtSecret);
1355
+ const keys = await getUserPublicKeys(
1356
+ this.credentialClient,
1357
+ serverPublicKey,
1358
+ payload.username,
1359
+ serverEncryptionPrivateKeyPem,
1360
+ this.logger
1361
+ );
1362
+ return c.json({
1363
+ success: true,
1364
+ accountPublicKeyHex: keys.accountPublicKeyHex,
1365
+ encryptionPublicKeyHex: keys.encryptionPublicKeyHex
1366
+ });
1367
+ } catch (error) {
1368
+ return c.json({
1369
+ success: false,
1370
+ error: error instanceof Error ? error.message : String(error)
1371
+ }, 400);
1372
+ }
1373
+ });
1374
+ app.get("/api/v1/auth/verify/:appKey", async (c) => {
1375
+ try {
1376
+ const appKey = c.req.param("appKey");
1377
+ const authHeader = c.req.header("Authorization");
1378
+ if (!appKey) {
1379
+ return c.json({ success: false, error: "appKey is required" }, 400);
1380
+ }
1381
+ if (!authHeader?.startsWith("Bearer ")) {
1382
+ return c.json({ success: false, error: "Authorization required" }, 401);
1383
+ }
1384
+ const token = authHeader.substring(7);
1385
+ const payload = await verifyJwt(token, this.config.jwtSecret);
1386
+ return c.json({
1387
+ success: true,
1388
+ username: payload.username,
1389
+ exp: payload.exp
1390
+ });
1391
+ } catch (error) {
1392
+ return c.json({
1393
+ success: false,
1394
+ error: error instanceof Error ? error.message : String(error)
1395
+ }, 401);
1396
+ }
1397
+ });
1398
+ app.post("/api/v1/auth/signup/:appKey", async (c) => {
1399
+ try {
1400
+ const appKey = c.req.param("appKey");
1401
+ const payload = await c.req.json();
1402
+ if (!appKey) {
1403
+ return c.json({ success: false, error: "appKey is required" }, 400);
1404
+ }
1405
+ if (!payload.type) {
1406
+ return c.json({
1407
+ success: false,
1408
+ error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1409
+ }, 400);
1410
+ }
1411
+ const handler = getCredentialHandler(payload.type);
1412
+ let googleClientId;
1413
+ if (payload.type === "google") {
1414
+ const appProfileUri = `mutable://accounts/${appKey}/app-profile`;
1415
+ const appProfileResult = await this.credentialClient.read(appProfileUri);
1416
+ if (appProfileResult.success && appProfileResult.record?.data) {
1417
+ const appProfile = appProfileResult.record.data;
1418
+ if (appProfile.payload && typeof appProfile.payload === "object") {
1419
+ googleClientId = appProfile.payload.googleClientId;
1420
+ } else {
1421
+ googleClientId = appProfile.googleClientId;
1422
+ }
1423
+ }
1424
+ if (!googleClientId) {
1425
+ throw new Error("Google Client ID not configured for this app");
1426
+ }
1427
+ }
1428
+ const context = {
1429
+ client: this.credentialClient,
1430
+ serverPublicKey,
1431
+ serverIdentityPrivateKeyPem,
1432
+ serverIdentityPublicKeyHex,
1433
+ serverEncryptionPublicKeyHex,
1434
+ serverEncryptionPrivateKeyPem,
1435
+ appKey,
1436
+ googleClientId,
1437
+ logger: this.logger,
1438
+ fetch: this.fetchImpl
1439
+ };
1440
+ const result = await handler.signup(payload, context);
1441
+ const jwt = await createJwt(
1442
+ result.username,
1443
+ this.config.jwtSecret,
1444
+ this.config.jwtExpirationSeconds
1445
+ );
1446
+ return c.json({
1447
+ success: true,
1448
+ username: result.username,
1449
+ token: jwt,
1450
+ expiresIn: this.config.jwtExpirationSeconds,
1451
+ ...result.metadata
1452
+ });
1453
+ } catch (error) {
1454
+ const message = error instanceof Error ? error.message : String(error);
1455
+ if (message.includes("already exists") || message.includes("already registered")) {
1456
+ return c.json({ success: false, error: message }, 409);
1457
+ }
1458
+ if (message.includes("Unknown credential type")) {
1459
+ return c.json({ success: false, error: message }, 400);
1460
+ }
1461
+ this.logger.error("Signup error:", error);
1462
+ return c.json({ success: false, error: message }, 500);
1463
+ }
1464
+ });
1465
+ app.post("/api/v1/auth/login/:appKey", async (c) => {
1466
+ try {
1467
+ const appKey = c.req.param("appKey");
1468
+ const payload = await c.req.json();
1469
+ if (!appKey) {
1470
+ return c.json({ success: false, error: "appKey is required" }, 400);
1471
+ }
1472
+ if (!payload.session) {
1473
+ return c.json({ success: false, error: "session is required" }, 400);
1474
+ }
1475
+ if (!payload.type) {
1476
+ return c.json({
1477
+ success: false,
1478
+ error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1479
+ }, 400);
1480
+ }
1481
+ if (!await this.sessionExists(appKey, payload.session)) {
1482
+ return c.json({ success: false, error: "Invalid session" }, 401);
1483
+ }
1484
+ const handler = getCredentialHandler(payload.type);
1485
+ let googleClientId;
1486
+ if (payload.type === "google") {
1487
+ const appProfileUri = `mutable://accounts/${appKey}/app-profile`;
1488
+ const appProfileResult = await this.credentialClient.read(appProfileUri);
1489
+ if (appProfileResult.success && appProfileResult.record?.data) {
1490
+ const appProfile = appProfileResult.record.data;
1491
+ if (appProfile.payload && typeof appProfile.payload === "object") {
1492
+ googleClientId = appProfile.payload.googleClientId;
1493
+ } else {
1494
+ googleClientId = appProfile.googleClientId;
1495
+ }
1496
+ }
1497
+ if (!googleClientId) {
1498
+ throw new Error("Google Client ID not configured for this app");
1499
+ }
1500
+ }
1501
+ const context = {
1502
+ client: this.credentialClient,
1503
+ serverPublicKey,
1504
+ serverIdentityPrivateKeyPem,
1505
+ serverIdentityPublicKeyHex,
1506
+ serverEncryptionPublicKeyHex,
1507
+ serverEncryptionPrivateKeyPem,
1508
+ appKey,
1509
+ googleClientId,
1510
+ logger: this.logger,
1511
+ fetch: this.fetchImpl
1512
+ };
1513
+ const result = await handler.login(payload, context);
1514
+ const jwt = await createJwt(
1515
+ result.username,
1516
+ this.config.jwtSecret,
1517
+ this.config.jwtExpirationSeconds
1518
+ );
1519
+ return c.json({
1520
+ success: true,
1521
+ username: result.username,
1522
+ token: jwt,
1523
+ expiresIn: this.config.jwtExpirationSeconds,
1524
+ ...result.metadata
1525
+ });
1526
+ } catch (error) {
1527
+ const message = error instanceof Error ? error.message : String(error);
1528
+ if (message.includes("Unknown credential type")) {
1529
+ return c.json({ success: false, error: message }, 400);
1530
+ }
1531
+ this.logger.error("Login error:", error);
1532
+ return c.json({ success: false, error: message }, 500);
1533
+ }
1534
+ });
1535
+ app.post("/api/v1/auth/credentials/change-password/:appKey", async (c) => {
1536
+ try {
1537
+ const appKey = c.req.param("appKey");
1538
+ const authHeader = c.req.header("Authorization");
1539
+ if (!appKey) {
1540
+ return c.json({ success: false, error: "appKey is required" }, 400);
1541
+ }
1542
+ if (!authHeader?.startsWith("Bearer ")) {
1543
+ return c.json({ success: false, error: "Authorization required" }, 401);
1544
+ }
1545
+ const token = authHeader.substring(7);
1546
+ const jwtPayload = await verifyJwt(token, this.config.jwtSecret);
1547
+ const { oldPassword, newPassword } = await c.req.json();
1548
+ if (!oldPassword || !newPassword) {
1549
+ return c.json({ success: false, error: "oldPassword and newPassword are required" }, 400);
1550
+ }
1551
+ if (newPassword.length < 8) {
1552
+ return c.json({ success: false, error: "newPassword must be at least 8 characters" }, 400);
1553
+ }
1554
+ await changePassword(
1555
+ this.credentialClient,
1556
+ serverPublicKey,
1557
+ jwtPayload.username,
1558
+ oldPassword,
1559
+ newPassword,
1560
+ serverIdentityPrivateKeyPem,
1561
+ serverIdentityPublicKeyHex,
1562
+ serverEncryptionPublicKeyHex,
1563
+ serverEncryptionPrivateKeyPem,
1564
+ appKey
1565
+ );
1566
+ return c.json({ success: true, message: "Password changed successfully" });
1567
+ } catch (error) {
1568
+ const message = error instanceof Error ? error.message : String(error);
1569
+ if (message.includes("expired") || message.includes("incorrect")) {
1570
+ return c.json({ success: false, error: message }, 401);
1571
+ }
1572
+ this.logger.error("Change password error:", error);
1573
+ return c.json({ success: false, error: message }, 500);
1574
+ }
1575
+ });
1576
+ app.post("/api/v1/auth/credentials/request-password-reset/:appKey", async (c) => {
1577
+ try {
1578
+ const appKey = c.req.param("appKey");
1579
+ const { username } = await c.req.json();
1580
+ if (!appKey) {
1581
+ return c.json({ success: false, error: "appKey is required" }, 400);
1582
+ }
1583
+ if (!username) {
1584
+ return c.json({ success: false, error: "username is required" }, 400);
1585
+ }
1586
+ const resetToken = await createPasswordResetToken(
1587
+ this.credentialClient,
1588
+ serverPublicKey,
1589
+ username,
1590
+ this.config.passwordResetTokenTtlSeconds,
1591
+ serverIdentityPrivateKeyPem,
1592
+ serverIdentityPublicKeyHex,
1593
+ serverEncryptionPublicKeyHex,
1594
+ appKey
1595
+ );
1596
+ return c.json({
1597
+ success: true,
1598
+ message: "Password reset token created",
1599
+ resetToken,
1600
+ expiresIn: this.config.passwordResetTokenTtlSeconds
1601
+ });
1602
+ } catch (error) {
1603
+ const message = error instanceof Error ? error.message : String(error);
1604
+ if (message.includes("not found")) {
1605
+ return c.json({ success: false, error: "User not found" }, 404);
1606
+ }
1607
+ this.logger.error("Request password reset error:", error);
1608
+ return c.json({ success: false, error: message }, 500);
1609
+ }
1610
+ });
1611
+ app.post("/api/v1/auth/credentials/reset-password/:appKey", async (c) => {
1612
+ try {
1613
+ const appKey = c.req.param("appKey");
1614
+ const { username, resetToken, newPassword } = await c.req.json();
1615
+ if (!appKey) {
1616
+ return c.json({ success: false, error: "appKey is required" }, 400);
1617
+ }
1618
+ if (!username || !resetToken || !newPassword) {
1619
+ return c.json({ success: false, error: "username, resetToken and newPassword are required" }, 400);
1620
+ }
1621
+ if (newPassword.length < 8) {
1622
+ return c.json({ success: false, error: "newPassword must be at least 8 characters" }, 400);
1623
+ }
1624
+ const resetUsername = await resetPasswordWithToken(
1625
+ this.credentialClient,
1626
+ serverPublicKey,
1627
+ resetToken,
1628
+ newPassword,
1629
+ serverIdentityPrivateKeyPem,
1630
+ serverIdentityPublicKeyHex,
1631
+ serverEncryptionPublicKeyHex,
1632
+ serverEncryptionPrivateKeyPem,
1633
+ username,
1634
+ appKey,
1635
+ this.logger
1636
+ );
1637
+ const newToken = await createJwt(
1638
+ resetUsername,
1639
+ this.config.jwtSecret,
1640
+ this.config.jwtExpirationSeconds
1641
+ );
1642
+ return c.json({
1643
+ success: true,
1644
+ message: "Password reset successful",
1645
+ username: resetUsername,
1646
+ token: newToken,
1647
+ expiresIn: this.config.jwtExpirationSeconds
1648
+ });
1649
+ } catch (error) {
1650
+ const message = error instanceof Error ? error.message : String(error);
1651
+ if (message.includes("invalid") || message.includes("expired")) {
1652
+ return c.json({ success: false, error: message }, 400);
1653
+ }
1654
+ this.logger.error("Reset password error:", error);
1655
+ return c.json({ success: false, error: message }, 500);
1656
+ }
1657
+ });
1658
+ app.post("/api/v1/proxy/write", async (c) => {
1659
+ try {
1660
+ const authHeader = c.req.header("Authorization");
1661
+ if (!authHeader?.startsWith("Bearer ")) {
1662
+ return c.json({ success: false, error: "Authorization required" }, 401);
1663
+ }
1664
+ const token = authHeader.substring(7);
1665
+ const payload = await verifyJwt(token, this.config.jwtSecret);
1666
+ const { uri, data, encrypt: encrypt2 } = await c.req.json();
1667
+ if (!uri) {
1668
+ return c.json({ success: false, error: "uri is required" }, 400);
1669
+ }
1670
+ if (data === void 0) {
1671
+ return c.json({ success: false, error: "data is required" }, 400);
1672
+ }
1673
+ const result = await proxyWrite(
1674
+ this.proxyClient,
1675
+ this.credentialClient,
1676
+ serverPublicKey,
1677
+ payload.username,
1678
+ serverEncryptionPrivateKeyPem,
1679
+ { uri, data, encrypt: encrypt2 === true }
1680
+ );
1681
+ if (!result.success) {
1682
+ return c.json({ success: false, error: result.error }, 400);
1683
+ }
1684
+ return c.json({
1685
+ success: true,
1686
+ uri,
1687
+ resolvedUri: result.resolvedUri,
1688
+ data,
1689
+ record: result.record
1690
+ });
1691
+ } catch (error) {
1692
+ const message = error instanceof Error ? error.message : String(error);
1693
+ if (message.includes("expired")) {
1694
+ return c.json({ success: false, error: message }, 401);
1695
+ }
1696
+ this.logger.error("Proxy write error:", error);
1697
+ return c.json({ success: false, error: message }, 500);
1698
+ }
1699
+ });
1700
+ app.get("/api/v1/proxy/read", async (c) => {
1701
+ try {
1702
+ const authHeader = c.req.header("Authorization");
1703
+ if (!authHeader?.startsWith("Bearer ")) {
1704
+ return c.json({ success: false, error: "Authorization required" }, 401);
1705
+ }
1706
+ const token = authHeader.substring(7);
1707
+ await verifyJwt(token, this.config.jwtSecret);
1708
+ const uri = c.req.query("uri");
1709
+ if (!uri) {
1710
+ return c.json({ success: false, error: "uri query parameter is required" }, 400);
1711
+ }
1712
+ const result = await proxyRead(
1713
+ this.proxyClient,
1714
+ uri,
1715
+ serverEncryptionPrivateKeyPem
1716
+ );
1717
+ if (!result.success) {
1718
+ return c.json({ success: false, error: result.error }, 400);
1719
+ }
1720
+ return c.json({
1721
+ success: true,
1722
+ uri,
1723
+ record: result.record,
1724
+ decrypted: result.decrypted
1725
+ });
1726
+ } catch (error) {
1727
+ const message = error instanceof Error ? error.message : String(error);
1728
+ if (message.includes("expired")) {
1729
+ return c.json({ success: false, error: message }, 401);
1730
+ }
1731
+ this.logger.error("Proxy read error:", error);
1732
+ return c.json({ success: false, error: message }, 500);
1733
+ }
1734
+ });
1735
+ return app;
1736
+ }
1737
+ };
1738
+
1739
+ export {
1740
+ defaultLogger,
1741
+ MemoryFileStorage,
1742
+ ConfigEnvironment,
1743
+ createJwt,
1744
+ verifyJwt,
1745
+ extractUsernameFromJwt,
1746
+ deriveObfuscatedPath,
1747
+ pemToCryptoKey,
1748
+ createSignedEncryptedPayload,
1749
+ decryptSignedEncryptedPayload,
1750
+ encryptForBackend,
1751
+ decryptFromBackend,
1752
+ generateUserKeys,
1753
+ loadUserAccountKey,
1754
+ loadUserEncryptionKey,
1755
+ loadUserKeys,
1756
+ getUserPublicKeys,
1757
+ proxyWrite,
1758
+ proxyRead,
1759
+ userExists,
1760
+ createUser,
1761
+ authenticateUser,
1762
+ changePassword,
1763
+ createPasswordResetToken,
1764
+ resetPasswordWithToken,
1765
+ googleUserExists,
1766
+ createGoogleUser,
1767
+ authenticateGoogleUser,
1768
+ clearGooglePublicKeyCache,
1769
+ verifyGoogleIdToken,
1770
+ generateGoogleUsername,
1771
+ getCredentialHandler,
1772
+ registerCredentialHandler,
1773
+ getSupportedCredentialTypes,
1774
+ WalletServerCore
1775
+ };