@bandeira-tech/b3nd-web 0.2.7 → 0.3.1

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.
@@ -5,11 +5,12 @@ import {
5
5
  decrypt,
6
6
  encodeHex,
7
7
  encrypt,
8
+ verify,
8
9
  verifyPayload
9
10
  } from "./chunk-JN75UL5C.js";
10
11
  import {
11
12
  HttpClient
12
- } from "./chunk-LFUC4ETD.js";
13
+ } from "./chunk-OY4CDOHY.js";
13
14
 
14
15
  // wallet-server/interfaces.ts
15
16
  var defaultLogger = {
@@ -446,27 +447,42 @@ async function proxyWrite(proxyClient, credentialClient, serverPublicKey, userna
446
447
  };
447
448
  }
448
449
  }
449
- async function proxyRead(proxyClient, uri, serverEncryptionPrivateKeyPem) {
450
+ async function proxyRead(proxyClient, credentialClient, serverPublicKey, username, serverEncryptionPrivateKeyPem, uri) {
450
451
  try {
451
- const result = await proxyClient.read(uri);
452
+ const accountKey = await loadUserAccountKey(
453
+ credentialClient,
454
+ serverPublicKey,
455
+ username,
456
+ serverEncryptionPrivateKeyPem
457
+ );
458
+ const resolvedUri = uri.replace(/:key/g, accountKey.publicKeyHex);
459
+ const result = await proxyClient.read(resolvedUri);
452
460
  if (!result.success) {
453
461
  return {
454
462
  success: false,
463
+ uri: resolvedUri,
455
464
  error: result.error || "Read failed"
456
465
  };
457
466
  }
458
467
  const response = {
459
468
  success: true,
469
+ uri: resolvedUri,
460
470
  record: result.record
461
471
  };
462
- if (serverEncryptionPrivateKeyPem && result.record?.data) {
472
+ if (result.record?.data) {
463
473
  try {
464
474
  const data = result.record.data;
465
475
  if (typeof data === "object" && data.payload && typeof data.payload === "object") {
466
476
  const payload = data.payload;
467
477
  if (payload.data && payload.nonce && payload.ephemeralPublicKey) {
478
+ const userEncryptionKey = await loadUserEncryptionKey(
479
+ credentialClient,
480
+ serverPublicKey,
481
+ username,
482
+ serverEncryptionPrivateKeyPem
483
+ );
468
484
  const privateKey = await pemToCryptoKey(
469
- serverEncryptionPrivateKeyPem,
485
+ userEncryptionKey.privateKeyPem,
470
486
  "X25519"
471
487
  );
472
488
  const encryptedPayload = {
@@ -491,10 +507,94 @@ async function proxyRead(proxyClient, uri, serverEncryptionPrivateKeyPem) {
491
507
  } catch (error) {
492
508
  return {
493
509
  success: false,
510
+ uri,
494
511
  error: `Proxy read failed: ${error instanceof Error ? error.message : String(error)}`
495
512
  };
496
513
  }
497
514
  }
515
+ async function proxyReadMulti(proxyClient, credentialClient, serverPublicKey, username, serverEncryptionPrivateKeyPem, uris) {
516
+ if (uris.length > 50) {
517
+ return {
518
+ success: false,
519
+ results: [],
520
+ summary: { total: uris.length, succeeded: 0, failed: uris.length },
521
+ error: "Maximum 50 URIs per request"
522
+ };
523
+ }
524
+ try {
525
+ const [accountKey, userEncryptionKey] = await Promise.all([
526
+ loadUserAccountKey(
527
+ credentialClient,
528
+ serverPublicKey,
529
+ username,
530
+ serverEncryptionPrivateKeyPem
531
+ ),
532
+ loadUserEncryptionKey(
533
+ credentialClient,
534
+ serverPublicKey,
535
+ username,
536
+ serverEncryptionPrivateKeyPem
537
+ )
538
+ ]);
539
+ const resolvedUris = uris.map(
540
+ (uri) => uri.replace(/:key/g, accountKey.publicKeyHex)
541
+ );
542
+ const multiResult = await proxyClient.readMulti(resolvedUris);
543
+ const privateKey = await pemToCryptoKey(
544
+ userEncryptionKey.privateKeyPem,
545
+ "X25519"
546
+ );
547
+ const results = await Promise.all(
548
+ multiResult.results.map(async (item, i) => {
549
+ const originalUri = uris[i];
550
+ if (!item.success) {
551
+ return {
552
+ uri: originalUri,
553
+ success: false,
554
+ error: "error" in item ? item.error : "Read failed"
555
+ };
556
+ }
557
+ const result = {
558
+ uri: originalUri,
559
+ success: true,
560
+ record: "record" in item ? item.record : void 0
561
+ };
562
+ if (result.record?.data) {
563
+ try {
564
+ const data = result.record.data;
565
+ if (typeof data === "object" && data.payload && typeof data.payload === "object") {
566
+ const payload = data.payload;
567
+ if (payload.data && payload.nonce && payload.ephemeralPublicKey) {
568
+ const encryptedPayload = {
569
+ data: payload.data,
570
+ nonce: payload.nonce,
571
+ ephemeralPublicKey: payload.ephemeralPublicKey
572
+ };
573
+ const decrypted = await decrypt(encryptedPayload, privateKey);
574
+ return { ...result, decrypted };
575
+ }
576
+ }
577
+ } catch {
578
+ }
579
+ }
580
+ return result;
581
+ })
582
+ );
583
+ const succeeded = results.filter((r) => r.success).length;
584
+ return {
585
+ success: succeeded > 0,
586
+ results,
587
+ summary: { total: uris.length, succeeded, failed: uris.length - succeeded }
588
+ };
589
+ } catch (error) {
590
+ return {
591
+ success: false,
592
+ results: [],
593
+ summary: { total: uris.length, succeeded: 0, failed: uris.length },
594
+ error: `Proxy read-multi failed: ${error instanceof Error ? error.message : String(error)}`
595
+ };
596
+ }
597
+ }
498
598
 
499
599
  // wallet-server/auth.ts
500
600
  function generateSalt() {
@@ -581,7 +681,7 @@ async function createUser(client, serverPublicKey, username, password, serverIde
581
681
  );
582
682
  return { salt, hash };
583
683
  }
584
- async function authenticateUser(client, serverPublicKey, username, password, serverIdentityPublicKeyHex, serverEncryptionPrivateKeyPem, appScope, logger) {
684
+ async function authenticateUser(client, serverPublicKey, username, password, serverEncryptionPrivateKeyPem, appScope, logger) {
585
685
  const passwordPath = await deriveObfuscatedPath(
586
686
  serverPublicKey,
587
687
  username,
@@ -613,7 +713,6 @@ async function changePassword(client, serverPublicKey, username, oldPassword, ne
613
713
  serverPublicKey,
614
714
  username,
615
715
  oldPassword,
616
- serverIdentityPublicKeyHex,
617
716
  serverEncryptionPrivateKeyPem,
618
717
  appScope
619
718
  );
@@ -987,7 +1086,6 @@ var PasswordCredentialHandler = class {
987
1086
  context.serverPublicKey,
988
1087
  username,
989
1088
  password,
990
- context.serverIdentityPublicKeyHex,
991
1089
  context.serverEncryptionPrivateKeyPem,
992
1090
  context.appKey,
993
1091
  context.logger
@@ -1294,13 +1392,42 @@ var WalletServerCore = class {
1294
1392
  async writeBootstrapState(path, state) {
1295
1393
  await this.storage.writeTextFile(path, JSON.stringify(state, null, 2));
1296
1394
  }
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}`;
1395
+ /**
1396
+ * Validate that a session is approved by the app.
1397
+ * Sessions are keypairs - the sessionPubkey is used directly as the identifier.
1398
+ * App approves sessions by writing 1 to mutable://accounts/{appKey}/sessions/{sessionPubkey}
1399
+ *
1400
+ * @param appKey - The app's public key
1401
+ * @param sessionPubkey - The session's public key (hex encoded)
1402
+ * @returns { valid: true } if approved, { valid: false, reason } if not
1403
+ */
1404
+ async sessionExists(appKey, sessionPubkey) {
1405
+ const uri = `mutable://accounts/${appKey}/sessions/${sessionPubkey}`;
1302
1406
  const res = await this.proxyClient.read(uri);
1303
- return res.success;
1407
+ if (!res.success) {
1408
+ return { valid: false, reason: "session_not_approved" };
1409
+ }
1410
+ if (res.record?.data === 1) {
1411
+ return { valid: true };
1412
+ }
1413
+ if (res.record?.data === 0) {
1414
+ return { valid: false, reason: "session_revoked" };
1415
+ }
1416
+ return { valid: false, reason: "invalid_session_status" };
1417
+ }
1418
+ /**
1419
+ * Verify that a login request signature is valid for the given session pubkey.
1420
+ * The signature should be over the stringified login payload (without the signature field).
1421
+ * Uses SDK crypto for consistent verification across the codebase.
1422
+ */
1423
+ async verifySessionSignature(sessionPubkey, signature, payload) {
1424
+ try {
1425
+ const { sessionSignature: _, ...signedPayload } = payload;
1426
+ return await verify(sessionPubkey, signature, signedPayload);
1427
+ } catch (error) {
1428
+ this.logger.error("Session signature verification failed:", error);
1429
+ return false;
1430
+ }
1304
1431
  }
1305
1432
  createApp() {
1306
1433
  const app = new Hono();
@@ -1402,12 +1529,33 @@ var WalletServerCore = class {
1402
1529
  if (!appKey) {
1403
1530
  return c.json({ success: false, error: "appKey is required" }, 400);
1404
1531
  }
1532
+ if (!payload.sessionPubkey) {
1533
+ return c.json({ success: false, error: "sessionPubkey is required" }, 400);
1534
+ }
1535
+ if (!payload.sessionSignature) {
1536
+ return c.json({ success: false, error: "sessionSignature is required" }, 400);
1537
+ }
1405
1538
  if (!payload.type) {
1406
1539
  return c.json({
1407
1540
  success: false,
1408
1541
  error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1409
1542
  }, 400);
1410
1543
  }
1544
+ const signatureValid = await this.verifySessionSignature(
1545
+ payload.sessionPubkey,
1546
+ payload.sessionSignature,
1547
+ payload
1548
+ );
1549
+ if (!signatureValid) {
1550
+ return c.json({ success: false, error: "Invalid session signature" }, 401);
1551
+ }
1552
+ const sessionResult = await this.sessionExists(appKey, payload.sessionPubkey);
1553
+ if (!sessionResult.valid) {
1554
+ return c.json({
1555
+ success: false,
1556
+ error: sessionResult.reason === "session_revoked" ? "Session has been revoked" : sessionResult.reason === "session_not_approved" ? "Session not approved by app" : "Invalid session"
1557
+ }, 401);
1558
+ }
1411
1559
  const handler = getCredentialHandler(payload.type);
1412
1560
  let googleClientId;
1413
1561
  if (payload.type === "google") {
@@ -1469,8 +1617,11 @@ var WalletServerCore = class {
1469
1617
  if (!appKey) {
1470
1618
  return c.json({ success: false, error: "appKey is required" }, 400);
1471
1619
  }
1472
- if (!payload.session) {
1473
- return c.json({ success: false, error: "session is required" }, 400);
1620
+ if (!payload.sessionPubkey) {
1621
+ return c.json({ success: false, error: "sessionPubkey is required" }, 400);
1622
+ }
1623
+ if (!payload.sessionSignature) {
1624
+ return c.json({ success: false, error: "sessionSignature is required" }, 400);
1474
1625
  }
1475
1626
  if (!payload.type) {
1476
1627
  return c.json({
@@ -1478,8 +1629,20 @@ var WalletServerCore = class {
1478
1629
  error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1479
1630
  }, 400);
1480
1631
  }
1481
- if (!await this.sessionExists(appKey, payload.session)) {
1482
- return c.json({ success: false, error: "Invalid session" }, 401);
1632
+ const signatureValid = await this.verifySessionSignature(
1633
+ payload.sessionPubkey,
1634
+ payload.sessionSignature,
1635
+ payload
1636
+ );
1637
+ if (!signatureValid) {
1638
+ return c.json({ success: false, error: "Invalid session signature" }, 401);
1639
+ }
1640
+ const sessionResult = await this.sessionExists(appKey, payload.sessionPubkey);
1641
+ if (!sessionResult.valid) {
1642
+ return c.json({
1643
+ success: false,
1644
+ error: sessionResult.reason === "session_revoked" ? "Session has been revoked" : sessionResult.reason === "session_not_approved" ? "Session not approved by app" : "Invalid session"
1645
+ }, 401);
1483
1646
  }
1484
1647
  const handler = getCredentialHandler(payload.type);
1485
1648
  let googleClientId;
@@ -1704,22 +1867,25 @@ var WalletServerCore = class {
1704
1867
  return c.json({ success: false, error: "Authorization required" }, 401);
1705
1868
  }
1706
1869
  const token = authHeader.substring(7);
1707
- await verifyJwt(token, this.config.jwtSecret);
1870
+ const payload = await verifyJwt(token, this.config.jwtSecret);
1708
1871
  const uri = c.req.query("uri");
1709
1872
  if (!uri) {
1710
1873
  return c.json({ success: false, error: "uri query parameter is required" }, 400);
1711
1874
  }
1712
1875
  const result = await proxyRead(
1713
1876
  this.proxyClient,
1714
- uri,
1715
- serverEncryptionPrivateKeyPem
1877
+ this.credentialClient,
1878
+ serverPublicKey,
1879
+ payload.username,
1880
+ serverEncryptionPrivateKeyPem,
1881
+ uri
1716
1882
  );
1717
1883
  if (!result.success) {
1718
1884
  return c.json({ success: false, error: result.error }, 400);
1719
1885
  }
1720
1886
  return c.json({
1721
1887
  success: true,
1722
- uri,
1888
+ uri: result.uri,
1723
1889
  record: result.record,
1724
1890
  decrypted: result.decrypted
1725
1891
  });
@@ -1732,6 +1898,36 @@ var WalletServerCore = class {
1732
1898
  return c.json({ success: false, error: message }, 500);
1733
1899
  }
1734
1900
  });
1901
+ app.post("/api/v1/proxy/read-multi", async (c) => {
1902
+ try {
1903
+ const authHeader = c.req.header("Authorization");
1904
+ if (!authHeader?.startsWith("Bearer ")) {
1905
+ return c.json({ success: false, error: "Authorization required" }, 401);
1906
+ }
1907
+ const token = authHeader.substring(7);
1908
+ const payload = await verifyJwt(token, this.config.jwtSecret);
1909
+ const body = await c.req.json();
1910
+ if (!Array.isArray(body.uris)) {
1911
+ return c.json({ success: false, error: "uris must be an array" }, 400);
1912
+ }
1913
+ const result = await proxyReadMulti(
1914
+ this.proxyClient,
1915
+ this.credentialClient,
1916
+ serverPublicKey,
1917
+ payload.username,
1918
+ serverEncryptionPrivateKeyPem,
1919
+ body.uris
1920
+ );
1921
+ return c.json(result);
1922
+ } catch (error) {
1923
+ const message = error instanceof Error ? error.message : String(error);
1924
+ if (message.includes("expired")) {
1925
+ return c.json({ success: false, error: message }, 401);
1926
+ }
1927
+ this.logger.error("Proxy read-multi error:", error);
1928
+ return c.json({ success: false, error: message }, 500);
1929
+ }
1930
+ });
1735
1931
  return app;
1736
1932
  }
1737
1933
  };