@atlastodo/client-core 0.1.11-rc.7 → 0.1.11-rc.8

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.
@@ -16,6 +16,8 @@
16
16
  */
17
17
  /** The app whose key is the account's original DEK. */
18
18
  export declare const TODO_APP = "todo";
19
+ /** Atlas Journal: its key and identity are a row under the root (`enableApp`). */
20
+ export declare const JOURNAL_APP = "journal";
19
21
  /** A key wrapped under another (root slot, app key, app identity). */
20
22
  export interface RootKeyWrap {
21
23
  v: 1;
@@ -7,7 +7,19 @@
7
7
  * reads against.
8
8
  */
9
9
  import type { EntityKind } from "./types.js";
10
- /** A bundled app manifest: its id, version and the entity kinds it syncs. */
10
+ /**
11
+ * What an app asks of the API tokens that include it (`api_tokens` in its manifest), which the
12
+ * server enforces. Absent keys are the defaults: no expiry needed, writes allowed.
13
+ */
14
+ export interface TokenPolicy {
15
+ /** A token must name an expiry. */
16
+ expiry_required: boolean;
17
+ /** The latest expiry a token may name, in days after it is made; null is no limit. */
18
+ max_lifetime_days: number | null;
19
+ /** Whether a token may write when its creator does not say. */
20
+ write_by_default: boolean;
21
+ }
22
+ /** A bundled app manifest: its id, version, the entity kinds it syncs and its token policy. */
11
23
  export interface AppManifest {
12
24
  app: string;
13
25
  name: string;
@@ -15,14 +27,27 @@ export interface AppManifest {
15
27
  kinds: Record<string, {
16
28
  plaintext_fields: readonly string[] | "all";
17
29
  }>;
30
+ api_tokens?: Partial<TokenPolicy>;
18
31
  }
19
32
  /**
20
33
  * The manifests bundled into this client, as the server bundles them. `EntityKind` (and so
21
- * {@link PLAINTEXT_FIELDS}) is todo's kinds: this store syncs todo. Another app's client reads its
22
- * kinds from its own manifest here (journal's encrypt every field).
34
+ * {@link PLAINTEXT_FIELDS}) holds every bundled app's kinds, as `atlas_core::EntityKind` does;
35
+ * which of them a client syncs is its session's app scope. Journal's and the vault's kinds
36
+ * encrypt every field.
23
37
  */
24
38
  export declare const BUNDLED_MANIFESTS: readonly AppManifest[];
39
+ /**
40
+ * The policy of a token for all of `apps` (bundled ids), the strictest of theirs as the server
41
+ * takes it: an expiry required if any requires one, the shortest lifetime, and read-only by
42
+ * default if any is. A client making a token reads it to ask for an expiry and write access.
43
+ */
44
+ export declare function tokenPolicyFor(apps: readonly string[]): TokenPolicy;
25
45
  /** The bundled app owning the kind with this wire name, if any. */
26
46
  export declare function appOfKind(kind: string): AppManifest | undefined;
27
47
  export declare const PLAINTEXT_FIELDS: Readonly<Record<EntityKind, readonly string[] | "all">>;
48
+ /**
49
+ * Whether a value of `entity`.`field` is end-to-end encrypted, for any wire kind: a kind no
50
+ * bundled manifest names encrypts everything.
51
+ */
52
+ export declare function shouldEncryptKindField(entity: string, field: string): boolean;
28
53
  export declare function shouldEncryptField(entity: EntityKind, field: string): boolean;
package/dist/index.js CHANGED
@@ -1099,6 +1099,7 @@ import { x25519 as x255193 } from "@noble/curves/ed25519.js";
1099
1099
  import { sha256 as sha2566 } from "@noble/hashes/sha2.js";
1100
1100
  import { hkdf as hkdf5 } from "@noble/hashes/hkdf.js";
1101
1101
  var TODO_APP = "todo";
1102
+ var JOURNAL_APP = "journal";
1102
1103
  var KEY_BYTES = 32;
1103
1104
  var json = (parts) => utf8ToBytes(JSON.stringify(parts));
1104
1105
  var rootAad = (userId, slot) => json(["atlas-root-key", 1, userId.toLowerCase(), slot]);
@@ -1475,12 +1476,50 @@ var todo_default = {
1475
1476
  }
1476
1477
  }
1477
1478
  };
