@cello-protocol/client 0.0.2

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.
Files changed (53) hide show
  1. package/dist/agent-hash-queue.d.ts +206 -0
  2. package/dist/agent-hash-queue.d.ts.map +1 -0
  3. package/dist/agent-hash-queue.js +380 -0
  4. package/dist/agent-hash-queue.js.map +1 -0
  5. package/dist/backup-key-derivation.d.ts +37 -0
  6. package/dist/backup-key-derivation.d.ts.map +1 -0
  7. package/dist/backup-key-derivation.js +48 -0
  8. package/dist/backup-key-derivation.js.map +1 -0
  9. package/dist/client-backup.d.ts +144 -0
  10. package/dist/client-backup.d.ts.map +1 -0
  11. package/dist/client-backup.js +273 -0
  12. package/dist/client-backup.js.map +1 -0
  13. package/dist/client.d.ts +249 -0
  14. package/dist/client.d.ts.map +1 -0
  15. package/dist/client.js +4664 -0
  16. package/dist/client.js.map +1 -0
  17. package/dist/connection-policy.d.ts +163 -0
  18. package/dist/connection-policy.d.ts.map +1 -0
  19. package/dist/connection-policy.js +248 -0
  20. package/dist/connection-policy.js.map +1 -0
  21. package/dist/db-key-derivation.d.ts +26 -0
  22. package/dist/db-key-derivation.d.ts.map +1 -0
  23. package/dist/db-key-derivation.js +37 -0
  24. package/dist/db-key-derivation.js.map +1 -0
  25. package/dist/encrypted-file-signing-key-provider.d.ts +92 -0
  26. package/dist/encrypted-file-signing-key-provider.d.ts.map +1 -0
  27. package/dist/encrypted-file-signing-key-provider.js +251 -0
  28. package/dist/encrypted-file-signing-key-provider.js.map +1 -0
  29. package/dist/index.d.ts +13 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +8 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mcp-server.d.ts +270 -0
  34. package/dist/mcp-server.d.ts.map +1 -0
  35. package/dist/mcp-server.js +1155 -0
  36. package/dist/mcp-server.js.map +1 -0
  37. package/dist/network-directory-node.d.ts +85 -0
  38. package/dist/network-directory-node.d.ts.map +1 -0
  39. package/dist/network-directory-node.js +584 -0
  40. package/dist/network-directory-node.js.map +1 -0
  41. package/dist/s3-cloud-storage-provider.d.ts +54 -0
  42. package/dist/s3-cloud-storage-provider.d.ts.map +1 -0
  43. package/dist/s3-cloud-storage-provider.js +78 -0
  44. package/dist/s3-cloud-storage-provider.js.map +1 -0
  45. package/dist/sqlcipher-client-store.d.ts +68 -0
  46. package/dist/sqlcipher-client-store.d.ts.map +1 -0
  47. package/dist/sqlcipher-client-store.js +382 -0
  48. package/dist/sqlcipher-client-store.js.map +1 -0
  49. package/dist/types.d.ts +408 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +7 -0
  52. package/dist/types.js.map +1 -0
  53. package/package.json +48 -0
