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