1479
+ // ../../manifests/vault.json
1480
+ var vault_default = {
1481
+ _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>`. 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 (no plaintext_fields), 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, and a token is read-only unless its creator opts in to writes. Bump `version` whenever the kinds, their plaintext fields or the token policy change.",
1482
+ app: "vault",
1483
+ name: "Atlas Vault",
1484
+ version: 1,
1485
+ kinds: {
1486
+ "vault.item": { plaintext_fields: [] },
1487
+ "vault.folder": { plaintext_fields: [] },
1488
+ "vault.preference": { plaintext_fields: [] }
1489
+ },
1490
+ api_tokens: {
1491
+ expiry_required: true,
1492
+ max_lifetime_days: 365,
1493
+ write_by_default: false
1494
+ }
1495
+ };
1478
1496
 
1479
1497
  // src/fieldPolicy.ts
1480
1498
  var BUNDLED_MANIFESTS = Object.freeze([
1481
1499
  todo_default,
1482
- journal_default
1500
+ journal_default,
1501
+ vault_default
1483
1502
  ]);
1503
+ function tokenPolicyFor(apps) {
1504
+ const policy = {
1505
+ expiry_required: false,
1506
+ max_lifetime_days: null,
1507
+ write_by_default: true
1508
+ };
1509
+ for (const app of apps) {
1510
+ const p = BUNDLED_MANIFESTS.find((m) => m.app === app)?.api_tokens;
1511
+ if (!p)
1512
+ continue;
1513
+ if (p.expiry_required)
1514
+ policy.expiry_required = true;
1515
+ if (p.max_lifetime_days != null) {
1516
+ policy.max_lifetime_days = policy.max_lifetime_days == null ? p.max_lifetime_days : Math.min(policy.max_lifetime_days, p.max_lifetime_days);
1517
+ }
1518
+ if (p.write_by_default === false)
1519
+ policy.write_by_default = false;
1520
+ }
1521
+ return policy;
1522
+ }
1484
1523
  function appOfKind(kind) {
1485
1524
  return BUNDLED_MANIFESTS.find((m) => Object.prototype.hasOwnProperty.call(m.kinds, kind));
1486
1525
  }
@@ -1498,7 +1537,12 @@ var ENTITY_KINDS = {
1498
1537
  focus_session: true,
1499
1538
  habit: true,
1500
1539
  habit_checkin: true,
1501
- attachment: true
1540
+ attachment: true,
1541
+ "journal.entry": true,
1542
+ "journal.preference": true,
1543
+ "vault.item": true,
1544
+ "vault.folder": true,
1545
+ "vault.preference": true
1502
1546
  };
1503
1547
  function loadTable(manifests) {
1504
1548
  const table = {};
@@ -1515,6 +1559,12 @@ function loadTable(manifests) {
1515
1559
  return Object.freeze(table);
1516
1560
  }
1517
1561
  var PLAINTEXT_FIELDS = loadTable(BUNDLED_MANIFESTS);
1562
+ function shouldEncryptKindField(entity, field) {
1563
+ const plain = appOfKind(entity)?.kinds[entity]?.plaintext_fields;
1564
+ if (plain === "all")
1565
+ return false;
1566
+ return !plain?.includes(field);
1567
+ }
1518
1568
  function shouldEncryptField(entity, field) {
1519
1569
  const plain = PLAINTEXT_FIELDS[entity];
1520
1570
  if (plain === "all")
@@ -6635,6 +6685,7 @@ export {
6635
6685
  HlcClock,
6636
6686
  IndexedDbPersistence,
6637
6687
  InvalidApiTokenError,
6688
+ JOURNAL_APP,
6638
6689
  KEY_TRUST_ID,
6639
6690
  Keyring,
6640
6691
  LEGACY_DISCOVERY,
@@ -6824,6 +6875,7 @@ export {
6824
6875
  setPbkdf2Provider,
6825
6876
  sharedProjectIds,
6826
6877
  shouldEncryptField,
6878
+ shouldEncryptKindField,
6827
6879
  signDelivery,
6828
6880
  signInWithPasskey,
6829
6881
  signingPublicKey,
@@ -6832,6 +6884,7 @@ export {
6832
6884
  toEncryptedWire,
6833
6885
  toWireReport,
6834
6886
  todoAppKeyRow,
6887
+ tokenPolicyFor,
6835
6888
  unpadPlaintext,
6836
6889
  unpinMemberKey,
6837
6890
  unsealKey,
package/dist/types.d.ts CHANGED
@@ -330,6 +330,10 @@ export interface ApiTokenView {
330
330
  apps?: string[];
331
331
  created_at: number;
332
332
  last_used_at?: number;
333
+ /** When the token stops working; absent when it never does. An expired token stays listed until revoked. */
334
+ expires_at?: number;
335
+ /** Whether its sessions may write (absent from servers predating read-only tokens: they may). */
336
+ write?: boolean;
333
337
  }
334
338
  /** One non-todo app's key and X25519 identity wrapped for an API token (`wrapAppKeysForApiToken`). */
335
339
  export interface ApiTokenAppKeyWraps {
@@ -365,6 +369,14 @@ export interface CreateApiTokenPayload {
365
369
  ct: string;
366
370
  };
367
371
  app_keys?: Record<string, ApiTokenAppKeyWraps>;
372
+ /**
373
+ * When the token stops working (unix ms). The apps' policy ({@link tokenPolicyFor}) may require
374
+ * one and cap how far out it is: a vault token needs one at most 365 days ahead (needs the
375
+ * `api_token_policies` capability).
376
+ */
377
+ expires_at?: number;
378
+ /** Whether its sessions may write; the policy's default when absent (a vault token is read-only). */
379
+ write?: boolean;
368
380
  }
369
381
  /** `POST /auth/api-token/session`: an access token (no refresh token) and the token's wraps. */
370
382
  export interface ApiTokenSessionResponse {
@@ -393,6 +405,10 @@ export interface ApiTokenSessionResponse {
393
405
  iv: string;
394
406
  ct: string;
395
407
  };
408
+ /** When the token expires (unix ms); `expires_in` ends no later. Absent when it never does. */
409
+ expires_at?: number;
410
+ /** Whether the session may write; a read-only one gets 403 `api_token_read_only` on writes. */
411
+ write?: boolean;
396
412
  }
397
413
  export interface MemberView {
398
414
  user_id: string;
@@ -442,7 +458,17 @@ export type Operation = {
442
458
  });
443
459
  export type EntityKind = "task" | "project" | "section" | "label" | "comment" | "preference" | "saved_filter" | "reminder" | "project_member" | "activity" | "focus_session" | "habit" | "habit_checkin"
444
460
  /** Metadata only; ciphertext travels via `/attachments/blobs/:sha256`. */
445
- | "attachment";
461
+ | "attachment"
462
+ /**
463
+ * Atlas Journal's kinds (`manifests/journal.json`), namespaced `<app>.<kind>` like every app's
464
+ * after todo. Personal and fully encrypted: no plaintext field, no project scope. A journal
465
+ * client syncs them in a journal-scoped session (`ApiClientOptions.apps`).
466
+ */
467
+ | JournalEntityKind
468
+ /** Atlas Vault (`manifests/vault.json`): personal, every field encrypted. */
469
+ | "vault.item" | "vault.folder" | "vault.preference";
470
+ /** Atlas Journal's entity kinds: `journal.entry` (one entry) and `journal.preference` (a setting). */
471
+ export type JournalEntityKind = "journal.entry" | "journal.preference";
446
472
  export interface CreateTaskInput {
447
473
  title: string;
448
474
  notes?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlastodo/client-core",
3
- "version": "0.1.11-rc.7",
3
+ "version": "0.1.11-rc.8",
4
4
  "description": "Atlas core client: hybrid logical clocks, the op log and per-field LWW merge, end-to-end encryption, the local store and the sync client",
5
5
  "license": "AGPL-3.0-only",
6
6
  "homepage": "https://github.com/atlastodo/atlas/tree/main/packages/client-core",
@@ -36,6 +36,7 @@
36
36
  "api_token_vectors.json",
37
37
  "conflict_vectors.json",
38
38
  "envelope_vectors.json",
39
+ "field_policy_vectors.json",
39
40
  "identity_vectors.json",
40
41
  "passkey_vectors.json",
41
42
  "password_kdf_vectors.json",
@@ -34,6 +34,20 @@
34
34
  "iv": "dXrtP9iobV/5dQK7",
35
35
  "ct": "YVQDhrVqw2A7XxrJ1DBI/qTTTVQ0FlPSaGNiMxAE5GyMh3JDOvj6Sl4JjdNi0xQY"
36
36
  }
37
+ },
38
+ "vault": {
39
+ "app_key": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
40
+ "app_private_key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
41
+ "wrapped_key": {
42
+ "v": 1,
43
+ "iv": "wm+GsbI61xROnC8+",
44
+ "ct": "3wMTljSLoopA53SXjhbdhPFksiuOdMXNmj7HGqiU+ByMqb794Vb57+NVh0Br9FZv"
45
+ },
46
+ "wrapped_private_key": {
47
+ "v": 1,
48
+ "iv": "/QKoOwtHubDapdW1",
49
+ "ct": "UFyTM//C7trgffAR2i0JDdLwtRf6I/9l+eq6oixMefK3KtXZixSE6RF5Flrdzf7C"
50
+ }
37
51
  }
38
52
  }
39
53
  }
@@ -0,0 +1,52 @@
1
+ {
2
+ "_doc": "What the bundled app manifests (manifests/*.json) decide, asserted by packages/client-core/src/fieldPolicy.test.ts and crates/atlas-client (field_policy.rs). `fields`: whether a value of `entity`.`field` leaves the device end-to-end encrypted (`encrypted`); a kind no manifest names, and any field a manifest does not list, is encrypted. Todo keeps only the ids the server routes and checks access by in plaintext; journal and vault keep nothing, so the server sees no vault item's type, name, folder, URL, timestamp or secret. `token_policies`: the API token policy of a token for `apps`, the strictest of their manifests' `api_tokens` (`max_lifetime_days` null is no limit).",
3
+ "fields": [
4
+ { "entity": "task", "field": "title", "encrypted": true },
5
+ { "entity": "task", "field": "project_id", "encrypted": false },
6
+ { "entity": "task", "field": "assignee_id", "encrypted": false },
7
+ { "entity": "task", "field": "a_field_nobody_declared", "encrypted": true },
8
+ { "entity": "comment", "field": "task_id", "encrypted": false },
9
+ { "entity": "attachment", "field": "blob_sha", "encrypted": false },
10
+ { "entity": "project_member", "field": "anything", "encrypted": false },
11
+ { "entity": "journal.entry", "field": "body", "encrypted": true },
12
+ { "entity": "journal.entry", "field": "date", "encrypted": true },
13
+ { "entity": "journal.entry", "field": "project_id", "encrypted": true },
14
+ { "entity": "vault.item", "field": "type", "encrypted": true },
15
+ { "entity": "vault.item", "field": "name", "encrypted": true },
16
+ { "entity": "vault.item", "field": "fields", "encrypted": true },
17
+ { "entity": "vault.item", "field": "urls", "encrypted": true },
18
+ { "entity": "vault.item", "field": "notes", "encrypted": true },
19
+ { "entity": "vault.item", "field": "tags", "encrypted": true },
20
+ { "entity": "vault.item", "field": "folder", "encrypted": true },
21
+ { "entity": "vault.item", "field": "favorite", "encrypted": true },
22
+ { "entity": "vault.item", "field": "history", "encrypted": true },
23
+ { "entity": "vault.item", "field": "created_at", "encrypted": true },
24
+ { "entity": "vault.item", "field": "updated_at", "encrypted": true },
25
+ { "entity": "vault.item", "field": "deleted_at", "encrypted": true },
26
+ { "entity": "vault.item", "field": "project_id", "encrypted": true },
27
+ { "entity": "vault.item", "field": "task_id", "encrypted": true },
28
+ { "entity": "vault.folder", "field": "name", "encrypted": true },
29
+ { "entity": "vault.folder", "field": "deleted_at", "encrypted": true },
30
+ { "entity": "vault.preference", "field": "value", "encrypted": true },
31
+ { "entity": "notes.note", "field": "body", "encrypted": true },
32
+ { "entity": "notes.note", "field": "project_id", "encrypted": true }
33
+ ],
34
+ "token_policies": [
35
+ {
36
+ "apps": ["todo"],
37
+ "policy": { "expiry_required": false, "max_lifetime_days": null, "write_by_default": true }
38
+ },
39
+ {
40
+ "apps": ["journal"],
41
+ "policy": { "expiry_required": false, "max_lifetime_days": null, "write_by_default": true }
42
+ },
43
+ {
44
+ "apps": ["vault"],
45
+ "policy": { "expiry_required": true, "max_lifetime_days": 365, "write_by_default": false }
46
+ },
47
+ {
48
+ "apps": ["todo", "journal", "vault"],
49
+ "policy": { "expiry_required": true, "max_lifetime_days": 365, "write_by_default": false }
50
+ }
51
+ ]
52
+ }