@arbidocs/client 0.4.127 → 0.4.129

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -100,11 +100,17 @@ async function initSodium() {
100
100
  function base64ToBytes(base64String) {
101
101
  return sodium__default.default.from_base64(base64String, sodium__default.default.base64_variants.ORIGINAL);
102
102
  }
103
- function bytesToBase64(bytes) {
104
- return sodium__default.default.to_base64(bytes, sodium__default.default.base64_variants.ORIGINAL);
103
+ function bytesToBase64(bytes2) {
104
+ return sodium__default.default.to_base64(bytes2, sodium__default.default.base64_variants.ORIGINAL);
105
105
  }
106
- function base64Encode(bytes) {
107
- const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join("");
106
+ function bytesToBase64Url(bytes2) {
107
+ return sodium__default.default.to_base64(bytes2, sodium__default.default.base64_variants.URLSAFE_NO_PADDING);
108
+ }
109
+ function base64UrlToBytes(encoded) {
110
+ return sodium__default.default.from_base64(encoded, sodium__default.default.base64_variants.URLSAFE_NO_PADDING);
111
+ }
112
+ function base64Encode(bytes2) {
113
+ const binString = Array.from(bytes2, (byte) => String.fromCodePoint(byte)).join("");
108
114
  return btoa(binString);
109
115
  }
110
116
  function base64Decode(base64) {
@@ -214,6 +220,20 @@ function deriveEncryptionKeypairFromSigning(signingKeyPair) {
214
220
  secretKey: encryptionPrivateKey
215
221
  };
216
222
  }
223
+ function encryptionKeypairFromSigningKey(signingPrivateKey) {
224
+ return deriveEncryptionKeypairFromSigning({
225
+ publicKey: signingPrivateKey.slice(32, 64),
226
+ secretKey: signingPrivateKey
227
+ });
228
+ }
229
+ function unwrapWorkspaceKey(wrappedKey, signingPrivateKey) {
230
+ const { secretKey } = encryptionKeypairFromSigningKey(signingPrivateKey);
231
+ try {
232
+ return sealedBoxDecrypt(wrappedKey, secretKey);
233
+ } finally {
234
+ secretKey.fill(0);
235
+ }
236
+ }
217
237
  function generateRandomSigningKeypair() {
218
238
  const kp = sodium__default.default.crypto_sign_keypair();
219
239
  return { publicKey: kp.publicKey, secretKey: kp.privateKey };
@@ -309,32 +329,23 @@ async function openSealedCallKey(sealedBase64, encryptionSecretKey) {
309
329
  }
310
330
  async function encryptCallTitle(title, key) {
311
331
  await initSodium();
312
- const nonce = sodium__default.default.randombytes_buf(sodium__default.default.crypto_secretbox_NONCEBYTES);
313
- const box = sodium__default.default.crypto_secretbox_easy(sodium__default.default.from_string(title), nonce, key);
314
- const out = new Uint8Array(nonce.length + box.length);
315
- out.set(nonce, 0);
316
- out.set(box, nonce.length);
317
- return bytesToBase64(out);
332
+ return bytesToBase64(encryptFile(sodium__default.default.from_string(title), key));
318
333
  }
319
334
  async function decryptCallTitle(ciphertext, key) {
320
335
  await initSodium();
321
336
  try {
322
- const raw = base64ToBytes(ciphertext);
323
- const nonce = raw.slice(0, sodium__default.default.crypto_secretbox_NONCEBYTES);
324
- const box = raw.slice(sodium__default.default.crypto_secretbox_NONCEBYTES);
325
- return sodium__default.default.to_string(sodium__default.default.crypto_secretbox_open_easy(box, nonce, key));
337
+ return sodium__default.default.to_string(decryptFile(base64ToBytes(ciphertext), key));
326
338
  } catch {
327
339
  return null;
328
340
  }
329
341
  }
330
342
  function encodeCallKey(key) {
331
- return bytesToBase64(key).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
343
+ return bytesToBase64Url(key);
332
344
  }
333
345
  function decodeCallKey(encoded) {
334
346
  if (!/^[A-Za-z0-9_-]+$/.test(encoded)) return null;
335
- const b64 = encoded.replace(/-/g, "+").replace(/_/g, "/");
336
347
  try {
337
- const key = base64ToBytes(b64 + "=".repeat((4 - b64.length % 4) % 4));
348
+ const key = base64UrlToBytes(encoded);
338
349
  return key.length === CALL_KEY_BYTES ? key : null;
339
350
  } catch {
340
351
  return null;
@@ -373,19 +384,11 @@ async function openSealedChatKey(sealedBase64, mySecretKey) {
373
384
  }
374
385
  async function encryptWithChatKey(plaintext, key) {
375
386
  await initSodium();
376
- const nonce = sodium__default.default.randombytes_buf(sodium__default.default.crypto_secretbox_NONCEBYTES);
377
- const box = sodium__default.default.crypto_secretbox_easy(sodium__default.default.from_string(plaintext), nonce, key);
378
- const out = new Uint8Array(nonce.length + box.length);
379
- out.set(nonce, 0);
380
- out.set(box, nonce.length);
381
- return bytesToBase64(out);
387
+ return bytesToBase64(encryptFile(sodium__default.default.from_string(plaintext), key));
382
388
  }
383
389
  async function decryptWithChatKey(ciphertext, key) {
384
390
  await initSodium();
385
- const raw = base64ToBytes(ciphertext);
386
- const nonce = raw.slice(0, sodium__default.default.crypto_secretbox_NONCEBYTES);
387
- const box = raw.slice(sodium__default.default.crypto_secretbox_NONCEBYTES);
388
- return sodium__default.default.to_string(sodium__default.default.crypto_secretbox_open_easy(box, nonce, key));
391
+ return sodium__default.default.to_string(decryptFile(base64ToBytes(ciphertext), key));
389
392
  }
390
393
 
391
394
  // src/crypto/keypairs.ts
@@ -433,12 +436,12 @@ function wrapIdentityKey(identitySecretKey, passwordSecretKey) {
433
436
  }
434
437
  function unwrapIdentityKey(wrapped, passwordSecretKey) {
435
438
  if (!wrapped) return passwordSecretKey;
436
- const bytes = base64Decode(wrapped);
437
- const nonce = bytes.slice(0, sodium__default.default.crypto_secretbox_NONCEBYTES);
439
+ const bytes2 = base64Decode(wrapped);
440
+ const nonce = bytes2.slice(0, sodium__default.default.crypto_secretbox_NONCEBYTES);
438
441
  const key = identityWrappingKey(passwordSecretKey);
439
442
  try {
440
443
  return sodium__default.default.crypto_secretbox_open_easy(
441
- bytes.slice(sodium__default.default.crypto_secretbox_NONCEBYTES),
444
+ bytes2.slice(sodium__default.default.crypto_secretbox_NONCEBYTES),
442
445
  nonce,
443
446
  key
444
447
  );
@@ -500,379 +503,303 @@ async function generatePasswordChangeCredentials(params, deploymentDomain) {
500
503
  }
501
504
  };
502
505
  }
503
-
504
- // src/middleware/bearer-auth.ts
505
- function createBearerAuthMiddleware(config) {
506
+ var UNENCRYPTED_RECOVERY_KEY_SIZE = 64;
507
+ var ENCRYPTED_RECOVERY_KEY_SIZE = 104;
508
+ var RECOVERY_KEY_ERRORS = {
509
+ INVALID_SIZE: `Invalid recovery key: file must be ${UNENCRYPTED_RECOVERY_KEY_SIZE} or ${ENCRYPTED_RECOVERY_KEY_SIZE} bytes`,
510
+ INVALID_FORMAT: "Invalid recovery key format",
511
+ READ_ERROR: "Failed to read recovery key file",
512
+ AUTH_FAILED: "Recovery key does not match this account",
513
+ ENCRYPTED_KEY_REQUIRES_SSO: 'This recovery key is encrypted with your SSO identity. Please use SSO login and then click "Forgot Password" to use this key.',
514
+ DECRYPTION_FAILED: "Failed to decrypt recovery key. This key may belong to a different SSO account."
515
+ };
516
+ function assertSigningKey(signingPrivateKey) {
517
+ if (signingPrivateKey.length !== UNENCRYPTED_RECOVERY_KEY_SIZE) {
518
+ throw new Error("Invalid signing key: must be exactly 64 bytes");
519
+ }
520
+ }
521
+ function ssoWrappingKey(auth0Sub) {
522
+ return sodium__default.default.crypto_hash_sha256(sodium__default.default.from_string(auth0Sub));
523
+ }
524
+ async function encryptRecoveryKey(signingPrivateKey, auth0Sub) {
525
+ await initSodium();
526
+ assertSigningKey(signingPrivateKey);
527
+ return encryptFile(signingPrivateKey, ssoWrappingKey(auth0Sub));
528
+ }
529
+ async function decryptRecoveryKey(encryptedKey, auth0Sub) {
530
+ await initSodium();
531
+ if (encryptedKey.length !== ENCRYPTED_RECOVERY_KEY_SIZE) {
532
+ throw new Error(`Invalid encrypted key: expected ${ENCRYPTED_RECOVERY_KEY_SIZE} bytes`);
533
+ }
534
+ try {
535
+ return decryptFile(encryptedKey, ssoWrappingKey(auth0Sub));
536
+ } catch {
537
+ throw new Error(RECOVERY_KEY_ERRORS.DECRYPTION_FAILED);
538
+ }
539
+ }
540
+ function isEncryptedRecoveryKey(byteLength) {
541
+ return byteLength === ENCRYPTED_RECOVERY_KEY_SIZE;
542
+ }
543
+ function getRecoveryKeyFilename() {
544
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
545
+ return `arbi-recovery-${timestamp}.html`;
546
+ }
547
+ function buildRecoveryUrl(args) {
548
+ const params = new URLSearchParams({ email: args.email });
549
+ if (args.sso) params.set("sso", "1");
550
+ return `${args.origin}/recover?${params.toString()}#KEY=${bytesToBase64Url(args.keyBytes)}`;
551
+ }
552
+ function parseRecoveryUrl(search, hash) {
553
+ const params = new URLSearchParams(search);
554
+ const email = params.get("email");
555
+ const keyMatch = /(?:^#|[&#])KEY=([A-Za-z0-9_-]+)/.exec(hash);
556
+ if (!email || !keyMatch) return null;
557
+ try {
558
+ return {
559
+ email,
560
+ recoveryKey: base64UrlToBytes(keyMatch[1]),
561
+ isEncrypted: params.get("sso") === "1"
562
+ };
563
+ } catch {
564
+ return null;
565
+ }
566
+ }
567
+ function escapeHtml(s) {
568
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
569
+ }
570
+ function buildRecoveryHtml(args) {
571
+ const email = escapeHtml(args.email);
572
+ const url = escapeHtml(args.url);
573
+ const ssoNote = args.ssoEncrypted ? '<p class="note">This recovery file is encrypted with your SSO identity \u2014 opening it on a different account will not work.</p>' : "";
574
+ return `<!DOCTYPE html>
575
+ <html lang="en">
576
+ <head>
577
+ <meta charset="utf-8">
578
+ <title>ARBI Recovery</title>
579
+ <meta name="viewport" content="width=device-width,initial-scale=1">
580
+ <style>
581
+ :root { color-scheme: light dark; }
582
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
583
+ max-width: 32rem; margin: 4rem auto; padding: 1.5rem; line-height: 1.5; }
584
+ h1 { font-size: 1.25rem; margin-bottom: 0.25rem; }
585
+ .email { font-weight: 600; }
586
+ .btn { display: inline-block; padding: 0.75rem 1.25rem; margin-top: 1.5rem;
587
+ background: #2563eb; color: #fff; text-decoration: none;
588
+ border-radius: 0.5rem; font-weight: 600; }
589
+ .btn:hover { background: #1d4ed8; }
590
+ .note { font-size: 0.85rem; color: #6b7280; margin-top: 1.5rem; }
591
+ details { margin-top: 1rem; font-size: 0.85rem; color: #6b7280; }
592
+ code { word-break: break-all; }
593
+ </style>
594
+ </head>
595
+ <body>
596
+ <h1>ARBI account recovery</h1>
597
+ <p>Click below to set a new password for <span class="email">${email}</span>.</p>
598
+ <p><a class="btn" href="${url}">Recover account</a></p>
599
+ ${ssoNote}
600
+ <details>
601
+ <summary>If the button doesn&apos;t work, open this URL manually</summary>
602
+ <p><code>${url}</code></p>
603
+ </details>
604
+ </body>
605
+ </html>
606
+ `;
607
+ }
608
+ async function createRecoveryUrl(params) {
609
+ assertSigningKey(params.signingPrivateKey);
610
+ const keyBytes = params.auth0Sub ? await encryptRecoveryKey(params.signingPrivateKey, params.auth0Sub) : params.signingPrivateKey;
611
+ return buildRecoveryUrl({
612
+ origin: params.origin,
613
+ email: params.email,
614
+ keyBytes,
615
+ sso: !!params.auth0Sub
616
+ });
617
+ }
618
+ async function createRecoveryFile(params) {
619
+ const url = await createRecoveryUrl(params);
506
620
  return {
507
- async onRequest({ request }) {
508
- const accessToken = config.tokenProvider.getAccessToken();
509
- if (accessToken) {
510
- request.headers.set("Authorization", `Bearer ${accessToken}`);
511
- }
512
- return request;
513
- }
621
+ filename: getRecoveryKeyFilename(),
622
+ html: buildRecoveryHtml({ email: params.email, url, ssoEncrypted: !!params.auth0Sub }),
623
+ url
514
624
  };
515
625
  }
516
-
517
- // src/middleware/auto-relogin.ts
518
- var NO_ACTIVE_WORKSPACE = /no active workspace/i;
519
- async function isNoActiveWorkspace(response) {
626
+ async function validateRecoveryKeyFile(file) {
627
+ if (file.size !== UNENCRYPTED_RECOVERY_KEY_SIZE && file.size !== ENCRYPTED_RECOVERY_KEY_SIZE) {
628
+ return {
629
+ valid: false,
630
+ isEncrypted: false,
631
+ data: null,
632
+ error: RECOVERY_KEY_ERRORS.INVALID_SIZE
633
+ };
634
+ }
520
635
  try {
521
- return NO_ACTIVE_WORKSPACE.test(await response.clone().text());
636
+ const data = new Uint8Array(await file.arrayBuffer());
637
+ return { valid: true, isEncrypted: isEncryptedRecoveryKey(data.length), data };
522
638
  } catch {
523
- return false;
639
+ return { valid: false, isEncrypted: false, data: null, error: RECOVERY_KEY_ERRORS.READ_ERROR };
524
640
  }
525
641
  }
526
- function safeClone(request) {
642
+ var SECURE_MAIL_KEY_BYTES = 32;
643
+ async function sealSecureMail(raw, metadata, senderPublicKey) {
644
+ await initSodium();
645
+ const key = sodium__default.default.randombytes_buf(SECURE_MAIL_KEY_BYTES);
527
646
  try {
528
- return request.clone();
647
+ return {
648
+ ciphertext: bytesToBase64(encryptFile(raw, key)),
649
+ metadata_ciphertext: bytesToBase64(
650
+ encryptFile(sodium__default.default.from_string(JSON.stringify(metadata)), key)
651
+ ),
652
+ fragment_key: bytesToBase64(key),
653
+ sender_wrapped_key: sealedBoxEncrypt(key, senderPublicKey)
654
+ };
655
+ } finally {
656
+ key.fill(0);
657
+ }
658
+ }
659
+ function decodeSecureMailKey(fragmentKey) {
660
+ let key;
661
+ try {
662
+ key = base64ToBytes(fragmentKey);
529
663
  } catch {
530
- return null;
664
+ throw new Error("Invalid secure message link");
665
+ }
666
+ if (key.length !== SECURE_MAIL_KEY_BYTES) {
667
+ key.fill(0);
668
+ throw new Error("Invalid secure message link");
531
669
  }
670
+ return key;
532
671
  }
533
- function createAutoReloginMiddleware(config) {
534
- const cloneCache = /* @__PURE__ */ new WeakMap();
672
+ async function openSecureMail(payload, key) {
673
+ await initSodium();
674
+ const raw = decryptFile(base64ToBytes(payload.ciphertext), key);
675
+ const meta = decryptFile(base64ToBytes(payload.metadata_ciphertext), key);
676
+ try {
677
+ return { raw, metadata: JSON.parse(sodium__default.default.to_string(meta)) };
678
+ } finally {
679
+ meta.fill(0);
680
+ }
681
+ }
682
+ async function secureMailClaim(key, recipientPublicKey) {
683
+ await initSodium();
535
684
  return {
536
- async onRequest({ request }) {
537
- try {
538
- cloneCache.set(request, request.clone());
539
- } catch {
685
+ claim_hash: sodium__default.default.to_hex(sodium__default.default.crypto_hash_sha256(key)),
686
+ wrapped_key: sealedBoxEncrypt(key, recipientPublicKey),
687
+ recipient_public_key: bytesToBase64(recipientPublicKey)
688
+ };
689
+ }
690
+
691
+ // src/storage/indexed-db.ts
692
+ var DB_NAME = "arbi-crypto";
693
+ var DB_VERSION = 2;
694
+ var db = null;
695
+ var memorySession = null;
696
+ function canPersistSession() {
697
+ return !!crypto?.subtle;
698
+ }
699
+ async function initializeDatabase() {
700
+ return new Promise((resolve, reject) => {
701
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
702
+ request.onerror = () => {
703
+ console.error("[STORAGE] Failed to open IndexedDB:", request.error);
704
+ reject(request.error);
705
+ };
706
+ request.onsuccess = () => {
707
+ db = request.result;
708
+ resolve(db);
709
+ };
710
+ request.onupgradeneeded = (event) => {
711
+ const database = event.target.result;
712
+ if (!database.objectStoreNames.contains("session")) {
713
+ database.createObjectStore("session", { keyPath: "id" });
540
714
  }
541
- },
542
- async onResponse({ response, request }) {
543
- if (response.status === 400 && await isNoActiveWorkspace(response)) {
544
- let token = null;
545
- let recovered = await config.reopenWorkspaceHandler?.() ?? false;
546
- if (!recovered) {
547
- token = await config.reloginHandler();
548
- recovered = token !== null;
549
- }
550
- if (!recovered) {
551
- config.onReloginFailed?.();
552
- return response;
553
- }
554
- const retry = cloneCache.get(request) ?? safeClone(request);
555
- if (retry) {
556
- if (token) retry.headers.set("Authorization", `Bearer ${token}`);
557
- return fetch(retry);
558
- }
559
- config.onRetryCloneFailed?.();
560
- return response;
715
+ if (!database.objectStoreNames.contains("wrapping-key")) {
716
+ database.createObjectStore("wrapping-key", { keyPath: "id" });
561
717
  }
562
- if (response.status === 400) {
563
- const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
564
- if (isSafeMethod) {
565
- const newToken = await config.reloginHandler();
566
- if (newToken) {
567
- const retryRequest = cloneCache.get(request) ?? request.clone();
568
- retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
569
- return fetch(retryRequest);
570
- }
718
+ };
719
+ });
720
+ }
721
+ async function ensureDatabase() {
722
+ if (!db) {
723
+ await initializeDatabase();
724
+ }
725
+ }
726
+ async function getOrCreateWrappingKey() {
727
+ await ensureDatabase();
728
+ if (!db) {
729
+ throw new Error("Database not initialized");
730
+ }
731
+ return new Promise((resolve, reject) => {
732
+ if (!db) {
733
+ reject(new Error("Database not initialized"));
734
+ return;
735
+ }
736
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
737
+ const store = transaction.objectStore("wrapping-key");
738
+ const request = store.get("master");
739
+ request.onsuccess = () => {
740
+ const result = request.result;
741
+ if (result?.key) {
742
+ resolve(result.key);
743
+ } else {
744
+ if (!crypto?.subtle) {
745
+ reject(
746
+ new Error(
747
+ "Web Crypto API not available. Ensure the page is served over HTTPS or localhost."
748
+ )
749
+ );
750
+ return;
571
751
  }
572
- }
573
- if (response.status === 401) {
574
- const excludePatterns = config.reloginExcludePatterns ?? [];
575
- const isExcluded = excludePatterns.some((pattern) => request.url.includes(pattern));
576
- if (!isExcluded) {
577
- const newToken = await config.reloginHandler();
578
- if (newToken) {
579
- const cached = cloneCache.get(request);
580
- if (cached) {
581
- cached.headers.set("Authorization", `Bearer ${newToken}`);
582
- return fetch(cached);
583
- }
584
- try {
585
- const retryRequest = request.clone();
586
- retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
587
- return fetch(retryRequest);
588
- } catch {
589
- console.warn("[API] Could not retry request after re-login (body already consumed)");
590
- config.onRetryCloneFailed?.();
591
- }
592
- } else {
593
- config.onReloginFailed?.();
752
+ crypto.subtle.generateKey(
753
+ {
754
+ name: "AES-GCM",
755
+ length: 256
756
+ },
757
+ false,
758
+ // ❌ NON-EXTRACTABLE - Can't be exported!
759
+ ["encrypt", "decrypt"]
760
+ ).then((key) => {
761
+ if (!db) {
762
+ reject(new Error("Database not initialized"));
763
+ return;
594
764
  }
595
- }
765
+ const putTransaction = db.transaction(["wrapping-key"], "readwrite");
766
+ const putStore = putTransaction.objectStore("wrapping-key");
767
+ const putRequest = putStore.put({
768
+ id: "master",
769
+ key,
770
+ created: Date.now()
771
+ });
772
+ putRequest.onsuccess = () => {
773
+ resolve(key);
774
+ };
775
+ putRequest.onerror = () => {
776
+ reject(new Error("Failed to store wrapping key"));
777
+ };
778
+ }).catch((error) => {
779
+ const errorMsg = error instanceof Error ? error.message : String(error);
780
+ reject(new Error(`Failed to generate wrapping key: ${errorMsg}`));
781
+ });
596
782
  }
597
- return response;
598
- }
599
- };
600
- }
601
-
602
- // src/relogin/handler.ts
603
- var RELOGIN_COOLDOWN_MS = 5e3;
604
- function createReloginHandler(deps) {
605
- let reloginPromise = null;
606
- let lastSuccessToken = null;
607
- let lastSuccessTime = 0;
608
- return function instantReLogin() {
609
- if (reloginPromise) {
610
- return reloginPromise;
611
- }
612
- const elapsed = Date.now() - lastSuccessTime;
613
- if (lastSuccessToken && elapsed < RELOGIN_COOLDOWN_MS) {
614
- return Promise.resolve(lastSuccessToken);
615
- }
616
- const userEmail = deps.authState.getUserEmail();
617
- if (!userEmail) {
618
- console.warn("[API] Re-login aborted: no user email");
619
- return Promise.resolve(null);
620
- }
621
- reloginPromise = (async () => {
622
- try {
623
- await deps.crypto.ensureReady();
624
- const session = await deps.sessionStorage.getSession();
625
- if (!session) {
626
- console.warn("[API] Re-login aborted: no session");
627
- return null;
628
- }
629
- const ed25519PublicKey = session.signingPrivateKey.slice(32, 64);
630
- const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
631
- publicKey: ed25519PublicKey,
632
- secretKey: session.signingPrivateKey
633
- });
634
- const timestamp = Math.floor(Date.now() / 1e3);
635
- const message = `${userEmail}|${timestamp}`;
636
- const signature = deps.crypto.signMessage(message, session.signingPrivateKey);
637
- const ssoState = deps.authState.getSsoState();
638
- const isSsoAccount = ssoState.isSsoMode || ssoState.isAuth0Authenticated || !!ssoState.auth0AccessToken;
639
- let ssoToken = null;
640
- if (deps.ssoTokenProvider) {
641
- try {
642
- ssoToken = await deps.ssoTokenProvider.getToken();
643
- } catch (error) {
644
- console.error("[API] Failed to get SSO token from provider:", error);
645
- }
646
- }
647
- if (!ssoToken && ssoState.auth0AccessToken) {
648
- ssoToken = ssoState.auth0AccessToken;
649
- }
650
- if (!ssoToken && isSsoAccount) {
651
- console.warn("[API] Re-login aborted: SSO account but no token available");
652
- return null;
653
- }
654
- const loginResult = await deps.loginProvider.login({
655
- email: userEmail,
656
- signature,
657
- timestamp,
658
- ssoToken: ssoToken ?? void 0
659
- });
660
- if (!loginResult) {
661
- console.warn("[API] Re-login failed");
662
- return null;
663
- }
664
- await deps.sessionStorage.saveSession({
665
- signingPrivateKey: session.signingPrivateKey,
666
- serverSessionKey: loginResult.sessionKey,
667
- userExtId: loginResult.userExtId,
668
- wrappedIdentityKey: loginResult.wrappedIdentityKey ?? null
669
- });
670
- await deps.onReloginSuccess?.({
671
- email: userEmail,
672
- accessToken: loginResult.accessToken,
673
- userExtId: loginResult.userExtId,
674
- serverSessionKey: loginResult.sessionKey
675
- });
676
- const activeToken = loginResult.accessToken;
677
- deps.authState.setAccessToken(activeToken);
678
- const selectedWorkspaceId = deps.authState.getSelectedWorkspaceId();
679
- if (selectedWorkspaceId) {
680
- try {
681
- const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
682
- loginResult.accessToken,
683
- selectedWorkspaceId
684
- );
685
- if (wrappedKey) {
686
- const workspaceKey = deps.crypto.sealedBoxDecrypt(
687
- wrappedKey,
688
- encryptionKeyPair.secretKey
689
- );
690
- const encryptedWorkspaceKey = await deps.crypto.sealKeyForSession(
691
- workspaceKey,
692
- loginResult.sessionKey
693
- );
694
- if (deps.workspaceOpenProvider) {
695
- await deps.workspaceOpenProvider.openWorkspace(
696
- loginResult.accessToken,
697
- selectedWorkspaceId,
698
- encryptedWorkspaceKey
699
- );
700
- }
701
- }
702
- } catch (error) {
703
- console.error("[API] Failed to re-open workspace after re-login:", error);
704
- }
705
- }
706
- console.info("[API] Re-login successful");
707
- lastSuccessToken = activeToken;
708
- lastSuccessTime = Date.now();
709
- return activeToken;
710
- } catch (error) {
711
- console.error("[API] Re-login error:", error);
712
- lastSuccessToken = null;
713
- lastSuccessTime = 0;
714
- return null;
715
- } finally {
716
- reloginPromise = null;
717
- }
718
- })();
719
- return reloginPromise;
720
- };
721
- }
722
-
723
- // src/relogin/reopen.ts
724
- function createWorkspaceReopener(deps) {
725
- let inFlight = null;
726
- return function reopenWorkspace() {
727
- if (inFlight) return inFlight;
728
- inFlight = (async () => {
729
- try {
730
- const workspaceId = deps.authState.getSelectedWorkspaceId();
731
- const accessToken = deps.getAccessToken();
732
- if (!workspaceId || !accessToken) return false;
733
- const session = await deps.sessionStorage.getSession();
734
- if (!session?.signingPrivateKey || !session.serverSessionKey) return false;
735
- await deps.crypto.ensureReady();
736
- const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
737
- accessToken,
738
- workspaceId
739
- );
740
- if (!wrappedKey) return false;
741
- const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
742
- publicKey: session.signingPrivateKey.slice(32, 64),
743
- secretKey: session.signingPrivateKey
744
- });
745
- const workspaceKey = deps.crypto.sealedBoxDecrypt(wrappedKey, encryptionKeyPair.secretKey);
746
- const sealed = await deps.crypto.sealKeyForSession(workspaceKey, session.serverSessionKey);
747
- const opened = await deps.workspaceOpenProvider.openWorkspace(
748
- accessToken,
749
- workspaceId,
750
- sealed
751
- );
752
- return opened === true;
753
- } catch (error) {
754
- console.error("[API] Failed to re-open workspace:", error);
755
- return false;
756
- } finally {
757
- inFlight = null;
758
- }
759
- })();
760
- return inFlight;
761
- };
762
- }
763
-
764
- // src/storage/indexed-db.ts
765
- var DB_NAME = "arbi-crypto";
766
- var DB_VERSION = 2;
767
- var db = null;
768
- var memorySession = null;
769
- function canPersistSession() {
770
- return !!crypto?.subtle;
771
- }
772
- async function initializeDatabase() {
773
- return new Promise((resolve, reject) => {
774
- const request = indexedDB.open(DB_NAME, DB_VERSION);
775
- request.onerror = () => {
776
- console.error("[STORAGE] Failed to open IndexedDB:", request.error);
777
- reject(request.error);
778
- };
779
- request.onsuccess = () => {
780
- db = request.result;
781
- resolve(db);
782
- };
783
- request.onupgradeneeded = (event) => {
784
- const database = event.target.result;
785
- if (!database.objectStoreNames.contains("session")) {
786
- database.createObjectStore("session", { keyPath: "id" });
787
- }
788
- if (!database.objectStoreNames.contains("wrapping-key")) {
789
- database.createObjectStore("wrapping-key", { keyPath: "id" });
790
- }
791
- };
792
- });
793
- }
794
- async function ensureDatabase() {
795
- if (!db) {
796
- await initializeDatabase();
797
- }
798
- }
799
- async function getOrCreateWrappingKey() {
800
- await ensureDatabase();
801
- if (!db) {
802
- throw new Error("Database not initialized");
803
- }
804
- return new Promise((resolve, reject) => {
805
- if (!db) {
806
- reject(new Error("Database not initialized"));
807
- return;
808
- }
809
- const transaction = db.transaction(["wrapping-key"], "readwrite");
810
- const store = transaction.objectStore("wrapping-key");
811
- const request = store.get("master");
812
- request.onsuccess = () => {
813
- const result = request.result;
814
- if (result?.key) {
815
- resolve(result.key);
816
- } else {
817
- if (!crypto?.subtle) {
818
- reject(
819
- new Error(
820
- "Web Crypto API not available. Ensure the page is served over HTTPS or localhost."
821
- )
822
- );
823
- return;
824
- }
825
- crypto.subtle.generateKey(
826
- {
827
- name: "AES-GCM",
828
- length: 256
829
- },
830
- false,
831
- // ❌ NON-EXTRACTABLE - Can't be exported!
832
- ["encrypt", "decrypt"]
833
- ).then((key) => {
834
- if (!db) {
835
- reject(new Error("Database not initialized"));
836
- return;
837
- }
838
- const putTransaction = db.transaction(["wrapping-key"], "readwrite");
839
- const putStore = putTransaction.objectStore("wrapping-key");
840
- const putRequest = putStore.put({
841
- id: "master",
842
- key,
843
- created: Date.now()
844
- });
845
- putRequest.onsuccess = () => {
846
- resolve(key);
847
- };
848
- putRequest.onerror = () => {
849
- reject(new Error("Failed to store wrapping key"));
850
- };
851
- }).catch((error) => {
852
- const errorMsg = error instanceof Error ? error.message : String(error);
853
- reject(new Error(`Failed to generate wrapping key: ${errorMsg}`));
854
- });
855
- }
856
- };
857
- request.onerror = () => {
858
- reject(new Error("Failed to retrieve wrapping key from IndexedDB"));
859
- };
860
- });
861
- }
862
- async function encryptWithWrappingKey(data) {
863
- const wrappingKey = await getOrCreateWrappingKey();
864
- const iv = crypto.getRandomValues(new Uint8Array(12));
865
- const ciphertext = await crypto.subtle.encrypt(
866
- {
867
- name: "AES-GCM",
868
- iv
869
- },
870
- wrappingKey,
871
- data
872
- );
873
- return {
874
- ciphertext: new Uint8Array(ciphertext),
875
- iv
783
+ };
784
+ request.onerror = () => {
785
+ reject(new Error("Failed to retrieve wrapping key from IndexedDB"));
786
+ };
787
+ });
788
+ }
789
+ async function encryptWithWrappingKey(data) {
790
+ const wrappingKey = await getOrCreateWrappingKey();
791
+ const iv = crypto.getRandomValues(new Uint8Array(12));
792
+ const ciphertext = await crypto.subtle.encrypt(
793
+ {
794
+ name: "AES-GCM",
795
+ iv
796
+ },
797
+ wrappingKey,
798
+ data
799
+ );
800
+ return {
801
+ ciphertext: new Uint8Array(ciphertext),
802
+ iv
876
803
  };
877
804
  }
878
805
  async function decryptWithWrappingKey(ciphertext, iv) {
@@ -964,104 +891,427 @@ async function getSession() {
964
891
  result.serverSessionKeyIV
965
892
  );
966
893
  }
967
- resolve({
968
- signingPrivateKey,
969
- serverSessionKey,
970
- userExtId: result.userExtId,
971
- wrappedIdentityKey: result.wrappedIdentityKey ?? null
972
- });
973
- } catch (error) {
974
- console.error("[STORAGE] Failed to decrypt session data:", error);
975
- reject(error);
894
+ resolve({
895
+ signingPrivateKey,
896
+ serverSessionKey,
897
+ userExtId: result.userExtId,
898
+ wrappedIdentityKey: result.wrappedIdentityKey ?? null
899
+ });
900
+ } catch (error) {
901
+ console.error("[STORAGE] Failed to decrypt session data:", error);
902
+ reject(error);
903
+ }
904
+ } else {
905
+ resolve(null);
906
+ }
907
+ };
908
+ request.onerror = () => {
909
+ console.error("[STORAGE] Failed to get session:", request.error);
910
+ reject(request.error);
911
+ };
912
+ });
913
+ }
914
+ async function updateWrappedIdentityKey(wrappedIdentityKey) {
915
+ const existingSession = await getSession();
916
+ if (!existingSession) {
917
+ throw new Error("No existing session to update");
918
+ }
919
+ await saveSession({ ...existingSession, wrappedIdentityKey });
920
+ }
921
+ async function clearSession() {
922
+ memorySession = null;
923
+ if (!canPersistSession()) {
924
+ return;
925
+ }
926
+ await ensureDatabase();
927
+ if (!db) {
928
+ throw new Error("Database not initialized");
929
+ }
930
+ return new Promise((resolve, reject) => {
931
+ if (!db) {
932
+ reject(new Error("Database not initialized"));
933
+ return;
934
+ }
935
+ const transaction = db.transaction(["session"], "readwrite");
936
+ const store = transaction.objectStore("session");
937
+ const request = store.delete("current");
938
+ request.onsuccess = () => {
939
+ resolve();
940
+ };
941
+ request.onerror = () => {
942
+ console.error("[STORAGE] Failed to clear session:", request.error);
943
+ reject(request.error);
944
+ };
945
+ });
946
+ }
947
+ async function clearWrappingKey() {
948
+ await ensureDatabase();
949
+ if (!db) {
950
+ throw new Error("Database not initialized");
951
+ }
952
+ return new Promise((resolve, reject) => {
953
+ if (!db) {
954
+ reject(new Error("Database not initialized"));
955
+ return;
956
+ }
957
+ const transaction = db.transaction(["wrapping-key"], "readwrite");
958
+ const store = transaction.objectStore("wrapping-key");
959
+ const request = store.delete("master");
960
+ request.onsuccess = () => {
961
+ resolve();
962
+ };
963
+ request.onerror = () => {
964
+ console.error("[STORAGE] Failed to clear wrapping key:", request.error);
965
+ reject(request.error);
966
+ };
967
+ });
968
+ }
969
+ async function clearAllData() {
970
+ memorySession = null;
971
+ if (!canPersistSession()) {
972
+ return;
973
+ }
974
+ await clearSession();
975
+ await clearWrappingKey();
976
+ }
977
+ async function hasSession() {
978
+ const session = await getSession();
979
+ return session !== null;
980
+ }
981
+ async function getUserEncryptionKeypair() {
982
+ await initSodium();
983
+ const session = await getSession();
984
+ if (!session?.signingPrivateKey) {
985
+ return null;
986
+ }
987
+ return encryptionKeypairFromSigningKey(session.signingPrivateKey);
988
+ }
989
+
990
+ // src/crypto/offline-sealer.ts
991
+ function bytes(value) {
992
+ return value instanceof Uint8Array ? value : Uint8Array.from(value);
993
+ }
994
+ function createOfflineSealer(namespace, { locked, keypair: getKeypair = getUserEncryptionKeypair }) {
995
+ const ENC_LABEL = `${namespace}:enc`;
996
+ const INDEX_LABEL = `${namespace}:index`;
997
+ let cached = null;
998
+ async function keys() {
999
+ await sodium__default.default.ready;
1000
+ const keypair = await getKeypair();
1001
+ if (!keypair) throw locked();
1002
+ const publicKey = sodium__default.default.to_hex(keypair.publicKey);
1003
+ if (cached?.publicKey === publicKey) {
1004
+ keypair.secretKey.fill(0);
1005
+ return cached.keys;
1006
+ }
1007
+ const derived = {
1008
+ enc: sodium__default.default.crypto_generichash(32, ENC_LABEL, bytes(keypair.secretKey)),
1009
+ index: sodium__default.default.crypto_generichash(32, INDEX_LABEL, bytes(keypair.secretKey))
1010
+ };
1011
+ keypair.secretKey.fill(0);
1012
+ cached = { publicKey, keys: derived };
1013
+ return derived;
1014
+ }
1015
+ async function seal(plain) {
1016
+ const { enc } = await keys();
1017
+ const nonce = sodium__default.default.randombytes_buf(sodium__default.default.crypto_secretbox_NONCEBYTES);
1018
+ const box = sodium__default.default.crypto_secretbox_easy(
1019
+ typeof plain === "string" ? plain : bytes(plain),
1020
+ nonce,
1021
+ enc
1022
+ );
1023
+ const out = new Uint8Array(nonce.length + box.length);
1024
+ out.set(nonce);
1025
+ out.set(box, nonce.length);
1026
+ return out;
1027
+ }
1028
+ async function openBytes(sealed) {
1029
+ const { enc } = await keys();
1030
+ const nonceBytes = sodium__default.default.crypto_secretbox_NONCEBYTES;
1031
+ const data = bytes(sealed);
1032
+ return sodium__default.default.crypto_secretbox_open_easy(
1033
+ data.subarray(nonceBytes),
1034
+ data.subarray(0, nonceBytes),
1035
+ enc
1036
+ );
1037
+ }
1038
+ return {
1039
+ sealBytes: (plain) => seal(plain),
1040
+ openBytes,
1041
+ sealJson: (value) => seal(JSON.stringify(value)),
1042
+ async openJson(sealed) {
1043
+ return JSON.parse(sodium__default.default.to_string(await openBytes(sealed)));
1044
+ },
1045
+ async blind(...parts) {
1046
+ const { index } = await keys();
1047
+ return sodium__default.default.to_hex(sodium__default.default.crypto_generichash(16, parts.join("\0"), index));
1048
+ },
1049
+ forget() {
1050
+ cached?.keys.enc.fill(0);
1051
+ cached?.keys.index.fill(0);
1052
+ cached = null;
1053
+ }
1054
+ };
1055
+ }
1056
+
1057
+ // src/middleware/bearer-auth.ts
1058
+ function createBearerAuthMiddleware(config) {
1059
+ return {
1060
+ async onRequest({ request }) {
1061
+ const accessToken = config.tokenProvider.getAccessToken();
1062
+ if (accessToken) {
1063
+ request.headers.set("Authorization", `Bearer ${accessToken}`);
1064
+ }
1065
+ return request;
1066
+ }
1067
+ };
1068
+ }
1069
+
1070
+ // src/middleware/auto-relogin.ts
1071
+ var NO_ACTIVE_WORKSPACE = /no active workspace/i;
1072
+ async function isNoActiveWorkspace(response) {
1073
+ try {
1074
+ return NO_ACTIVE_WORKSPACE.test(await response.clone().text());
1075
+ } catch {
1076
+ return false;
1077
+ }
1078
+ }
1079
+ function safeClone(request) {
1080
+ try {
1081
+ return request.clone();
1082
+ } catch {
1083
+ return null;
1084
+ }
1085
+ }
1086
+ function createAutoReloginMiddleware(config) {
1087
+ const cloneCache = /* @__PURE__ */ new WeakMap();
1088
+ return {
1089
+ async onRequest({ request }) {
1090
+ try {
1091
+ cloneCache.set(request, request.clone());
1092
+ } catch {
1093
+ }
1094
+ },
1095
+ async onResponse({ response, request }) {
1096
+ if (response.status === 400 && await isNoActiveWorkspace(response)) {
1097
+ let token = null;
1098
+ let recovered = await config.reopenWorkspaceHandler?.() ?? false;
1099
+ if (!recovered) {
1100
+ token = await config.reloginHandler();
1101
+ recovered = token !== null;
1102
+ }
1103
+ if (!recovered) {
1104
+ config.onReloginFailed?.();
1105
+ return response;
1106
+ }
1107
+ const retry = cloneCache.get(request) ?? safeClone(request);
1108
+ if (retry) {
1109
+ if (token) retry.headers.set("Authorization", `Bearer ${token}`);
1110
+ return fetch(retry);
1111
+ }
1112
+ config.onRetryCloneFailed?.();
1113
+ return response;
1114
+ }
1115
+ if (response.status === 400) {
1116
+ const isSafeMethod = ["GET", "HEAD", "DELETE"].includes(request.method);
1117
+ if (isSafeMethod) {
1118
+ const newToken = await config.reloginHandler();
1119
+ if (newToken) {
1120
+ const retryRequest = cloneCache.get(request) ?? request.clone();
1121
+ retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
1122
+ return fetch(retryRequest);
1123
+ }
1124
+ }
1125
+ }
1126
+ if (response.status === 401) {
1127
+ const excludePatterns = config.reloginExcludePatterns ?? [];
1128
+ const isExcluded = excludePatterns.some((pattern) => request.url.includes(pattern));
1129
+ if (!isExcluded) {
1130
+ const newToken = await config.reloginHandler();
1131
+ if (newToken) {
1132
+ const cached = cloneCache.get(request);
1133
+ if (cached) {
1134
+ cached.headers.set("Authorization", `Bearer ${newToken}`);
1135
+ return fetch(cached);
1136
+ }
1137
+ try {
1138
+ const retryRequest = request.clone();
1139
+ retryRequest.headers.set("Authorization", `Bearer ${newToken}`);
1140
+ return fetch(retryRequest);
1141
+ } catch {
1142
+ console.warn("[API] Could not retry request after re-login (body already consumed)");
1143
+ config.onRetryCloneFailed?.();
1144
+ }
1145
+ } else {
1146
+ config.onReloginFailed?.();
1147
+ }
976
1148
  }
977
- } else {
978
- resolve(null);
979
1149
  }
980
- };
981
- request.onerror = () => {
982
- console.error("[STORAGE] Failed to get session:", request.error);
983
- reject(request.error);
984
- };
985
- });
986
- }
987
- async function updateWrappedIdentityKey(wrappedIdentityKey) {
988
- const existingSession = await getSession();
989
- if (!existingSession) {
990
- throw new Error("No existing session to update");
991
- }
992
- await saveSession({ ...existingSession, wrappedIdentityKey });
993
- }
994
- async function clearSession() {
995
- memorySession = null;
996
- if (!canPersistSession()) {
997
- return;
998
- }
999
- await ensureDatabase();
1000
- if (!db) {
1001
- throw new Error("Database not initialized");
1002
- }
1003
- return new Promise((resolve, reject) => {
1004
- if (!db) {
1005
- reject(new Error("Database not initialized"));
1006
- return;
1150
+ return response;
1007
1151
  }
1008
- const transaction = db.transaction(["session"], "readwrite");
1009
- const store = transaction.objectStore("session");
1010
- const request = store.delete("current");
1011
- request.onsuccess = () => {
1012
- resolve();
1013
- };
1014
- request.onerror = () => {
1015
- console.error("[STORAGE] Failed to clear session:", request.error);
1016
- reject(request.error);
1017
- };
1018
- });
1152
+ };
1019
1153
  }
1020
- async function clearWrappingKey() {
1021
- await ensureDatabase();
1022
- if (!db) {
1023
- throw new Error("Database not initialized");
1024
- }
1025
- return new Promise((resolve, reject) => {
1026
- if (!db) {
1027
- reject(new Error("Database not initialized"));
1028
- return;
1154
+
1155
+ // src/relogin/handler.ts
1156
+ var RELOGIN_COOLDOWN_MS = 5e3;
1157
+ function createReloginHandler(deps) {
1158
+ let reloginPromise = null;
1159
+ let lastSuccessToken = null;
1160
+ let lastSuccessTime = 0;
1161
+ return function instantReLogin() {
1162
+ if (reloginPromise) {
1163
+ return reloginPromise;
1029
1164
  }
1030
- const transaction = db.transaction(["wrapping-key"], "readwrite");
1031
- const store = transaction.objectStore("wrapping-key");
1032
- const request = store.delete("master");
1033
- request.onsuccess = () => {
1034
- resolve();
1035
- };
1036
- request.onerror = () => {
1037
- console.error("[STORAGE] Failed to clear wrapping key:", request.error);
1038
- reject(request.error);
1039
- };
1040
- });
1041
- }
1042
- async function clearAllData() {
1043
- memorySession = null;
1044
- if (!canPersistSession()) {
1045
- return;
1046
- }
1047
- await clearSession();
1048
- await clearWrappingKey();
1049
- }
1050
- async function hasSession() {
1051
- const session = await getSession();
1052
- return session !== null;
1165
+ const elapsed = Date.now() - lastSuccessTime;
1166
+ if (lastSuccessToken && elapsed < RELOGIN_COOLDOWN_MS) {
1167
+ return Promise.resolve(lastSuccessToken);
1168
+ }
1169
+ const userEmail = deps.authState.getUserEmail();
1170
+ if (!userEmail) {
1171
+ console.warn("[API] Re-login aborted: no user email");
1172
+ return Promise.resolve(null);
1173
+ }
1174
+ reloginPromise = (async () => {
1175
+ try {
1176
+ await deps.crypto.ensureReady();
1177
+ const session = await deps.sessionStorage.getSession();
1178
+ if (!session) {
1179
+ console.warn("[API] Re-login aborted: no session");
1180
+ return null;
1181
+ }
1182
+ const ed25519PublicKey = session.signingPrivateKey.slice(32, 64);
1183
+ const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
1184
+ publicKey: ed25519PublicKey,
1185
+ secretKey: session.signingPrivateKey
1186
+ });
1187
+ const timestamp = Math.floor(Date.now() / 1e3);
1188
+ const message = `${userEmail}|${timestamp}`;
1189
+ const signature = deps.crypto.signMessage(message, session.signingPrivateKey);
1190
+ const ssoState = deps.authState.getSsoState();
1191
+ const isSsoAccount = ssoState.isSsoMode || ssoState.isAuth0Authenticated || !!ssoState.auth0AccessToken;
1192
+ let ssoToken = null;
1193
+ if (deps.ssoTokenProvider) {
1194
+ try {
1195
+ ssoToken = await deps.ssoTokenProvider.getToken();
1196
+ } catch (error) {
1197
+ console.error("[API] Failed to get SSO token from provider:", error);
1198
+ }
1199
+ }
1200
+ if (!ssoToken && ssoState.auth0AccessToken) {
1201
+ ssoToken = ssoState.auth0AccessToken;
1202
+ }
1203
+ if (!ssoToken && isSsoAccount) {
1204
+ console.warn("[API] Re-login aborted: SSO account but no token available");
1205
+ return null;
1206
+ }
1207
+ const loginResult = await deps.loginProvider.login({
1208
+ email: userEmail,
1209
+ signature,
1210
+ timestamp,
1211
+ ssoToken: ssoToken ?? void 0
1212
+ });
1213
+ if (!loginResult) {
1214
+ console.warn("[API] Re-login failed");
1215
+ return null;
1216
+ }
1217
+ await deps.sessionStorage.saveSession({
1218
+ signingPrivateKey: session.signingPrivateKey,
1219
+ serverSessionKey: loginResult.sessionKey,
1220
+ userExtId: loginResult.userExtId,
1221
+ wrappedIdentityKey: loginResult.wrappedIdentityKey ?? null
1222
+ });
1223
+ await deps.onReloginSuccess?.({
1224
+ email: userEmail,
1225
+ accessToken: loginResult.accessToken,
1226
+ userExtId: loginResult.userExtId,
1227
+ serverSessionKey: loginResult.sessionKey
1228
+ });
1229
+ const activeToken = loginResult.accessToken;
1230
+ deps.authState.setAccessToken(activeToken);
1231
+ const selectedWorkspaceId = deps.authState.getSelectedWorkspaceId();
1232
+ if (selectedWorkspaceId) {
1233
+ try {
1234
+ const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
1235
+ loginResult.accessToken,
1236
+ selectedWorkspaceId
1237
+ );
1238
+ if (wrappedKey) {
1239
+ const workspaceKey = deps.crypto.sealedBoxDecrypt(
1240
+ wrappedKey,
1241
+ encryptionKeyPair.secretKey
1242
+ );
1243
+ const encryptedWorkspaceKey = await deps.crypto.sealKeyForSession(
1244
+ workspaceKey,
1245
+ loginResult.sessionKey
1246
+ );
1247
+ if (deps.workspaceOpenProvider) {
1248
+ await deps.workspaceOpenProvider.openWorkspace(
1249
+ loginResult.accessToken,
1250
+ selectedWorkspaceId,
1251
+ encryptedWorkspaceKey
1252
+ );
1253
+ }
1254
+ }
1255
+ } catch (error) {
1256
+ console.error("[API] Failed to re-open workspace after re-login:", error);
1257
+ }
1258
+ }
1259
+ console.info("[API] Re-login successful");
1260
+ lastSuccessToken = activeToken;
1261
+ lastSuccessTime = Date.now();
1262
+ return activeToken;
1263
+ } catch (error) {
1264
+ console.error("[API] Re-login error:", error);
1265
+ lastSuccessToken = null;
1266
+ lastSuccessTime = 0;
1267
+ return null;
1268
+ } finally {
1269
+ reloginPromise = null;
1270
+ }
1271
+ })();
1272
+ return reloginPromise;
1273
+ };
1053
1274
  }
1054
- async function getUserEncryptionKeypair() {
1055
- await initSodium();
1056
- const session = await getSession();
1057
- if (!session?.signingPrivateKey) {
1058
- return null;
1059
- }
1060
- const publicKey = session.signingPrivateKey.slice(32, 64);
1061
- return deriveEncryptionKeypairFromSigning({
1062
- publicKey,
1063
- secretKey: session.signingPrivateKey
1064
- });
1275
+
1276
+ // src/relogin/reopen.ts
1277
+ function createWorkspaceReopener(deps) {
1278
+ let inFlight = null;
1279
+ return function reopenWorkspace() {
1280
+ if (inFlight) return inFlight;
1281
+ inFlight = (async () => {
1282
+ try {
1283
+ const workspaceId = deps.authState.getSelectedWorkspaceId();
1284
+ const accessToken = deps.getAccessToken();
1285
+ if (!workspaceId || !accessToken) return false;
1286
+ const session = await deps.sessionStorage.getSession();
1287
+ if (!session?.signingPrivateKey || !session.serverSessionKey) return false;
1288
+ await deps.crypto.ensureReady();
1289
+ const wrappedKey = await deps.workspaceKeyRefreshProvider.getWrappedKey(
1290
+ accessToken,
1291
+ workspaceId
1292
+ );
1293
+ if (!wrappedKey) return false;
1294
+ const encryptionKeyPair = deps.crypto.deriveEncryptionKeypair({
1295
+ publicKey: session.signingPrivateKey.slice(32, 64),
1296
+ secretKey: session.signingPrivateKey
1297
+ });
1298
+ const workspaceKey = deps.crypto.sealedBoxDecrypt(wrappedKey, encryptionKeyPair.secretKey);
1299
+ const sealed = await deps.crypto.sealKeyForSession(workspaceKey, session.serverSessionKey);
1300
+ const opened = await deps.workspaceOpenProvider.openWorkspace(
1301
+ accessToken,
1302
+ workspaceId,
1303
+ sealed
1304
+ );
1305
+ return opened === true;
1306
+ } catch (error) {
1307
+ console.error("[API] Failed to re-open workspace:", error);
1308
+ return false;
1309
+ } finally {
1310
+ inFlight = null;
1311
+ }
1312
+ })();
1313
+ return inFlight;
1314
+ };
1065
1315
  }
1066
1316
 
1067
1317
  // src/auth/register.ts
@@ -1093,12 +1343,7 @@ function bytesEqual(a, b) {
1093
1343
  return a.length === b.length && a.every((byte, i) => byte === b[i]);
1094
1344
  }
1095
1345
  function encryptionPublicKeyOf(identitySecretKey) {
1096
- return base64Encode(
1097
- deriveEncryptionKeypairFromSigning({
1098
- publicKey: identitySecretKey.slice(32, 64),
1099
- secretKey: identitySecretKey
1100
- }).publicKey
1101
- );
1346
+ return base64Encode(encryptionKeypairFromSigningKey(identitySecretKey).publicKey);
1102
1347
  }
1103
1348
  function identityFromLoginResponse(passwordPrivateKey, response) {
1104
1349
  const identity = unwrapIdentityKey(response.wrapped_identity_key, passwordPrivateKey);
@@ -1464,12 +1709,18 @@ function isMessageType(msg, type) {
1464
1709
  exports.API_PREFIX = API_PREFIX;
1465
1710
  exports.CALL_KEY_BYTES = CALL_KEY_BYTES;
1466
1711
  exports.CHAT_KEY_BYTES = CHAT_KEY_BYTES;
1712
+ exports.ENCRYPTED_RECOVERY_KEY_SIZE = ENCRYPTED_RECOVERY_KEY_SIZE;
1713
+ exports.RECOVERY_KEY_ERRORS = RECOVERY_KEY_ERRORS;
1714
+ exports.SECURE_MAIL_KEY_BYTES = SECURE_MAIL_KEY_BYTES;
1715
+ exports.UNENCRYPTED_RECOVERY_KEY_SIZE = UNENCRYPTED_RECOVERY_KEY_SIZE;
1467
1716
  exports.base64Decode = base64Decode;
1468
1717
  exports.base64Encode = base64Encode;
1469
1718
  exports.base64ToBytes = base64ToBytes;
1719
+ exports.base64UrlToBytes = base64UrlToBytes;
1470
1720
  exports.buildCallFragment = buildCallFragment;
1471
1721
  exports.buildWebSocketUrl = buildWebSocketUrl;
1472
1722
  exports.bytesToBase64 = bytesToBase64;
1723
+ exports.bytesToBase64Url = bytesToBase64Url;
1473
1724
  exports.canPersistSession = canPersistSession;
1474
1725
  exports.changePassword = changePassword;
1475
1726
  exports.clearAllData = clearAllData;
@@ -1480,14 +1731,19 @@ exports.createAuthMessage = createAuthMessage;
1480
1731
  exports.createAutoReloginMiddleware = createAutoReloginMiddleware;
1481
1732
  exports.createBearerAuthMiddleware = createBearerAuthMiddleware;
1482
1733
  exports.createContentHash = createContentHash;
1734
+ exports.createOfflineSealer = createOfflineSealer;
1735
+ exports.createRecoveryFile = createRecoveryFile;
1736
+ exports.createRecoveryUrl = createRecoveryUrl;
1483
1737
  exports.createReloginHandler = createReloginHandler;
1484
1738
  exports.createSessionManager = createSessionManager;
1485
1739
  exports.createWorkspaceReopener = createWorkspaceReopener;
1486
1740
  exports.decodeCallKey = decodeCallKey;
1741
+ exports.decodeSecureMailKey = decodeSecureMailKey;
1487
1742
  exports.decryptCallTitle = decryptCallTitle;
1488
1743
  exports.decryptFile = decryptFile;
1489
1744
  exports.decryptMessage = decryptMessage;
1490
1745
  exports.decryptMessageWithSharedSecret = decryptMessageWithSharedSecret;
1746
+ exports.decryptRecoveryKey = decryptRecoveryKey;
1491
1747
  exports.decryptWithChatKey = decryptWithChatKey;
1492
1748
  exports.deriveDirectChatKey = deriveDirectChatKey;
1493
1749
  exports.deriveEncryptionKeypairFromSigning = deriveEncryptionKeypairFromSigning;
@@ -1497,7 +1753,9 @@ exports.encryptCallTitle = encryptCallTitle;
1497
1753
  exports.encryptFile = encryptFile;
1498
1754
  exports.encryptMessage = encryptMessage;
1499
1755
  exports.encryptMessageWithSharedSecret = encryptMessageWithSharedSecret;
1756
+ exports.encryptRecoveryKey = encryptRecoveryKey;
1500
1757
  exports.encryptWithChatKey = encryptWithChatKey;
1758
+ exports.encryptionKeypairFromSigningKey = encryptionKeypairFromSigningKey;
1501
1759
  exports.generateCallKey = generateCallKey;
1502
1760
  exports.generateChatKey = generateChatKey;
1503
1761
  exports.generateIdentityKeypair = generateIdentityKeypair;
@@ -1508,16 +1766,20 @@ exports.generatePasswordChangeCredentials = generatePasswordChangeCredentials;
1508
1766
  exports.generateRandomSigningKeypair = generateRandomSigningKeypair;
1509
1767
  exports.generateRegistrationCredentials = generateRegistrationCredentials;
1510
1768
  exports.generateUserKeypairs = generateUserKeypairs;
1769
+ exports.getRecoveryKeyFilename = getRecoveryKeyFilename;
1511
1770
  exports.getSession = getSession;
1512
1771
  exports.getUserEncryptionKeypair = getUserEncryptionKeypair;
1513
1772
  exports.hasSession = hasSession;
1514
1773
  exports.identityFromLoginResponse = identityFromLoginResponse;
1515
1774
  exports.initSodium = initSodium;
1516
1775
  exports.initializeDatabase = initializeDatabase;
1776
+ exports.isEncryptedRecoveryKey = isEncryptedRecoveryKey;
1517
1777
  exports.isMessageType = isMessageType;
1518
1778
  exports.openSealedCallKey = openSealedCallKey;
1519
1779
  exports.openSealedChatKey = openSealedChatKey;
1780
+ exports.openSecureMail = openSecureMail;
1520
1781
  exports.parseCallFragment = parseCallFragment;
1782
+ exports.parseRecoveryUrl = parseRecoveryUrl;
1521
1783
  exports.parseServerMessage = parseServerMessage;
1522
1784
  exports.passwordChangeErrorMessage = passwordChangeErrorMessage;
1523
1785
  exports.prewarmKeyPairs = prewarmKeyPairs;
@@ -1525,12 +1787,16 @@ exports.saveSession = saveSession;
1525
1787
  exports.sealCallKey = sealCallKey;
1526
1788
  exports.sealChatKey = sealChatKey;
1527
1789
  exports.sealKeyForSession = sealKeyForSession;
1790
+ exports.sealSecureMail = sealSecureMail;
1528
1791
  exports.sealedBoxDecrypt = sealedBoxDecrypt;
1529
1792
  exports.sealedBoxEncrypt = sealedBoxEncrypt;
1793
+ exports.secureMailClaim = secureMailClaim;
1530
1794
  exports.signMessage = signMessage;
1531
1795
  exports.unlockIdentityWithPassword = unlockIdentityWithPassword;
1532
1796
  exports.unwrapIdentityKey = unwrapIdentityKey;
1797
+ exports.unwrapWorkspaceKey = unwrapWorkspaceKey;
1533
1798
  exports.updateWrappedIdentityKey = updateWrappedIdentityKey;
1799
+ exports.validateRecoveryKeyFile = validateRecoveryKeyFile;
1534
1800
  exports.wrapIdentityKey = wrapIdentityKey;
1535
1801
  //# sourceMappingURL=index.cjs.map
1536
1802
  //# sourceMappingURL=index.cjs.map