@bandeira-tech/b3nd-web 0.3.0 → 0.3.2

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.
@@ -1,3 +1,6 @@
1
+ import {
2
+ HttpClient
3
+ } from "./chunk-OY4CDOHY.js";
1
4
  import {
2
5
  createAuthenticatedMessage,
3
6
  createSignedEncryptedMessage,
@@ -5,11 +8,9 @@ import {
5
8
  decrypt,
6
9
  encodeHex,
7
10
  encrypt,
11
+ verify,
8
12
  verifyPayload
9
13
  } from "./chunk-JN75UL5C.js";
10
- import {
11
- HttpClient
12
- } from "./chunk-LFUC4ETD.js";
13
14
 
14
15
  // wallet-server/interfaces.ts
15
16
  var defaultLogger = {
@@ -511,6 +512,89 @@ async function proxyRead(proxyClient, credentialClient, serverPublicKey, usernam
511
512
  };
512
513
  }
513
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
+ }
514
598
 
515
599
  // wallet-server/auth.ts
516
600
  function generateSalt() {
@@ -597,7 +681,7 @@ async function createUser(client, serverPublicKey, username, password, serverIde
597
681
  );
598
682
  return { salt, hash };
599
683
  }
600
- async function authenticateUser(client, serverPublicKey, username, password, serverIdentityPublicKeyHex, serverEncryptionPrivateKeyPem, appScope, logger) {
684
+ async function authenticateUser(client, serverPublicKey, username, password, serverEncryptionPrivateKeyPem, appScope, logger) {
601
685
  const passwordPath = await deriveObfuscatedPath(
602
686
  serverPublicKey,
603
687
  username,
@@ -629,7 +713,6 @@ async function changePassword(client, serverPublicKey, username, oldPassword, ne
629
713
  serverPublicKey,
630
714
  username,
631
715
  oldPassword,
632
- serverIdentityPublicKeyHex,
633
716
  serverEncryptionPrivateKeyPem,
634
717
  appScope
635
718
  );
@@ -1003,7 +1086,6 @@ var PasswordCredentialHandler = class {
1003
1086
  context.serverPublicKey,
1004
1087
  username,
1005
1088
  password,
1006
- context.serverIdentityPublicKeyHex,
1007
1089
  context.serverEncryptionPrivateKeyPem,
1008
1090
  context.appKey,
1009
1091
  context.logger
@@ -1310,13 +1392,42 @@ var WalletServerCore = class {
1310
1392
  async writeBootstrapState(path, state) {
1311
1393
  await this.storage.writeTextFile(path, JSON.stringify(state, null, 2));
1312
1394
  }
1313
- async sessionExists(appKey, sessionKey) {
1314
- const input = new TextEncoder().encode(sessionKey);
1315
- const digest = await crypto.subtle.digest("SHA-256", input);
1316
- const sigHex = Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("").substring(0, 32);
1317
- 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}`;
1318
1406
  const res = await this.proxyClient.read(uri);
1319
- 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
+ }
1320
1431
  }
1321
1432
  createApp() {
1322
1433
  const app = new Hono();
@@ -1418,12 +1529,33 @@ var WalletServerCore = class {
1418
1529
  if (!appKey) {
1419
1530
  return c.json({ success: false, error: "appKey is required" }, 400);
1420
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
+ }
1421
1538
  if (!payload.type) {
1422
1539
  return c.json({
1423
1540
  success: false,
1424
1541
  error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1425
1542
  }, 400);
1426
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
+ }
1427
1559
  const handler = getCredentialHandler(payload.type);
1428
1560
  let googleClientId;
1429
1561
  if (payload.type === "google") {
@@ -1485,8 +1617,11 @@ var WalletServerCore = class {
1485
1617
  if (!appKey) {
1486
1618
  return c.json({ success: false, error: "appKey is required" }, 400);
1487
1619
  }
1488
- if (!payload.session) {
1489
- 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);
1490
1625
  }
1491
1626
  if (!payload.type) {
1492
1627
  return c.json({
@@ -1494,8 +1629,20 @@ var WalletServerCore = class {
1494
1629
  error: `type is required. Supported: ${getSupportedCredentialTypes().join(", ")}`
1495
1630
  }, 400);
1496
1631
  }
1497
- if (!await this.sessionExists(appKey, payload.session)) {
1498
- 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);
1499
1646
  }
1500
1647
  const handler = getCredentialHandler(payload.type);
1501
1648
  let googleClientId;
@@ -1751,6 +1898,36 @@ var WalletServerCore = class {
1751
1898
  return c.json({ success: false, error: message }, 500);
1752
1899
  }
1753
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
+ });
1754
1931
  return app;
1755
1932
  }
1756
1933
  };
@@ -1,15 +1,16 @@
1
- import {
2
- MemoryClient,
3
- createTestSchema
4
- } from "./chunk-OXHGNAQJ.js";
5
1
  import {
6
2
  WalletServerCore
7
- } from "./chunk-4C4YY2X7.js";
3
+ } from "./chunk-EF5ZUB4O.js";
8
4
  import {
9
5
  exportPrivateKeyPem,
10
6
  generateEncryptionKeyPair,
11
- generateSigningKeyPair
7
+ generateSigningKeyPair,
8
+ signWithHex
12
9
  } from "./chunk-JN75UL5C.js";
10
+ import {
11
+ MemoryClient,
12
+ createTestSchema
13
+ } from "./chunk-O53KW746.js";
13
14
 
14
15
  // wallet/client.ts
15
16
  var WalletClient = class {
@@ -143,19 +144,34 @@ var WalletClient = class {
143
144
  throw new Error("Use resetPasswordWithToken(appKey, username, resetToken, newPassword)");
144
145
  }
145
146
  /**
146
- * Sign up with app token (scoped to an app)
147
+ * Sign up with session keypair (scoped to an app)
148
+ *
149
+ * The session must be approved by the app beforehand:
150
+ * 1. Client writes request to: immutable://inbox/{appKey}/sessions/{sessionPubkey} = 1
151
+ * 2. App approves by writing: mutable://accounts/{appKey}/sessions/{sessionPubkey} = 1
152
+ * 3. Client calls this method with the session keypair
153
+ *
154
+ * @param appKey - The app's public key
155
+ * @param session - Session keypair (generated via generateSessionKeypair)
156
+ * @param credentials - User credentials (username/password)
147
157
  */
148
- async signupWithToken(appKey, tokenOrCredentials, maybeCredentials) {
149
- const credentials = typeof tokenOrCredentials === "string" ? maybeCredentials : tokenOrCredentials;
150
- if (!credentials) throw new Error("credentials are required");
158
+ async signupWithToken(appKey, session, credentials) {
159
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
160
+ throw new Error("session keypair is required");
161
+ }
162
+ const payloadToSign = {
163
+ sessionPubkey: session.publicKeyHex,
164
+ type: "password",
165
+ username: credentials.username,
166
+ password: credentials.password
167
+ };
168
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
151
169
  const response = await this.fetchImpl(this.buildAppKeyUrl("/auth/signup", appKey), {
152
170
  method: "POST",
153
171
  headers: { "Content-Type": "application/json" },
154
172
  body: JSON.stringify({
155
- token: typeof tokenOrCredentials === "string" ? tokenOrCredentials : void 0,
156
- type: "password",
157
- username: credentials.username,
158
- password: credentials.password
173
+ ...payloadToSign,
174
+ sessionSignature
159
175
  })
160
176
  });
161
177
  const data = await response.json();
@@ -165,21 +181,34 @@ var WalletClient = class {
165
181
  return { username: data.username, token: data.token, expiresIn: data.expiresIn };
166
182
  }
167
183
  /**
168
- * Login with app token and session (scoped to an app)
184
+ * Login with session keypair (scoped to an app)
185
+ *
186
+ * The session must be approved by the app beforehand:
187
+ * 1. Client writes request to: immutable://inbox/{appKey}/sessions/{sessionPubkey} = 1
188
+ * 2. App approves by writing: mutable://accounts/{appKey}/sessions/{sessionPubkey} = 1
189
+ * 3. Client calls this method with the session keypair
190
+ *
191
+ * @param appKey - The app's public key
192
+ * @param session - Session keypair (generated via generateSessionKeypair)
193
+ * @param credentials - User credentials (username/password)
169
194
  */
170
- async loginWithTokenSession(appKey, tokenOrSession, sessionOrCredentials, maybeCredentials) {
171
- const session = typeof sessionOrCredentials === "string" && maybeCredentials ? sessionOrCredentials : tokenOrSession;
172
- const credentials = maybeCredentials || sessionOrCredentials;
173
- if (!session || typeof session !== "string") throw new Error("session is required");
195
+ async loginWithTokenSession(appKey, session, credentials) {
196
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
197
+ throw new Error("session keypair is required");
198
+ }
199
+ const payloadToSign = {
200
+ sessionPubkey: session.publicKeyHex,
201
+ type: "password",
202
+ username: credentials.username,
203
+ password: credentials.password
204
+ };
205
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
174
206
  const response = await this.fetchImpl(this.buildAppKeyUrl("/auth/login", appKey), {
175
207
  method: "POST",
176
208
  headers: { "Content-Type": "application/json" },
177
209
  body: JSON.stringify({
178
- token: typeof tokenOrSession === "string" && maybeCredentials ? tokenOrSession : void 0,
179
- session,
180
- type: "password",
181
- username: credentials.username,
182
- password: credentials.password
210
+ ...payloadToSign,
211
+ sessionSignature
183
212
  })
184
213
  });
185
214
  const data = await response.json();
@@ -299,6 +328,31 @@ var WalletClient = class {
299
328
  const data = await response.json();
300
329
  return data;
301
330
  }
331
+ /**
332
+ * Proxy multiple read requests through the wallet server
333
+ * Reads multiple URIs in a single request (max 50 URIs)
334
+ * The server decrypts encrypted data using user's encryption key
335
+ * Requires active authentication session
336
+ *
337
+ * @returns ProxyReadMultiResponse - check `success` for overall result, `results` for per-URI results
338
+ */
339
+ async proxyReadMulti(request) {
340
+ if (!this.currentSession) {
341
+ throw new Error("Not authenticated. Please login first.");
342
+ }
343
+ const response = await this.fetchImpl(
344
+ `${this.walletServerUrl}${this.apiBasePath}/proxy/read-multi`,
345
+ {
346
+ method: "POST",
347
+ headers: {
348
+ "Content-Type": "application/json",
349
+ Authorization: `Bearer ${this.currentSession.token}`
350
+ },
351
+ body: JSON.stringify({ uris: request.uris })
352
+ }
353
+ );
354
+ return await response.json();
355
+ }
302
356
  /**
303
357
  * Convenience method: Get current user's public keys
304
358
  * Requires active authentication session
@@ -356,22 +410,37 @@ var WalletClient = class {
356
410
  };
357
411
  }
358
412
  /**
359
- * Login with Google OAuth (scoped to app token and session)
413
+ * Login with Google OAuth (scoped to app token and session keypair)
360
414
  * Returns session data with Google profile info - call setSession() to activate it
361
415
  *
416
+ * The session must be approved by the app beforehand.
417
+ *
418
+ * @param appKey - The app's public key
362
419
  * @param token - App token from app server
363
- * @param session - Session key from app server
420
+ * @param session - Session keypair (generated via generateSessionKeypair)
364
421
  * @param googleIdToken - Google ID token from Google Sign-In
365
422
  * @returns GoogleAuthSession with username, JWT token, and Google profile info
366
423
  */
367
424
  async loginWithGoogle(appKey, token, session, googleIdToken) {
368
425
  if (!token) throw new Error("token is required");
369
- if (!session) throw new Error("session is required");
426
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
427
+ throw new Error("session keypair is required");
428
+ }
370
429
  if (!googleIdToken) throw new Error("googleIdToken is required");
430
+ const payloadToSign = {
431
+ token,
432
+ sessionPubkey: session.publicKeyHex,
433
+ type: "google",
434
+ googleIdToken
435
+ };
436
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
371
437
  const response = await this.fetchImpl(this.buildAppKeyUrl("/auth/login", appKey), {
372
438
  method: "POST",
373
439
  headers: { "Content-Type": "application/json" },
374
- body: JSON.stringify({ token, session, type: "google", googleIdToken })
440
+ body: JSON.stringify({
441
+ ...payloadToSign,
442
+ sessionSignature
443
+ })
375
444
  });
376
445
  const data = await response.json();
377
446
  if (!response.ok || !data.success) {
@@ -387,6 +456,13 @@ var WalletClient = class {
387
456
  };
388
457
  }
389
458
  };
459
+ async function generateSessionKeypair() {
460
+ const keyPair = await generateSigningKeyPair();
461
+ return {
462
+ publicKeyHex: keyPair.publicKeyHex,
463
+ privateKeyHex: keyPair.privateKeyHex
464
+ };
465
+ }
390
466
 
391
467
  // wallet/memory-client.ts
392
468
  async function generateTestServerKeys() {
@@ -541,14 +617,32 @@ var MemoryWalletClient = class _MemoryWalletClient {
541
617
  async login(_credentials) {
542
618
  throw new Error("Use loginWithTokenSession(appKey, session, credentials) \u2014 app token + session required");
543
619
  }
544
- async signupWithToken(appKey, tokenOrCredentials, maybeCredentials) {
545
- const credentials = typeof tokenOrCredentials === "string" ? maybeCredentials : tokenOrCredentials;
546
- if (!credentials) throw new Error("credentials are required");
547
- const response = await this.request("POST", `/auth/signup/${appKey}`, {
548
- token: typeof tokenOrCredentials === "string" ? tokenOrCredentials : void 0,
620
+ /**
621
+ * Sign up with session keypair (scoped to an app)
622
+ *
623
+ * The session must be approved by the app beforehand:
624
+ * 1. Client writes request to: immutable://inbox/{appKey}/sessions/{sessionPubkey} = 1
625
+ * 2. App approves by writing: mutable://accounts/{appKey}/sessions/{sessionPubkey} = 1
626
+ * 3. Client calls this method with the session keypair
627
+ *
628
+ * @param appKey - The app's public key
629
+ * @param session - Session keypair (generated via generateSessionKeypair)
630
+ * @param credentials - User credentials (username/password)
631
+ */
632
+ async signupWithToken(appKey, session, credentials) {
633
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
634
+ throw new Error("session keypair is required");
635
+ }
636
+ const payloadToSign = {
637
+ sessionPubkey: session.publicKeyHex,
549
638
  type: "password",
550
639
  username: credentials.username,
551
640
  password: credentials.password
641
+ };
642
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
643
+ const response = await this.request("POST", `/auth/signup/${appKey}`, {
644
+ ...payloadToSign,
645
+ sessionSignature
552
646
  });
553
647
  const data = await response.json();
554
648
  if (!response.ok || !data.success) {
@@ -560,16 +654,32 @@ var MemoryWalletClient = class _MemoryWalletClient {
560
654
  expiresIn: data.expiresIn
561
655
  };
562
656
  }
563
- async loginWithTokenSession(appKey, tokenOrSession, sessionOrCredentials, maybeCredentials) {
564
- const session = typeof sessionOrCredentials === "string" && maybeCredentials ? sessionOrCredentials : tokenOrSession;
565
- const credentials = maybeCredentials || sessionOrCredentials;
566
- if (!session || typeof session !== "string") throw new Error("session is required");
567
- const response = await this.request("POST", `/auth/login/${appKey}`, {
568
- token: typeof tokenOrSession === "string" && maybeCredentials ? tokenOrSession : void 0,
569
- session,
657
+ /**
658
+ * Login with session keypair (scoped to an app)
659
+ *
660
+ * The session must be approved by the app beforehand:
661
+ * 1. Client writes request to: immutable://inbox/{appKey}/sessions/{sessionPubkey} = 1
662
+ * 2. App approves by writing: mutable://accounts/{appKey}/sessions/{sessionPubkey} = 1
663
+ * 3. Client calls this method with the session keypair
664
+ *
665
+ * @param appKey - The app's public key
666
+ * @param session - Session keypair (generated via generateSessionKeypair)
667
+ * @param credentials - User credentials (username/password)
668
+ */
669
+ async loginWithTokenSession(appKey, session, credentials) {
670
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
671
+ throw new Error("session keypair is required");
672
+ }
673
+ const payloadToSign = {
674
+ sessionPubkey: session.publicKeyHex,
570
675
  type: "password",
571
676
  username: credentials.username,
572
677
  password: credentials.password
678
+ };
679
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
680
+ const response = await this.request("POST", `/auth/login/${appKey}`, {
681
+ ...payloadToSign,
682
+ sessionSignature
573
683
  });
574
684
  const data = await response.json();
575
685
  if (!response.ok || !data.success) {
@@ -675,6 +785,12 @@ var MemoryWalletClient = class _MemoryWalletClient {
675
785
  const data = await response.json();
676
786
  return data;
677
787
  }
788
+ async proxyReadMulti(request) {
789
+ const response = await this.authRequest("POST", "/proxy/read-multi", {
790
+ uris: request.uris
791
+ });
792
+ return await response.json();
793
+ }
678
794
  // ============================================================
679
795
  // Google OAuth (for completeness - may not work without real Google)
680
796
  // ============================================================
@@ -699,15 +815,34 @@ var MemoryWalletClient = class _MemoryWalletClient {
699
815
  picture: data.picture
700
816
  };
701
817
  }
818
+ /**
819
+ * Login with Google OAuth (scoped to app token and session keypair)
820
+ * Returns session data with Google profile info - call setSession() to activate it
821
+ *
822
+ * The session must be approved by the app beforehand.
823
+ *
824
+ * @param appKey - The app's public key
825
+ * @param token - App token from app server
826
+ * @param session - Session keypair (generated via generateSessionKeypair)
827
+ * @param googleIdToken - Google ID token from Google Sign-In
828
+ * @returns GoogleAuthSession with username, JWT token, and Google profile info
829
+ */
702
830
  async loginWithGoogle(appKey, token, session, googleIdToken) {
703
831
  if (!token) throw new Error("token is required");
704
- if (!session) throw new Error("session is required");
832
+ if (!session?.publicKeyHex || !session?.privateKeyHex) {
833
+ throw new Error("session keypair is required");
834
+ }
705
835
  if (!googleIdToken) throw new Error("googleIdToken is required");
706
- const response = await this.request("POST", `/auth/login/${appKey}`, {
836
+ const payloadToSign = {
707
837
  token,
708
- session,
838
+ sessionPubkey: session.publicKeyHex,
709
839
  type: "google",
710
840
  googleIdToken
841
+ };
842
+ const sessionSignature = await signWithHex(session.privateKeyHex, payloadToSign);
843
+ const response = await this.request("POST", `/auth/login/${appKey}`, {
844
+ ...payloadToSign,
845
+ sessionSignature
711
846
  });
712
847
  const data = await response.json();
713
848
  if (!response.ok || !data.success) {
@@ -755,16 +890,30 @@ async function createTestEnvironment(config = {}) {
755
890
  wallet,
756
891
  serverKeys,
757
892
  async signupTestUser(appKey, username, password) {
758
- const session = await wallet.signupWithToken(appKey, { username, password });
893
+ const keypair = await generateSigningKeyPair();
894
+ const sessionKeypair = {
895
+ publicKeyHex: keypair.publicKeyHex,
896
+ privateKeyHex: keypair.privateKeyHex
897
+ };
898
+ const sessionUri = `mutable://accounts/${appKey}/sessions/${sessionKeypair.publicKeyHex}`;
899
+ await backend.write(sessionUri, 1);
900
+ const session = await wallet.signupWithToken(appKey, sessionKeypair, { username, password });
759
901
  wallet.setSession(session);
760
902
  const keys = await wallet.getPublicKeys(appKey);
761
- return { session, keys };
903
+ return { session, keys, sessionKeypair };
762
904
  },
763
- async loginTestUser(appKey, sessionKey, username, password) {
764
- const session = await wallet.loginWithTokenSession(appKey, sessionKey, { username, password });
905
+ async loginTestUser(appKey, username, password) {
906
+ const keypair = await generateSigningKeyPair();
907
+ const sessionKeypair = {
908
+ publicKeyHex: keypair.publicKeyHex,
909
+ privateKeyHex: keypair.privateKeyHex
910
+ };
911
+ const sessionUri = `mutable://accounts/${appKey}/sessions/${sessionKeypair.publicKeyHex}`;
912
+ await backend.write(sessionUri, 1);
913
+ const session = await wallet.loginWithTokenSession(appKey, sessionKeypair, { username, password });
765
914
  wallet.setSession(session);
766
915
  const keys = await wallet.getPublicKeys(appKey);
767
- return { session, keys };
916
+ return { session, keys, sessionKeypair };
768
917
  },
769
918
  async cleanup() {
770
919
  await backend.cleanup();
@@ -775,6 +924,7 @@ async function createTestEnvironment(config = {}) {
775
924
 
776
925
  export {
777
926
  WalletClient,
927
+ generateSessionKeypair,
778
928
  generateTestServerKeys,
779
929
  MemoryWalletClient,
780
930
  createTestEnvironment