@@ -0,0 +1,48 @@
1
+ /**
2
+ * backup-key-derivation.ts — HKDF derivation of the cloud backup encryption key.
3
+ *
4
+ * PERSIST-011: backup_key = HKDF-SHA256(ikm=identity_key, salt=none,
5
+ * info='backup-key' || agent_id, length=32)
6
+ *
7
+ * Security invariants (PERSIST-011 SI-001):
8
+ * - identity_key is passed in by the caller; it is NEVER stored here.
9
+ * - The returned backup_key is NEVER logged or persisted by this function.
10
+ * - This function has no side effects — pure transformation only.
11
+ *
12
+ * Key independence (AC-001):
13
+ * - backup_key and db_key use distinct info strings:
14
+ * db_key: info = 'local-db-key\x00' + agentId
15
+ * backup_key: info = 'backup-key\x00' + agentId
16
+ * - For the same identity_key and agentId, backup_key != db_key.
17
+ * - Losing one key does not compromise the other.
18
+ *
19
+ * RFC reference: RFC 5869 (HKDF). Node.js implementation: crypto.hkdfSync.
20
+ */
21
+ import { hkdfSync } from "node:crypto";
22
+ /**
23
+ * Derive a 32-byte cloud backup encryption key from the agent's identity_key.
24
+ *
25
+ * @param identityKey - The agent's long-term root key (32 bytes). Never stored or logged.
26
+ * @param agentId - The stable agent identifier. Binds the key to a specific agent.
27
+ * @returns - A 32-byte Uint8Array suitable for use as an AES-256-GCM key.
28
+ *
29
+ * Security: The backup_key is deterministic — same inputs always produce the same output.
30
+ * This is intentional: the client must re-derive the key on every backup/restore without
31
+ * storing it, using only the identity_key (which is stored separately, protected
32
+ * by the OS keychain or equivalent).
33
+ *
34
+ * The null-byte separator between the literal and agentId prevents prefix-collision
35
+ * attacks where two different (literal, agentId) pairs produce the same concatenation.
36
+ */
37
+ export function deriveBackupKey(identityKey, agentId) {
38
+ // info = 'backup-key' || NUL || agentId (UTF-8 encoded).
39
+ // Distinct from db_key info string ('local-db-key\x00' + agentId) — same identity_key
40
+ // cannot produce the same output for both keys.
41
+ const infoStr = `backup-key\x00${agentId}`;
42
+ const info = Buffer.from(infoStr, "utf8");
43
+ // HKDF-SHA256: salt=none (empty Buffer), length=32 bytes
44
+ // RFC 5869 §2.2: when salt is not provided, a string of HashLen zeros is used.
45
+ const derived = hkdfSync("sha256", identityKey, Buffer.alloc(0), info, 32);
46
+ return new Uint8Array(derived);
47
+ }
48
+ //# sourceMappingURL=backup-key-derivation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-key-derivation.js","sourceRoot":"","sources":["../src/backup-key-derivation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,eAAe,CAAC,WAAuB,EAAE,OAAe;IACtE,yDAAyD;IACzD,sFAAsF;IACtF,gDAAgD;IAChD,MAAM,OAAO,GAAG,iBAAiB,OAAO,EAAE,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAE1C,yDAAyD;IACzD,+EAA+E;IAC/E,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAE3E,OAAO,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;AACjC,CAAC"}
@@ -0,0 +1,144 @@
1
+ /**
2
+ * client-backup.ts — Encrypted cloud backup for the CELLO client.
3
+ *
4
+ * PERSIST-011: Encrypts the local SQLCipher database file with AES-256-GCM
5
+ * using a backup_key derived from identity_key via HKDF, and uploads the
6
+ * ciphertext to the configured CloudStorageProvider.
7
+ *
8
+ * Security invariants:
9
+ * SI-001: backup_key is NEVER uploaded to cloud storage, stored in backup
10
+ * metadata, or included in any log event.
11
+ * SI-002: AES-256-GCM with a fresh random 96-bit nonce per backup.
12
+ * Nonce reuse with GCM leaks both the key and plaintext.
13
+ * SI-003: Restore writes the decrypted plaintext to a temporary file first.
14
+ * The live database is replaced only after checksum verification
15
+ * passes (atomic rename). Corrupt downloads are discarded.
16
+ *
17
+ * Key derivation (RFC 5869 HKDF, NIST SP 800-38D AES-GCM):
18
+ * backup_key = HKDF-SHA256(ikm=identity_key, salt=none,
19
+ * info='backup-key' || NUL || agent_id, length=32)
20
+ *
21
+ * Ciphertext wire format:
22
+ * [nonce(12)] [auth_tag(16)] [encrypted_plaintext]
23
+ *
24
+ * Backup object key (cloud storage path):
25
+ * backups/<agentId>/<timestamp>.enc (timestamp = Date.now() at backup initiation)
26
+ *
27
+ * Backup metadata (stored in ClientStore under 'backup:metadata'):
28
+ * { timestamp: number, destinationUrl: string, checksum: string }
29
+ * where checksum = SHA-256 hex of the full ciphertext blob (nonce+tag+ciphertext).
30
+ */
31
+ import type { CloudStorageProvider, Logger } from "@cello-protocol/interfaces";
32
+ /**
33
+ * AC-007: Operator-facing warning about data that cannot be reconstructed.
34
+ * This constant is exported so the composition root can surface it in operator runbooks
35
+ * and configuration screens. The story requires it to be present in operator-facing
36
+ * documentation, not just in code comments.
37
+ */
38
+ export declare const BACKUP_WARNING: string;
39
+ /** Options for constructing a ClientBackup instance. */
40
+ export interface ClientBackupOptions {
41
+ /** Stable agent identifier — used in log events and storage key derivation. */
42
+ agentId: string;
43
+ /**
44
+ * The agent's long-term identity_key (32 bytes).
45
+ * backup_key and db_key are both derived from this.
46
+ * NEVER stored, NEVER logged (SI-001).
47
+ */
48
+ identityKey: Uint8Array;
49
+ /** Absolute path to the local SQLCipher database file. */
50
+ dbPath: string;
51
+ /**
52
+ * Cloud storage adapter. If null, backup operations are skipped with a WARN log.
53
+ * Null models the case where the operator has not configured cloud storage (AC-005).
54
+ */
55
+ cloudStorage: CloudStorageProvider | null;
56
+ /** Structured logger injected from the composition root. */
57
+ logger: Logger;
58
+ /**
59
+ * Optional: read backup metadata from the local store.
60
+ * Defaults to no-op returning undefined if not provided (useful for pure encryption tests).
61
+ */
62
+ getMetadata?: (key: string) => Promise<Uint8Array | undefined>;
63
+ /**
64
+ * Optional: write backup metadata to the local store.
65
+ * Defaults to no-op if not provided.
66
+ */
67
+ setMetadata?: (key: string, value: Uint8Array) => Promise<void>;
68
+ /**
69
+ * Optional: verify the restored database is readable after restore completes.
70
+ * AC-003: After writing the decrypted file, restore() calls this callback
71
+ * to open the database with db_key and verify records are readable before
72
+ * declaring restore complete. If this callback throws, restore fails.
73
+ * Defaults to no-op if not provided (useful for pure encryption tests).
74
+ */
75
+ verifyRestored?: (dbPath: string) => Promise<void>;
76
+ /**
77
+ * PERSIST-022: Storage destination type for observability.
78
+ * Set by the composition root to "local" or "s3" depending on which
79
+ * CloudStorageProvider is wired in. Logged in client.backup.completed.
80
+ * Defaults to "local" for backward compatibility with M4 LocalCloudStorageProvider usage.
81
+ */
82
+ destinationType?: "local" | "s3";
83
+ }
84
+ /**
85
+ * Manages encrypted cloud backup and restore for the CELLO client database.
86
+ *
87
+ * Lifecycle: construct → backup() | restore() (may be called multiple times).
88
+ */
89
+ export declare class ClientBackup {
90
+ #private;
91
+ constructor(options: ClientBackupOptions);
92
+ /**
93
+ * Encrypt the local SQLCipher database and upload it to cloud storage.
94
+ *
95
+ * On upload failure: logs client.backup.upload.failed and returns without throwing.
96
+ * On no cloud storage configured: logs client.backup.not.configured (WARN) and returns.
97
+ * On success: logs client.backup.completed and stores backup metadata.
98
+ *
99
+ * Pseudocode:
100
+ * 1. If no cloudStorage → warn client.backup.not.configured, return.
101
+ * 2. Derive backup_key via HKDF (RFC 5869).
102
+ * 3. Read plaintext DB file.
103
+ * 4. Generate fresh random 12-byte nonce (SI-002).
104
+ * 5. Encrypt with AES-256-GCM (NIST SP 800-38D).
105
+ * Wire: [nonce(12)] [tag(16)] [ciphertext]
106
+ * 6. Compute SHA-256 of the full blob.
107
+ * 7. Upload blob to cloudStorage.
108
+ * 8. On failure: log client.backup.upload.failed, return { ok: false, reason }.
109
+ * 9. Store metadata { timestamp, destinationUrl, checksum } in local store.
110
+ * 10. Log client.backup.completed.
111
+ * 11. Return { ok: true }.
112
+ */
113
+ backup(): Promise<{
114
+ ok: true;
115
+ } | {
116
+ ok: false;
117
+ reason: string;
118
+ }>;
119
+ /**
120
+ * Download and decrypt the backup, replacing the local database file.
121
+ *
122
+ * On checksum mismatch: logs client.backup.restore.failed with reason 'checksum_mismatch',
123
+ * discards the corrupt download, does NOT overwrite the local DB, and throws.
124
+ *
125
+ * On decrypt failure: logs client.backup.restore.failed with reason 'decrypt_failed',
126
+ * does NOT overwrite the local DB, and throws.
127
+ *
128
+ * On success: logs client.backup.restore.completed.
129
+ *
130
+ * Pseudocode:
131
+ * 1. Derive backup_key via HKDF.
132
+ * 2. Download ciphertext blob from cloudStorage.
133
+ * 3. Read stored checksum from metadata.
134
+ * 4. Compute SHA-256 of downloaded blob.
135
+ * 5. If mismatch → log client.backup.restore.failed (checksum_mismatch), throw (SI-003).
136
+ * 6. Decrypt with AES-256-GCM.
137
+ * 7. Write plaintext to temp path: dbPath + '.restore-tmp' (SI-003).
138
+ * 8. Atomic rename temp → dbPath.
139
+ * 9. Call verifyRestored callback to open DB with db_key and verify readable (AC-003).
140
+ * 10. Log client.backup.restore.completed.
141
+ */
142
+ restore(): Promise<void>;
143
+ }
144
+ //# sourceMappingURL=client-backup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-backup.d.ts","sourceRoot":"","sources":["../src/client-backup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,EAAE,MAAM,4BAA4B,CAAC;AAQ/E;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAGW,CAAC;AAiBvC,wDAAwD;AACxD,MAAM,WAAW,mBAAmB;IAClC,+EAA+E;IAC/E,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,WAAW,EAAE,UAAU,CAAC;IACxB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,YAAY,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAC1C,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC,CAAC;IAC/D;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAChE;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAClC;AAID;;;;GAIG;AACH,qBAAa,YAAY;;gBAWX,OAAO,EAAE,mBAAmB;IAYxC;;;;;;;;;;;;;;;;;;;;OAoBG;IACG,MAAM,IAAI,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAmErE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAkH/B"}
@@ -0,0 +1,273 @@
1
+ /**
2
+ * client-backup.ts — Encrypted cloud backup for the CELLO client.
3
+ *
4
+ * PERSIST-011: Encrypts the local SQLCipher database file with AES-256-GCM
5
+ * using a backup_key derived from identity_key via HKDF, and uploads the
6
+ * ciphertext to the configured CloudStorageProvider.
7
+ *
8
+ * Security invariants:
9
+ * SI-001: backup_key is NEVER uploaded to cloud storage, stored in backup
10
+ * metadata, or included in any log event.
11
+ * SI-002: AES-256-GCM with a fresh random 96-bit nonce per backup.
12
+ * Nonce reuse with GCM leaks both the key and plaintext.
13
+ * SI-003: Restore writes the decrypted plaintext to a temporary file first.
14
+ * The live database is replaced only after checksum verification
15
+ * passes (atomic rename). Corrupt downloads are discarded.
16
+ *
17
+ * Key derivation (RFC 5869 HKDF, NIST SP 800-38D AES-GCM):
18
+ * backup_key = HKDF-SHA256(ikm=identity_key, salt=none,
19
+ * info='backup-key' || NUL || agent_id, length=32)
20
+ *
21
+ * Ciphertext wire format:
22
+ * [nonce(12)] [auth_tag(16)] [encrypted_plaintext]
23
+ *
24
+ * Backup object key (cloud storage path):
25
+ * backups/<agentId>/<timestamp>.enc (timestamp = Date.now() at backup initiation)
26
+ *
27
+ * Backup metadata (stored in ClientStore under 'backup:metadata'):
28
+ * { timestamp: number, destinationUrl: string, checksum: string }
29
+ * where checksum = SHA-256 hex of the full ciphertext blob (nonce+tag+ciphertext).
30
+ */
31
+ import { randomBytes, createCipheriv, createDecipheriv, createHash } from "node:crypto";
32
+ import { readFile, writeFile, rename, unlink } from "node:fs/promises";
33
+ import { deriveBackupKey } from "./backup-key-derivation.js";
34
+ // ─── Constants ────────────────────────────────────────────────────────────────
35
+ const NONCE_BYTES = 12; // 96-bit nonce for AES-256-GCM (NIST SP 800-38D)
36
+ const TAG_BYTES = 16; // 128-bit authentication tag
37
+ /**
38
+ * AC-007: Operator-facing warning about data that cannot be reconstructed.
39
+ * This constant is exported so the composition root can surface it in operator runbooks
40
+ * and configuration screens. The story requires it to be present in operator-facing
41
+ * documentation, not just in code comments.
42
+ */
43
+ export const BACKUP_WARNING = "Conversation Merkle trees are the only data that cannot be reconstructed from the directory. " +
44
+ "If you lose your local database and have no backup, you lose the ability to substantiate any " +
45
+ "dispute about those conversations.";
46
+ // ─── ClientBackup ─────────────────────────────────────────────────────────────
47
+ /**
48
+ * Manages encrypted cloud backup and restore for the CELLO client database.
49
+ *
50
+ * Lifecycle: construct → backup() | restore() (may be called multiple times).
51
+ */
52
+ export class ClientBackup {
53
+ #agentId;
54
+ #identityKey;
55
+ #dbPath;
56
+ #cloudStorage;
57
+ #logger;
58
+ #getMetadata;
59
+ #setMetadata;
60
+ #verifyRestored;
61
+ #destinationType;
62
+ constructor(options) {
63
+ this.#agentId = options.agentId;
64
+ this.#identityKey = options.identityKey;
65
+ this.#dbPath = options.dbPath;
66
+ this.#cloudStorage = options.cloudStorage;
67
+ this.#logger = options.logger;
68
+ this.#getMetadata = options.getMetadata ?? (() => Promise.resolve(undefined));
69
+ this.#setMetadata = options.setMetadata ?? (() => Promise.resolve());
70
+ this.#verifyRestored = options.verifyRestored ?? (() => Promise.resolve());
71
+ this.#destinationType = options.destinationType ?? "local";
72
+ }
73
+ /**
74
+ * Encrypt the local SQLCipher database and upload it to cloud storage.
75
+ *
76
+ * On upload failure: logs client.backup.upload.failed and returns without throwing.
77
+ * On no cloud storage configured: logs client.backup.not.configured (WARN) and returns.
78
+ * On success: logs client.backup.completed and stores backup metadata.
79
+ *
80
+ * Pseudocode:
81
+ * 1. If no cloudStorage → warn client.backup.not.configured, return.
82
+ * 2. Derive backup_key via HKDF (RFC 5869).
83
+ * 3. Read plaintext DB file.
84
+ * 4. Generate fresh random 12-byte nonce (SI-002).
85
+ * 5. Encrypt with AES-256-GCM (NIST SP 800-38D).
86
+ * Wire: [nonce(12)] [tag(16)] [ciphertext]
87
+ * 6. Compute SHA-256 of the full blob.
88
+ * 7. Upload blob to cloudStorage.
89
+ * 8. On failure: log client.backup.upload.failed, return { ok: false, reason }.
90
+ * 9. Store metadata { timestamp, destinationUrl, checksum } in local store.
91
+ * 10. Log client.backup.completed.
92
+ * 11. Return { ok: true }.
93
+ */
94
+ async backup() {
95
+ // AC-005: no cloud storage destination configured
96
+ if (this.#cloudStorage === null) {
97
+ this.#logger.warn("client.backup.not.configured", { agentId: this.#agentId });
98
+ return { ok: true }; // Not configured is not a failure — warn is sufficient
99
+ }
100
+ const startMs = Date.now();
101
+ // Derive backup_key — never stored, never logged (SI-001)
102
+ const backupKey = deriveBackupKey(this.#identityKey, this.#agentId);
103
+ // Read the plaintext database file
104
+ const plaintext = await readFile(this.#dbPath);
105
+ // Generate fresh random nonce per backup (SI-002 — never reuse nonce with GCM)
106
+ const nonce = randomBytes(NONCE_BYTES);
107
+ // Encrypt with AES-256-GCM (NIST SP 800-38D)
108
+ const cipher = createCipheriv("aes-256-gcm", backupKey, nonce);
109
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
110
+ const tag = cipher.getAuthTag();
111
+ // Wire format: [nonce(12)] [tag(16)] [encrypted_plaintext]
112
+ const blob = Buffer.concat([nonce, tag, encrypted]);
113
+ // Zero backup_key immediately after use — it must not linger in memory (SI-001)
114
+ backupKey.fill(0);
115
+ // Compute SHA-256 checksum of the full blob (nonce+tag+ciphertext)
116
+ const checksum = createHash("sha256").update(blob).digest("hex");
117
+ // AC-002: key includes timestamp for uniqueness — backups/{agentId}/{timestamp}.enc
118
+ const storageKey = `backups/${this.#agentId}/${startMs}.enc`;
119
+ const destinationUrl = storageKey;
120
+ // Upload to cloud storage
121
+ try {
122
+ await this.#cloudStorage.upload(storageKey, new Uint8Array(blob));
123
+ }
124
+ catch (err) {
125
+ const reason = err instanceof Error ? err.message : String(err);
126
+ // SI-001: context contains only { reason, agentId } — no key material
127
+ this.#logger.error("client.backup.upload.failed", { reason, agentId: this.#agentId });
128
+ return { ok: false, reason }; // local DB is unaffected; next triggered backup retries the full upload
129
+ }
130
+ // Store backup metadata in the local store
131
+ const metadata = {
132
+ timestamp: Date.now(),
133
+ destinationUrl,
134
+ checksum,
135
+ };
136
+ const metaBytes = Buffer.from(JSON.stringify(metadata), "utf8");
137
+ await this.#setMetadata("backup:metadata", new Uint8Array(metaBytes));
138
+ const durationMs = Date.now() - startMs;
139
+ this.#logger.info("client.backup.completed", {
140
+ agentId: this.#agentId,
141
+ destinationType: this.#destinationType,
142
+ ciphertextBytes: blob.length,
143
+ durationMs,
144
+ });
145
+ return { ok: true };
146
+ }
147
+ /**
148
+ * Download and decrypt the backup, replacing the local database file.
149
+ *
150
+ * On checksum mismatch: logs client.backup.restore.failed with reason 'checksum_mismatch',
151
+ * discards the corrupt download, does NOT overwrite the local DB, and throws.
152
+ *
153
+ * On decrypt failure: logs client.backup.restore.failed with reason 'decrypt_failed',
154
+ * does NOT overwrite the local DB, and throws.
155
+ *
156
+ * On success: logs client.backup.restore.completed.
157
+ *
158
+ * Pseudocode:
159
+ * 1. Derive backup_key via HKDF.
160
+ * 2. Download ciphertext blob from cloudStorage.
161
+ * 3. Read stored checksum from metadata.
162
+ * 4. Compute SHA-256 of downloaded blob.
163
+ * 5. If mismatch → log client.backup.restore.failed (checksum_mismatch), throw (SI-003).
164
+ * 6. Decrypt with AES-256-GCM.
165
+ * 7. Write plaintext to temp path: dbPath + '.restore-tmp' (SI-003).
166
+ * 8. Atomic rename temp → dbPath.
167
+ * 9. Call verifyRestored callback to open DB with db_key and verify readable (AC-003).
168
+ * 10. Log client.backup.restore.completed.
169
+ */
170
+ async restore() {
171
+ // AC-005: no cloud storage destination configured
172
+ if (this.#cloudStorage === null) {
173
+ this.#logger.warn("client.backup.not.configured", { agentId: this.#agentId });
174
+ throw new Error("Cloud storage not configured; cannot restore backup");
175
+ }
176
+ const startMs = Date.now();
177
+ const tempPath = this.#dbPath + ".restore-tmp";
178
+ // Derive backup_key — never stored, never logged (SI-001)
179
+ const backupKey = deriveBackupKey(this.#identityKey, this.#agentId);
180
+ let alreadyLogged = false;
181
+ try {
182
+ // Read stored metadata to get the storage key (destinationUrl).
183
+ // The storage key is timestamp-based (backups/{agentId}/{timestamp}.enc),
184
+ // so we must read it from metadata rather than reconstruct it.
185
+ const storedMeta = await this.#readMetadata();
186
+ // Without stored metadata we cannot verify the checksum — fail immediately.
187
+ if (storedMeta === undefined) {
188
+ this.#logger.error("client.backup.restore.failed", {
189
+ reason: "no_backup_metadata",
190
+ agentId: this.#agentId,
191
+ });
192
+ alreadyLogged = true;
193
+ throw new Error("no_backup_metadata");
194
+ }
195
+ const storageKey = storedMeta.destinationUrl;
196
+ // Download and verify
197
+ const downloaded = await this.#cloudStorage.download(storageKey);
198
+ if (downloaded === undefined) {
199
+ const reason = "backup_not_found";
200
+ this.#logger.error("client.backup.restore.failed", { reason, agentId: this.#agentId });
201
+ alreadyLogged = true;
202
+ throw new Error(`Backup not found at ${storageKey}`);
203
+ }
204
+ const blob = downloaded;
205
+ // Verify checksum against stored metadata (SI-003)
206
+ const actualChecksum = createHash("sha256").update(blob).digest("hex");
207
+ if (storedMeta.checksum !== actualChecksum) {
208
+ // SI-001: no key material in error context
209
+ this.#logger.error("client.backup.restore.failed", {
210
+ reason: "checksum_mismatch",
211
+ agentId: this.#agentId,
212
+ });
213
+ alreadyLogged = true;
214
+ // SI-003: temp file was not written yet; local DB untouched
215
+ throw new Error("checksum_mismatch");
216
+ }
217
+ // Decrypt with AES-256-GCM
218
+ // Wire format: [nonce(12)] [tag(16)] [encrypted_plaintext]
219
+ const buf = Buffer.from(blob);
220
+ const nonce = buf.subarray(0, NONCE_BYTES);
221
+ const tag = buf.subarray(NONCE_BYTES, NONCE_BYTES + TAG_BYTES);
222
+ const encryptedBody = buf.subarray(NONCE_BYTES + TAG_BYTES);
223
+ const decipher = createDecipheriv("aes-256-gcm", backupKey, nonce);
224
+ decipher.setAuthTag(tag);
225
+ const plaintext = Buffer.concat([decipher.update(encryptedBody), decipher.final()]);
226
+ // SI-003: write to temp path first, then atomically rename
227
+ // If write fails mid-way, the live DB is untouched
228
+ await writeFile(tempPath, plaintext);
229
+ await rename(tempPath, this.#dbPath);
230
+ // AC-003: After writing the restored file, open the database with db_key
231
+ // and verify it is readable before declaring restore complete.
232
+ // If verification throws, the error propagates and restore fails.
233
+ await this.#verifyRestored(this.#dbPath);
234
+ const durationMs = Date.now() - startMs;
235
+ this.#logger.info("client.backup.restore.completed", {
236
+ agentId: this.#agentId,
237
+ sourceUrl: storageKey,
238
+ durationMs,
239
+ });
240
+ }
241
+ catch (err) {
242
+ // Log errors if not already logged
243
+ if (!alreadyLogged) {
244
+ const reason = err instanceof Error ? err.message : String(err);
245
+ this.#logger.error("client.backup.restore.failed", {
246
+ reason,
247
+ agentId: this.#agentId,
248
+ });
249
+ }
250
+ // Attempt to clean up the temp file if it exists (ignore if already gone)
251
+ await unlink(tempPath).catch(() => { });
252
+ throw err;
253
+ }
254
+ finally {
255
+ // Zero backup_key on all paths (SI-001)
256
+ backupKey.fill(0);
257
+ }
258
+ }
259
+ // ─── Private helpers ─────────────────────────────────────────────────────────
260
+ /** Read and parse backup metadata from the local store. Returns undefined if not found. */
261
+ async #readMetadata() {
262
+ const raw = await this.#getMetadata("backup:metadata");
263
+ if (raw === undefined)
264
+ return undefined;
265
+ try {
266
+ return JSON.parse(Buffer.from(raw).toString("utf8"));
267
+ }
268
+ catch {
269
+ return undefined;
270
+ }
271
+ }
272
+ }
273
+ //# sourceMappingURL=client-backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-backup.js","sourceRoot":"","sources":["../src/client-backup.ts"],"names":[],"mappings":"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"}