mem-os-python 1.6.27__py3-none-any.whl
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.
- mem_os_python-1.6.27.dist-info/METADATA +1032 -0
- mem_os_python-1.6.27.dist-info/RECORD +253 -0
- mem_os_python-1.6.27.dist-info/WHEEL +4 -0
- mem_os_python-1.6.27.dist-info/entry_points.txt +2 -0
- mem_os_python-1.6.27.dist-info/licenses/LICENSE +21 -0
- memos/__init__.py +16 -0
- memos/_js/adapters/anthropic.d.ts +279 -0
- memos/_js/adapters/anthropic.d.ts.map +1 -0
- memos/_js/adapters/anthropic.js +348 -0
- memos/_js/adapters/anthropic.js.map +1 -0
- memos/_js/adapters/base.d.ts +10 -0
- memos/_js/adapters/base.d.ts.map +1 -0
- memos/_js/adapters/base.js +2 -0
- memos/_js/adapters/base.js.map +1 -0
- memos/_js/adapters/openai.d.ts +264 -0
- memos/_js/adapters/openai.d.ts.map +1 -0
- memos/_js/adapters/openai.js +342 -0
- memos/_js/adapters/openai.js.map +1 -0
- memos/_js/apps/explorer.d.ts +244 -0
- memos/_js/apps/explorer.d.ts.map +1 -0
- memos/_js/apps/explorer.js +738 -0
- memos/_js/apps/explorer.js.map +1 -0
- memos/_js/citations.d.ts +65 -0
- memos/_js/citations.d.ts.map +1 -0
- memos/_js/citations.js +143 -0
- memos/_js/citations.js.map +1 -0
- memos/_js/cli-browse.d.ts +3 -0
- memos/_js/cli-browse.d.ts.map +1 -0
- memos/_js/cli-browse.js +129 -0
- memos/_js/cli-browse.js.map +1 -0
- memos/_js/cli-completion.d.ts +31 -0
- memos/_js/cli-completion.d.ts.map +1 -0
- memos/_js/cli-completion.js +340 -0
- memos/_js/cli-completion.js.map +1 -0
- memos/_js/cli-help.d.ts +18 -0
- memos/_js/cli-help.d.ts.map +1 -0
- memos/_js/cli-help.js +357 -0
- memos/_js/cli-help.js.map +1 -0
- memos/_js/cli.d.ts +28 -0
- memos/_js/cli.d.ts.map +1 -0
- memos/_js/cli.js +2880 -0
- memos/_js/cli.js.map +1 -0
- memos/_js/compression.d.ts +25 -0
- memos/_js/compression.d.ts.map +1 -0
- memos/_js/compression.js +319 -0
- memos/_js/compression.js.map +1 -0
- memos/_js/confidence-machine.d.ts +99 -0
- memos/_js/confidence-machine.d.ts.map +1 -0
- memos/_js/confidence-machine.js +222 -0
- memos/_js/confidence-machine.js.map +1 -0
- memos/_js/context-pack.d.ts +506 -0
- memos/_js/context-pack.d.ts.map +1 -0
- memos/_js/context-pack.js +1435 -0
- memos/_js/context-pack.js.map +1 -0
- memos/_js/contradictions.d.ts +160 -0
- memos/_js/contradictions.d.ts.map +1 -0
- memos/_js/contradictions.js +255 -0
- memos/_js/contradictions.js.map +1 -0
- memos/_js/dashboard.d.ts +30 -0
- memos/_js/dashboard.d.ts.map +1 -0
- memos/_js/dashboard.js +302 -0
- memos/_js/dashboard.js.map +1 -0
- memos/_js/ebbinghaus.d.ts +29 -0
- memos/_js/ebbinghaus.d.ts.map +1 -0
- memos/_js/ebbinghaus.js +32 -0
- memos/_js/ebbinghaus.js.map +1 -0
- memos/_js/embedding-queue.d.ts +190 -0
- memos/_js/embedding-queue.d.ts.map +1 -0
- memos/_js/embedding-queue.js +316 -0
- memos/_js/embedding-queue.js.map +1 -0
- memos/_js/embeddings.d.ts +187 -0
- memos/_js/embeddings.d.ts.map +1 -0
- memos/_js/embeddings.js +633 -0
- memos/_js/embeddings.js.map +1 -0
- memos/_js/entity-extraction.d.ts +54 -0
- memos/_js/entity-extraction.d.ts.map +1 -0
- memos/_js/entity-extraction.js +325 -0
- memos/_js/entity-extraction.js.map +1 -0
- memos/_js/entity-index.d.ts +93 -0
- memos/_js/entity-index.d.ts.map +1 -0
- memos/_js/entity-index.js +165 -0
- memos/_js/entity-index.js.map +1 -0
- memos/_js/event-memory.d.ts +147 -0
- memos/_js/event-memory.d.ts.map +1 -0
- memos/_js/event-memory.js +290 -0
- memos/_js/event-memory.js.map +1 -0
- memos/_js/external-import.d.ts +98 -0
- memos/_js/external-import.d.ts.map +1 -0
- memos/_js/external-import.js +669 -0
- memos/_js/external-import.js.map +1 -0
- memos/_js/fidelity.d.ts +367 -0
- memos/_js/fidelity.d.ts.map +1 -0
- memos/_js/fidelity.js +911 -0
- memos/_js/fidelity.js.map +1 -0
- memos/_js/graph-expansion.d.ts +119 -0
- memos/_js/graph-expansion.d.ts.map +1 -0
- memos/_js/graph-expansion.js +213 -0
- memos/_js/graph-expansion.js.map +1 -0
- memos/_js/graph-mermaid.d.ts +13 -0
- memos/_js/graph-mermaid.d.ts.map +1 -0
- memos/_js/graph-mermaid.js +32 -0
- memos/_js/graph-mermaid.js.map +1 -0
- memos/_js/graph.d.ts +170 -0
- memos/_js/graph.d.ts.map +1 -0
- memos/_js/graph.js +482 -0
- memos/_js/graph.js.map +1 -0
- memos/_js/harness.d.ts +63 -0
- memos/_js/harness.d.ts.map +1 -0
- memos/_js/harness.js +93 -0
- memos/_js/harness.js.map +1 -0
- memos/_js/importance.d.ts +118 -0
- memos/_js/importance.d.ts.map +1 -0
- memos/_js/importance.js +144 -0
- memos/_js/importance.js.map +1 -0
- memos/_js/index.d.ts +51 -0
- memos/_js/index.d.ts.map +1 -0
- memos/_js/index.js +53 -0
- memos/_js/index.js.map +1 -0
- memos/_js/mcp.d.ts +58 -0
- memos/_js/mcp.d.ts.map +1 -0
- memos/_js/mcp.js +1853 -0
- memos/_js/mcp.js.map +1 -0
- memos/_js/memory.d.ts +1762 -0
- memos/_js/memory.d.ts.map +1 -0
- memos/_js/memory.js +6283 -0
- memos/_js/memory.js.map +1 -0
- memos/_js/procedural.d.ts +74 -0
- memos/_js/procedural.d.ts.map +1 -0
- memos/_js/procedural.js +125 -0
- memos/_js/procedural.js.map +1 -0
- memos/_js/project-config.d.ts +68 -0
- memos/_js/project-config.d.ts.map +1 -0
- memos/_js/project-config.js +59 -0
- memos/_js/project-config.js.map +1 -0
- memos/_js/proof-of-forgetting.d.ts +57 -0
- memos/_js/proof-of-forgetting.d.ts.map +1 -0
- memos/_js/proof-of-forgetting.js +80 -0
- memos/_js/proof-of-forgetting.js.map +1 -0
- memos/_js/provenance.d.ts +122 -0
- memos/_js/provenance.d.ts.map +1 -0
- memos/_js/provenance.js +182 -0
- memos/_js/provenance.js.map +1 -0
- memos/_js/quarantine-commands.d.ts +23 -0
- memos/_js/quarantine-commands.d.ts.map +1 -0
- memos/_js/quarantine-commands.js +42 -0
- memos/_js/quarantine-commands.js.map +1 -0
- memos/_js/quarantine.d.ts +120 -0
- memos/_js/quarantine.d.ts.map +1 -0
- memos/_js/quarantine.js +873 -0
- memos/_js/quarantine.js.map +1 -0
- memos/_js/redact.d.ts +12 -0
- memos/_js/redact.d.ts.map +1 -0
- memos/_js/redact.js +26 -0
- memos/_js/redact.js.map +1 -0
- memos/_js/repl.d.ts +57 -0
- memos/_js/repl.d.ts.map +1 -0
- memos/_js/repl.js +295 -0
- memos/_js/repl.js.map +1 -0
- memos/_js/rerank-local.d.ts +61 -0
- memos/_js/rerank-local.d.ts.map +1 -0
- memos/_js/rerank-local.js +210 -0
- memos/_js/rerank-local.js.map +1 -0
- memos/_js/retain-filter.d.ts +98 -0
- memos/_js/retain-filter.d.ts.map +1 -0
- memos/_js/retain-filter.js +232 -0
- memos/_js/retain-filter.js.map +1 -0
- memos/_js/retrieval.d.ts +265 -0
- memos/_js/retrieval.d.ts.map +1 -0
- memos/_js/retrieval.js +1018 -0
- memos/_js/retrieval.js.map +1 -0
- memos/_js/revert.d.ts +77 -0
- memos/_js/revert.d.ts.map +1 -0
- memos/_js/revert.js +252 -0
- memos/_js/revert.js.map +1 -0
- memos/_js/scope.d.ts +30 -0
- memos/_js/scope.d.ts.map +1 -0
- memos/_js/scope.js +71 -0
- memos/_js/scope.js.map +1 -0
- memos/_js/semantic-screen.d.ts +152 -0
- memos/_js/semantic-screen.d.ts.map +1 -0
- memos/_js/semantic-screen.js +290 -0
- memos/_js/semantic-screen.js.map +1 -0
- memos/_js/session-hooks.d.ts +127 -0
- memos/_js/session-hooks.d.ts.map +1 -0
- memos/_js/session-hooks.js +482 -0
- memos/_js/session-hooks.js.map +1 -0
- memos/_js/storage/sqlite.d.ts +427 -0
- memos/_js/storage/sqlite.d.ts.map +1 -0
- memos/_js/storage/sqlite.js +2428 -0
- memos/_js/storage/sqlite.js.map +1 -0
- memos/_js/sync.d.ts +213 -0
- memos/_js/sync.d.ts.map +1 -0
- memos/_js/sync.js +389 -0
- memos/_js/sync.js.map +1 -0
- memos/_js/tamper-log.d.ts +98 -0
- memos/_js/tamper-log.d.ts.map +1 -0
- memos/_js/tamper-log.js +327 -0
- memos/_js/tamper-log.js.map +1 -0
- memos/_js/temporal.d.ts +92 -0
- memos/_js/temporal.d.ts.map +1 -0
- memos/_js/temporal.js +536 -0
- memos/_js/temporal.js.map +1 -0
- memos/_js/time-travel.d.ts +38 -0
- memos/_js/time-travel.d.ts.map +1 -0
- memos/_js/time-travel.js +46 -0
- memos/_js/time-travel.js.map +1 -0
- memos/_js/timestamp-format.d.ts +18 -0
- memos/_js/timestamp-format.d.ts.map +1 -0
- memos/_js/timestamp-format.js +52 -0
- memos/_js/timestamp-format.js.map +1 -0
- memos/_js/triple-extraction.d.ts +31 -0
- memos/_js/triple-extraction.d.ts.map +1 -0
- memos/_js/triple-extraction.js +116 -0
- memos/_js/triple-extraction.js.map +1 -0
- memos/_js/types.d.ts +2474 -0
- memos/_js/types.d.ts.map +1 -0
- memos/_js/types.js +20 -0
- memos/_js/types.js.map +1 -0
- memos/_js/updater.d.ts +95 -0
- memos/_js/updater.d.ts.map +1 -0
- memos/_js/updater.js +268 -0
- memos/_js/updater.js.map +1 -0
- memos/_js/version.d.ts +17 -0
- memos/_js/version.d.ts.map +1 -0
- memos/_js/version.js +34 -0
- memos/_js/version.js.map +1 -0
- memos/_js/vision/clip-query-cache.d.ts +58 -0
- memos/_js/vision/clip-query-cache.d.ts.map +1 -0
- memos/_js/vision/clip-query-cache.js +79 -0
- memos/_js/vision/clip-query-cache.js.map +1 -0
- memos/_js/vision/clip.d.ts +113 -0
- memos/_js/vision/clip.d.ts.map +1 -0
- memos/_js/vision/clip.js +224 -0
- memos/_js/vision/clip.js.map +1 -0
- memos/_js/vision/fake-clip.d.ts +55 -0
- memos/_js/vision/fake-clip.d.ts.map +1 -0
- memos/_js/vision/fake-clip.js +122 -0
- memos/_js/vision/fake-clip.js.map +1 -0
- memos/_js/vision/index.d.ts +15 -0
- memos/_js/vision/index.d.ts.map +1 -0
- memos/_js/vision/index.js +13 -0
- memos/_js/vision/index.js.map +1 -0
- memos/_js/vision/store.d.ts +137 -0
- memos/_js/vision/store.d.ts.map +1 -0
- memos/_js/vision/store.js +383 -0
- memos/_js/vision/store.js.map +1 -0
- memos/adapters/__init__.py +0 -0
- memos/adapters/crewai.py +385 -0
- memos/adapters/langchain.py +301 -0
- memos/adapters/ollama.py +396 -0
- memos/server/__init__.py +0 -0
- memos/server/main.py +547 -0
- memos/server/routes.py +360 -0
memos/_js/sync.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted cross-machine sync (bundle format v2).
|
|
3
|
+
*
|
|
4
|
+
* Export memories to an encrypted bundle file, move it to another machine
|
|
5
|
+
* (via cloud drive, USB, etc.), and import it there. The bundle is encrypted
|
|
6
|
+
* with AES-256-GCM; the storage medium never sees plaintext.
|
|
7
|
+
*
|
|
8
|
+
* ## Threat model (v2)
|
|
9
|
+
*
|
|
10
|
+
* - The sync bundle may be stored on untrusted storage (cloud drives, USB
|
|
11
|
+
* sticks, email). EVERYTHING about each record — id, content, type,
|
|
12
|
+
* tags, timestamps, namespace, metadata — is encrypted client-side
|
|
13
|
+
* before writing the bundle. Each record gets a fresh random nonce.
|
|
14
|
+
* - Bundle metadata (`version`, `exportedAt`, `salt`, `recordCount`) is
|
|
15
|
+
* plaintext but AUTHENTICATED: it is bound into every record's
|
|
16
|
+
* AES-GCM additional authenticated data (AAD). Tampering with it
|
|
17
|
+
* fails decryption.
|
|
18
|
+
* - The encryption key is derived from a user passphrase via PBKDF2
|
|
19
|
+
* (100k iterations, SHA-256) or read directly from a key file. The key
|
|
20
|
+
* never leaves the user's devices.
|
|
21
|
+
* - An attacker with the bundle but not the key learns only: the bundle
|
|
22
|
+
* version, export timestamp, salt, record count, and per-record
|
|
23
|
+
* ciphertext sizes. They do NOT learn IDs, content, types, tags,
|
|
24
|
+
* timestamps, or any other memory field.
|
|
25
|
+
* - This does NOT protect against an attacker who obtains the passphrase
|
|
26
|
+
* or key file. Choose a strong passphrase.
|
|
27
|
+
* - This does NOT hide access patterns (when you sync, how many records).
|
|
28
|
+
*
|
|
29
|
+
* ## Conflict semantics
|
|
30
|
+
*
|
|
31
|
+
* - `skip-existing` (default): records with IDs already in the local store
|
|
32
|
+
* are skipped. New records are imported with their original IDs and
|
|
33
|
+
* timestamps.
|
|
34
|
+
* - `last-write-wins`: if a record exists locally, the version with the
|
|
35
|
+
* newer `updatedAt` wins and replaces the local copy (same ID,
|
|
36
|
+
* incoming timestamps preserved). On equal timestamps the local copy
|
|
37
|
+
* is kept (deterministic tiebreak).
|
|
38
|
+
* - Content-identical records are never duplicated: if any local record
|
|
39
|
+
* already has the same content hash, the incoming record is skipped.
|
|
40
|
+
*
|
|
41
|
+
* ## Local-first
|
|
42
|
+
*
|
|
43
|
+
* The local SQLite database remains the source of truth. Sync is additive:
|
|
44
|
+
* export writes a snapshot, import merges into the local store. Import
|
|
45
|
+
* never deletes records except replacing the single conflicting record
|
|
46
|
+
* under `last-write-wins`.
|
|
47
|
+
*
|
|
48
|
+
* ## Transport
|
|
49
|
+
*
|
|
50
|
+
* Transport is manual: you move the bundle file yourself (cloud drive,
|
|
51
|
+
* USB stick, `scp`, …). There is no network sync backend — the bundle
|
|
52
|
+
* file IS the transport, and the encryption above is what makes that safe.
|
|
53
|
+
*
|
|
54
|
+
* @module @memos/sync
|
|
55
|
+
*/
|
|
56
|
+
import { createCipheriv, createDecipheriv, createHash, pbkdf2Sync, randomBytes, } from "node:crypto";
|
|
57
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
58
|
+
const SYNC_VERSION = 2;
|
|
59
|
+
const PBKDF2_ITERATIONS = 100_000;
|
|
60
|
+
const KEY_LENGTH = 32; // AES-256
|
|
61
|
+
const NONCE_LENGTH = 12; // GCM standard
|
|
62
|
+
const SALT_LENGTH = 16;
|
|
63
|
+
const MEMORY_TYPES = [
|
|
64
|
+
"fact",
|
|
65
|
+
"preference",
|
|
66
|
+
"context",
|
|
67
|
+
"relationship",
|
|
68
|
+
"entity",
|
|
69
|
+
"custom",
|
|
70
|
+
];
|
|
71
|
+
/**
|
|
72
|
+
* Derive a 32-byte AES key from a passphrase using PBKDF2.
|
|
73
|
+
*/
|
|
74
|
+
export function deriveKey(passphrase, salt) {
|
|
75
|
+
return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, "sha256");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build the AES-GCM additional authenticated data for a bundle.
|
|
79
|
+
* Binds the plaintext bundle metadata to every record's ciphertext so
|
|
80
|
+
* any tampering with version/exportedAt/salt/recordCount fails auth.
|
|
81
|
+
* Key order is fixed so encrypt and decrypt compute identical bytes.
|
|
82
|
+
*/
|
|
83
|
+
export function buildAAD(bundle) {
|
|
84
|
+
return Buffer.from(JSON.stringify({
|
|
85
|
+
v: bundle.version,
|
|
86
|
+
exportedAt: bundle.exportedAt,
|
|
87
|
+
salt: bundle.salt,
|
|
88
|
+
recordCount: bundle.recordCount,
|
|
89
|
+
}), "utf-8");
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the raw encryption key from a passphrase or key buffer.
|
|
93
|
+
* Raw-key length errors use a constant message (never derived from key
|
|
94
|
+
* material — see the CodeQL js/clear-text-logging note in v1).
|
|
95
|
+
*/
|
|
96
|
+
function resolveKey(keyOrPassphrase, salt) {
|
|
97
|
+
if (typeof keyOrPassphrase === "string") {
|
|
98
|
+
return deriveKey(keyOrPassphrase, salt);
|
|
99
|
+
}
|
|
100
|
+
if (keyOrPassphrase.length !== KEY_LENGTH) {
|
|
101
|
+
throw new Error(`raw key must be ${KEY_LENGTH} bytes`);
|
|
102
|
+
}
|
|
103
|
+
return keyOrPassphrase;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Encrypt a plaintext string with AES-256-GCM.
|
|
107
|
+
* `aad`, when given, is authenticated but not encrypted.
|
|
108
|
+
* Returns { nonce, ciphertext, tag } as base64 strings.
|
|
109
|
+
*/
|
|
110
|
+
export function encryptRecord(plaintext, key, aad) {
|
|
111
|
+
const nonce = randomBytes(NONCE_LENGTH);
|
|
112
|
+
const cipher = createCipheriv("aes-256-gcm", key, nonce);
|
|
113
|
+
if (aad)
|
|
114
|
+
cipher.setAAD(aad);
|
|
115
|
+
const ciphertext = Buffer.concat([
|
|
116
|
+
cipher.update(plaintext, "utf-8"),
|
|
117
|
+
cipher.final(),
|
|
118
|
+
]);
|
|
119
|
+
const tag = cipher.getAuthTag();
|
|
120
|
+
return {
|
|
121
|
+
nonce: nonce.toString("base64"),
|
|
122
|
+
ciphertext: ciphertext.toString("base64"),
|
|
123
|
+
tag: tag.toString("base64"),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Decrypt a record. Throws if the key is wrong, data is tampered,
|
|
128
|
+
* or the AAD does not match.
|
|
129
|
+
*/
|
|
130
|
+
export function decryptRecord(encrypted, key, aad) {
|
|
131
|
+
const nonce = Buffer.from(encrypted.nonce, "base64");
|
|
132
|
+
const ciphertext = Buffer.from(encrypted.ciphertext, "base64");
|
|
133
|
+
const tag = Buffer.from(encrypted.tag, "base64");
|
|
134
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
135
|
+
if (aad)
|
|
136
|
+
decipher.setAAD(aad);
|
|
137
|
+
decipher.setAuthTag(tag);
|
|
138
|
+
const plaintext = Buffer.concat([
|
|
139
|
+
decipher.update(ciphertext),
|
|
140
|
+
decipher.final(),
|
|
141
|
+
]);
|
|
142
|
+
return plaintext.toString("utf-8");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Content hash for dedup: SHA-256 over the record's content.
|
|
146
|
+
*/
|
|
147
|
+
export function contentHash(content) {
|
|
148
|
+
return createHash("sha256").update(content, "utf-8").digest("hex");
|
|
149
|
+
}
|
|
150
|
+
/** Validate a decrypted record's shape; throws on mismatch. */
|
|
151
|
+
function validateSyncRecord(data) {
|
|
152
|
+
if (!data || typeof data !== "object") {
|
|
153
|
+
throw new Error("bundle contains a malformed record");
|
|
154
|
+
}
|
|
155
|
+
const r = data;
|
|
156
|
+
if (typeof r.id !== "string" || !r.id) {
|
|
157
|
+
throw new Error("bundle record has an invalid id");
|
|
158
|
+
}
|
|
159
|
+
if (typeof r.content !== "string") {
|
|
160
|
+
throw new Error(`bundle record ${r.id} has invalid content`);
|
|
161
|
+
}
|
|
162
|
+
if (typeof r.type !== "string" ||
|
|
163
|
+
!MEMORY_TYPES.includes(r.type)) {
|
|
164
|
+
throw new Error(`bundle record ${r.id} has invalid type`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof r.createdAt !== "number" || typeof r.updatedAt !== "number") {
|
|
167
|
+
throw new Error(`bundle record ${r.id} has invalid timestamps`);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: r.id,
|
|
171
|
+
content: r.content,
|
|
172
|
+
summary: typeof r.summary === "string" ? r.summary : undefined,
|
|
173
|
+
type: r.type,
|
|
174
|
+
tags: Array.isArray(r.tags)
|
|
175
|
+
? r.tags.filter((t) => typeof t === "string")
|
|
176
|
+
: [],
|
|
177
|
+
importance: typeof r.importance === "number" ? r.importance : 0.5,
|
|
178
|
+
createdAt: r.createdAt,
|
|
179
|
+
updatedAt: r.updatedAt,
|
|
180
|
+
namespace: typeof r.namespace === "string" ? r.namespace : "default",
|
|
181
|
+
metadata: r.metadata && typeof r.metadata === "object"
|
|
182
|
+
? r.metadata
|
|
183
|
+
: {},
|
|
184
|
+
source: typeof r.source === "string" ? r.source : undefined,
|
|
185
|
+
provenance: typeof r.provenance === "string"
|
|
186
|
+
? r.provenance
|
|
187
|
+
: undefined,
|
|
188
|
+
pool: r.pool === "note" || r.pool === "procedure" ? r.pool : "event",
|
|
189
|
+
expiresAt: typeof r.expiresAt === "number" ? r.expiresAt : null,
|
|
190
|
+
validFrom: typeof r.validFrom === "number" ? r.validFrom : null,
|
|
191
|
+
validTo: typeof r.validTo === "number" ? r.validTo : null,
|
|
192
|
+
harness: typeof r.harness === "string" ? r.harness : undefined,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Create an encrypted sync bundle (v2) from memory records.
|
|
197
|
+
*
|
|
198
|
+
* Every record field is encrypted; only the bundle envelope
|
|
199
|
+
* (version/exportedAt/salt/recordCount) is plaintext, and it is
|
|
200
|
+
* authenticated via GCM AAD.
|
|
201
|
+
*/
|
|
202
|
+
export function createBundle(records, keyOrPassphrase) {
|
|
203
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
204
|
+
const key = resolveKey(keyOrPassphrase, salt);
|
|
205
|
+
const exportedAt = new Date().toISOString();
|
|
206
|
+
const aad = buildAAD({
|
|
207
|
+
version: SYNC_VERSION,
|
|
208
|
+
exportedAt,
|
|
209
|
+
salt: salt.toString("base64"),
|
|
210
|
+
recordCount: records.length,
|
|
211
|
+
});
|
|
212
|
+
const encryptedRecords = records.map((r) => {
|
|
213
|
+
const plaintext = JSON.stringify({
|
|
214
|
+
id: r.id,
|
|
215
|
+
content: r.content,
|
|
216
|
+
summary: r.summary,
|
|
217
|
+
type: r.type,
|
|
218
|
+
tags: r.tags,
|
|
219
|
+
importance: r.importance,
|
|
220
|
+
createdAt: r.createdAt,
|
|
221
|
+
updatedAt: r.updatedAt,
|
|
222
|
+
namespace: r.namespace,
|
|
223
|
+
metadata: r.metadata,
|
|
224
|
+
source: r.source,
|
|
225
|
+
provenance: r.provenance,
|
|
226
|
+
pool: r.pool,
|
|
227
|
+
expiresAt: r.expiresAt ?? null,
|
|
228
|
+
validFrom: r.validFrom ?? null,
|
|
229
|
+
validTo: r.validTo ?? null,
|
|
230
|
+
harness: r.harness,
|
|
231
|
+
});
|
|
232
|
+
const { nonce, ciphertext, tag } = encryptRecord(plaintext, key, aad);
|
|
233
|
+
return { nonce, ciphertext, tag };
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
version: SYNC_VERSION,
|
|
237
|
+
exportedAt,
|
|
238
|
+
salt: salt.toString("base64"),
|
|
239
|
+
recordCount: records.length,
|
|
240
|
+
records: encryptedRecords,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Decrypt all records in a bundle. Returns validated SyncRecords with
|
|
245
|
+
* their original IDs and timestamps.
|
|
246
|
+
*
|
|
247
|
+
* Rejects v1 bundles outright: v1 left record IDs and timestamps in
|
|
248
|
+
* plaintext and cannot be upgraded safely — re-export from the source
|
|
249
|
+
* machine with the current memos.
|
|
250
|
+
*/
|
|
251
|
+
export function decryptBundle(bundle, keyOrPassphrase) {
|
|
252
|
+
if (bundle.version === 1) {
|
|
253
|
+
throw new Error("unsupported bundle version 1: v1 bundles expose record IDs and " +
|
|
254
|
+
"timestamps in plaintext and cannot be imported. Re-export from the " +
|
|
255
|
+
"source machine with the current memos version.");
|
|
256
|
+
}
|
|
257
|
+
if (bundle.version !== SYNC_VERSION) {
|
|
258
|
+
throw new Error(`unsupported bundle version: ${bundle.version}`);
|
|
259
|
+
}
|
|
260
|
+
if (!Array.isArray(bundle.records) ||
|
|
261
|
+
bundle.recordCount !== bundle.records.length) {
|
|
262
|
+
throw new Error("bundle metadata mismatch: recordCount does not match records");
|
|
263
|
+
}
|
|
264
|
+
const salt = Buffer.from(bundle.salt, "base64");
|
|
265
|
+
const key = resolveKey(keyOrPassphrase, salt);
|
|
266
|
+
const aad = buildAAD(bundle);
|
|
267
|
+
return bundle.records.map((r) => {
|
|
268
|
+
const plaintext = decryptRecord({ nonce: r.nonce, ciphertext: r.ciphertext, tag: r.tag }, key, aad);
|
|
269
|
+
return validateSyncRecord(JSON.parse(plaintext));
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Write a bundle to a file (JSON).
|
|
274
|
+
*/
|
|
275
|
+
export function writeBundle(path, bundle) {
|
|
276
|
+
writeFileSync(path, JSON.stringify(bundle, null, 2), "utf-8");
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Read a bundle from a file. Validates the envelope shape only —
|
|
280
|
+
* decryption (which authenticates the envelope) happens in
|
|
281
|
+
* `decryptBundle`.
|
|
282
|
+
*/
|
|
283
|
+
export function readBundle(path) {
|
|
284
|
+
if (!existsSync(path)) {
|
|
285
|
+
throw new Error(`bundle file not found: ${path}`);
|
|
286
|
+
}
|
|
287
|
+
const data = JSON.parse(readFileSync(path, "utf-8"));
|
|
288
|
+
if (typeof data.version !== "number" ||
|
|
289
|
+
typeof data.exportedAt !== "string" ||
|
|
290
|
+
typeof data.salt !== "string" ||
|
|
291
|
+
typeof data.recordCount !== "number" ||
|
|
292
|
+
!Array.isArray(data.records)) {
|
|
293
|
+
throw new Error("invalid sync bundle format");
|
|
294
|
+
}
|
|
295
|
+
return data;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Merge decrypted records into a MemOS instance, preserving original
|
|
299
|
+
* IDs and timestamps.
|
|
300
|
+
*
|
|
301
|
+
* - Content-identical records (by SHA-256 of content) are never
|
|
302
|
+
* duplicated: skipped when any local record already has the hash.
|
|
303
|
+
* - `skip-existing`: IDs already present are skipped.
|
|
304
|
+
* - `last-write-wins`: when the incoming `updatedAt` is newer than the
|
|
305
|
+
* local one, the local record is replaced (same ID, incoming
|
|
306
|
+
* timestamps preserved). Equal timestamps keep the local copy.
|
|
307
|
+
*/
|
|
308
|
+
export async function importRecords(memos, records, strategy) {
|
|
309
|
+
const result = {
|
|
310
|
+
imported: 0,
|
|
311
|
+
skipped: 0,
|
|
312
|
+
updated: 0,
|
|
313
|
+
errors: [],
|
|
314
|
+
};
|
|
315
|
+
const graph = await memos.getGraph();
|
|
316
|
+
const nodes = graph.nodes ?? [];
|
|
317
|
+
const updatedAtById = new Map();
|
|
318
|
+
const hashes = new Set();
|
|
319
|
+
for (const n of nodes) {
|
|
320
|
+
updatedAtById.set(n.id, n.updatedAt ?? 0);
|
|
321
|
+
hashes.add(contentHash(n.content));
|
|
322
|
+
}
|
|
323
|
+
for (const r of records) {
|
|
324
|
+
try {
|
|
325
|
+
// Content-hash dedup first: identical content is never duplicated,
|
|
326
|
+
// regardless of ID or strategy.
|
|
327
|
+
if (hashes.has(contentHash(r.content))) {
|
|
328
|
+
result.skipped++;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const existingUpdatedAt = updatedAtById.get(r.id);
|
|
332
|
+
if (existingUpdatedAt === undefined) {
|
|
333
|
+
await memos.importRecord(r);
|
|
334
|
+
updatedAtById.set(r.id, r.updatedAt);
|
|
335
|
+
hashes.add(contentHash(r.content));
|
|
336
|
+
result.imported++;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (strategy === "last-write-wins" && r.updatedAt > existingUpdatedAt) {
|
|
340
|
+
await memos.forget(r.id);
|
|
341
|
+
await memos.importRecord(r);
|
|
342
|
+
updatedAtById.set(r.id, r.updatedAt);
|
|
343
|
+
hashes.add(contentHash(r.content));
|
|
344
|
+
result.updated++;
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
result.skipped++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
result.errors.push(`${r.id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Resolve the sync key from CLI options.
|
|
358
|
+
* Priority: --key-file > --key > MEMOS_SYNC_KEY env var.
|
|
359
|
+
* When none is provided, throws — the CLI prompts interactively on a TTY.
|
|
360
|
+
*/
|
|
361
|
+
/**
|
|
362
|
+
* Resolve the sync key from CLI options.
|
|
363
|
+
* Priority: --passphrase-file > --passphrase > MEMOS_SYNC_KEY env var.
|
|
364
|
+
* When none is provided, throws — the CLI prompts interactively on a TTY.
|
|
365
|
+
*
|
|
366
|
+
* Note: the flag is --passphrase (not --key) because --key is already the
|
|
367
|
+
* global at-rest database encryption key in the CLI.
|
|
368
|
+
*/
|
|
369
|
+
export function resolveSyncKey(opts) {
|
|
370
|
+
if (opts.passphraseFile) {
|
|
371
|
+
const keyData = readFileSync(opts.passphraseFile);
|
|
372
|
+
// Allow both raw 32-byte keys and hex-encoded keys
|
|
373
|
+
const hex = keyData.toString("utf-8").trim();
|
|
374
|
+
if (/^[0-9a-fA-F]{64}$/.test(hex)) {
|
|
375
|
+
return Buffer.from(hex, "hex");
|
|
376
|
+
}
|
|
377
|
+
if (keyData.length === KEY_LENGTH) {
|
|
378
|
+
return keyData;
|
|
379
|
+
}
|
|
380
|
+
throw new Error(`key file must contain a 32-byte raw key or 64-char hex string, got ${keyData.length} bytes`);
|
|
381
|
+
}
|
|
382
|
+
if (opts.passphrase)
|
|
383
|
+
return opts.passphrase;
|
|
384
|
+
const envKey = process.env.MEMOS_SYNC_KEY;
|
|
385
|
+
if (envKey)
|
|
386
|
+
return envKey;
|
|
387
|
+
throw new Error("sync key required: use --passphrase <passphrase>, --passphrase-file <path>, or MEMOS_SYNC_KEY env var");
|
|
388
|
+
}
|
|
389
|
+
//# sourceMappingURL=sync.js.map
|
memos/_js/sync.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync.js","sourceRoot":"","sources":["../src/sync.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tamper-evident mutation log.
|
|
3
|
+
*
|
|
4
|
+
* Every memory mutation (store / import / update / forget) appends one
|
|
5
|
+
* entry to an append-only, hash-chained log stored alongside the nodes
|
|
6
|
+
* in the same database. `verifyTamperLog()` recomputes the chain and —
|
|
7
|
+
* with the deep check — cross-checks each node's current full state
|
|
8
|
+
* against the latest log entry, so it detects:
|
|
9
|
+
*
|
|
10
|
+
* - edited, deleted, or reordered log rows (chain linkage breaks),
|
|
11
|
+
* - nodes edited/deleted directly in the DB without a log entry
|
|
12
|
+
* (state-hash mismatch / missing-entry mismatch),
|
|
13
|
+
* - nodes resurrected after a `forget` entry.
|
|
14
|
+
*
|
|
15
|
+
* Honest threat model: the log is tamper-*evident*, not tamper-*proof*.
|
|
16
|
+
* An attacker with full write access to the database file can rewrite
|
|
17
|
+
* the entire chain from genesis and the log alone cannot tell. What
|
|
18
|
+
* stops that is an externally anchored checkpoint: `memos log
|
|
19
|
+
* checkpoint` prints the tip hash; write it down (paper, another
|
|
20
|
+
* machine, a signed commit) and `memos log verify --expect <hash>`
|
|
21
|
+
* later proves the chain still ends where you left it. Without an
|
|
22
|
+
* external anchor, a full-chain rewrite is undetectable — by anyone,
|
|
23
|
+
* with any local-only scheme.
|
|
24
|
+
*/
|
|
25
|
+
import type { MemoryNode, StorageAdapter, TamperOp } from "./types.js";
|
|
26
|
+
/** Fixed `prevHash` for the first entry — a domain-separated constant. */
|
|
27
|
+
export declare const GENESIS_PREV: string;
|
|
28
|
+
export declare function sha256hex(s: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* Canonical JSON: object keys sorted recursively, arrays in order.
|
|
31
|
+
* Two equal states always serialize to the same string regardless of
|
|
32
|
+
* insertion order — required for a stable hash.
|
|
33
|
+
*/
|
|
34
|
+
export declare function canonicalJson(value: unknown): string;
|
|
35
|
+
/**
|
|
36
|
+
* Hash of a node's full mutable state at mutation time. Covers content,
|
|
37
|
+
* summary, type, tags (sorted), importance, namespace, metadata,
|
|
38
|
+
* provenance/source, validity window, TTL, and creation/update
|
|
39
|
+
* timestamps — everything a mutation could change. Deliberately excludes
|
|
40
|
+
* `accessCount`/`lastAccessed`: reads bump those without a log entry,
|
|
41
|
+
* so hashing them would false-positive on every retrieval.
|
|
42
|
+
*/
|
|
43
|
+
export declare function nodeHash(node: MemoryNode): string;
|
|
44
|
+
/**
|
|
45
|
+
* Hash of one log entry. The chain position `seq` is bound into the
|
|
46
|
+
* hash, so an entry cannot be replayed at a different position without
|
|
47
|
+
* invalidating it. Canonical field order and a versioned prefix make
|
|
48
|
+
* the serialization unambiguous — no JSON key-ordering games.
|
|
49
|
+
*/
|
|
50
|
+
export declare function entryHash(e: {
|
|
51
|
+
seq: number;
|
|
52
|
+
ts: number;
|
|
53
|
+
op: TamperOp;
|
|
54
|
+
nodeId: string;
|
|
55
|
+
nodeHash: string;
|
|
56
|
+
prevHash: string;
|
|
57
|
+
}): string;
|
|
58
|
+
/**
|
|
59
|
+
* Append one mutation to the log. Fail-open: when the storage adapter
|
|
60
|
+
* doesn't implement the log, or the write fails, the mutation itself
|
|
61
|
+
* is never blocked — the log is evidence, not a gate. The adapter
|
|
62
|
+
* assigns the chain position atomically (tip lookup, seq allocation,
|
|
63
|
+
* hash, insert in one transaction).
|
|
64
|
+
*/
|
|
65
|
+
export declare function appendTamperEntry(storage: StorageAdapter, op: TamperOp, node: MemoryNode, ts?: number): Promise<void>;
|
|
66
|
+
export interface TamperVerifyIssue {
|
|
67
|
+
kind: "broken_chain" | "content_mismatch" | "deleted_without_log" | "resurrected" | "tip_mismatch";
|
|
68
|
+
seq?: number;
|
|
69
|
+
nodeId?: string;
|
|
70
|
+
detail: string;
|
|
71
|
+
}
|
|
72
|
+
export interface TamperVerifyResult {
|
|
73
|
+
ok: boolean;
|
|
74
|
+
supported: boolean;
|
|
75
|
+
entriesChecked: number;
|
|
76
|
+
/** Nodes that exist but predate the log (no entries) — informational. */
|
|
77
|
+
unloggedNodes: number;
|
|
78
|
+
/**
|
|
79
|
+
* Entries written by the first-day content-only format (v1). Their
|
|
80
|
+
* chain linkage is verified and their content cross-checked, but
|
|
81
|
+
* they predate full-state hashing — informational.
|
|
82
|
+
*/
|
|
83
|
+
legacyEntries: number;
|
|
84
|
+
issues: TamperVerifyIssue[];
|
|
85
|
+
tipHash: string | null;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Verify the log: chain integrity (seq continuity, prev-hash linkage,
|
|
89
|
+
* entry-hash recomputation) plus a deep cross-check of every node's
|
|
90
|
+
* current content against its latest log entry.
|
|
91
|
+
*
|
|
92
|
+
* `expectTip` anchors the chain to an externally recorded checkpoint:
|
|
93
|
+
* verification fails unless the current tip hash equals it.
|
|
94
|
+
*/
|
|
95
|
+
export declare function verifyTamperLog(storage: StorageAdapter, opts?: {
|
|
96
|
+
expectTip?: string;
|
|
97
|
+
}): Promise<TamperVerifyResult>;
|
|
98
|
+
//# sourceMappingURL=tamper-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tamper-log.d.ts","sourceRoot":"","sources":["../src/tamper-log.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,KAAK,EACV,UAAU,EAEV,cAAc,EAEd,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB,0EAA0E;AAC1E,eAAO,MAAM,YAAY,QAEiD,CAAC;AAE3E,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAapD;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAiCjD;AA+BD;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,CAAC,EAAE;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,QAAQ,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,MAAM,CAYT;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,cAAc,EACvB,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,UAAU,EAChB,EAAE,GAAE,MAAmB,GACtB,OAAO,CAAC,IAAI,CAAC,CAYf;AAED,MAAM,WAAW,iBAAiB;IAChC,IAAI,EACA,cAAc,GACd,kBAAkB,GAClB,qBAAqB,GACrB,aAAa,GACb,cAAc,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,OAAO,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;IACvB,yEAAyE;IACzE,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,cAAc,EACvB,IAAI,GAAE;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA8K7B"}
|