@atlastodo/client-core 0.1.13 → 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.
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The migration of rows written before sync protocol 7 (docs/decisions/0006): their plaintext kind,
3
+ * field names and links stay on the server until the client that holds them re-encodes them and
4
+ * drops the originals.
5
+ *
6
+ * The server lists what the partition still holds (`GET /sync/legacy/keys`). A key a current copy
7
+ * already covers (another device or member re-encoded it) is dropped as it is. Any other is
8
+ * re-encoded from this device's copy: every visible field and the tombstone, each with its original
9
+ * stamp, so the re-encoded copy changes nothing on any device, then dropped
10
+ * (`POST /sync/legacy/drop`). The local store is not written. An entity this device cannot re-encode
11
+ * yet (a value no key opens, a scope without its key, an activity of another member, a shared
12
+ * project of another owner) stays until a later pass or its owner's client.
13
+ */
14
+ import type { LocalStore } from "./store.js";
15
+ import type { EntityKind, Operation } from "./types.js";
16
+ export interface LegacyKey {
17
+ entity: string;
18
+ entity_id: string;
19
+ covered: boolean;
20
+ }
21
+ export interface LegacyKeysPage {
22
+ entities: LegacyKey[];
23
+ next?: string;
24
+ }
25
+ export interface LegacyTransport {
26
+ legacyKeys(next?: string): Promise<LegacyKeysPage>;
27
+ dropLegacy(keys: {
28
+ entity: string;
29
+ entity_id: string;
30
+ }[]): Promise<number>;
31
+ syncPush(ops: Operation[]): Promise<{
32
+ deferred?: string[];
33
+ }>;
34
+ /** Whether this account may write every field of the entity (its role, its activity). */
35
+ mayReencode?(kind: EntityKind, entityId: string): boolean;
36
+ }
37
+ export interface LegacyMigration {
38
+ /** Entities re-encoded and dropped. */
39
+ reencoded: number;
40
+ /** Keys dropped as they were, already covered by a current copy. */
41
+ covered: number;
42
+ /** Keys left for a later pass. */
43
+ left: number;
44
+ }
45
+ /** One pass over the partition's legacy keys. Throws what the transport throws. */
46
+ export declare function migrateLegacy(store: LocalStore, transport: LegacyTransport): Promise<LegacyMigration>;
@@ -41,9 +41,10 @@ export interface RealtimeClientOptions {
41
41
  }
42
42
  /**
43
43
  * Sent on every request (`x-atlas-sync-protocol`) and in the WebSocket URL; the server answers 426
44
- * below its minimum. 4: `__enc: 2`, 5: ticket instead of token in the URL, 6: key rotations.
44
+ * below its minimum. 4: `__enc: 2`, 5: ticket instead of token in the URL, 6: key rotations, 7: wire
45
+ * kinds, scopes and field tags (docs/decisions/0006).
45
46
  */
46
- export declare const SYNC_PROTOCOL = 6;
47
+ export declare const SYNC_PROTOCOL = 7;
47
48
  /** The protocol rides as a param since headers are unavailable. */
48
49
  export declare function buildSyncWsUrl(baseUrl: string, ticket: string, since: number): string;
