@atlastodo/client-core 0.1.12 → 0.1.14-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +26 -20
- package/dist/crypto/envelope.d.ts +4 -4
- package/dist/crypto/field.d.ts +52 -9
- package/dist/crypto/index.d.ts +1 -0
- package/dist/crypto/invitePreview.d.ts +23 -0
- package/dist/fieldPolicy.d.ts +37 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +650 -108
- package/dist/legacy.d.ts +46 -0
- package/dist/realtime.d.ts +3 -2
- package/dist/scope.d.ts +1 -1
- package/dist/store.d.ts +22 -0
- package/dist/sync-client.d.ts +17 -0
- package/dist/types.d.ts +7 -1
- package/dist/wire.d.ts +100 -0
- package/package.json +1 -1
- package/test-vectors/envelope_vectors.json +171 -2
- package/test-vectors/field_policy_vectors.json +335 -38
package/dist/index.js
CHANGED
|
@@ -496,6 +496,9 @@ function safetyNumberGroups(number) {
|
|
|
496
496
|
return number.match(/\d{5}/g) ?? [];
|
|
497
497
|
}
|
|
498
498
|
// src/crypto/field.ts
|
|
499
|
+
import { hkdf as hkdf4 } from "@noble/hashes/hkdf.js";
|
|
500
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
501
|
+
import { sha256 as sha2564 } from "@noble/hashes/sha2.js";
|
|
499
502
|
var FIELD_ENVELOPE_VERSION = 2;
|
|
500
503
|
function sameKeyScope(a, b) {
|
|
501
504
|
if (a.kind !== b.kind)
|
|
@@ -516,16 +519,16 @@ function fieldAad(at, scope, kid) {
|
|
|
516
519
|
kid
|
|
517
520
|
]));
|
|
518
521
|
}
|
|
519
|
-
function paddedLength(n) {
|
|
522
|
+
function paddedLength(n, min = 32) {
|
|
520
523
|
const needed = n + 1;
|
|
521
|
-
if (needed <=
|
|
522
|
-
return
|
|
524
|
+
if (needed <= min)
|
|
525
|
+
return min;
|
|
523
526
|
if (needed <= 1024)
|
|
524
527
|
return 2 ** Math.ceil(Math.log2(needed));
|
|
525
528
|
return Math.ceil(needed / 1024) * 1024;
|
|
526
529
|
}
|
|
527
|
-
function padPlaintext(data) {
|
|
528
|
-
const out = new Uint8Array(paddedLength(data.length));
|
|
530
|
+
function padPlaintext(data, min = 32) {
|
|
531
|
+
const out = new Uint8Array(paddedLength(data.length, min));
|
|
529
532
|
out.set(data, 0);
|
|
530
533
|
out[data.length] = 128;
|
|
531
534
|
return out;
|
|
@@ -547,6 +550,49 @@ function decryptField(key, scope, at, envelope) {
|
|
|
547
550
|
const plain = decryptAesGcm(key, envelope, fieldAad(at, scope, envelope.kid));
|
|
548
551
|
return JSON.parse(bytesToUtf8(unpadPlaintext(plain)));
|
|
549
552
|
}
|
|
553
|
+
var FIELD_ENVELOPE_V3 = 3;
|
|
554
|
+
var FIELD_V3_MIN_PADDING = 64;
|
|
555
|
+
function fieldAadV3(at, scope, kid) {
|
|
556
|
+
return utf8ToBytes(JSON.stringify([
|
|
557
|
+
"atlas-field",
|
|
558
|
+
FIELD_ENVELOPE_V3,
|
|
559
|
+
at.entity,
|
|
560
|
+
at.entityId,
|
|
561
|
+
at.field,
|
|
562
|
+
keyScopeLabel(scope),
|
|
563
|
+
kid
|
|
564
|
+
]));
|
|
565
|
+
}
|
|
566
|
+
function sealField(key, kid, scope, at, sealed) {
|
|
567
|
+
const plain = padPlaintext(utf8ToBytes(JSON.stringify([sealed.kind, sealed.field, sealed.value ?? null])), FIELD_V3_MIN_PADDING);
|
|
568
|
+
const { iv, ct } = encryptAesGcm(key, plain, fieldAadV3(at, scope, kid));
|
|
569
|
+
return { __enc: 3, kid, iv, ct };
|
|
570
|
+
}
|
|
571
|
+
function openSealedField(key, scope, at, envelope) {
|
|
572
|
+
const plain = decryptAesGcm(key, envelope, fieldAadV3(at, scope, envelope.kid));
|
|
573
|
+
const parsed = JSON.parse(bytesToUtf8(unpadPlaintext(plain)));
|
|
574
|
+
if (!Array.isArray(parsed) || parsed.length !== 3 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
|
|
575
|
+
throw new Error("malformed sealed field");
|
|
576
|
+
}
|
|
577
|
+
return { kind: parsed[0], field: parsed[1], value: parsed[2] };
|
|
578
|
+
}
|
|
579
|
+
function isFieldEnvelopeV3(v) {
|
|
580
|
+
if (typeof v !== "object" || v === null)
|
|
581
|
+
return false;
|
|
582
|
+
const o = v;
|
|
583
|
+
return o.__enc === 3 && typeof o.kid === "string" && typeof o.iv === "string" && typeof o.ct === "string";
|
|
584
|
+
}
|
|
585
|
+
function fieldTagKey(key) {
|
|
586
|
+
return hkdf4(sha2564, key, undefined, utf8ToBytes("atlas-field-tag-v1"), 32);
|
|
587
|
+
}
|
|
588
|
+
var TAG_RE = /^[0-9a-f]{32}$/;
|
|
589
|
+
function fieldTag(tagKey, entityId, kind, field) {
|
|
590
|
+
const mac = hmac(sha2564, tagKey, utf8ToBytes(JSON.stringify(["atlas-field-tag", 1, entityId, kind, field])));
|
|
591
|
+
return bytesToHex(mac.subarray(0, 16));
|
|
592
|
+
}
|
|
593
|
+
function isFieldTag(name) {
|
|
594
|
+
return TAG_RE.test(name);
|
|
595
|
+
}
|
|
550
596
|
function isFieldEnvelopeV2(v) {
|
|
551
597
|
if (typeof v !== "object" || v === null)
|
|
552
598
|
return false;
|
|
@@ -595,17 +641,17 @@ function isEncryptedEnvelope(v) {
|
|
|
595
641
|
const o = v;
|
|
596
642
|
if (typeof o.iv !== "string" || typeof o.ct !== "string")
|
|
597
643
|
return false;
|
|
598
|
-
return o.__enc === 1 || o.__enc === 2 && typeof o.kid === "string";
|
|
644
|
+
return o.__enc === 1 || (o.__enc === 2 || o.__enc === 3) && typeof o.kid === "string";
|
|
599
645
|
}
|
|
600
646
|
// src/crypto/keyring.ts
|
|
601
|
-
import { sha256 as
|
|
647
|
+
import { sha256 as sha2565 } from "@noble/hashes/sha2.js";
|
|
602
648
|
var DEK_KEY_ID = "dek";
|
|
603
649
|
var KEY_ID_DOMAIN = utf8ToBytes("atlas-pek-id-v1");
|
|
604
650
|
function projectKeyId(pek) {
|
|
605
651
|
const input = new Uint8Array(KEY_ID_DOMAIN.length + pek.length);
|
|
606
652
|
input.set(KEY_ID_DOMAIN, 0);
|
|
607
653
|
input.set(pek, KEY_ID_DOMAIN.length);
|
|
608
|
-
return bytesToHex(
|
|
654
|
+
return bytesToHex(sha2565(input).subarray(0, 16));
|
|
609
655
|
}
|
|
610
656
|
|
|
611
657
|
class MissingScopeKey extends Error {
|
|
@@ -994,8 +1040,8 @@ class BlobOpener {
|
|
|
994
1040
|
}
|
|
995
1041
|
}
|
|
996
1042
|
// src/crypto/apiToken.ts
|
|
997
|
-
import { sha256 as
|
|
998
|
-
import { hkdf as
|
|
1043
|
+
import { sha256 as sha2566 } from "@noble/hashes/sha2.js";
|
|
1044
|
+
import { hkdf as hkdf5 } from "@noble/hashes/hkdf.js";
|
|
999
1045
|
var API_TOKEN_PREFIX = "atlas_pat_";
|
|
1000
1046
|
var ID_BYTES = 16;
|
|
1001
1047
|
var SECRET_BYTES = 32;
|
|
@@ -1036,8 +1082,8 @@ function parseApiToken(token) {
|
|
|
1036
1082
|
function deriveApiTokenKeys(parts) {
|
|
1037
1083
|
const salt = hexToBytes(parts.id.replace(/-/g, ""));
|
|
1038
1084
|
return {
|
|
1039
|
-
auth: bytesToHex(
|
|
1040
|
-
wrapKey:
|
|
1085
|
+
auth: bytesToHex(hkdf5(sha2566, parts.secret, salt, utf8ToBytes("atlas-pat-auth-v1"), 32)),
|
|
1086
|
+
wrapKey: hkdf5(sha2566, parts.secret, salt, utf8ToBytes("atlas-pat-wrap-v1"), 32)
|
|
1041
1087
|
};
|
|
1042
1088
|
}
|
|
1043
1089
|
var wrapAad = (userId, tokenId, slot) => utf8ToBytes(JSON.stringify(["atlas-pat-wrap", 1, userId.toLowerCase(), tokenId.toLowerCase(), slot]));
|
|
@@ -1096,8 +1142,8 @@ function unwrapApiTokenAppKeys(parts, userId, app, wrapped) {
|
|
|
1096
1142
|
}
|
|
1097
1143
|
// src/crypto/rootKey.ts
|
|
1098
1144
|
import { x25519 as x255193 } from "@noble/curves/ed25519.js";
|
|
1099
|
-
import { sha256 as
|
|
1100
|
-
import { hkdf as
|
|
1145
|
+
import { sha256 as sha2567 } from "@noble/hashes/sha2.js";
|
|
1146
|
+
import { hkdf as hkdf6 } from "@noble/hashes/hkdf.js";
|
|
1101
1147
|
var TODO_APP = "todo";
|
|
1102
1148
|
var JOURNAL_APP = "journal";
|
|
1103
1149
|
var KEY_BYTES = 32;
|
|
@@ -1135,7 +1181,7 @@ function wrapRootKey(root, wrappingKey, userId, slot) {
|
|
|
1135
1181
|
function unwrapRootKey(wrap, wrappingKey, userId, slot) {
|
|
1136
1182
|
return unwrapWith(wrappingKey, wrap, rootAad(userId, slot), "root key");
|
|
1137
1183
|
}
|
|
1138
|
-
var sealKeyFrom = (shared) =>
|
|
1184
|
+
var sealKeyFrom = (shared) => hkdf6(sha2567, shared, undefined, utf8ToBytes("atlas-root-seal-v1"), 32);
|
|
1139
1185
|
function sealRootKey(root, recoveryPublicKeyHex, userId) {
|
|
1140
1186
|
const ephemeral = x255193.keygen();
|
|
1141
1187
|
const shared = x255193.getSharedSecret(ephemeral.secretKey, hexToBytes(recoveryPublicKeyHex));
|
|
@@ -1170,8 +1216,8 @@ function unwrapAppPrivateKey(wrap, appKey, userId, app) {
|
|
|
1170
1216
|
return unwrapWith(appKey, wrap, appIdentityAad(userId, app), "private key");
|
|
1171
1217
|
}
|
|
1172
1218
|
// src/crypto/passkey.ts
|
|
1173
|
-
import { sha256 as
|
|
1174
|
-
import { hkdf as
|
|
1219
|
+
import { sha256 as sha2568 } from "@noble/hashes/sha2.js";
|
|
1220
|
+
import { hkdf as hkdf7 } from "@noble/hashes/hkdf.js";
|
|
1175
1221
|
|
|
1176
1222
|
// src/text.ts
|
|
1177
1223
|
function trimTrailing(text, char) {
|
|
@@ -1187,7 +1233,7 @@ function trimTrailing(text, char) {
|
|
|
1187
1233
|
var PASSKEY_PRF_LABEL = "atlas-passkey-prf-v1";
|
|
1188
1234
|
var SLOT_INFO = "atlas-passkey-slot-v1";
|
|
1189
1235
|
function passkeyPrfInput() {
|
|
1190
|
-
return
|
|
1236
|
+
return sha2568(utf8ToBytes(PASSKEY_PRF_LABEL));
|
|
1191
1237
|
}
|
|
1192
1238
|
function passkeyPrfInputBase64Url() {
|
|
1193
1239
|
return bytesToBase64Url(passkeyPrfInput());
|
|
@@ -1207,7 +1253,32 @@ function passkeySlot(credentialId) {
|
|
|
1207
1253
|
function derivePasskeySlotKey(prfOutput, credentialId) {
|
|
1208
1254
|
if (prfOutput.byteLength !== 32)
|
|
1209
1255
|
throw new Error("a PRF output is 32 bytes");
|
|
1210
|
-
return
|
|
1256
|
+
return hkdf7(sha2568, prfOutput, decodeBase64Url(credentialId), utf8ToBytes(SLOT_INFO), 32);
|
|
1257
|
+
}
|
|
1258
|
+
// src/crypto/invitePreview.ts
|
|
1259
|
+
function previewAad(projectId, kid) {
|
|
1260
|
+
return utf8ToBytes(JSON.stringify(["atlas-invite-preview", 1, projectId, kid]));
|
|
1261
|
+
}
|
|
1262
|
+
function sealInvitePreview(pek, kid, projectId, preview) {
|
|
1263
|
+
const { name, icon, color, kind } = preview;
|
|
1264
|
+
const plain = padPlaintext(utf8ToBytes(JSON.stringify({ name, icon, color, kind })), 64);
|
|
1265
|
+
const { iv, ct } = encryptAesGcm(pek, plain, previewAad(projectId, kid));
|
|
1266
|
+
return { v: 1, kid, iv, ct };
|
|
1267
|
+
}
|
|
1268
|
+
function openInvitePreview(pek, projectId, sealed) {
|
|
1269
|
+
const plain = decryptAesGcm(pek, sealed, previewAad(projectId, sealed.kid));
|
|
1270
|
+
const parsed = JSON.parse(bytesToUtf8(unpadPlaintext(plain)));
|
|
1271
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1272
|
+
throw new Error("malformed invite preview");
|
|
1273
|
+
}
|
|
1274
|
+
const { name, icon, color, kind } = parsed;
|
|
1275
|
+
return { name, icon, color, kind };
|
|
1276
|
+
}
|
|
1277
|
+
function isSealedInvitePreview(v) {
|
|
1278
|
+
if (typeof v !== "object" || v === null)
|
|
1279
|
+
return false;
|
|
1280
|
+
const o = v;
|
|
1281
|
+
return o.v === 1 && typeof o.kid === "string" && typeof o.iv === "string" && typeof o.ct === "string";
|
|
1211
1282
|
}
|
|
1212
1283
|
// src/realtime.ts
|
|
1213
1284
|
var REALTIME_BACKOFF_BASE_MS = 1000;
|
|
@@ -1227,7 +1298,7 @@ function retryAfterOf(err) {
|
|
|
1227
1298
|
const ms = err?.retryAfterMs;
|
|
1228
1299
|
return typeof ms === "number" && ms > 0 ? ms : undefined;
|
|
1229
1300
|
}
|
|
1230
|
-
var SYNC_PROTOCOL =
|
|
1301
|
+
var SYNC_PROTOCOL = 7;
|
|
1231
1302
|
function buildSyncWsUrl(baseUrl, ticket, since) {
|
|
1232
1303
|
const base = baseUrl.replace(/\/$/, "").replace(/^http/, "ws");
|
|
1233
1304
|
return `${base}/sync/ws?ticket=${encodeURIComponent(ticket)}&since=${since}&protocol=${SYNC_PROTOCOL}`;
|
|
@@ -1454,50 +1525,59 @@ function assertServerCompatible(discovery, required) {
|
|
|
1454
1525
|
}
|
|
1455
1526
|
// ../../manifests/journal.json
|
|
1456
1527
|
var journal_default = {
|
|
1457
|
-
_doc: "The Atlas Journal app manifest: daily journal entries, end-to-end encrypted. Its kinds are namespaced `journal.<kind
|
|
1528
|
+
_doc: "The Atlas Journal app manifest: daily journal entries, end-to-end encrypted. Its kinds are namespaced `journal.<kind>`; on the wire (sync protocol 7, docs/decisions/0006) every one is `journal.sealed`, with opaque field tags and the real kind inside the encrypted value. Journal v1 is personal: the server never fans its ops out to another account and reads none of their fields, so nothing is public or plaintext. `journal.entry` is one entry (date, title, body, mood, tags, …), `journal.preference` one of the app's settings. Rows written before protocol 7 carry the real kind as their wire kind and stay readable until clients migrate them. Bump `version` whenever the kinds or their fields change.",
|
|
1458
1529
|
app: "journal",
|
|
1459
1530
|
name: "Atlas Journal",
|
|
1460
|
-
version:
|
|
1531
|
+
version: 2,
|
|
1461
1532
|
kinds: {
|
|
1462
|
-
"journal.entry": {
|
|
1463
|
-
"journal.preference": {
|
|
1533
|
+
"journal.entry": { wire: "journal.sealed" },
|
|
1534
|
+
"journal.preference": { wire: "journal.sealed" }
|
|
1464
1535
|
}
|
|
1465
1536
|
};
|
|
1466
1537
|
// ../../manifests/todo.json
|
|
1467
1538
|
var todo_default = {
|
|
1468
|
-
_doc:
|
|
1539
|
+
_doc: "The Atlas Todo app manifest, compiled into the server (atlas-core) and bundled by the clients (packages/client-core); operators never load manifests from disk. `kinds` lists the entity kinds the app syncs, by their real names, which only clients see (sync protocol 7, docs/decisions/0006). `wire` is the kind the server sees instead: `todo.project` for a project, `todo.sealed` for everything else, `project_member` for the server-authored membership rows. Every field travels under an opaque per-entity tag inside an encrypted value, except `public_fields`, which keep their names because the server acts on them: a project's `deleted_at`/`archived_at` are owner-only (values still encrypted), an attachment's `blob_sha`/`thumb_sha` address its blobs. `plaintext_fields` (a subset of `public_fields`, or \"all\" for server-authored kinds) travel unencrypted. `legacy_plaintext_fields` are the fields protocol 6 sent in plaintext, under the real kind name as the wire kind; such rows stay readable until clients migrate them (POST /sync/legacy/drop). Bump `version` whenever the kinds or their fields change.",
|
|
1469
1540
|
app: "todo",
|
|
1470
1541
|
name: "Atlas Todo",
|
|
1471
|
-
version:
|
|
1542
|
+
version: 2,
|
|
1472
1543
|
kinds: {
|
|
1473
|
-
task: {
|
|
1474
|
-
project: {
|
|
1475
|
-
section: {
|
|
1476
|
-
label: {
|
|
1477
|
-
comment: {
|
|
1478
|
-
preference: {
|
|
1479
|
-
saved_filter: {
|
|
1480
|
-
reminder: {
|
|
1481
|
-
project_member: { plaintext_fields: "all" },
|
|
1482
|
-
activity: {
|
|
1483
|
-
focus_session: {
|
|
1484
|
-
habit: {
|
|
1485
|
-
habit_checkin: {
|
|
1544
|
+
task: { wire: "todo.sealed", legacy_plaintext_fields: ["project_id", "assignee_id"] },
|
|
1545
|
+
project: { wire: "todo.project", public_fields: ["deleted_at", "archived_at"] },
|
|
1546
|
+
section: { wire: "todo.sealed", legacy_plaintext_fields: ["project_id"] },
|
|
1547
|
+
label: { wire: "todo.sealed" },
|
|
1548
|
+
comment: { wire: "todo.sealed", legacy_plaintext_fields: ["task_id"] },
|
|
1549
|
+
preference: { wire: "todo.sealed" },
|
|
1550
|
+
saved_filter: { wire: "todo.sealed" },
|
|
1551
|
+
reminder: { wire: "todo.sealed" },
|
|
1552
|
+
project_member: { wire: "project_member", plaintext_fields: "all" },
|
|
1553
|
+
activity: { wire: "todo.sealed", legacy_plaintext_fields: ["task_id", "actor_id"] },
|
|
1554
|
+
focus_session: { wire: "todo.sealed" },
|
|
1555
|
+
habit: { wire: "todo.sealed" },
|
|
1556
|
+
habit_checkin: { wire: "todo.sealed" },
|
|
1486
1557
|
attachment: {
|
|
1487
|
-
|
|
1558
|
+
wire: "todo.sealed",
|
|
1559
|
+
public_fields: ["blob_sha", "thumb_sha"],
|
|
1560
|
+
plaintext_fields: ["blob_sha", "thumb_sha"],
|
|
1561
|
+
legacy_plaintext_fields: ["task_id", "blob_sha", "thumb_sha", "wrapped_key", "meta"]
|
|
1488
1562
|
}
|
|
1489
1563
|
}
|
|
1490
1564
|
};
|
|
1491
1565
|
// ../../manifests/vault.json
|
|
1492
1566
|
var vault_default = {
|
|
1493
|
-
_doc: "The Atlas Vault app manifest: a password manager, end-to-end encrypted, held to a stricter threat model than todo. Its kinds are namespaced `vault.<kind
|
|
1567
|
+
_doc: "The Atlas Vault app manifest: a password manager, end-to-end encrypted, held to a stricter threat model than todo. Its kinds are namespaced `vault.<kind>`; on the wire (sync protocol 7, docs/decisions/0006) every one is `vault.sealed`, with opaque field tags and the real kind inside the encrypted value, and rows written before protocol 7 (under the real kind) stay readable until clients migrate them. Vault v1 is personal: the server never fans its ops out to another account and reads none of their fields, so every field value is encrypted (nothing public or plaintext), including the item type, its folder and its timestamps. `vault.item` is one item (login, note, card, identity, ssh_key, api_key: name, typed fields, urls, notes, tags, folder, favorite, secret field history), `vault.folder` one folder, `vault.preference` one of the app's settings. `api_tokens` is the policy for API tokens that include the app, which the server enforces: an expiry is required, at most `max_lifetime_days` after creation, a token is read-only unless its creator opts in to writes, and it is for the vault alone (`exclusive`: no other app's key travels with a vault secret). Bump `version` whenever the kinds, their plaintext fields or the token policy change.",
|
|
1494
1568
|
app: "vault",
|
|
1495
1569
|
name: "Atlas Vault",
|
|
1496
|
-
version:
|
|
1570
|
+
version: 3,
|
|
1497
1571
|
kinds: {
|
|
1498
|
-
"vault.item": {
|
|
1499
|
-
|
|
1500
|
-
|
|
1572
|
+
"vault.item": {
|
|
1573
|
+
wire: "vault.sealed"
|
|
1574
|
+
},
|
|
1575
|
+
"vault.folder": {
|
|
1576
|
+
wire: "vault.sealed"
|
|
1577
|
+
},
|
|
1578
|
+
"vault.preference": {
|
|
1579
|
+
wire: "vault.sealed"
|
|
1580
|
+
}
|
|
1501
1581
|
},
|
|
1502
1582
|
api_tokens: {
|
|
1503
1583
|
expiry_required: true,
|
|
@@ -1560,11 +1640,18 @@ var ENTITY_KINDS = {
|
|
|
1560
1640
|
"vault.folder": true,
|
|
1561
1641
|
"vault.preference": true
|
|
1562
1642
|
};
|
|
1643
|
+
function kindSpec(kind) {
|
|
1644
|
+
return appOfKind(kind)?.kinds[kind];
|
|
1645
|
+
}
|
|
1563
1646
|
function loadTable(manifests) {
|
|
1564
1647
|
const table = {};
|
|
1565
1648
|
for (const kind of Object.keys(ENTITY_KINDS)) {
|
|
1566
1649
|
const owner = manifests.find((m) => Object.prototype.hasOwnProperty.call(m.kinds, kind));
|
|
1567
|
-
const
|
|
1650
|
+
const spec = owner?.kinds[kind];
|
|
1651
|
+
if (typeof spec?.wire !== "string") {
|
|
1652
|
+
throw new Error(`app manifests: no wire kind for "${kind}"`);
|
|
1653
|
+
}
|
|
1654
|
+
const entry = spec.plaintext_fields ?? [];
|
|
1568
1655
|
if (entry === "all")
|
|
1569
1656
|
table[kind] = "all";
|
|
1570
1657
|
else if (Array.isArray(entry) && entry.every((f) => typeof f === "string"))
|
|
@@ -1575,8 +1662,20 @@ function loadTable(manifests) {
|
|
|
1575
1662
|
return Object.freeze(table);
|
|
1576
1663
|
}
|
|
1577
1664
|
var PLAINTEXT_FIELDS = loadTable(BUNDLED_MANIFESTS);
|
|
1665
|
+
function appOfWire(name) {
|
|
1666
|
+
return appOfKind(name) ?? BUNDLED_MANIFESTS.find((m) => Object.values(m.kinds).some((k) => k.wire === name));
|
|
1667
|
+
}
|
|
1668
|
+
function wireKindOf(kind) {
|
|
1669
|
+
return kindSpec(kind)?.wire ?? null;
|
|
1670
|
+
}
|
|
1671
|
+
function isPublicField(kind, field) {
|
|
1672
|
+
const spec = kindSpec(kind);
|
|
1673
|
+
if (!spec)
|
|
1674
|
+
return false;
|
|
1675
|
+
return spec.plaintext_fields === "all" || (spec.public_fields ?? []).includes(field);
|
|
1676
|
+
}
|
|
1578
1677
|
function shouldEncryptKindField(entity, field) {
|
|
1579
|
-
const plain =
|
|
1678
|
+
const plain = kindSpec(entity)?.plaintext_fields;
|
|
1580
1679
|
if (plain === "all")
|
|
1581
1680
|
return false;
|
|
1582
1681
|
return !plain?.includes(field);
|
|
@@ -1587,6 +1686,28 @@ function shouldEncryptField(entity, field) {
|
|
|
1587
1686
|
return false;
|
|
1588
1687
|
return !plain?.includes(field);
|
|
1589
1688
|
}
|
|
1689
|
+
function legacyEncrypted(kind, field) {
|
|
1690
|
+
const spec = kindSpec(kind);
|
|
1691
|
+
if (spec?.plaintext_fields === "all")
|
|
1692
|
+
return false;
|
|
1693
|
+
return !(spec?.legacy_plaintext_fields ?? []).includes(field);
|
|
1694
|
+
}
|
|
1695
|
+
function isLegacyWireKind(name) {
|
|
1696
|
+
const spec = kindSpec(name);
|
|
1697
|
+
return spec !== undefined && spec.wire !== name;
|
|
1698
|
+
}
|
|
1699
|
+
function kindsOfWire(wire) {
|
|
1700
|
+
return Object.keys(ENTITY_KINDS).filter((k) => kindSpec(k)?.wire === wire);
|
|
1701
|
+
}
|
|
1702
|
+
function realKindOf(wire, publicField) {
|
|
1703
|
+
const kinds = kindsOfWire(wire);
|
|
1704
|
+
if (kinds.length === 1)
|
|
1705
|
+
return kinds[0];
|
|
1706
|
+
if (publicField === undefined)
|
|
1707
|
+
return null;
|
|
1708
|
+
const naming = kinds.filter((k) => isPublicField(k, publicField));
|
|
1709
|
+
return naming.length === 1 ? naming[0] : null;
|
|
1710
|
+
}
|
|
1590
1711
|
|
|
1591
1712
|
// src/scope.ts
|
|
1592
1713
|
import { sha1 } from "@noble/hashes/legacy.js";
|
|
@@ -1787,6 +1908,178 @@ function recordKeyTrustBaseline(store, keyring, userId) {
|
|
|
1787
1908
|
write(store, "keys_initialized", true);
|
|
1788
1909
|
}
|
|
1789
1910
|
|
|
1911
|
+
// src/wire.ts
|
|
1912
|
+
function isSealedPlaceholder(v) {
|
|
1913
|
+
return isFieldEnvelopeV3(v) && "scope" in v;
|
|
1914
|
+
}
|
|
1915
|
+
function isSealedWireKind(name) {
|
|
1916
|
+
return kindsOfWire(name).length > 0 && !isLegacyWireKind(name) && name !== "project_member";
|
|
1917
|
+
}
|
|
1918
|
+
var tagKeys = new WeakMap;
|
|
1919
|
+
function tagKeyOf(key) {
|
|
1920
|
+
let tagKey = tagKeys.get(key);
|
|
1921
|
+
if (!tagKey)
|
|
1922
|
+
tagKeys.set(key, tagKey = fieldTagKey(key));
|
|
1923
|
+
return tagKey;
|
|
1924
|
+
}
|
|
1925
|
+
function wireFieldOf(key, kind, entityId, field) {
|
|
1926
|
+
return isPublicField(kind, field) ? field : fieldTag(tagKeyOf(key), entityId, kind, field);
|
|
1927
|
+
}
|
|
1928
|
+
function scopeOfKey(scope) {
|
|
1929
|
+
return scope.kind === "project" ? scope.projectId : null;
|
|
1930
|
+
}
|
|
1931
|
+
function encodeOp(op, k) {
|
|
1932
|
+
const wire = wireKindOf(op.entity);
|
|
1933
|
+
if (!wire || wire === "project_member")
|
|
1934
|
+
throw new Error(`cannot push ${op.entity} ops`);
|
|
1935
|
+
const ts = { wall_ms: op.ts.wallMs, counter: op.ts.counter, node: op.ts.node };
|
|
1936
|
+
const base = { id: op.id, entity: wire, entity_id: op.entityId, scope: scopeOfKey(k.scope), ts };
|
|
1937
|
+
if (op.op === "delete")
|
|
1938
|
+
return { ...base, op: "delete" };
|
|
1939
|
+
const field = wireFieldOf(k.key, op.entity, op.entityId, op.field);
|
|
1940
|
+
const value = shouldEncryptField(op.entity, op.field) ? sealField(k.key, k.keyId, k.scope, { entity: wire, entityId: op.entityId, field }, { kind: op.entity, field: op.field, value: op.value }) : op.value;
|
|
1941
|
+
return { ...base, op: "set", field, value };
|
|
1942
|
+
}
|
|
1943
|
+
function keysFor(keyring, scope, kid) {
|
|
1944
|
+
if (scope === null) {
|
|
1945
|
+
if (kid !== DEK_KEY_ID)
|
|
1946
|
+
return [];
|
|
1947
|
+
return [{ key: keyring.getDek(), keyId: DEK_KEY_ID, scope: { kind: "personal" } }];
|
|
1948
|
+
}
|
|
1949
|
+
const key = keyring.projectKey(scope, kid);
|
|
1950
|
+
return key ? [{ key, keyId: kid, scope: { kind: "project", projectId: scope } }] : [];
|
|
1951
|
+
}
|
|
1952
|
+
function openSealed(keyring, wire, entityId, wireField, scope, envelope) {
|
|
1953
|
+
for (const k of keysFor(keyring, scope, envelope.kid)) {
|
|
1954
|
+
let sealed;
|
|
1955
|
+
try {
|
|
1956
|
+
sealed = openSealedField(k.key, k.scope, { entity: wire, entityId, field: wireField }, envelope);
|
|
1957
|
+
} catch {
|
|
1958
|
+
continue;
|
|
1959
|
+
}
|
|
1960
|
+
const kind = sealed.kind;
|
|
1961
|
+
if (wireKindOf(kind) !== wire)
|
|
1962
|
+
return null;
|
|
1963
|
+
if (wireFieldOf(k.key, kind, entityId, sealed.field) !== wireField)
|
|
1964
|
+
return null;
|
|
1965
|
+
return { kind, field: sealed.field, value: sealed.value, keyId: k.keyId, scope: k.scope };
|
|
1966
|
+
}
|
|
1967
|
+
return null;
|
|
1968
|
+
}
|
|
1969
|
+
function hlcOf(w) {
|
|
1970
|
+
return { wallMs: w.ts.wall_ms, counter: w.ts.counter, node: w.ts.node };
|
|
1971
|
+
}
|
|
1972
|
+
function decodeOps(wireOps, ctx) {
|
|
1973
|
+
const out = [];
|
|
1974
|
+
const pageKinds = new Map;
|
|
1975
|
+
const pendingDeletes = [];
|
|
1976
|
+
for (const w of wireOps) {
|
|
1977
|
+
const ts = hlcOf(w);
|
|
1978
|
+
const base = { id: w.id, entityId: w.entity_id, ts };
|
|
1979
|
+
if (isLegacyWireKind(w.entity) || w.entity === "project_member") {
|
|
1980
|
+
const kind = w.entity;
|
|
1981
|
+
pageKinds.set(w.entity_id, kind);
|
|
1982
|
+
if (w.op === "delete") {
|
|
1983
|
+
out.push({
|
|
1984
|
+
op: { ...base, entity: kind, op: "delete" },
|
|
1985
|
+
legacy: w.entity !== "project_member"
|
|
1986
|
+
});
|
|
1987
|
+
continue;
|
|
1988
|
+
}
|
|
1989
|
+
const op = { ...base, entity: kind, op: "set", field: w.field, value: w.value };
|
|
1990
|
+
if (w.entity === "project_member" || !legacyEncrypted(kind, w.field)) {
|
|
1991
|
+
out.push({ op, legacy: w.entity !== "project_member" });
|
|
1992
|
+
continue;
|
|
1993
|
+
}
|
|
1994
|
+
const opened = ctx.openLegacy(op);
|
|
1995
|
+
out.push(opened ? { op: opened, legacy: true } : null);
|
|
1996
|
+
continue;
|
|
1997
|
+
}
|
|
1998
|
+
if (w.op === "delete") {
|
|
1999
|
+
const kind = realKindOf(w.entity) ?? ctx.store.kindOf(w.entity_id);
|
|
2000
|
+
out.push({
|
|
2001
|
+
op: { ...base, entity: kind ?? w.entity, op: "delete" },
|
|
2002
|
+
legacy: false
|
|
2003
|
+
});
|
|
2004
|
+
if (!kind)
|
|
2005
|
+
pendingDeletes.push(out.length - 1);
|
|
2006
|
+
continue;
|
|
2007
|
+
}
|
|
2008
|
+
const scope = w.scope ?? null;
|
|
2009
|
+
const publicKind = isPublicFieldOfWire(w.entity, w.field) ? realKindOf(w.entity, w.field) : null;
|
|
2010
|
+
if (publicKind && !shouldEncryptField(publicKind, w.field)) {
|
|
2011
|
+
pageKinds.set(w.entity_id, publicKind);
|
|
2012
|
+
out.push({
|
|
2013
|
+
op: { ...base, entity: publicKind, op: "set", field: w.field, value: w.value },
|
|
2014
|
+
legacy: false
|
|
2015
|
+
});
|
|
2016
|
+
continue;
|
|
2017
|
+
}
|
|
2018
|
+
if (!isFieldEnvelopeV3(w.value)) {
|
|
2019
|
+
console.warn(`[atlas-e2ee] dropped a value that is no sealed envelope (${w.entity_id})`);
|
|
2020
|
+
out.push(null);
|
|
2021
|
+
continue;
|
|
2022
|
+
}
|
|
2023
|
+
const opened = ctx.keyring ? openSealed(ctx.keyring, w.entity, w.entity_id, w.field, scope, w.value) : null;
|
|
2024
|
+
if (!opened) {
|
|
2025
|
+
console.warn(`[atlas-e2ee] no key opens ${w.entity} ${w.field} (${w.entity_id})`);
|
|
2026
|
+
const placeholder = { ...w.value, scope };
|
|
2027
|
+
out.push({
|
|
2028
|
+
op: {
|
|
2029
|
+
...base,
|
|
2030
|
+
entity: w.entity,
|
|
2031
|
+
op: "set",
|
|
2032
|
+
field: w.field,
|
|
2033
|
+
value: placeholder
|
|
2034
|
+
},
|
|
2035
|
+
legacy: false
|
|
2036
|
+
});
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
if (opened.kind === "activity" && opened.field === "actor_id") {
|
|
2040
|
+
if (!w.pushed_by || opened.value !== w.pushed_by) {
|
|
2041
|
+
console.warn(`[atlas-e2ee] dropped an activity whose actor did not push it (${w.entity_id})`);
|
|
2042
|
+
out.push(null);
|
|
2043
|
+
continue;
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
pageKinds.set(w.entity_id, opened.kind);
|
|
2047
|
+
out.push({
|
|
2048
|
+
op: { ...base, entity: opened.kind, op: "set", field: opened.field, value: opened.value },
|
|
2049
|
+
opened: { keyId: opened.keyId, scope: opened.scope },
|
|
2050
|
+
legacy: false
|
|
2051
|
+
});
|
|
2052
|
+
}
|
|
2053
|
+
for (const i of pendingDeletes) {
|
|
2054
|
+
const d = out[i];
|
|
2055
|
+
const kind = pageKinds.get(d.op.entityId);
|
|
2056
|
+
if (kind)
|
|
2057
|
+
d.op = { ...d.op, entity: kind };
|
|
2058
|
+
}
|
|
2059
|
+
return out.filter((d) => d !== null);
|
|
2060
|
+
}
|
|
2061
|
+
function isPublicFieldOfWire(wire, field) {
|
|
2062
|
+
return kindsOfWire(wire).some((k) => isPublicField(k, field));
|
|
2063
|
+
}
|
|
2064
|
+
function sealedRepair(d, expected, isCanonical) {
|
|
2065
|
+
if (d.op.op !== "set" || !d.opened)
|
|
2066
|
+
return null;
|
|
2067
|
+
const { keyId, scope } = d.opened;
|
|
2068
|
+
if (expected && sameKeyScope(expected, scope) && isCanonical(keyId, expected))
|
|
2069
|
+
return null;
|
|
2070
|
+
return {
|
|
2071
|
+
entity: d.op.entity,
|
|
2072
|
+
entityId: d.op.entityId,
|
|
2073
|
+
field: d.op.field,
|
|
2074
|
+
ts: d.op.ts,
|
|
2075
|
+
value: d.op.value,
|
|
2076
|
+
reason: "key",
|
|
2077
|
+
keyId,
|
|
2078
|
+
scope,
|
|
2079
|
+
version: 3
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
|
|
1790
2083
|
// src/scope.ts
|
|
1791
2084
|
var EMPTY_SCOPE_STORE = {
|
|
1792
2085
|
list: () => [],
|
|
@@ -2049,9 +2342,9 @@ function confirmRepairs(store, keyring, userId, repairs) {
|
|
|
2049
2342
|
const desired = preferredKey(keyring, resolveScope(store, r.entity, r.entityId), shared);
|
|
2050
2343
|
if (!desired || !r.scope || !sameKeyScope(desired.scope, r.scope))
|
|
2051
2344
|
return false;
|
|
2052
|
-
if (desired.keyId === r.keyId && r.version === 2)
|
|
2345
|
+
if (desired.keyId === r.keyId && (r.version === 2 || r.version === 3))
|
|
2053
2346
|
return false;
|
|
2054
|
-
if (r.version === 2 && r.keyId !== undefined && r.scope.kind === "project" && keyring.isRetired(r.scope.projectId, r.keyId)) {
|
|
2347
|
+
if ((r.version === 2 || r.version === 3) && r.keyId !== undefined && r.scope.kind === "project" && keyring.isRetired(r.scope.projectId, r.keyId)) {
|
|
2055
2348
|
return false;
|
|
2056
2349
|
}
|
|
2057
2350
|
}
|
|
@@ -2088,8 +2381,22 @@ function reviveLockedValues(store, keyring, userId, personalV1 = true) {
|
|
|
2088
2381
|
adoptPersonalV1: expected?.kind === "project" && sole.has(expected.projectId)
|
|
2089
2382
|
});
|
|
2090
2383
|
});
|
|
2091
|
-
const
|
|
2092
|
-
|
|
2384
|
+
const sealed = store.reviveSealed((wire, entityId, wireField, placeholder) => openSealed(keyring, wire, entityId, wireField, placeholder.scope, placeholder));
|
|
2385
|
+
const repairs = confirmRepairs(store, keyring, userId, [
|
|
2386
|
+
...revived.map((r) => ({ ...r, reason: "key" })),
|
|
2387
|
+
...sealed.map((r) => ({
|
|
2388
|
+
entity: r.entity,
|
|
2389
|
+
entityId: r.entityId,
|
|
2390
|
+
field: r.field,
|
|
2391
|
+
ts: r.ts,
|
|
2392
|
+
value: r.value,
|
|
2393
|
+
reason: "key",
|
|
2394
|
+
keyId: r.keyId,
|
|
2395
|
+
scope: r.scope,
|
|
2396
|
+
version: 3
|
|
2397
|
+
}))
|
|
2398
|
+
]);
|
|
2399
|
+
return { revived: revived.length + sealed.length, repaired: applyRepairs(store, repairs) };
|
|
2093
2400
|
}
|
|
2094
2401
|
function isPayload(v) {
|
|
2095
2402
|
return typeof v === "object" && v !== null && typeof v.iv === "string" && typeof v.ct === "string";
|
|
@@ -2187,20 +2494,9 @@ class E2eeLockedError extends Error {
|
|
|
2187
2494
|
}
|
|
2188
2495
|
}
|
|
2189
2496
|
function encodeWireOp(op, keyring, key) {
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
return { ...base, op: "delete" };
|
|
2194
|
-
let value = op.value;
|
|
2195
|
-
if (shouldEncryptField(op.entity, op.field)) {
|
|
2196
|
-
if (!keyring?.hasKeys())
|
|
2197
|
-
throw new E2eeLockedError;
|
|
2198
|
-
const k = key();
|
|
2199
|
-
const at = { entity: op.entity, entityId: op.entityId, field: op.field };
|
|
2200
|
-
const encryptedValue = encryptField(k.key, k.keyId, k.scope, at, op.value);
|
|
2201
|
-
value = encryptedValue;
|
|
2202
|
-
}
|
|
2203
|
-
return { ...base, op: "set", field: op.field, value };
|
|
2497
|
+
if (!keyring?.hasKeys())
|
|
2498
|
+
throw new E2eeLockedError;
|
|
2499
|
+
return encodeOp(op, key());
|
|
2204
2500
|
}
|
|
2205
2501
|
function toEncryptedWire(op, keyring, projectId) {
|
|
2206
2502
|
return encodeWireOp(op, keyring, () => {
|
|
@@ -2214,15 +2510,25 @@ function toEncryptedWire(op, keyring, projectId) {
|
|
|
2214
2510
|
});
|
|
2215
2511
|
}
|
|
2216
2512
|
function fromEncryptedWire(w, keyring, projectId) {
|
|
2513
|
+
if (!isLegacyWireKind(w.entity) && w.entity !== "project_member") {
|
|
2514
|
+
const [d] = decodeOps([w], {
|
|
2515
|
+
keyring: keyring?.hasKeys() ? keyring : null,
|
|
2516
|
+
store: { kindOf: () => null },
|
|
2517
|
+
openLegacy: () => null
|
|
2518
|
+
});
|
|
2519
|
+
if (!d)
|
|
2520
|
+
throw new Error(`dropped ${w.entity} op ${w.id}`);
|
|
2521
|
+
return d.op;
|
|
2522
|
+
}
|
|
2217
2523
|
const ts = { wallMs: w.ts.wall_ms, counter: w.ts.counter, node: w.ts.node };
|
|
2218
2524
|
const base = { id: w.id, entity: w.entity, entityId: w.entity_id, ts };
|
|
2219
2525
|
if (w.op === "delete")
|
|
2220
2526
|
return { ...base, op: "delete" };
|
|
2221
2527
|
let value = w.value;
|
|
2222
|
-
if (keyring?.hasKeys() && isEncryptedEnvelope(value)) {
|
|
2528
|
+
if (keyring?.hasKeys() && isEncryptedEnvelope(value) && legacyEncrypted(w.entity, w.field)) {
|
|
2223
2529
|
const expected = projectId === undefined ? null : projectId === null ? { kind: "personal" } : { kind: "project", projectId };
|
|
2224
2530
|
const at = { entity: w.entity, entityId: w.entity_id, field: w.field };
|
|
2225
|
-
const opened = openFieldValue(keyring, value, at, expected, openOptionsFor(
|
|
2531
|
+
const opened = openFieldValue(keyring, value, at, expected, openOptionsFor(base.entity, w.entity_id, true));
|
|
2226
2532
|
if (opened)
|
|
2227
2533
|
value = opened.value;
|
|
2228
2534
|
else
|
|
@@ -2431,6 +2737,10 @@ class ApiClient {
|
|
|
2431
2737
|
isLegacyMigrated() {
|
|
2432
2738
|
return this.legacyDone || readKeyTrust(this.scopeStore ?? EMPTY_SCOPE_STORE).legacyMigrated;
|
|
2433
2739
|
}
|
|
2740
|
+
kindOfEntity(id) {
|
|
2741
|
+
const store = this.scopeStore;
|
|
2742
|
+
return store?.kindOf?.(id) ?? null;
|
|
2743
|
+
}
|
|
2434
2744
|
isCanonical(keyId, scope) {
|
|
2435
2745
|
if (scope.kind === "personal")
|
|
2436
2746
|
return keyId === DEK_KEY_ID;
|
|
@@ -2975,43 +3285,76 @@ class ApiClient {
|
|
|
2975
3285
|
throw new Error("malformed sync payload");
|
|
2976
3286
|
const wireOps = operations;
|
|
2977
3287
|
const store = this.scopeStore ?? EMPTY_SCOPE_STORE;
|
|
2978
|
-
const decoded = wireOps.map((w) => fromEncryptedWire(w, null));
|
|
2979
|
-
const batch = batchLinksOf(decoded);
|
|
2980
|
-
const shared = sharedProjectIds(store);
|
|
2981
3288
|
const legacy = !this.isLegacyMigrated();
|
|
2982
|
-
const
|
|
2983
|
-
const
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
3289
|
+
const legacyLinks = batchLinksOf(wireOps.filter((w) => isLegacyWireKind(w.entity)).map((w) => fromEncryptedWire(w, null)));
|
|
3290
|
+
const shared = sharedProjectIds(store);
|
|
3291
|
+
const ctx = {
|
|
3292
|
+
keyring,
|
|
3293
|
+
store: { kindOf: (id) => this.kindOfEntity(id) },
|
|
3294
|
+
openLegacy: (op) => {
|
|
3295
|
+
const at = { entity: op.entity, entityId: op.entityId, field: op.field, ts: op.ts };
|
|
3296
|
+
const trustState = op.entity === "preference" && op.entityId === KEY_TRUST_ID;
|
|
3297
|
+
if (!isEncryptedEnvelope(op.value)) {
|
|
3298
|
+
if (!legacy || trustState) {
|
|
3299
|
+
console.warn(`[atlas-e2ee] dropped plaintext in ${op.entity}.${op.field} (${op.entityId})`);
|
|
3300
|
+
return null;
|
|
3301
|
+
}
|
|
3302
|
+
return op;
|
|
2995
3303
|
}
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
3304
|
+
const expected = expectedKeyScope(keyring, resolveScope(store, op.entity, op.entityId, legacyLinks), shared);
|
|
3305
|
+
const opened = openFieldValue(keyring, op.value, at, expected, openOptionsFor(op.entity, op.entityId, legacy));
|
|
3306
|
+
if (!opened) {
|
|
3307
|
+
console.warn(`[atlas-e2ee] no key opens ${op.entity}.${op.field} (${op.entityId})`);
|
|
3308
|
+
return op;
|
|
3309
|
+
}
|
|
3310
|
+
return { ...op, value: opened.value };
|
|
2999
3311
|
}
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3312
|
+
};
|
|
3313
|
+
const decoded = decodeOps(wireOps, ctx);
|
|
3314
|
+
const batch = batchLinksOf(decoded.map((d) => d.op));
|
|
3315
|
+
const repairs = [];
|
|
3316
|
+
for (const d of decoded) {
|
|
3317
|
+
if (d.legacy || !d.opened)
|
|
3005
3318
|
continue;
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
repairs.push(
|
|
3010
|
-
}
|
|
3011
|
-
out.push({ ...op, value: opened.value });
|
|
3319
|
+
const expected = expectedKeyScope(keyring, resolveScope(store, d.op.entity, d.op.entityId, batch), shared);
|
|
3320
|
+
const repair = sealedRepair(d, expected, (keyId, scope) => this.isCanonical(keyId, scope));
|
|
3321
|
+
if (repair)
|
|
3322
|
+
repairs.push(repair);
|
|
3012
3323
|
}
|
|
3324
|
+
const out = decoded.map((d) => d.op);
|
|
3013
3325
|
return { operations: out, cursor, from: typeof from === "number" ? from : undefined, repairs };
|
|
3014
3326
|
}
|
|
3327
|
+
legacyKeys(next) {
|
|
3328
|
+
const q = next === undefined ? "" : `?next=${encodeURIComponent(next)}`;
|
|
3329
|
+
return this.request("GET", `/sync/legacy/keys${q}`);
|
|
3330
|
+
}
|
|
3331
|
+
async dropLegacy(keys) {
|
|
3332
|
+
const res = await this.request("POST", "/sync/legacy/drop", {
|
|
3333
|
+
entities: keys
|
|
3334
|
+
});
|
|
3335
|
+
return res.dropped;
|
|
3336
|
+
}
|
|
3337
|
+
mayReencode(kind, entityId) {
|
|
3338
|
+
const store = this.scopeStore;
|
|
3339
|
+
const user = this.scopeUserId;
|
|
3340
|
+
if (!store || !user)
|
|
3341
|
+
return false;
|
|
3342
|
+
if (kind === "activity" && store.rawField("activity", entityId, "actor_id") !== user) {
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
const scope = resolveScope(store, kind, entityId);
|
|
3346
|
+
if (scope.kind === "unknown")
|
|
3347
|
+
return false;
|
|
3348
|
+
if (scope.kind === "personal")
|
|
3349
|
+
return true;
|
|
3350
|
+
const rows = store.list("project_member").filter((e) => e.fields.project_id === scope.projectId && e.fields.state === "active");
|
|
3351
|
+
if (rows.length === 0)
|
|
3352
|
+
return true;
|
|
3353
|
+
const mine = rows.find((e) => e.fields.user_id === user);
|
|
3354
|
+
if (!mine)
|
|
3355
|
+
return false;
|
|
3356
|
+
return kind !== "project" || mine.fields.role === "owner";
|
|
3357
|
+
}
|
|
3015
3358
|
async syncWsUrl(since) {
|
|
3016
3359
|
if (!this.token)
|
|
3017
3360
|
return null;
|
|
@@ -3021,8 +3364,12 @@ class ApiClient {
|
|
|
3021
3364
|
listMembers(projectId) {
|
|
3022
3365
|
return this.request("GET", `/projects/${projectId}/members`);
|
|
3023
3366
|
}
|
|
3024
|
-
inviteMember(projectId, email, role) {
|
|
3025
|
-
return this.request("POST", `/projects/${projectId}/members`, {
|
|
3367
|
+
inviteMember(projectId, email, role, preview) {
|
|
3368
|
+
return this.request("POST", `/projects/${projectId}/members`, {
|
|
3369
|
+
email,
|
|
3370
|
+
role,
|
|
3371
|
+
...preview ? { preview } : {}
|
|
3372
|
+
});
|
|
3026
3373
|
}
|
|
3027
3374
|
acceptInvite(projectId) {
|
|
3028
3375
|
return this.request("POST", `/projects/${projectId}/accept`);
|
|
@@ -3816,6 +4163,8 @@ class LocalStore {
|
|
|
3816
4163
|
let state = byId.get(op.entityId);
|
|
3817
4164
|
if (!state) {
|
|
3818
4165
|
state = { fields: new Map, deleted: null, deletedOp: null };
|
|
4166
|
+
if (!isSealedWireKind(op.entity))
|
|
4167
|
+
this.inheritTombstone(op.entityId, state);
|
|
3819
4168
|
byId.set(op.entityId, state);
|
|
3820
4169
|
}
|
|
3821
4170
|
let changed;
|
|
@@ -3836,6 +4185,26 @@ class LocalStore {
|
|
|
3836
4185
|
this.kindChanged.set(op.entity, ++this.changeSeq);
|
|
3837
4186
|
return changed;
|
|
3838
4187
|
}
|
|
4188
|
+
inheritTombstone(entityId, state) {
|
|
4189
|
+
for (const [kind, byId] of this.entities) {
|
|
4190
|
+
if (!isSealedWireKind(kind))
|
|
4191
|
+
continue;
|
|
4192
|
+
const pending = byId.get(entityId);
|
|
4193
|
+
if (!pending?.deleted)
|
|
4194
|
+
continue;
|
|
4195
|
+
if (state.deleted === null || compareHlc(pending.deleted, state.deleted) > 0) {
|
|
4196
|
+
state.deleted = pending.deleted;
|
|
4197
|
+
state.deletedOp = pending.deletedOp;
|
|
4198
|
+
}
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
kindOf(entityId) {
|
|
4202
|
+
for (const [kind, byId] of this.entities) {
|
|
4203
|
+
if (!isSealedWireKind(kind) && byId.has(entityId))
|
|
4204
|
+
return kind;
|
|
4205
|
+
}
|
|
4206
|
+
return null;
|
|
4207
|
+
}
|
|
3839
4208
|
state(entity, entityId) {
|
|
3840
4209
|
return this.entities.get(entity)?.get(entityId);
|
|
3841
4210
|
}
|
|
@@ -3886,6 +4255,10 @@ class LocalStore {
|
|
|
3886
4255
|
return null;
|
|
3887
4256
|
return { ...fs.ts };
|
|
3888
4257
|
}
|
|
4258
|
+
tombstoneTs(entity, entityId) {
|
|
4259
|
+
const deleted = this.state(entity, entityId)?.deleted;
|
|
4260
|
+
return deleted ? { ...deleted } : null;
|
|
4261
|
+
}
|
|
3889
4262
|
visibleFieldStates(entity, entityId) {
|
|
3890
4263
|
const state = this.state(entity, entityId);
|
|
3891
4264
|
if (!state)
|
|
@@ -3913,6 +4286,8 @@ class LocalStore {
|
|
|
3913
4286
|
const revived = [];
|
|
3914
4287
|
const ops = [];
|
|
3915
4288
|
for (const [entity, byId] of this.entities) {
|
|
4289
|
+
if (isSealedWireKind(entity))
|
|
4290
|
+
continue;
|
|
3916
4291
|
for (const [entityId, state] of byId) {
|
|
3917
4292
|
for (const [field, fs] of state.fields) {
|
|
3918
4293
|
if (!isEncryptedEnvelope(fs.value))
|
|
@@ -3944,6 +4319,56 @@ class LocalStore {
|
|
|
3944
4319
|
}
|
|
3945
4320
|
return revived;
|
|
3946
4321
|
}
|
|
4322
|
+
reviveSealed(open) {
|
|
4323
|
+
const revived = [];
|
|
4324
|
+
const ops = [];
|
|
4325
|
+
const gone = [];
|
|
4326
|
+
for (const [wire, byId] of this.entities) {
|
|
4327
|
+
if (!isSealedWireKind(wire))
|
|
4328
|
+
continue;
|
|
4329
|
+
for (const [entityId, state] of byId) {
|
|
4330
|
+
for (const [wireField, fs] of state.fields) {
|
|
4331
|
+
if (!isSealedPlaceholder(fs.value))
|
|
4332
|
+
continue;
|
|
4333
|
+
const opened = open(wire, entityId, wireField, fs.value);
|
|
4334
|
+
if (!opened)
|
|
4335
|
+
continue;
|
|
4336
|
+
state.fields.delete(wireField);
|
|
4337
|
+
gone.push(fs.op);
|
|
4338
|
+
const op = {
|
|
4339
|
+
id: this.newId(),
|
|
4340
|
+
entity: opened.kind,
|
|
4341
|
+
entityId,
|
|
4342
|
+
ts: { ...fs.ts },
|
|
4343
|
+
op: "set",
|
|
4344
|
+
field: opened.field,
|
|
4345
|
+
value: opened.value
|
|
4346
|
+
};
|
|
4347
|
+
this.applied.add(op.id);
|
|
4348
|
+
if (this.mergeInto(op))
|
|
4349
|
+
ops.push(op);
|
|
4350
|
+
revived.push({
|
|
4351
|
+
...opened,
|
|
4352
|
+
entity: opened.kind,
|
|
4353
|
+
entityId,
|
|
4354
|
+
field: opened.field,
|
|
4355
|
+
ts: { ...fs.ts }
|
|
4356
|
+
});
|
|
4357
|
+
}
|
|
4358
|
+
if (state.fields.size === 0 && state.deleted === null)
|
|
4359
|
+
byId.delete(entityId);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
if (ops.length > 0)
|
|
4363
|
+
this.persistBatch(ops, true);
|
|
4364
|
+
const p = this.persistence;
|
|
4365
|
+
if (p?.deleteOps && gone.length > 0) {
|
|
4366
|
+
this.writeChain = this.writeChain.then(() => p.deleteOps(gone)).catch((err) => this.reportPersistError(err));
|
|
4367
|
+
}
|
|
4368
|
+
if (revived.length > 0)
|
|
4369
|
+
this.emit();
|
|
4370
|
+
return revived;
|
|
4371
|
+
}
|
|
3947
4372
|
visibleFields(state) {
|
|
3948
4373
|
const visible = {};
|
|
3949
4374
|
for (const [name, fs] of state.fields) {
|
|
@@ -4716,14 +5141,14 @@ function rowToOp(r) {
|
|
|
4716
5141
|
};
|
|
4717
5142
|
}
|
|
4718
5143
|
// src/sealedPersistence.ts
|
|
4719
|
-
import { hkdf as
|
|
4720
|
-
import { sha256 as
|
|
5144
|
+
import { hkdf as hkdf8 } from "@noble/hashes/hkdf.js";
|
|
5145
|
+
import { sha256 as sha2569 } from "@noble/hashes/sha2.js";
|
|
4721
5146
|
var LOCAL_STORE_KEY_ID = "local-v1";
|
|
4722
5147
|
var SCOPE = { kind: "personal" };
|
|
4723
5148
|
function deriveLocalStoreKey(appKey, userId) {
|
|
4724
5149
|
if (appKey.length !== 32)
|
|
4725
5150
|
throw new Error("the app key must be 32 bytes");
|
|
4726
|
-
return
|
|
5151
|
+
return hkdf8(sha2569, appKey, utf8ToBytes(userId), utf8ToBytes("atlas-local-store-v1"), 32);
|
|
4727
5152
|
}
|
|
4728
5153
|
|
|
4729
5154
|
class SealedStoreError extends Error {
|
|
@@ -4818,7 +5243,7 @@ function sealPersistence(inner, key) {
|
|
|
4818
5243
|
return sealed;
|
|
4819
5244
|
}
|
|
4820
5245
|
// src/attachments.ts
|
|
4821
|
-
import { sha256 as
|
|
5246
|
+
import { sha256 as sha25610 } from "@noble/hashes/sha2.js";
|
|
4822
5247
|
var DEFAULT_MAX_BLOB_BYTES = 25 * 1024 * 1024;
|
|
4823
5248
|
function maxAttachmentBytes(maxBlobBytes = DEFAULT_MAX_BLOB_BYTES) {
|
|
4824
5249
|
return maxPlainSize(maxBlobBytes);
|
|
@@ -4836,7 +5261,7 @@ function generateAek() {
|
|
|
4836
5261
|
return randomBytes(32);
|
|
4837
5262
|
}
|
|
4838
5263
|
function sha256Hex(bytes) {
|
|
4839
|
-
return bytesToHex(
|
|
5264
|
+
return bytesToHex(sha25610(bytes));
|
|
4840
5265
|
}
|
|
4841
5266
|
function bytesSource(bytes) {
|
|
4842
5267
|
return {
|
|
@@ -4895,8 +5320,8 @@ async function sealSource(aek, source, opts = {}) {
|
|
|
4895
5320
|
const expected = sealedBlobSize(total);
|
|
4896
5321
|
const sink = opts.asBlob ? new BlobSink : new BufferSink(expected);
|
|
4897
5322
|
const sealer = new BlobSealer(aek);
|
|
4898
|
-
const blobHash =
|
|
4899
|
-
const plainHash =
|
|
5323
|
+
const blobHash = sha25610.create();
|
|
5324
|
+
const plainHash = sha25610.create();
|
|
4900
5325
|
let size = 0;
|
|
4901
5326
|
const emit = (bytes) => {
|
|
4902
5327
|
blobHash.update(bytes);
|
|
@@ -4959,7 +5384,7 @@ class PlainCollector {
|
|
|
4959
5384
|
out;
|
|
4960
5385
|
parts = [];
|
|
4961
5386
|
length = 0;
|
|
4962
|
-
hash =
|
|
5387
|
+
hash = sha25610.create();
|
|
4963
5388
|
constructor(blobSize) {
|
|
4964
5389
|
this.out = blobSize !== null && blobSize <= MAX_PREALLOCATION ? new Uint8Array(blobSize) : null;
|
|
4965
5390
|
}
|
|
@@ -5037,7 +5462,7 @@ async function fetchBlob(transport, sha, aek, plainSha) {
|
|
|
5037
5462
|
const download = body instanceof Uint8Array ? { size: body.length, chunks: [body] } : body;
|
|
5038
5463
|
const opener = new BlobOpener(aek);
|
|
5039
5464
|
const plain = new PlainCollector(download.size);
|
|
5040
|
-
const received =
|
|
5465
|
+
const received = sha25610.create();
|
|
5041
5466
|
for await (const piece of download.chunks) {
|
|
5042
5467
|
received.update(piece);
|
|
5043
5468
|
for (const chunk of opener.push(piece))
|
|
@@ -5662,6 +6087,70 @@ class ExpoSqlDatabase {
|
|
|
5662
6087
|
await this.db.closeAsync?.();
|
|
5663
6088
|
}
|
|
5664
6089
|
}
|
|
6090
|
+
// src/legacy.ts
|
|
6091
|
+
var ENTITIES_PER_PUSH = 100;
|
|
6092
|
+
function reencode(store, kind, id) {
|
|
6093
|
+
if (!store.exists(kind, id))
|
|
6094
|
+
return null;
|
|
6095
|
+
const fields = store.visibleFieldStates(kind, id);
|
|
6096
|
+
if (fields.some((f) => isEncryptedEnvelope(f.value)))
|
|
6097
|
+
return null;
|
|
6098
|
+
const ops = fields.map((f) => ({
|
|
6099
|
+
id: store.newEntityId(),
|
|
6100
|
+
entity: kind,
|
|
6101
|
+
entityId: id,
|
|
6102
|
+
ts: f.ts,
|
|
6103
|
+
op: "set",
|
|
6104
|
+
field: f.field,
|
|
6105
|
+
value: f.value
|
|
6106
|
+
}));
|
|
6107
|
+
const deleted = store.tombstoneTs(kind, id);
|
|
6108
|
+
if (deleted)
|
|
6109
|
+
ops.push({ id: store.newEntityId(), entity: kind, entityId: id, ts: deleted, op: "delete" });
|
|
6110
|
+
return ops;
|
|
6111
|
+
}
|
|
6112
|
+
async function migrateLegacy(store, transport) {
|
|
6113
|
+
const result = { reencoded: 0, covered: 0, left: 0 };
|
|
6114
|
+
let next;
|
|
6115
|
+
do {
|
|
6116
|
+
const page = await transport.legacyKeys(next);
|
|
6117
|
+
next = page.next;
|
|
6118
|
+
const drop = [];
|
|
6119
|
+
const pending = [];
|
|
6120
|
+
for (const key of page.entities) {
|
|
6121
|
+
if (key.covered) {
|
|
6122
|
+
drop.push(key);
|
|
6123
|
+
result.covered++;
|
|
6124
|
+
continue;
|
|
6125
|
+
}
|
|
6126
|
+
const kind = key.entity;
|
|
6127
|
+
const ops = transport.mayReencode?.(kind, key.entity_id) === false ? null : reencode(store, kind, key.entity_id);
|
|
6128
|
+
if (!ops || ops.length === 0) {
|
|
6129
|
+
result.left++;
|
|
6130
|
+
continue;
|
|
6131
|
+
}
|
|
6132
|
+
pending.push({ key, ops });
|
|
6133
|
+
}
|
|
6134
|
+
for (let i = 0;i < pending.length; i += ENTITIES_PER_PUSH) {
|
|
6135
|
+
const chunk = pending.slice(i, i + ENTITIES_PER_PUSH);
|
|
6136
|
+
const { deferred = [] } = await transport.syncPush(chunk.flatMap((c) => c.ops));
|
|
6137
|
+
const waiting = new Set(deferred);
|
|
6138
|
+
for (const { key, ops } of chunk) {
|
|
6139
|
+
if (ops.some((op) => waiting.has(op.id))) {
|
|
6140
|
+
result.left++;
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
drop.push(key);
|
|
6144
|
+
result.reencoded++;
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
if (drop.length > 0) {
|
|
6148
|
+
await transport.dropLegacy(drop.map(({ entity, entity_id }) => ({ entity, entity_id })));
|
|
6149
|
+
}
|
|
6150
|
+
} while (next);
|
|
6151
|
+
return result;
|
|
6152
|
+
}
|
|
6153
|
+
|
|
5665
6154
|
// src/sync-client.ts
|
|
5666
6155
|
var MAX_PUSH_OPS = 500;
|
|
5667
6156
|
var MAX_PUSH_BYTES = 1.5 * 1024 * 1024;
|
|
@@ -5716,6 +6205,7 @@ function isBadSnapshotToken(err) {
|
|
|
5716
6205
|
function isSnapshotUnsupported(err) {
|
|
5717
6206
|
return err instanceof ApiError && err.status === 404;
|
|
5718
6207
|
}
|
|
6208
|
+
var LEGACY_RETRY_MS = 10 * 60000;
|
|
5719
6209
|
var THROTTLE_BASE_MS = 5000;
|
|
5720
6210
|
var THROTTLE_MAX_MS = 5 * 60000;
|
|
5721
6211
|
function isRateLimited(err) {
|
|
@@ -5752,6 +6242,8 @@ class SyncClient {
|
|
|
5752
6242
|
clockSkew = null;
|
|
5753
6243
|
resumeTimer = null;
|
|
5754
6244
|
epoch = 0;
|
|
6245
|
+
legacyDone = false;
|
|
6246
|
+
legacyNextAt = 0;
|
|
5755
6247
|
constructor(store, transport, opts = {}) {
|
|
5756
6248
|
this.store = store;
|
|
5757
6249
|
this.transport = transport;
|
|
@@ -5878,6 +6370,7 @@ class SyncClient {
|
|
|
5878
6370
|
}
|
|
5879
6371
|
if (repaired > 0)
|
|
5880
6372
|
await this.pushOutbox(this.store.unsyncedOps());
|
|
6373
|
+
await this.migrateLegacyRows();
|
|
5881
6374
|
this.initialSyncDone = true;
|
|
5882
6375
|
this.firstCycleDone = true;
|
|
5883
6376
|
this.throttleStreak = 0;
|
|
@@ -5907,6 +6400,28 @@ class SyncClient {
|
|
|
5907
6400
|
throw err;
|
|
5908
6401
|
}
|
|
5909
6402
|
}
|
|
6403
|
+
async migrateLegacyRows() {
|
|
6404
|
+
const t = this.transport;
|
|
6405
|
+
if (this.legacyDone || !t.legacyKeys || !t.dropLegacy || Date.now() < this.legacyNextAt)
|
|
6406
|
+
return;
|
|
6407
|
+
try {
|
|
6408
|
+
const result = await migrateLegacy(this.store, {
|
|
6409
|
+
legacyKeys: (next) => t.legacyKeys(next),
|
|
6410
|
+
dropLegacy: (keys) => t.dropLegacy(keys),
|
|
6411
|
+
syncPush: (ops) => t.syncPush(ops),
|
|
6412
|
+
mayReencode: t.mayReencode?.bind(t)
|
|
6413
|
+
});
|
|
6414
|
+
this.legacyDone = result.left === 0;
|
|
6415
|
+
if (result.reencoded + result.covered > 0)
|
|
6416
|
+
this.opts.onLegacyMigrated?.(result);
|
|
6417
|
+
} catch (err) {
|
|
6418
|
+
if (err instanceof ApiError && err.status === 404)
|
|
6419
|
+
this.legacyDone = true;
|
|
6420
|
+
else
|
|
6421
|
+
console.warn("[atlas SyncClient] protocol 6 migration pass failed:", err);
|
|
6422
|
+
}
|
|
6423
|
+
this.legacyNextAt = Date.now() + LEGACY_RETRY_MS;
|
|
6424
|
+
}
|
|
5910
6425
|
async pullPhase(firstPage) {
|
|
5911
6426
|
if (this.needsBootstrap && await this.bootstrapFromSnapshot(firstPage) === null) {
|
|
5912
6427
|
return this.replayFromZero();
|
|
@@ -6799,7 +7314,9 @@ export {
|
|
|
6799
7314
|
EMPTY_KEY_TRUST,
|
|
6800
7315
|
EMPTY_SCOPE_STORE,
|
|
6801
7316
|
ExpoSqlDatabase,
|
|
7317
|
+
FIELD_ENVELOPE_V3,
|
|
6802
7318
|
FIELD_ENVELOPE_VERSION,
|
|
7319
|
+
FIELD_V3_MIN_PADDING,
|
|
6803
7320
|
HlcClock,
|
|
6804
7321
|
IndexedDbPersistence,
|
|
6805
7322
|
InvalidApiTokenError,
|
|
@@ -6808,6 +7325,7 @@ export {
|
|
|
6808
7325
|
Keyring,
|
|
6809
7326
|
LEGACY_DISCOVERY,
|
|
6810
7327
|
LEGACY_KDF,
|
|
7328
|
+
LEGACY_RETRY_MS,
|
|
6811
7329
|
LOCAL_STORE_KEY_ID,
|
|
6812
7330
|
LocalStore,
|
|
6813
7331
|
MAX_CLOCK_DRIFT_MS,
|
|
@@ -6848,6 +7366,7 @@ export {
|
|
|
6848
7366
|
addPasskey,
|
|
6849
7367
|
apiErrorCode,
|
|
6850
7368
|
appOfKind,
|
|
7369
|
+
appOfWire,
|
|
6851
7370
|
applyRepairs,
|
|
6852
7371
|
argon2idPortable,
|
|
6853
7372
|
assertServerCompatible,
|
|
@@ -6876,6 +7395,7 @@ export {
|
|
|
6876
7395
|
createPasskeySlot,
|
|
6877
7396
|
decodeBase64Url,
|
|
6878
7397
|
decodeOpLogFile,
|
|
7398
|
+
decodeOps,
|
|
6879
7399
|
decryptAesGcm,
|
|
6880
7400
|
decryptBlob,
|
|
6881
7401
|
decryptField,
|
|
@@ -6891,6 +7411,7 @@ export {
|
|
|
6891
7411
|
deriveRecoveryKey,
|
|
6892
7412
|
deriveRecoveryKeys,
|
|
6893
7413
|
enableApp,
|
|
7414
|
+
encodeOp,
|
|
6894
7415
|
encodeOpLogFile,
|
|
6895
7416
|
encryptAesGcm,
|
|
6896
7417
|
encryptBlob,
|
|
@@ -6901,6 +7422,8 @@ export {
|
|
|
6901
7422
|
ensureSigningKey,
|
|
6902
7423
|
expectedKeyScope,
|
|
6903
7424
|
fetchBlob,
|
|
7425
|
+
fieldTag,
|
|
7426
|
+
fieldTagKey,
|
|
6904
7427
|
forgetIdentity,
|
|
6905
7428
|
formatApiToken,
|
|
6906
7429
|
fromEncryptedWire,
|
|
@@ -6921,9 +7444,16 @@ export {
|
|
|
6921
7444
|
isAdmin,
|
|
6922
7445
|
isEncryptedEnvelope,
|
|
6923
7446
|
isFieldEnvelopeV2,
|
|
7447
|
+
isFieldEnvelopeV3,
|
|
7448
|
+
isFieldTag,
|
|
7449
|
+
isLegacyWireKind,
|
|
6924
7450
|
isProjectCreator,
|
|
6925
7451
|
isProjectShared,
|
|
7452
|
+
isPublicField,
|
|
7453
|
+
isSealedInvitePreview,
|
|
6926
7454
|
isSealedKeyRow,
|
|
7455
|
+
isSealedPlaceholder,
|
|
7456
|
+
isSealedWireKind,
|
|
6927
7457
|
isVerified,
|
|
6928
7458
|
isWellFormedOp,
|
|
6929
7459
|
isWrappedAttachmentKey,
|
|
@@ -6932,21 +7462,27 @@ export {
|
|
|
6932
7462
|
kdfToWire,
|
|
6933
7463
|
keyForScope,
|
|
6934
7464
|
keyScopeLabel,
|
|
7465
|
+
kindsOfWire,
|
|
7466
|
+
legacyEncrypted,
|
|
6935
7467
|
maxAttachmentBytes,
|
|
6936
7468
|
maxPlainSize,
|
|
6937
7469
|
memberEntityId,
|
|
6938
7470
|
mergeOpLogs,
|
|
6939
7471
|
mergeRotatedTokens,
|
|
7472
|
+
migrateLegacy,
|
|
6940
7473
|
normalizeCredentialId,
|
|
6941
7474
|
observeIdentity,
|
|
6942
7475
|
openAccountKeys,
|
|
6943
7476
|
openAppKey,
|
|
6944
7477
|
openFieldValue,
|
|
7478
|
+
openInvitePreview,
|
|
6945
7479
|
openOptionsFor,
|
|
6946
7480
|
openPasskeyAccountKeys,
|
|
6947
7481
|
openPasskeySlot,
|
|
6948
7482
|
openProjectKeyRow,
|
|
6949
7483
|
openRecoverySlot,
|
|
7484
|
+
openSealed,
|
|
7485
|
+
openSealedField,
|
|
6950
7486
|
openTodoAppKey,
|
|
6951
7487
|
padPlaintext,
|
|
6952
7488
|
paddedLength,
|
|
@@ -6963,6 +7499,7 @@ export {
|
|
|
6963
7499
|
projectKeyId,
|
|
6964
7500
|
randomBytes,
|
|
6965
7501
|
readKeyTrust,
|
|
7502
|
+
realKindOf,
|
|
6966
7503
|
reauthenticateWithPasskey,
|
|
6967
7504
|
reconcileStoredSession,
|
|
6968
7505
|
recordAcceptedProject,
|
|
@@ -6986,11 +7523,14 @@ export {
|
|
|
6986
7523
|
sameKdf,
|
|
6987
7524
|
sameKeyScope,
|
|
6988
7525
|
sealDelivery,
|
|
7526
|
+
sealField,
|
|
7527
|
+
sealInvitePreview,
|
|
6989
7528
|
sealKey,
|
|
6990
7529
|
sealPersistence,
|
|
6991
7530
|
sealRootKey,
|
|
6992
7531
|
sealSource,
|
|
6993
7532
|
sealedBlobSize,
|
|
7533
|
+
sealedRepair,
|
|
6994
7534
|
serverIncompatibility,
|
|
6995
7535
|
sessionFromAuth,
|
|
6996
7536
|
setArgon2idProvider,
|
|
@@ -7029,6 +7569,8 @@ export {
|
|
|
7029
7569
|
validateRecoveryPhrase,
|
|
7030
7570
|
verifyAccountPassword,
|
|
7031
7571
|
verifyDelivery,
|
|
7572
|
+
wireFieldOf,
|
|
7573
|
+
wireKindOf,
|
|
7032
7574
|
withoutCiphertext,
|
|
7033
7575
|
withoutPrfOutput,
|
|
7034
7576
|
wrapAek,
|