@arbidocs/client 0.4.128 → 0.4.130
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 +750 -484
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +228 -3
- package/dist/index.d.ts +228 -3
- package/dist/index.js +730 -485
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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(
|
|
104
|
-
return sodium__default.default.to_base64(
|
|
103
|
+
function bytesToBase64(bytes2) {
|
|
104
|
+
return sodium__default.default.to_base64(bytes2, sodium__default.default.base64_variants.ORIGINAL);
|
|
105
105
|
}
|
|
106
|
-
function
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
437
|
-
const nonce =
|
|
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
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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'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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
534
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|