enigma-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +55 -0
- package/README.md +379 -0
- package/apps/cli/bin/enigma.mjs +1117 -0
- package/apps/desktop/src/app.js +1327 -0
- package/apps/desktop/src/index.html +1373 -0
- package/apps/desktop/src/styles.css +386 -0
- package/apps/gateway/bin/enigma-gateway.mjs +52 -0
- package/apps/gateway/src/server.mjs +1403 -0
- package/apps/native-host/README.md +265 -0
- package/apps/native-host/bin/enigma-native-host.mjs +499 -0
- package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
- package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
- package/apps/relay/bin/enigma-relay.mjs +52 -0
- package/apps/relay/src/server.mjs +1571 -0
- package/apps/verifier/bin/enigma-verify.mjs +240 -0
- package/deploy/docker-compose.production.example.yml +150 -0
- package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
- package/package.json +196 -0
- package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
- package/packages/adapters/src/index.js +183 -0
- package/packages/boundary/src/index.js +468 -0
- package/packages/connectors/src/index.js +533 -0
- package/packages/core/src/index.js +766 -0
- package/packages/enterprise/src/index.js +505 -0
- package/packages/importers/src/index.js +1083 -0
- package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
- package/packages/mcp-server/src/index.js +1185 -0
- package/packages/mesh/src/index.js +531 -0
- package/packages/metering/PACKAGE_CONTRACT.md +20 -0
- package/packages/metering/src/index.js +246 -0
- package/packages/optimizer/src/index.js +707 -0
- package/packages/passport/src/index.js +651 -0
- package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
- package/packages/settlement/src/index.js +369 -0
- package/packages/storage/PACKAGE_CONTRACT.md +23 -0
- package/packages/storage/src/index.js +392 -0
- package/packages/vault/src/index.js +904 -0
- package/scripts/build-ai-orchestration-plan.mjs +248 -0
- package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
- package/scripts/build-cloudflare-token-policy.mjs +239 -0
- package/scripts/build-cloudflare-token-request.mjs +314 -0
- package/scripts/build-demo-assets.mjs +263 -0
- package/scripts/build-edge-backend-workers.mjs +353 -0
- package/scripts/build-goal-completion-audit.mjs +398 -0
- package/scripts/build-hosted-probe-worker.mjs +191 -0
- package/scripts/build-operator-acceptance-packet.mjs +254 -0
- package/scripts/build-operator-evidence-starter.mjs +525 -0
- package/scripts/build-production-backend-env-kit.mjs +384 -0
- package/scripts/build-production-dependency-report.mjs +369 -0
- package/scripts/build-production-handoff-packet.mjs +475 -0
- package/scripts/build-production-readiness-manifest.mjs +209 -0
- package/scripts/build-production-status-board.mjs +297 -0
- package/scripts/build-production-storage-migration.mjs +86 -0
- package/scripts/build-production-workplan.mjs +371 -0
- package/scripts/build-review-packet.mjs +594 -0
- package/scripts/check.mjs +419 -0
- package/scripts/cloudflare-ops.mjs +1672 -0
- package/scripts/cloudflare-secret-env.mjs +128 -0
- package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
- package/scripts/infrastructure-readiness.mjs +944 -0
- package/scripts/install-enigma-local.mjs +270 -0
- package/scripts/memory-optimization-benchmark.mjs +242 -0
- package/scripts/monitor-live-endpoints.mjs +358 -0
- package/scripts/release-audit.mjs +5588 -0
- package/scripts/release-provenance.mjs +250 -0
- package/scripts/run-backend-readiness-smoke.mjs +220 -0
- package/scripts/validate-backup-restore-drill.mjs +166 -0
- package/scripts/validate-cloudflare-credentials.mjs +112 -0
- package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
- package/scripts/validate-domain-tls.mjs +248 -0
- package/scripts/validate-hosted-backend-live.mjs +319 -0
- package/scripts/validate-incident-drill.mjs +221 -0
- package/scripts/validate-kms-custody.mjs +235 -0
- package/scripts/validate-legal-compliance-approval.mjs +274 -0
- package/scripts/validate-monitoring-alerting.mjs +263 -0
- package/scripts/validate-network-access-policy.mjs +285 -0
- package/scripts/validate-operator-acceptance.mjs +298 -0
- package/scripts/validate-production-manifests.mjs +325 -0
- package/scripts/validate-public-site-security.mjs +252 -0
- package/scripts/validate-security-threat-model.mjs +285 -0
- package/scripts/validate-service-settlement.mjs +256 -0
- package/scripts/validate-support-sla.mjs +190 -0
- package/scripts/validate-tenant-policy-approval.mjs +228 -0
- package/scripts/validate-usage-metering.mjs +224 -0
- package/scripts/validate-whitepaper-claims.mjs +166 -0
- package/specs/boundary-manifest-v1.schema.json +35 -0
- package/specs/capsule-v1.schema.json +55 -0
- package/specs/claim-boundary-manifest-v1.schema.json +22 -0
- package/specs/connector-profile-v1.schema.json +62 -0
- package/specs/deletion-tombstone-v1.schema.json +26 -0
- package/specs/enterprise-policy-v1.schema.json +59 -0
- package/specs/gateway-request-v1.schema.json +32 -0
- package/specs/goal-completion-audit-v1.schema.json +70 -0
- package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
- package/specs/import-report-v1.schema.json +214 -0
- package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
- package/specs/memory-event-v1.schema.json +42 -0
- package/specs/mesh-node-v1.schema.json +126 -0
- package/specs/passport-v1.schema.json +50 -0
- package/specs/production-handoff-packet-v1.schema.json +149 -0
- package/specs/receipt-v1.schema.json +61 -0
- package/specs/relay-record-v1.schema.json +43 -0
- package/specs/state-checkpoint-v1.schema.json +37 -0
- package/specs/trust-bundle-v1.schema.json +56 -0
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
createHmac,
|
|
6
|
+
generateKeyPairSync,
|
|
7
|
+
randomBytes,
|
|
8
|
+
randomUUID,
|
|
9
|
+
} from 'node:crypto';
|
|
10
|
+
import {
|
|
11
|
+
canonicalize,
|
|
12
|
+
createMemoryAddress,
|
|
13
|
+
createReceipt,
|
|
14
|
+
generateSigningKeyPair,
|
|
15
|
+
MerkleSet,
|
|
16
|
+
hmacSha256Hex,
|
|
17
|
+
receiptHash,
|
|
18
|
+
sha256Hex,
|
|
19
|
+
} from '../../core/src/index.js';
|
|
20
|
+
|
|
21
|
+
const VAULT_SCHEMA = 'enigma.vault.local.v1';
|
|
22
|
+
const EVENT_SCHEMA = 'enigma.memory_event.v1';
|
|
23
|
+
const MEMORY_DOMAIN = 'enigma.memory.v1';
|
|
24
|
+
const ZERO_ROOT = new MerkleSet().root();
|
|
25
|
+
const RECEIPT_PLAINTEXT_KEYS = new Set(['content', 'plaintext', 'text', 'memory', 'prompt', 'response']);
|
|
26
|
+
const PUBLIC_PLAINTEXT_KEYS = new Set([
|
|
27
|
+
'body',
|
|
28
|
+
'content',
|
|
29
|
+
'context',
|
|
30
|
+
'contexttext',
|
|
31
|
+
'description',
|
|
32
|
+
'memory',
|
|
33
|
+
'memories',
|
|
34
|
+
'note',
|
|
35
|
+
'notes',
|
|
36
|
+
'plain',
|
|
37
|
+
'plaintext',
|
|
38
|
+
'prompt',
|
|
39
|
+
'raw',
|
|
40
|
+
'rawmemory',
|
|
41
|
+
'rawmemories',
|
|
42
|
+
'response',
|
|
43
|
+
'summary',
|
|
44
|
+
'text',
|
|
45
|
+
]);
|
|
46
|
+
const SAFE_METADATA_DIGEST_RE = /^(?:sha256:)?[a-f0-9]{64}$|^hmac-sha256:[a-f0-9]{64}$/;
|
|
47
|
+
const SOURCE_REF_ROOT_RE = /^sha256:[a-f0-9]{64}$/;
|
|
48
|
+
|
|
49
|
+
function normalizedPublicKey(key) {
|
|
50
|
+
return String(key).toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isPlaintextLikeKey(key) {
|
|
54
|
+
return PUBLIC_PLAINTEXT_KEYS.has(normalizedPublicKey(key));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isSourceRefPlaintextLikeKey(key) {
|
|
58
|
+
return isPlaintextLikeKey(key) || normalizedPublicKey(key) === 'value';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function privateCommitmentKey(key) {
|
|
62
|
+
return `private_field_${sha256Hex(normalizedPublicKey(key)).slice(0, 12)}_commitment`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function metadataKeyStem(key) {
|
|
66
|
+
const stem = String(key)
|
|
67
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
68
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
69
|
+
.replace(/^_+|_+$/g, '')
|
|
70
|
+
.toLowerCase();
|
|
71
|
+
return stem || `field_${sha256Hex(String(key)).slice(0, 12)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function metadataCommitmentKey(key) {
|
|
75
|
+
return `${metadataKeyStem(key)}_commitment`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isMetadataPrimitive(value) {
|
|
79
|
+
return value === null || ['string', 'number', 'boolean'].includes(typeof value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isSafeMetadataPrimitive(key, value) {
|
|
83
|
+
const exactKey = String(key);
|
|
84
|
+
if (isPlaintextLikeKey(exactKey)) return false;
|
|
85
|
+
if (exactKey.endsWith('_count')) return typeof value === 'number' && Number.isFinite(value);
|
|
86
|
+
if ((exactKey.endsWith('_commitment') || exactKey.endsWith('_hash')) && typeof value === 'string') return SAFE_METADATA_DIGEST_RE.test(value);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function commitMetadataValue(out, key, value) {
|
|
91
|
+
out[metadataCommitmentKey(key)] = asSha256(isMetadataPrimitive(value) ? String(value) : canonical(value));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function sanitizeSourceRefValue(value) {
|
|
95
|
+
if (value === undefined || value === null) return undefined;
|
|
96
|
+
if (typeof value === 'string') return asSha256(value);
|
|
97
|
+
if (['number', 'boolean'].includes(typeof value)) return value;
|
|
98
|
+
if (Array.isArray(value)) return value.map(sanitizeSourceRefValue).filter((item) => item !== undefined);
|
|
99
|
+
if (typeof value !== 'object') return undefined;
|
|
100
|
+
|
|
101
|
+
const out = {};
|
|
102
|
+
for (const [key, item] of Object.entries(value)) {
|
|
103
|
+
if (isSourceRefPlaintextLikeKey(key)) {
|
|
104
|
+
if (item !== undefined && item !== null && ['string', 'number', 'boolean'].includes(typeof item)) {
|
|
105
|
+
out[privateCommitmentKey(key)] = asSha256(String(item));
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (key === 'source_hash' && typeof item === 'string' && SOURCE_REF_ROOT_RE.test(item)) {
|
|
110
|
+
out[key] = item;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const sanitized = sanitizeSourceRefValue(item);
|
|
114
|
+
if (sanitized !== undefined) out[key] = sanitized;
|
|
115
|
+
}
|
|
116
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function sanitizeSourceRefs(refs) {
|
|
120
|
+
if (!Array.isArray(refs)) return [];
|
|
121
|
+
const out = [];
|
|
122
|
+
for (const ref of refs) {
|
|
123
|
+
if (typeof ref === 'string') {
|
|
124
|
+
out.push({ source_hash: asSha256(ref) });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const sanitized = sanitizeSourceRefValue(ref);
|
|
128
|
+
if (sanitized !== undefined) out.push(sanitized);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function sanitizePublicValue(value) {
|
|
134
|
+
if (Array.isArray(value)) return value.map(sanitizePublicValue);
|
|
135
|
+
if (!value || typeof value !== 'object') return value;
|
|
136
|
+
|
|
137
|
+
const out = {};
|
|
138
|
+
for (const [key, item] of Object.entries(value)) {
|
|
139
|
+
if (isPlaintextLikeKey(key)) continue;
|
|
140
|
+
if (key === 'metadata') {
|
|
141
|
+
out.metadata = metadata(item);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (key === 'source_refs' || key === 'sourceRefs') {
|
|
145
|
+
out.source_refs = sanitizeSourceRefs(item);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (item !== undefined) out[key] = sanitizePublicValue(item);
|
|
149
|
+
}
|
|
150
|
+
return out;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sanitizeImportedEventEntry(entry) {
|
|
154
|
+
if (!entry || typeof entry !== 'object') return undefined;
|
|
155
|
+
const event = sanitizePublicValue(entry.event ?? {});
|
|
156
|
+
const hash = typeof entry.hash === 'string' && SOURCE_REF_ROOT_RE.test(entry.hash) ? entry.hash : asSha256(event);
|
|
157
|
+
return { event, canonical: canonical(event), hash };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function sortedStrings(values) {
|
|
161
|
+
return [...values].map(String).sort();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function sameStringSet(left, right) {
|
|
165
|
+
const a = sortedStrings(left);
|
|
166
|
+
const b = sortedStrings(right);
|
|
167
|
+
return a.length === b.length && a.every((value, index) => value === b[index]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function receiptEvent(receipt) {
|
|
171
|
+
if (!receipt || typeof receipt !== 'object') return undefined;
|
|
172
|
+
return receipt.event && typeof receipt.event === 'object' ? receipt.event : receipt;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function deriveReceiptLifecycle(receipts) {
|
|
176
|
+
const active = new Set();
|
|
177
|
+
const tombstones = new Map();
|
|
178
|
+
for (const receipt of receipts) {
|
|
179
|
+
const event = receiptEvent(receipt);
|
|
180
|
+
if (!event) continue;
|
|
181
|
+
if (event.operation === 'create' && event.memory_addr) {
|
|
182
|
+
active.add(event.memory_addr);
|
|
183
|
+
tombstones.delete(event.memory_addr);
|
|
184
|
+
} else if (event.operation === 'update' && event.memory_addr) {
|
|
185
|
+
if (event.source_addr) {
|
|
186
|
+
active.delete(event.source_addr);
|
|
187
|
+
tombstones.set(event.source_addr, {
|
|
188
|
+
schema: 'enigma.deletion_tombstone.v1',
|
|
189
|
+
memory_addr: event.source_addr,
|
|
190
|
+
operation: 'update',
|
|
191
|
+
successor_addr: event.memory_addr,
|
|
192
|
+
tombstoned_at: event.timestamp,
|
|
193
|
+
reason: 'updated',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
active.add(event.memory_addr);
|
|
197
|
+
tombstones.delete(event.memory_addr);
|
|
198
|
+
} else if (event.operation === 'delete' && event.memory_addr) {
|
|
199
|
+
active.delete(event.memory_addr);
|
|
200
|
+
tombstones.set(event.memory_addr, {
|
|
201
|
+
schema: 'enigma.deletion_tombstone.v1',
|
|
202
|
+
memory_addr: event.memory_addr,
|
|
203
|
+
operation: 'delete',
|
|
204
|
+
tombstoned_at: event.timestamp,
|
|
205
|
+
reason: 'deleted',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (typeof receipt.active_set_root === 'string' && receipt.active_set_root !== computeSetRoot(active)) {
|
|
209
|
+
throw new Error('importBundle receipt active_set_root mismatch');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return { active, tombstones };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function importedTombstonesByAddress(tombstones) {
|
|
216
|
+
const out = new Map();
|
|
217
|
+
for (const tombstone of Array.isArray(tombstones) ? tombstones : []) {
|
|
218
|
+
if (tombstone?.memory_addr) out.set(tombstone.memory_addr, { ...sanitizePublicValue(tombstone) });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function reconcileImportedLifecycle(bundle, records, receipts) {
|
|
224
|
+
const derived = deriveReceiptLifecycle(receipts);
|
|
225
|
+
const declaredActive = new Set(Array.isArray(bundle.active_memory_addresses) ? bundle.active_memory_addresses.map(String) : []);
|
|
226
|
+
if (!sameStringSet(declaredActive, derived.active)) throw new Error('importBundle active_memory_addresses do not match receipt-derived state');
|
|
227
|
+
if (bundle.vault?.active_set_root && bundle.vault.active_set_root !== computeSetRoot(derived.active)) {
|
|
228
|
+
throw new Error('importBundle vault active_set_root does not match receipt-derived state');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const importedTombstones = importedTombstonesByAddress(bundle.tombstones);
|
|
232
|
+
if (!sameStringSet(importedTombstones.keys(), derived.tombstones.keys())) {
|
|
233
|
+
throw new Error('importBundle tombstones do not match receipt-derived state');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const [memoryAddr, tombstone] of derived.tombstones) {
|
|
237
|
+
const imported = importedTombstones.get(memoryAddr);
|
|
238
|
+
if (imported?.operation && imported.operation !== tombstone.operation) {
|
|
239
|
+
throw new Error('importBundle tombstone operation does not match receipt-derived state');
|
|
240
|
+
}
|
|
241
|
+
derived.tombstones.set(memoryAddr, { ...tombstone, ...imported, memory_addr: memoryAddr, operation: tombstone.operation });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [memoryAddr, record] of records) {
|
|
245
|
+
if (derived.active.has(memoryAddr)) {
|
|
246
|
+
record.state = 'active';
|
|
247
|
+
} else if (derived.tombstones.has(memoryAddr)) {
|
|
248
|
+
record.state = derived.tombstones.get(memoryAddr).operation === 'update' ? 'superseded' : 'tombstoned';
|
|
249
|
+
} else if (record.state === 'active') {
|
|
250
|
+
throw new Error('importBundle memory object active state is not receipt-derived');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return derived;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
function canonical(value) {
|
|
259
|
+
try {
|
|
260
|
+
const encoded = canonicalize(value);
|
|
261
|
+
return typeof encoded === 'string' ? encoded : JSON.stringify(encoded);
|
|
262
|
+
} catch {
|
|
263
|
+
return stableStringify(value);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function stableStringify(value) {
|
|
268
|
+
if (value === null || typeof value !== 'object') {
|
|
269
|
+
return JSON.stringify(value);
|
|
270
|
+
}
|
|
271
|
+
if (Array.isArray(value)) {
|
|
272
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
273
|
+
}
|
|
274
|
+
return `{${Object.keys(value)
|
|
275
|
+
.sort()
|
|
276
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
277
|
+
.join(',')}}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function stripShaPrefix(value) {
|
|
281
|
+
return String(value).startsWith('sha256:') ? String(value).slice(7) : String(value);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function asSha256(value) {
|
|
285
|
+
try {
|
|
286
|
+
const digest = sha256Hex(value);
|
|
287
|
+
return String(digest).startsWith('sha256:') ? String(digest) : `sha256:${digest}`;
|
|
288
|
+
} catch {
|
|
289
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function asHmacSha256(key, value) {
|
|
294
|
+
try {
|
|
295
|
+
const digest = hmacSha256Hex(key, value);
|
|
296
|
+
return stripShaPrefix(digest);
|
|
297
|
+
} catch {
|
|
298
|
+
return createHmac('sha256', key).update(value).digest('hex');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function keyBytes(key, label) {
|
|
303
|
+
if (Buffer.isBuffer(key)) {
|
|
304
|
+
if (key.length !== 32) throw new Error(`${label} must be 32 bytes`);
|
|
305
|
+
return Buffer.from(key);
|
|
306
|
+
}
|
|
307
|
+
if (typeof key === 'string') {
|
|
308
|
+
const bytes = Buffer.from(key, 'base64');
|
|
309
|
+
if (bytes.length !== 32) throw new Error(`${label} must decode to 32 bytes`);
|
|
310
|
+
return bytes;
|
|
311
|
+
}
|
|
312
|
+
if (key == null) return randomBytes(32);
|
|
313
|
+
throw new Error(`${label} must be a Buffer or base64 string`);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function signingPair() {
|
|
317
|
+
try {
|
|
318
|
+
const pair = generateSigningKeyPair();
|
|
319
|
+
if (pair && typeof pair.then !== 'function') return pair;
|
|
320
|
+
} catch {
|
|
321
|
+
// Fall through to Node's built-in Ed25519 implementation.
|
|
322
|
+
}
|
|
323
|
+
const { publicKey, privateKey } = generateKeyPairSync('ed25519');
|
|
324
|
+
return {
|
|
325
|
+
key_id: `local-ed25519-${randomUUID()}`,
|
|
326
|
+
publicKey,
|
|
327
|
+
privateKey,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function signerFor(pair) {
|
|
332
|
+
return {
|
|
333
|
+
key_id: pair?.key_id ?? pair?.keyId ?? `local-ed25519-${randomUUID()}`,
|
|
334
|
+
alg: 'Ed25519',
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function exportPublicKey(pair) {
|
|
339
|
+
const key = pair?.publicKey;
|
|
340
|
+
if (typeof key === 'string') return key;
|
|
341
|
+
if (key?.export) {
|
|
342
|
+
try {
|
|
343
|
+
return key.export({ type: 'spki', format: 'pem' });
|
|
344
|
+
} catch {
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function exportPrivateKey(pair) {
|
|
352
|
+
const key = pair?.privateKey;
|
|
353
|
+
if (typeof key === 'string') return key;
|
|
354
|
+
if (key?.export) {
|
|
355
|
+
try {
|
|
356
|
+
return key.export({ type: 'pkcs8', format: 'pem' });
|
|
357
|
+
} catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function nowIso(now) {
|
|
365
|
+
if (now instanceof Date) return now.toISOString();
|
|
366
|
+
if (typeof now === 'string') return new Date(now).toISOString();
|
|
367
|
+
return new Date().toISOString();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function metadata(input) {
|
|
371
|
+
const out = {};
|
|
372
|
+
if (!input || typeof input !== 'object') return out;
|
|
373
|
+
for (const [key, value] of Object.entries(input)) {
|
|
374
|
+
if (value === undefined) continue;
|
|
375
|
+
if (isMetadataPrimitive(value)) {
|
|
376
|
+
if (isSafeMetadataPrimitive(key, value)) {
|
|
377
|
+
out[key] = value;
|
|
378
|
+
} else {
|
|
379
|
+
commitMetadataValue(out, key, value);
|
|
380
|
+
}
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
if (Array.isArray(value) || typeof value === 'object') {
|
|
384
|
+
commitMetadataValue(out, key, value);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function pruneUndefined(value) {
|
|
391
|
+
if (Array.isArray(value)) return value.map((item) => pruneUndefined(item));
|
|
392
|
+
if (value && typeof value === 'object') {
|
|
393
|
+
const out = {};
|
|
394
|
+
for (const [key, item] of Object.entries(value)) {
|
|
395
|
+
if (item !== undefined) out[key] = pruneUndefined(item);
|
|
396
|
+
}
|
|
397
|
+
return out;
|
|
398
|
+
}
|
|
399
|
+
return value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function computeSetRoot(values) {
|
|
403
|
+
return new MerkleSet([...values]).root();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function computeReceiptLogRootFrom(receipts, pendingHash) {
|
|
407
|
+
const hashes = receipts.map((receipt) => hashReceipt(receipt));
|
|
408
|
+
if (pendingHash) hashes.push(pendingHash);
|
|
409
|
+
return hashes.length === 0 ? ZERO_ROOT : new MerkleSet(hashes).root();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function hashReceipt(receipt) {
|
|
413
|
+
try {
|
|
414
|
+
const digest = receiptHash(receipt);
|
|
415
|
+
return String(digest).startsWith('sha256:') ? String(digest) : `sha256:${digest}`;
|
|
416
|
+
} catch {
|
|
417
|
+
return asSha256(canonical(receipt));
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function receiptArgs(vault, event, eventHash, previousReceiptHash, activeSetRoot, receiptLogRoot) {
|
|
422
|
+
return pruneUndefined({
|
|
423
|
+
event,
|
|
424
|
+
event_hash: eventHash,
|
|
425
|
+
operation: event.operation,
|
|
426
|
+
tenant_id: event.tenant_id,
|
|
427
|
+
subject_id: event.subject_id,
|
|
428
|
+
memory_addr: event.memory_addr,
|
|
429
|
+
source_addr: event.source_addr,
|
|
430
|
+
provider: event.provider,
|
|
431
|
+
model: event.model,
|
|
432
|
+
policy_id: event.policy_id,
|
|
433
|
+
sequence: event.sequence,
|
|
434
|
+
previous_receipt_hash: previousReceiptHash,
|
|
435
|
+
active_set_root: activeSetRoot,
|
|
436
|
+
receipt_log_root: receiptLogRoot,
|
|
437
|
+
timestamp: event.timestamp,
|
|
438
|
+
signer: vault.signer,
|
|
439
|
+
privateKey: vault.signingKeyPair?.privateKey,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function buildReceipt(vault, event, eventHash, previousReceiptHash, activeSetRoot, receiptLogRoot) {
|
|
444
|
+
const receipt = createReceipt(receiptArgs(vault, event, eventHash, previousReceiptHash, activeSetRoot, receiptLogRoot));
|
|
445
|
+
if (containsPlaintextMarker(receipt)) throw new Error('receipt must not contain plaintext memory content');
|
|
446
|
+
return pruneUndefined(receipt);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function containsPlaintextMarker(value) {
|
|
450
|
+
const encoded = JSON.stringify(value).toLowerCase();
|
|
451
|
+
return ['plaintext', 'content\":', 'text\":'].some((marker) => encoded.includes(marker));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function appendEvent(vault, fields) {
|
|
455
|
+
const sequence = vault.sequence;
|
|
456
|
+
const event = pruneUndefined({
|
|
457
|
+
schema: EVENT_SCHEMA,
|
|
458
|
+
event_id: `evt_${randomUUID()}`,
|
|
459
|
+
operation: fields.operation,
|
|
460
|
+
tenant_id: fields.tenant_id ?? vault.tenant_id,
|
|
461
|
+
subject_id: fields.subject_id ?? vault.subject_id,
|
|
462
|
+
actor_id: fields.actor_id ?? vault.actor_id,
|
|
463
|
+
memory_addr: fields.memory_addr,
|
|
464
|
+
source_addr: fields.source_addr,
|
|
465
|
+
provider: fields.provider,
|
|
466
|
+
model: fields.model,
|
|
467
|
+
purpose: fields.purpose,
|
|
468
|
+
timestamp: nowIso(fields.now),
|
|
469
|
+
sequence,
|
|
470
|
+
policy_id: fields.policy_id ?? vault.policy_id,
|
|
471
|
+
decision: fields.decision,
|
|
472
|
+
metadata: metadata(fields.metadata),
|
|
473
|
+
});
|
|
474
|
+
const eventCanonical = canonical(event);
|
|
475
|
+
const eventHash = asSha256(eventCanonical);
|
|
476
|
+
const previousReceiptHash = vault.receipts.length > 0 ? hashReceipt(vault.receipts[vault.receipts.length - 1]) : 'GENESIS';
|
|
477
|
+
const activeSetRoot = computeSetRoot(vault.activeAddresses);
|
|
478
|
+
const priorReceiptLogRoot = computeReceiptLogRootFrom(vault.receipts);
|
|
479
|
+
const receipt = buildReceipt(vault, event, eventHash, previousReceiptHash, activeSetRoot, priorReceiptLogRoot);
|
|
480
|
+
vault.events.push({ event, canonical: eventCanonical, hash: eventHash });
|
|
481
|
+
vault.receipts.push(receipt);
|
|
482
|
+
vault.sequence += 1;
|
|
483
|
+
vault.updated_at = event.timestamp;
|
|
484
|
+
vault.active_set_root = activeSetRoot;
|
|
485
|
+
vault.receipt_log_root = computeReceiptLogRootFrom(vault.receipts);
|
|
486
|
+
return { event, receipt };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function memoryAddress(vault, subjectId, memoryId, contentHash) {
|
|
490
|
+
const payload = canonical({ domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash });
|
|
491
|
+
try {
|
|
492
|
+
const addr = createMemoryAddress({
|
|
493
|
+
vault_addr_key: vault.addressKey,
|
|
494
|
+
vaultAddressKey: vault.addressKey,
|
|
495
|
+
key: vault.addressKey,
|
|
496
|
+
value: payload,
|
|
497
|
+
components: { domain: MEMORY_DOMAIN, subject_id: subjectId, memory_id: memoryId, content_hash: contentHash },
|
|
498
|
+
domain: MEMORY_DOMAIN,
|
|
499
|
+
subject_id: subjectId,
|
|
500
|
+
subjectId,
|
|
501
|
+
memory_id: memoryId,
|
|
502
|
+
memoryId,
|
|
503
|
+
content_hash: contentHash,
|
|
504
|
+
contentHash,
|
|
505
|
+
});
|
|
506
|
+
if (typeof addr === 'string' && addr.length > 0) return addr;
|
|
507
|
+
} catch {
|
|
508
|
+
// Local HMAC construction below follows the protocol domain separation.
|
|
509
|
+
}
|
|
510
|
+
return `mem:${asHmacSha256(vault.addressKey, payload)}`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function encryptContent(vault, memoryAddr, plaintext) {
|
|
514
|
+
const iv = randomBytes(12);
|
|
515
|
+
const cipher = createCipheriv('aes-256-gcm', vault.vaultKey, iv);
|
|
516
|
+
cipher.setAAD(Buffer.from(memoryAddr));
|
|
517
|
+
const ciphertext = Buffer.concat([cipher.update(String(plaintext), 'utf8'), cipher.final()]);
|
|
518
|
+
const tag = cipher.getAuthTag();
|
|
519
|
+
return {
|
|
520
|
+
alg: 'AES-256-GCM',
|
|
521
|
+
iv: iv.toString('base64'),
|
|
522
|
+
aad: memoryAddr,
|
|
523
|
+
ciphertext: ciphertext.toString('base64'),
|
|
524
|
+
tag: tag.toString('base64'),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function setPlaintextCache(record, plaintext) {
|
|
529
|
+
Object.defineProperty(record, 'plaintext', {
|
|
530
|
+
value: String(plaintext),
|
|
531
|
+
enumerable: false,
|
|
532
|
+
configurable: true,
|
|
533
|
+
writable: true,
|
|
534
|
+
});
|
|
535
|
+
return record.plaintext;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function getPlaintextCache(record) {
|
|
539
|
+
if (!Object.prototype.hasOwnProperty.call(record, 'plaintext')) return undefined;
|
|
540
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, 'plaintext');
|
|
541
|
+
if (descriptor?.enumerable) {
|
|
542
|
+
delete record.plaintext;
|
|
543
|
+
return undefined;
|
|
544
|
+
}
|
|
545
|
+
return record.plaintext;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function decryptContent(vault, record) {
|
|
549
|
+
const cached = getPlaintextCache(record);
|
|
550
|
+
if (cached !== undefined) return cached;
|
|
551
|
+
const encrypted = record.content_ciphertext;
|
|
552
|
+
const decipher = createDecipheriv('aes-256-gcm', vault.vaultKey, Buffer.from(encrypted.iv, 'base64'));
|
|
553
|
+
decipher.setAAD(Buffer.from(encrypted.aad ?? record.memory_addr));
|
|
554
|
+
decipher.setAuthTag(Buffer.from(encrypted.tag, 'base64'));
|
|
555
|
+
const plaintext = Buffer.concat([
|
|
556
|
+
decipher.update(Buffer.from(encrypted.ciphertext, 'base64')),
|
|
557
|
+
decipher.final(),
|
|
558
|
+
]).toString('utf8');
|
|
559
|
+
return setPlaintextCache(record, plaintext);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function publicRecord(record) {
|
|
563
|
+
return pruneUndefined(sanitizePublicValue(record));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function attachInternals(vault) {
|
|
567
|
+
Object.defineProperties(vault, {
|
|
568
|
+
__recordEvent: { value: (fields) => appendEvent(vault, fields), enumerable: false },
|
|
569
|
+
__getRecord: { value: (memoryAddr) => vault.memories.get(memoryAddr), enumerable: false },
|
|
570
|
+
__getPlaintext: { value: (memoryAddr) => decryptContent(vault, vault.memories.get(memoryAddr)), enumerable: false },
|
|
571
|
+
__computeRoots: {
|
|
572
|
+
value: () => ({
|
|
573
|
+
active_set_root: computeSetRoot(vault.activeAddresses),
|
|
574
|
+
receipt_log_root: computeReceiptLogRootFrom(vault.receipts),
|
|
575
|
+
}),
|
|
576
|
+
enumerable: false,
|
|
577
|
+
},
|
|
578
|
+
});
|
|
579
|
+
return vault;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function createVault(args = {}) {
|
|
583
|
+
const createdAt = nowIso(args.now);
|
|
584
|
+
const signingKeyPair = args.signingKeyPair ?? signingPair();
|
|
585
|
+
const vault = {
|
|
586
|
+
schema: VAULT_SCHEMA,
|
|
587
|
+
vault_id: args.vault_id ?? args.vaultId ?? `vault_${randomUUID()}`,
|
|
588
|
+
tenant_id: args.tenant_id ?? args.tenantId ?? 'local',
|
|
589
|
+
subject_id: args.subject_id ?? args.subjectId ?? 'local-subject',
|
|
590
|
+
actor_id: args.actor_id ?? args.actorId ?? 'local-user',
|
|
591
|
+
policy_id: args.policy_id ?? args.policyId ?? 'local-default',
|
|
592
|
+
created_at: createdAt,
|
|
593
|
+
updated_at: createdAt,
|
|
594
|
+
encryption: 'local_aead',
|
|
595
|
+
key_id: args.key_id ?? args.keyId ?? `vault-key-${randomUUID()}`,
|
|
596
|
+
vaultKey: keyBytes(args.vault_key ?? args.vaultKey, 'vault key'),
|
|
597
|
+
addressKey: keyBytes(args.address_key ?? args.addressKey, 'address key'),
|
|
598
|
+
signingKeyPair,
|
|
599
|
+
signer: args.signer ?? signerFor(signingKeyPair),
|
|
600
|
+
memories: new Map(),
|
|
601
|
+
events: [],
|
|
602
|
+
receipts: [],
|
|
603
|
+
activeAddresses: new Set(),
|
|
604
|
+
tombstones: new Map(),
|
|
605
|
+
sequence: 0,
|
|
606
|
+
active_set_root: ZERO_ROOT,
|
|
607
|
+
receipt_log_root: ZERO_ROOT,
|
|
608
|
+
};
|
|
609
|
+
return attachInternals(vault);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
export function remember(args = {}) {
|
|
613
|
+
const vault = args.vault;
|
|
614
|
+
if (!vault) throw new Error('remember requires a vault');
|
|
615
|
+
const plaintext = args.content ?? args.text ?? args.plaintext;
|
|
616
|
+
if (plaintext === undefined || plaintext === null) throw new Error('remember requires content');
|
|
617
|
+
|
|
618
|
+
const memoryId = args.memory_id ?? args.memoryId ?? `mem_${randomUUID()}`;
|
|
619
|
+
const subjectId = args.subject_id ?? args.subjectId ?? vault.subject_id;
|
|
620
|
+
const createdAt = nowIso(args.now);
|
|
621
|
+
const contentHash = asSha256(String(plaintext));
|
|
622
|
+
const memoryAddr = memoryAddress(vault, subjectId, memoryId, contentHash);
|
|
623
|
+
if (vault.tombstones.has(memoryAddr)) throw new Error(`memory address is tombstoned: ${memoryAddr}`);
|
|
624
|
+
|
|
625
|
+
const record = pruneUndefined({
|
|
626
|
+
schema: 'enigma.memory_object.v1',
|
|
627
|
+
memory_id: memoryId,
|
|
628
|
+
memory_addr: memoryAddr,
|
|
629
|
+
subject_id: subjectId,
|
|
630
|
+
kind: args.kind ?? 'fact',
|
|
631
|
+
content_ciphertext: encryptContent(vault, memoryAddr, plaintext),
|
|
632
|
+
content_hash: contentHash,
|
|
633
|
+
content_commitment: `hmac-sha256:${asHmacSha256(vault.addressKey, contentHash)}`,
|
|
634
|
+
source_refs: sanitizeSourceRefs(args.source_refs ?? args.sourceRefs),
|
|
635
|
+
confidence: args.confidence ?? 'user_confirmed',
|
|
636
|
+
sensitivity: args.sensitivity ?? 'normal',
|
|
637
|
+
purpose_tags: Array.isArray(args.purpose_tags ?? args.purposeTags) ? (args.purpose_tags ?? args.purposeTags) : [],
|
|
638
|
+
retention: args.retention ?? 'durable',
|
|
639
|
+
state: 'active',
|
|
640
|
+
metadata: metadata(args.metadata),
|
|
641
|
+
created_at: createdAt,
|
|
642
|
+
updated_at: createdAt,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
vault.memories.set(memoryAddr, record);
|
|
646
|
+
vault.activeAddresses.add(memoryAddr);
|
|
647
|
+
const { event, receipt } = appendEvent(vault, {
|
|
648
|
+
operation: 'create',
|
|
649
|
+
memory_addr: memoryAddr,
|
|
650
|
+
subject_id: subjectId,
|
|
651
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
652
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
653
|
+
now: args.now,
|
|
654
|
+
metadata: {
|
|
655
|
+
memory_id: memoryId,
|
|
656
|
+
kind: record.kind,
|
|
657
|
+
sensitivity: record.sensitivity,
|
|
658
|
+
content_commitment: record.content_commitment,
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
return { memory: publicRecord(record), memory_addr: memoryAddr, event, receipt };
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function recall(args = {}) {
|
|
666
|
+
const vault = args.vault;
|
|
667
|
+
const memoryAddr = args.memory_addr ?? args.memoryAddr;
|
|
668
|
+
if (!vault) throw new Error('recall requires a vault');
|
|
669
|
+
if (!memoryAddr) throw new Error('recall requires memory_addr');
|
|
670
|
+
if (vault.tombstones.has(memoryAddr)) throw new Error(`memory is tombstoned: ${memoryAddr}`);
|
|
671
|
+
if (!vault.activeAddresses.has(memoryAddr)) throw new Error(`memory is not active: ${memoryAddr}`);
|
|
672
|
+
const record = vault.memories.get(memoryAddr);
|
|
673
|
+
if (!record || record.state !== 'active') throw new Error(`memory is not active: ${memoryAddr}`);
|
|
674
|
+
|
|
675
|
+
const plaintext = decryptContent(vault, record);
|
|
676
|
+
const { event, receipt } = appendEvent(vault, {
|
|
677
|
+
operation: args.operation ?? 'read',
|
|
678
|
+
memory_addr: memoryAddr,
|
|
679
|
+
provider: args.provider,
|
|
680
|
+
model: args.model,
|
|
681
|
+
purpose: args.purpose,
|
|
682
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
683
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
684
|
+
now: args.now,
|
|
685
|
+
metadata: { kind: record.kind, sensitivity: record.sensitivity },
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
return { memory: { ...publicRecord(record), content: plaintext }, memory_addr: memoryAddr, content: plaintext, event, receipt };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function updateMemory(args = {}) {
|
|
692
|
+
const vault = args.vault;
|
|
693
|
+
const oldAddr = args.memory_addr ?? args.memoryAddr;
|
|
694
|
+
const plaintext = args.content ?? args.text ?? args.plaintext;
|
|
695
|
+
if (!vault) throw new Error('updateMemory requires a vault');
|
|
696
|
+
if (!oldAddr) throw new Error('updateMemory requires memory_addr');
|
|
697
|
+
if (plaintext === undefined || plaintext === null) throw new Error('updateMemory requires content');
|
|
698
|
+
if (vault.tombstones.has(oldAddr) || !vault.activeAddresses.has(oldAddr)) throw new Error(`memory is not active: ${oldAddr}`);
|
|
699
|
+
|
|
700
|
+
const oldRecord = vault.memories.get(oldAddr);
|
|
701
|
+
if (!oldRecord) throw new Error(`unknown memory: ${oldAddr}`);
|
|
702
|
+
oldRecord.state = 'superseded';
|
|
703
|
+
oldRecord.updated_at = nowIso(args.now);
|
|
704
|
+
vault.activeAddresses.delete(oldAddr);
|
|
705
|
+
|
|
706
|
+
const contentHash = asSha256(String(plaintext));
|
|
707
|
+
const newAddr = memoryAddress(vault, oldRecord.subject_id, oldRecord.memory_id, contentHash);
|
|
708
|
+
const updatedAt = nowIso(args.now);
|
|
709
|
+
const newRecord = pruneUndefined({
|
|
710
|
+
...publicRecord(oldRecord),
|
|
711
|
+
memory_addr: newAddr,
|
|
712
|
+
content_ciphertext: encryptContent(vault, newAddr, plaintext),
|
|
713
|
+
content_hash: contentHash,
|
|
714
|
+
content_commitment: `hmac-sha256:${asHmacSha256(vault.addressKey, contentHash)}`,
|
|
715
|
+
state: 'active',
|
|
716
|
+
updated_at: updatedAt,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
vault.memories.set(newAddr, newRecord);
|
|
720
|
+
vault.activeAddresses.add(newAddr);
|
|
721
|
+
const tombstone = {
|
|
722
|
+
schema: 'enigma.deletion_tombstone.v1',
|
|
723
|
+
memory_addr: oldAddr,
|
|
724
|
+
operation: 'update',
|
|
725
|
+
successor_addr: newAddr,
|
|
726
|
+
tombstoned_at: updatedAt,
|
|
727
|
+
reason: args.reason ?? 'updated',
|
|
728
|
+
};
|
|
729
|
+
vault.tombstones.set(oldAddr, tombstone);
|
|
730
|
+
|
|
731
|
+
const { event, receipt } = appendEvent(vault, {
|
|
732
|
+
operation: 'update',
|
|
733
|
+
memory_addr: newAddr,
|
|
734
|
+
source_addr: oldAddr,
|
|
735
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
736
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
737
|
+
now: args.now,
|
|
738
|
+
metadata: {
|
|
739
|
+
memory_id: oldRecord.memory_id,
|
|
740
|
+
old_memory_addr: oldAddr,
|
|
741
|
+
content_commitment: newRecord.content_commitment,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
old_memory_addr: oldAddr,
|
|
747
|
+
memory_addr: newAddr,
|
|
748
|
+
memory: publicRecord(newRecord),
|
|
749
|
+
tombstone,
|
|
750
|
+
event,
|
|
751
|
+
receipt,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
export function deleteMemory(args = {}) {
|
|
756
|
+
const vault = args.vault;
|
|
757
|
+
const memoryAddr = args.memory_addr ?? args.memoryAddr;
|
|
758
|
+
if (!vault) throw new Error('deleteMemory requires a vault');
|
|
759
|
+
if (!memoryAddr) throw new Error('deleteMemory requires memory_addr');
|
|
760
|
+
if (vault.tombstones.has(memoryAddr)) throw new Error(`memory is already tombstoned: ${memoryAddr}`);
|
|
761
|
+
if (!vault.activeAddresses.has(memoryAddr)) throw new Error(`memory is not active: ${memoryAddr}`);
|
|
762
|
+
|
|
763
|
+
const record = vault.memories.get(memoryAddr);
|
|
764
|
+
if (record) {
|
|
765
|
+
record.state = 'tombstoned';
|
|
766
|
+
record.updated_at = nowIso(args.now);
|
|
767
|
+
delete record.plaintext;
|
|
768
|
+
}
|
|
769
|
+
vault.activeAddresses.delete(memoryAddr);
|
|
770
|
+
const tombstone = {
|
|
771
|
+
schema: 'enigma.deletion_tombstone.v1',
|
|
772
|
+
memory_addr: memoryAddr,
|
|
773
|
+
operation: 'delete',
|
|
774
|
+
tombstoned_at: nowIso(args.now),
|
|
775
|
+
reason: args.reason ?? 'user_deleted',
|
|
776
|
+
};
|
|
777
|
+
vault.tombstones.set(memoryAddr, tombstone);
|
|
778
|
+
|
|
779
|
+
const { event, receipt } = appendEvent(vault, {
|
|
780
|
+
operation: 'delete',
|
|
781
|
+
memory_addr: memoryAddr,
|
|
782
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
783
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
784
|
+
now: args.now,
|
|
785
|
+
metadata: { reason: tombstone.reason },
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
return { memory_addr: memoryAddr, tombstone, event, receipt };
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export function exportBundle(args = {}) {
|
|
792
|
+
const vault = args.vault;
|
|
793
|
+
if (!vault) throw new Error('exportBundle requires a vault');
|
|
794
|
+
const exportReceipt = appendEvent(vault, {
|
|
795
|
+
operation: 'export',
|
|
796
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
797
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
798
|
+
now: args.now,
|
|
799
|
+
metadata: {
|
|
800
|
+
memory_count: vault.memories.size,
|
|
801
|
+
active_count: vault.activeAddresses.size,
|
|
802
|
+
tombstone_count: vault.tombstones.size,
|
|
803
|
+
},
|
|
804
|
+
}).receipt;
|
|
805
|
+
|
|
806
|
+
const roots = vault.__computeRoots();
|
|
807
|
+
const bundle = {
|
|
808
|
+
schema: 'enigma.vault_bundle.v1',
|
|
809
|
+
exported_at: nowIso(args.now),
|
|
810
|
+
vault: {
|
|
811
|
+
schema: vault.schema,
|
|
812
|
+
vault_id: vault.vault_id,
|
|
813
|
+
tenant_id: vault.tenant_id,
|
|
814
|
+
subject_id: vault.subject_id,
|
|
815
|
+
actor_id: vault.actor_id,
|
|
816
|
+
policy_id: vault.policy_id,
|
|
817
|
+
created_at: vault.created_at,
|
|
818
|
+
updated_at: vault.updated_at,
|
|
819
|
+
encryption: vault.encryption,
|
|
820
|
+
key_id: vault.key_id,
|
|
821
|
+
active_set_root: roots.active_set_root,
|
|
822
|
+
receipt_log_root: roots.receipt_log_root,
|
|
823
|
+
sequence: vault.sequence,
|
|
824
|
+
},
|
|
825
|
+
keyring: {
|
|
826
|
+
type: 'local_secret',
|
|
827
|
+
warning: 'Contains local vault keys for offline import; memory payloads remain AES-256-GCM encrypted.',
|
|
828
|
+
vault_key_b64: vault.vaultKey.toString('base64'),
|
|
829
|
+
address_key_b64: vault.addressKey.toString('base64'),
|
|
830
|
+
signer: vault.signer,
|
|
831
|
+
publicKey: exportPublicKey(vault.signingKeyPair),
|
|
832
|
+
privateKey: exportPrivateKey(vault.signingKeyPair),
|
|
833
|
+
},
|
|
834
|
+
memory_objects: [...vault.memories.values()].map(publicRecord),
|
|
835
|
+
active_memory_addresses: [...vault.activeAddresses].sort(),
|
|
836
|
+
tombstones: [...vault.tombstones.values()].sort((a, b) => a.memory_addr.localeCompare(b.memory_addr)),
|
|
837
|
+
events: vault.events,
|
|
838
|
+
receipts: vault.receipts,
|
|
839
|
+
export_receipt: exportReceipt,
|
|
840
|
+
};
|
|
841
|
+
Object.defineProperties(bundle, {
|
|
842
|
+
bundle: { value: bundle, enumerable: false },
|
|
843
|
+
canonical: { value: canonical(bundle), enumerable: false },
|
|
844
|
+
receipt: { value: exportReceipt, enumerable: false },
|
|
845
|
+
});
|
|
846
|
+
return bundle;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function importBundle(args = {}) {
|
|
850
|
+
const source = args.schema === 'enigma.vault_bundle.v1' ? args : (args.bundle?.bundle ?? args.bundle ?? args);
|
|
851
|
+
const bundle = typeof source === 'string' ? JSON.parse(source) : source;
|
|
852
|
+
if (!bundle || bundle.schema !== 'enigma.vault_bundle.v1') throw new Error('importBundle requires an enigma.vault_bundle.v1 bundle');
|
|
853
|
+
const restoredSigningKeyPair = bundle.keyring?.privateKey
|
|
854
|
+
? {
|
|
855
|
+
key_id: bundle.keyring?.signer?.key_id,
|
|
856
|
+
signer: bundle.keyring?.signer,
|
|
857
|
+
publicKey: bundle.keyring?.publicKey,
|
|
858
|
+
privateKey: bundle.keyring?.privateKey,
|
|
859
|
+
}
|
|
860
|
+
: undefined;
|
|
861
|
+
const vault = createVault({
|
|
862
|
+
vault_id: bundle.vault?.vault_id,
|
|
863
|
+
tenant_id: bundle.vault?.tenant_id,
|
|
864
|
+
subject_id: bundle.vault?.subject_id,
|
|
865
|
+
actor_id: args.actor_id ?? args.actorId ?? bundle.vault?.actor_id,
|
|
866
|
+
policy_id: bundle.vault?.policy_id,
|
|
867
|
+
vault_key: bundle.keyring?.vault_key_b64,
|
|
868
|
+
address_key: bundle.keyring?.address_key_b64,
|
|
869
|
+
signingKeyPair: restoredSigningKeyPair,
|
|
870
|
+
signer: bundle.keyring?.signer,
|
|
871
|
+
now: bundle.vault?.created_at,
|
|
872
|
+
});
|
|
873
|
+
vault.created_at = bundle.vault?.created_at ?? vault.created_at;
|
|
874
|
+
vault.updated_at = bundle.vault?.updated_at ?? vault.updated_at;
|
|
875
|
+
vault.sequence = Number.isInteger(bundle.vault?.sequence) ? bundle.vault.sequence : 0;
|
|
876
|
+
vault.events = Array.isArray(bundle.events) ? bundle.events.map(sanitizeImportedEventEntry).filter(Boolean) : [];
|
|
877
|
+
vault.receipts = Array.isArray(bundle.receipts) ? bundle.receipts.map((receipt) => ({
|
|
878
|
+
...sanitizePublicValue(receipt),
|
|
879
|
+
signer: receipt.signer ? { ...sanitizePublicValue(receipt.signer) } : receipt.signer,
|
|
880
|
+
signature: receipt.signature ? { ...sanitizePublicValue(receipt.signature) } : receipt.signature,
|
|
881
|
+
})) : [];
|
|
882
|
+
vault.memories = new Map();
|
|
883
|
+
for (const record of bundle.memory_objects ?? []) {
|
|
884
|
+
vault.memories.set(record.memory_addr, publicRecord(record));
|
|
885
|
+
}
|
|
886
|
+
const lifecycle = reconcileImportedLifecycle(bundle, vault.memories, vault.receipts);
|
|
887
|
+
vault.activeAddresses = lifecycle.active;
|
|
888
|
+
vault.tombstones = lifecycle.tombstones;
|
|
889
|
+
const roots = vault.__computeRoots();
|
|
890
|
+
vault.active_set_root = roots.active_set_root;
|
|
891
|
+
vault.receipt_log_root = roots.receipt_log_root;
|
|
892
|
+
appendEvent(vault, {
|
|
893
|
+
operation: 'import',
|
|
894
|
+
actor_id: args.actor_id ?? args.actorId,
|
|
895
|
+
policy_id: args.policy_id ?? args.policyId,
|
|
896
|
+
now: args.now,
|
|
897
|
+
metadata: {
|
|
898
|
+
imported_memory_count: vault.memories.size,
|
|
899
|
+
imported_active_count: vault.activeAddresses.size,
|
|
900
|
+
imported_tombstone_count: vault.tombstones.size,
|
|
901
|
+
},
|
|
902
|
+
});
|
|
903
|
+
return { vault, active_memory_addresses: [...vault.activeAddresses].sort(), tombstones: [...vault.tombstones.values()] };
|
|
904
|
+
}
|