49
50
  export declare class RealtimeClient {
package/dist/scope.d.ts CHANGED
@@ -81,7 +81,7 @@ export interface Repair {
81
81
  reason: "key" | "plaintext";
82
82
  keyId?: string;
83
83
  scope?: KeyScope;
84
- version?: 1 | 2;
84
+ version?: 1 | 2 | 3;
85
85
  }
86
86
  /** A current value under a rotation-retired key is history and stays. */
87
87
  export declare function confirmRepairs(store: ScopeStore, keyring: Keyring, userId: string | undefined, repairs: readonly Repair[]): Repair[];
package/dist/store.d.ts CHANGED
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Local-first store. Entities are bags of fields resolved by field-level LWW over HLC (as on the
3
3
  * server). Local mutations stamp an HLC and enqueue an {@link Operation}; remote ops merge idempotently.
4
+ *
5
+ * What the sync decoder could not attribute to a real kind waits under its wire kind (sync protocol
6
+ * 7): a value no key here opened ({@link SealedPlaceholder}, revived by {@link LocalStore.reviveSealed})
7
+ * and a tombstone of an entity not seen yet, which the entity takes over when it arrives.
4
8
  */
5
9
  import { type Hlc } from "./hlc.js";
6
10
  import type { PersistedOp, Persistence } from "./persistence.js";
7
11
  import type { EntityKind, Operation } from "./types.js";
12
+ import { type SealedPlaceholder } from "./wire.js";
8
13
  /** Superseded ops tolerated in the log before a hydrate compacts it (see `LocalStore.compact`). */
9
14
  export declare const COMPACT_AFTER_SUPERSEDED = 1000;
10
15
  /** The server's `MAX_FUTURE_SKEW_MS`: a later timestamp would pin the clock and get every local op refused. */
@@ -123,6 +128,10 @@ export declare class LocalStore {
123
128
  private emit;
124
129
  private merge;
125
130
  private mergeInto;
131
+ /** A tombstone that arrived before its entity's kind was known, now the entity's. */
132
+ private inheritTombstone;
133
+ /** The real kind of the entity held by this id, or null. */
134
+ kindOf(entityId: string): EntityKind | null;
126
135
  private state;
127
136
  private allStates;
128
137
  revision(entity: EntityKind): number;
@@ -132,6 +141,8 @@ export declare class LocalStore {
132
141
  exists(entity: EntityKind, entityId: string): boolean;
133
142
  rawField(entity: EntityKind, entityId: string, field: string): unknown;
134
143
  fieldTs(entity: EntityKind, entityId: string, field: string): Hlc | null;
144
+ /** The entity's tombstone stamp, or null. */
145
+ tombstoneTs(entity: EntityKind, entityId: string): Hlc | null;
135
146
  visibleFieldStates(entity: EntityKind, entityId: string): {
136
147
  field: string;
137
148
  value: unknown;
@@ -143,6 +154,17 @@ export declare class LocalStore {
143
154
  value: unknown;
144
155
  keyId: string;
145
156
  }>(open: (entity: EntityKind, entityId: string, field: string, envelope: unknown) => O | null): (O & RevivedField)[];
157
+ /**
158
+ * Opens the values that arrived sealed under a wire kind (protocol 7) with the keys now held:
159
+ * each becomes its real kind's field at the same timestamp, and the sealed copy goes. Nothing is
160
+ * pushed.
161
+ */
162
+ reviveSealed<O extends {
163
+ kind: EntityKind;
164
+ field: string;
165
+ value: unknown;
166
+ keyId: string;
167
+ }>(open: (wire: string, entityId: string, wireField: string, sealed: SealedPlaceholder) => O | null): (O & RevivedField)[];
146
168
  private visibleFields;
147
169
  unsyncedOps(): Operation[];
148
170
  /** Rebuilds their entities from the remaining ops so they stop masking the server's values. */
@@ -7,6 +7,7 @@ import { ApiError } from "./api.js";
7
7
  import { type RealtimeSocket } from "./realtime.js";
8
8
  import type { BootstrapProgress, PageRepairs } from "./persistence.js";
9
9
  import { type Repair } from "./scope.js";
10
+ import { type LegacyKeysPage, type LegacyMigration } from "./legacy.js";
10
11
  import type { LocalStore } from "./store.js";
11
12
  import type { Operation } from "./types.js";
12
13
  /** Ops per push request; the server caps a push by body size and op count. */
@@ -45,7 +46,16 @@ export interface SyncTransport {
45
46
  repairs?: Repair[];
46
47
  };
47
48
  confirmRepairs?(repairs: Repair[]): Repair[];
49
+ /** With both, rows written before protocol 7 are re-encoded and dropped (`migrateLegacy`). */
50
+ legacyKeys?(next?: string): Promise<LegacyKeysPage>;
51
+ dropLegacy?(keys: {
52
+ entity: string;
53
+ entity_id: string;
54
+ }[]): Promise<number>;
55
+ mayReencode?(kind: Operation["entity"], entityId: string): boolean;
48
56
  }
57
+ /** A pass that left keys it could not re-encode yet runs again after this long. */
58
+ export declare const LEGACY_RETRY_MS: number;
49
59
  export type SyncStatus = "idle" | "syncing" | "offline" | "throttled" | "live-ws";
50
60
  export declare const THROTTLE_BASE_MS = 5000;
51
61
  export declare const THROTTLE_MAX_MS: number;
@@ -68,6 +78,8 @@ export interface SyncClientOptions {
68
78
  /** Omit for polling only. The poll reconciles messages the hub dropped for a lagging device. */
69
79
  realtime?: SyncRealtimeOptions;
70
80
  onRemoteApplied?: (ops: Operation[]) => void;
81
+ /** After each pass of the protocol 6 migration that found something to do. */
82
+ onLegacyMigrated?: (result: LegacyMigration) => void;
71
83
  }
72
84
  export interface SyncResult {
73
85
  pushed: number;
@@ -112,6 +124,9 @@ export declare class SyncClient {
112
124
  private clockSkew;
113
125
  private resumeTimer;
114
126
  private epoch;
127
+ /** Done: the partition holds no protocol 6 rows this device can move; else the next pass's time. */
128
+ private legacyDone;
129
+ private legacyNextAt;
115
130
  constructor(store: LocalStore, transport: SyncTransport, opts?: SyncClientOptions);
116
131
  currentCursor(): number;
117
132
  currentStatus(): SyncStatus;
@@ -131,6 +146,8 @@ export declare class SyncClient {
131
146
  private throttle;
132
147
  private requestSync;
133
148
  private run;
149
+ /** One pass of the protocol 6 migration, when due. Never fails the cycle. */
150
+ private migrateLegacyRows;
134
151
  /** Applies all pages at once so listeners render only the converged state. */
135
152
  private pullPhase;
136
153
  private resetFromServer;
package/dist/types.d.ts CHANGED
@@ -431,13 +431,19 @@ export interface InviteView {
431
431
  email: string;
432
432
  display_name: string;
433
433
  } | null;
434
- /** Fields may be envelopes readable only with `sealed_key`. */
434
+ /**
435
+ * The project's fields as the inviter's rows written before sync protocol 7 hold them, each an
436
+ * envelope readable only with `sealed_key`, or null. Null once those rows are migrated: see
437
+ * `preview`.
438
+ */
435
439
  project: {
436
440
  name: unknown;
437
441
  icon: unknown;
438
442
  color: unknown;
439
443
  kind: unknown;
440
444
  };
445
+ /** What the inviter sealed under the project key for the invitee (`openInvitePreview`). */
446
+ preview?: import("./crypto/index.js").SealedInvitePreview | null;
441
447
  sealed_key: {
442
448
  key_id: string;
443
449
  encrypted_pek: unknown;
package/dist/wire.d.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * The sync wire format, protocol 7 (docs/decisions/0006): what an {@link Operation} looks like to
3
+ * the server, and back.
4
+ *
5
+ * On the wire an op names a wire kind (`todo.sealed`, …) instead of its real kind, the scope whose
6
+ * key encrypted it (`scope`: a project id, or null for the account's own key), and a field that is
7
+ * an opaque per-entity tag unless the manifest makes it public. The real kind and field travel
8
+ * inside the value's v3 envelope. Rows written before protocol 7 (legacy kinds, real kind names on
9
+ * the wire) are read as before, and re-encoded by the migration (`migrateLegacy`), never by a
10
+ * repair, which would move their timestamps.
11
+ */
12
+ import { type FieldEnvelopeV3, type KeyScope, type Keyring } from "./crypto/index.js";
13
+ import type { Repair, ScopeKey } from "./scope.js";
14
+ import type { EntityKind, Operation } from "./types.js";
15
+ export type WireOp = {
16
+ id: string;
17
+ /** A wire kind, or a real kind name on a row written before protocol 7. */
18
+ entity: string;
19
+ entity_id: string;
20
+ /** The project whose key encrypted the op; null for the account's own key. */
21
+ scope?: string | null;
22
+ /** The account that pushed it, as the server says; null for the server's own ops. */
23
+ pushed_by?: string | null;
24
+ ts: {
25
+ wall_ms: number;
26
+ counter: number;
27
+ node: string;
28
+ };
29
+ } & ({
30
+ op: "set";
31
+ field: string;
32
+ value: unknown;
33
+ } | {
34
+ op: "delete";
35
+ });
36
+ /** A v3 value no key here opened yet: kept where it arrived, with the scope it was written in. */
37
+ export type SealedPlaceholder = FieldEnvelopeV3 & {
38
+ scope: string | null;
39
+ };
40
+ export declare function isSealedPlaceholder(v: unknown): v is SealedPlaceholder;
41
+ /** Whether `name` is a wire kind of the current protocol (not a legacy or server-authored one). */
42
+ export declare function isSealedWireKind(name: string): boolean;
43
+ /** The wire name of `field` of `kind` entity `entityId` under `key`: its tag, or the field itself
44
+ * when public. */
45
+ export declare function wireFieldOf(key: Uint8Array, kind: string, entityId: string, field: string): string;
46
+ /**
47
+ * Encode `op` for the wire with `k`, the key of its scope. Throws for a kind no manifest names
48
+ * (the server would refuse it) and for server-authored kinds.
49
+ */
50
+ export declare function encodeOp(op: Operation, k: ScopeKey): WireOp;
51
+ export interface OpenedSealed {
52
+ kind: EntityKind;
53
+ field: string;
54
+ value: unknown;
55
+ keyId: string;
56
+ scope: KeyScope;
57
+ }
58
+ /**
59
+ * Open a v3 value that arrived as `wire`.`wireField` of `entityId` in `scope`. Null unless a held
60
+ * key of that scope opens it there, its plaintext names a kind of that wire kind, and the wire field
61
+ * is that field's tag (or the field itself, when public).
62
+ */
63
+ export declare function openSealed(keyring: Keyring, wire: string, entityId: string, wireField: string, scope: string | null, envelope: FieldEnvelopeV3): OpenedSealed | null;
64
+ /** Where the decoder finds an entity's real kind and its own entities. */
65
+ export interface DecodeStore {
66
+ /** The real kind of an entity this device holds, if it holds one by that id. */
67
+ kindOf(entityId: string): EntityKind | null;
68
+ }
69
+ export interface DecodeContext {
70
+ /** Null decodes nothing: every current value stays sealed. */
71
+ keyring: Keyring | null;
72
+ store: DecodeStore;
73
+ /** The legacy (protocol 6) decode of one op: the old envelope rules. */
74
+ openLegacy(op: Operation & {
75
+ op: "set";
76
+ }): Operation | null;
77
+ }
78
+ export interface DecodedOp {
79
+ op: Operation;
80
+ /** For a value opened from a v3 envelope: the key and scope it opened with. */
81
+ opened?: {
82
+ keyId: string;
83
+ scope: KeyScope;
84
+ };
85
+ /** The op arrived under a legacy kind. */
86
+ legacy: boolean;
87
+ }
88
+ /**
89
+ * Decode one page of wire ops. A delete names no real kind: it takes the kind of the entity this
90
+ * device or the same page knows by its id, else stays under its wire kind until one arrives
91
+ * (`LocalStore` hands such a tombstone on). A value no held key opens stays a
92
+ * {@link SealedPlaceholder} under its wire kind and tag; `reviveLockedValues` opens it once a key
93
+ * arrives.
94
+ */
95
+ export declare function decodeOps(wireOps: readonly WireOp[], ctx: DecodeContext): DecodedOp[];
96
+ /**
97
+ * A key repair for a v3 value that opened under a scope other than its entity's (`expected`) or
98
+ * with a key other than the canonical one: `confirmRepairs` decides whether to re-write it.
99
+ */
100
+ export declare function sealedRepair(d: DecodedOp, expected: KeyScope | null, isCanonical: (keyId: string, scope: KeyScope) => boolean): Repair | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlastodo/client-core",
3
- "version": "0.1.13",
3
+ "version": "0.1.14-rc.1",
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",
@@ -1,5 +1,5 @@
1
1
  {
2
- "_doc": "Field envelopes and key wraps (packages/client-core/src/crypto/field.ts, envelope.ts, keyring.ts, identity.ts, asym.ts). AES-256-GCM is deterministic given the IV, so a reimplementation must both open each `envelope`/wrap and reproduce its `ct` from the recorded `iv`. Field v2: plaintext = JSON of `value`, ISO/IEC 7816-4 padded to `paddedLength` (0x80 then zeros; at least 32 bytes, then powers of two up to 1 KiB, then whole KiB); associated data = UTF-8 of JSON.stringify([\"atlas-field\", 2, entity, entity_id, field, scope, kid]). Field v1: JSON of value, unpadded, no associated data. PEK wrap v2: associated data JSON.stringify([\"atlas-pek-wrap\", 2, project_id]); the unbound form has none. project_key_id = hex of the first 16 bytes of SHA-256(\"atlas-pek-id-v1\" || pek). Signing key wrap: associated data JSON.stringify([\"atlas-signing-key-wrap\", 1, lowercase public key hex]). Sealed: X25519 with an ephemeral key, HKDF-SHA256(no salt, info 'atlas-seal-v1'), AES-256-GCM without associated data. Asserted by packages/client-core/src/crypto/envelope-vectors.test.ts and crates/atlas-client.",
2
+ "_doc": "Field envelopes and key wraps (packages/client-core/src/crypto/field.ts, envelope.ts, keyring.ts, identity.ts, asym.ts). AES-256-GCM is deterministic given the IV, so a reimplementation must both open each `envelope`/wrap and reproduce its `ct` from the recorded `iv`. Field v2: plaintext = JSON of `value`, ISO/IEC 7816-4 padded to `paddedLength` (0x80 then zeros; at least 32 bytes, then powers of two up to 1 KiB, then whole KiB); associated data = UTF-8 of JSON.stringify([\"atlas-field\", 2, entity, entity_id, field, scope, kid]). Field v3 (sync protocol 7, docs/decisions/0006): plaintext = JSON of [kind, field, value], padded as v2 but to at least 64 bytes (`padding_v3`); associated data = UTF-8 of JSON.stringify([\"atlas-field\", 3, wire_entity, entity_id, wire_field, scope, kid]), where wire_field is the tag or a public name. Field tags: tag_key = HKDF-SHA256(ikm key, no salt, info 'atlas-field-tag-v1', 32 bytes); tag = hex of the first 16 bytes of HMAC-SHA256(tag_key, UTF-8 of JSON.stringify([\"atlas-field-tag\", 1, entity_id, kind, field])). Field v1: JSON of value, unpadded, no associated data. PEK wrap v2: associated data JSON.stringify([\"atlas-pek-wrap\", 2, project_id]); the unbound form has none. project_key_id = hex of the first 16 bytes of SHA-256(\"atlas-pek-id-v1\" || pek). Signing key wrap: associated data JSON.stringify([\"atlas-signing-key-wrap\", 1, lowercase public key hex]). Sealed: X25519 with an ephemeral key, HKDF-SHA256(no salt, info 'atlas-seal-v1'), AES-256-GCM without associated data. Asserted by packages/client-core/src/crypto/envelope-vectors.test.ts and crates/atlas-client.",
3
3
  "padding": [
4
4
  {
5
5
  "n": 0,
@@ -309,5 +309,174 @@
309
309
  "ct": "0DIwXsDjSjvwJtoy2dkoUv0FliHcWGcF+yCdz6Dg9ftpREyMIQljYNTSPxjAtBtH"
310
310
  }
311
311
  }
312
- }
312
+ },
313
+ "field_v3": [
314
+ {
315
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
316
+ "scope": "personal",
317
+ "entity": "todo.sealed",
318
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
319
+ "field": "1016533f5038961939f84e4de968f7ca",
320
+ "kind": "task",
321
+ "real_field": "title",
322
+ "value": "Buy milk",
323
+ "envelope": {
324
+ "__enc": 3,
325
+ "kid": "dek",
326
+ "iv": "AAECAwQFBgcICQoL",
327
+ "ct": "HCCieraO4DevNf7/3YxaQaGU8k3QFjYQU0W4BR1pALIBEK78r8ESmHSkf+2Ihyg47llgjVrWo9o/lyoZGOP17gyymbCdZV+TFLMtpIs5VG8="
328
+ }
329
+ },
330
+ {
331
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
332
+ "scope": "personal",
333
+ "entity": "todo.sealed",
334
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
335
+ "field": "d8f50043473bd8b76d68b625fb6a3aea",
336
+ "kind": "task",
337
+ "real_field": "is_completed",
338
+ "value": true,
339
+ "envelope": {
340
+ "__enc": 3,
341
+ "kid": "dek",
342
+ "iv": "DA0ODxAREhMUFRYX",
343
+ "ct": "w9wduQwd3n8VQzuK8uRLlzlmmYuSXxfe6defvWSgwPos92ol4cUmYR/zDlLurBrajFa70an0FLNO/3pcuu1KuExVasrmycgCPcLULcZAsno="
344
+ }
345
+ },
346
+ {
347
+ "key": "f0e1d2c3b4a5968778695a4b3c2d1e0f00112233445566778899aabbccddeeff",
348
+ "scope": "project:8f9e0d1c-2b3a-4f5e-8d7c-6b5a49382716",
349
+ "entity": "todo.sealed",
350
+ "entity_id": "5d6e7f80-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
351
+ "field": "83f03fc82fcb43d0e619d152872e5473",
352
+ "kind": "comment",
353
+ "real_field": "body",
354
+ "value": {
355
+ "mentions": [],
356
+ "text": "Looks good"
357
+ },
358
+ "envelope": {
359
+ "__enc": 3,
360
+ "kid": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
361
+ "iv": "GBkaGxwdHh8gISIj",
362
+ "ct": "gM0BOi3BReVqddhqLwhPCwV0kuvTv6bYHRoRgQydJjMyL35iHh3mMqFiua3j+VojoKlqiag/ErJLBG46yLqjUCE7lh7EjjVu2Fpfla1m89w="
363
+ }
364
+ },
365
+ {
366
+ "key": "f0e1d2c3b4a5968778695a4b3c2d1e0f00112233445566778899aabbccddeeff",
367
+ "scope": "project:8f9e0d1c-2b3a-4f5e-8d7c-6b5a49382716",
368
+ "entity": "todo.project",
369
+ "entity_id": "8f9e0d1c-2b3a-4f5e-8d7c-6b5a49382716",
370
+ "field": "archived_at",
371
+ "kind": "project",
372
+ "real_field": "archived_at",
373
+ "value": 1767225600000,
374
+ "envelope": {
375
+ "__enc": 3,
376
+ "kid": "a1b2c3d4e5f60718293a4b5c6d7e8f90",
377
+ "iv": "JCUmJygpKissLS4v",
378
+ "ct": "ziPpSJ2/s+FmTFVoCEiT0qOWycJ9b01Q3jZBdkoi+Nas+Fkl5HXIW+R7YEp1nTOWUF1A3TDFFo+vSVg28cnavZZrVx3Hpg48rpBM0L/sj0M="
379
+ }
380
+ },
381
+ {
382
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
383
+ "scope": "personal",
384
+ "entity": "vault.sealed",
385
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
386
+ "field": "2a2a666e3a097cba81ac5ab96938b0de",
387
+ "kind": "vault.item",
388
+ "real_field": "notes",
389
+ "value": null,
390
+ "envelope": {
391
+ "__enc": 3,
392
+ "kid": "dek",
393
+ "iv": "MDEyMzQ1Njc4OTo7",
394
+ "ct": "6BdHoZwDPi1DR+uBMLUcBvNTKiThEipwS9IMzOizfj5DYAxkLMCbp64kEkl6xtMtrkxdSSuxxSetQGFUQEhR9MS7KHMeOLdxaZlLD9nMEFs="
395
+ }
396
+ }
397
+ ],
398
+ "field_tags": [
399
+ {
400
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
401
+ "tag_key": "572bd3fbe1e9c7e9726d8aad148784bcf83f473cfa0a69065e35b092dbc13fcb",
402
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
403
+ "kind": "task",
404
+ "field": "title",
405
+ "tag": "1016533f5038961939f84e4de968f7ca"
406
+ },
407
+ {
408
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
409
+ "tag_key": "572bd3fbe1e9c7e9726d8aad148784bcf83f473cfa0a69065e35b092dbc13fcb",
410
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
411
+ "kind": "task",
412
+ "field": "project_id",
413
+ "tag": "71a930596b7083c6af5498a40a73e20f"
414
+ },
415
+ {
416
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
417
+ "tag_key": "572bd3fbe1e9c7e9726d8aad148784bcf83f473cfa0a69065e35b092dbc13fcb",
418
+ "entity_id": "5d6e7f80-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
419
+ "kind": "task",
420
+ "field": "title",
421
+ "tag": "fe3259da59051bd1b8f20291bc3d7d1b"
422
+ },
423
+ {
424
+ "key": "f0e1d2c3b4a5968778695a4b3c2d1e0f00112233445566778899aabbccddeeff",
425
+ "tag_key": "346fb253d66eba03ea9300e99e109d69b1f8e1f7b4ce2677a089a9e969eae88e",
426
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
427
+ "kind": "task",
428
+ "field": "title",
429
+ "tag": "5d9f33dc8e6c3acf5e7c6a81cc3b1703"
430
+ },
431
+ {
432
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
433
+ "tag_key": "572bd3fbe1e9c7e9726d8aad148784bcf83f473cfa0a69065e35b092dbc13fcb",
434
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
435
+ "kind": "preference",
436
+ "field": "pin:8f9e0d1c-2b3a-4f5e-8d7c-6b5a49382716:5d6e7f80-1a2b-4c3d-9e8f-0a1b2c3d4e5f",
437
+ "tag": "c3682378312dc5e6ceaad26593274006"
438
+ },
439
+ {
440
+ "key": "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
441
+ "tag_key": "572bd3fbe1e9c7e9726d8aad148784bcf83f473cfa0a69065e35b092dbc13fcb",
442
+ "entity_id": "0b7c3f4e-9a1d-4c2b-8e5f-6a7b8c9d0e1f",
443
+ "kind": "journal.entry",
444
+ "field": "body",
445
+ "tag": "d7f595d8d67e04b6380c408ea90fbf71"
446
+ }
447
+ ],
448
+ "padding_v3": [
449
+ {
450
+ "n": 0,
451
+ "padded": 64
452
+ },
453
+ {
454
+ "n": 30,
455
+ "padded": 64
456
+ },
457
+ {
458
+ "n": 62,
459
+ "padded": 64
460
+ },
461
+ {
462
+ "n": 63,
463
+ "padded": 64
464
+ },
465
+ {
466
+ "n": 64,
467
+ "padded": 128
468
+ },
469
+ {
470
+ "n": 100,
471
+ "padded": 128
472
+ },
473
+ {
474
+ "n": 1023,
475
+ "padded": 1024
476
+ },
477
+ {
478
+ "n": 1024,
479
+ "padded": 2048
480
+ }
481
+ ]
313
482
  }