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.
Files changed (105) hide show
  1. package/LICENSE +55 -0
  2. package/README.md +379 -0
  3. package/apps/cli/bin/enigma.mjs +1117 -0
  4. package/apps/desktop/src/app.js +1327 -0
  5. package/apps/desktop/src/index.html +1373 -0
  6. package/apps/desktop/src/styles.css +386 -0
  7. package/apps/gateway/bin/enigma-gateway.mjs +52 -0
  8. package/apps/gateway/src/server.mjs +1403 -0
  9. package/apps/native-host/README.md +265 -0
  10. package/apps/native-host/bin/enigma-native-host.mjs +499 -0
  11. package/apps/native-host/manifests/com.enigma.native_host.chrome.json +9 -0
  12. package/apps/native-host/manifests/com.enigma.native_host.edge.json +9 -0
  13. package/apps/native-host/manifests/com.enigma.native_host.firefox.json +9 -0
  14. package/apps/relay/bin/enigma-relay.mjs +52 -0
  15. package/apps/relay/src/server.mjs +1571 -0
  16. package/apps/verifier/bin/enigma-verify.mjs +240 -0
  17. package/deploy/docker-compose.production.example.yml +150 -0
  18. package/deploy/kubernetes/enigma-backend.example.yaml +580 -0
  19. package/package.json +196 -0
  20. package/packages/adapters/PACKAGE_CONTRACT.md +47 -0
  21. package/packages/adapters/src/index.js +183 -0
  22. package/packages/boundary/src/index.js +468 -0
  23. package/packages/connectors/src/index.js +533 -0
  24. package/packages/core/src/index.js +766 -0
  25. package/packages/enterprise/src/index.js +505 -0
  26. package/packages/importers/src/index.js +1083 -0
  27. package/packages/mcp-server/bin/enigma-mcp.mjs +19 -0
  28. package/packages/mcp-server/src/index.js +1185 -0
  29. package/packages/mesh/src/index.js +531 -0
  30. package/packages/metering/PACKAGE_CONTRACT.md +20 -0
  31. package/packages/metering/src/index.js +246 -0
  32. package/packages/optimizer/src/index.js +707 -0
  33. package/packages/passport/src/index.js +651 -0
  34. package/packages/settlement/PACKAGE_CONTRACT.md +28 -0
  35. package/packages/settlement/src/index.js +369 -0
  36. package/packages/storage/PACKAGE_CONTRACT.md +23 -0
  37. package/packages/storage/src/index.js +392 -0
  38. package/packages/vault/src/index.js +904 -0
  39. package/scripts/build-ai-orchestration-plan.mjs +248 -0
  40. package/scripts/build-cloudflare-pages-release-packet.mjs +287 -0
  41. package/scripts/build-cloudflare-token-policy.mjs +239 -0
  42. package/scripts/build-cloudflare-token-request.mjs +314 -0
  43. package/scripts/build-demo-assets.mjs +263 -0
  44. package/scripts/build-edge-backend-workers.mjs +353 -0
  45. package/scripts/build-goal-completion-audit.mjs +398 -0
  46. package/scripts/build-hosted-probe-worker.mjs +191 -0
  47. package/scripts/build-operator-acceptance-packet.mjs +254 -0
  48. package/scripts/build-operator-evidence-starter.mjs +525 -0
  49. package/scripts/build-production-backend-env-kit.mjs +384 -0
  50. package/scripts/build-production-dependency-report.mjs +369 -0
  51. package/scripts/build-production-handoff-packet.mjs +475 -0
  52. package/scripts/build-production-readiness-manifest.mjs +209 -0
  53. package/scripts/build-production-status-board.mjs +297 -0
  54. package/scripts/build-production-storage-migration.mjs +86 -0
  55. package/scripts/build-production-workplan.mjs +371 -0
  56. package/scripts/build-review-packet.mjs +594 -0
  57. package/scripts/check.mjs +419 -0
  58. package/scripts/cloudflare-ops.mjs +1672 -0
  59. package/scripts/cloudflare-secret-env.mjs +128 -0
  60. package/scripts/collect-hosted-backend-live-evidence.mjs +263 -0
  61. package/scripts/infrastructure-readiness.mjs +944 -0
  62. package/scripts/install-enigma-local.mjs +270 -0
  63. package/scripts/memory-optimization-benchmark.mjs +242 -0
  64. package/scripts/monitor-live-endpoints.mjs +358 -0
  65. package/scripts/release-audit.mjs +5588 -0
  66. package/scripts/release-provenance.mjs +250 -0
  67. package/scripts/run-backend-readiness-smoke.mjs +220 -0
  68. package/scripts/validate-backup-restore-drill.mjs +166 -0
  69. package/scripts/validate-cloudflare-credentials.mjs +112 -0
  70. package/scripts/validate-cloudflare-worker-inspect.mjs +179 -0
  71. package/scripts/validate-domain-tls.mjs +248 -0
  72. package/scripts/validate-hosted-backend-live.mjs +319 -0
  73. package/scripts/validate-incident-drill.mjs +221 -0
  74. package/scripts/validate-kms-custody.mjs +235 -0
  75. package/scripts/validate-legal-compliance-approval.mjs +274 -0
  76. package/scripts/validate-monitoring-alerting.mjs +263 -0
  77. package/scripts/validate-network-access-policy.mjs +285 -0
  78. package/scripts/validate-operator-acceptance.mjs +298 -0
  79. package/scripts/validate-production-manifests.mjs +325 -0
  80. package/scripts/validate-public-site-security.mjs +252 -0
  81. package/scripts/validate-security-threat-model.mjs +285 -0
  82. package/scripts/validate-service-settlement.mjs +256 -0
  83. package/scripts/validate-support-sla.mjs +190 -0
  84. package/scripts/validate-tenant-policy-approval.mjs +228 -0
  85. package/scripts/validate-usage-metering.mjs +224 -0
  86. package/scripts/validate-whitepaper-claims.mjs +166 -0
  87. package/specs/boundary-manifest-v1.schema.json +35 -0
  88. package/specs/capsule-v1.schema.json +55 -0
  89. package/specs/claim-boundary-manifest-v1.schema.json +22 -0
  90. package/specs/connector-profile-v1.schema.json +62 -0
  91. package/specs/deletion-tombstone-v1.schema.json +26 -0
  92. package/specs/enterprise-policy-v1.schema.json +59 -0
  93. package/specs/gateway-request-v1.schema.json +32 -0
  94. package/specs/goal-completion-audit-v1.schema.json +70 -0
  95. package/specs/hosted-backend-live-evidence-v1.schema.json +147 -0
  96. package/specs/import-report-v1.schema.json +214 -0
  97. package/specs/infrastructure-readiness-manifest-v1.schema.json +130 -0
  98. package/specs/memory-event-v1.schema.json +42 -0
  99. package/specs/mesh-node-v1.schema.json +126 -0
  100. package/specs/passport-v1.schema.json +50 -0
  101. package/specs/production-handoff-packet-v1.schema.json +149 -0
  102. package/specs/receipt-v1.schema.json +61 -0
  103. package/specs/relay-record-v1.schema.json +43 -0
  104. package/specs/state-checkpoint-v1.schema.json +37 -0
  105. package/specs/trust-bundle-v1.schema.json +56 -0
@@ -0,0 +1,1327 @@
1
+ const DESKTOP_SCHEMA = 'enigma.desktop.state.v1';
2
+ const EXPORT_SCHEMA = 'enigma.desktop.export.v1';
3
+ const DELETE_EVIDENCE_SCHEMA = 'enigma.desktop.delete_evidence.v1';
4
+ const VERIFIER_EVIDENCE_SCHEMA = 'enigma.desktop.verifier_evidence.v1';
5
+
6
+ export const DESKTOP_SCREENS = Object.freeze([
7
+ 'vault',
8
+ 'mcp',
9
+ 'clients',
10
+ 'import-export',
11
+ 'verifier',
12
+ 'delete-prove',
13
+ 'mesh',
14
+ 'enterprise'
15
+ ]);
16
+
17
+ const CLIENT_TEMPLATES = Object.freeze([
18
+ Object.freeze({ id: 'claude-desktop', name: 'Claude Desktop', kind: 'mcp-client' }),
19
+ Object.freeze({ id: 'cursor', name: 'Cursor', kind: 'mcp-client' }),
20
+ Object.freeze({ id: 'vscode', name: 'VS Code', kind: 'mcp-client' }),
21
+ Object.freeze({ id: 'browser-bridge', name: 'Browser bridge', kind: 'extension' })
22
+ ]);
23
+
24
+ const INITIAL_NOW = '1970-01-01T00:00:00.000Z';
25
+ const ROOT_RE = /^sha256:[a-f0-9]{64}$/;
26
+ const SIGNATURE_RE = /^[A-Za-z0-9_-]+={0,2}$/;
27
+ const IMPORT_REDACTED_TEXT = '[redacted imported metadata]';
28
+ const BODY_FINGERPRINT_NOTE = 'body_fingerprint is a local descriptor fingerprint, not cryptographic proof unless tied to signed receipts or offline verifier output.';
29
+ const BROWSER_BRIDGE_STATUS_COPY = Object.freeze([
30
+ 'User-click required before browser capture.',
31
+ 'No auto-inject workflow is represented by this shell.',
32
+ 'No sync storage permission is required or claimed.',
33
+ 'Native host local-only.',
34
+ 'Bridge exports a receipt commitment only, never raw memory text.'
35
+ ]);
36
+ const FORBIDDEN_IMPORT_RAW_KEYS = Object.freeze(new Set([
37
+ 'body',
38
+ 'text',
39
+ 'content',
40
+ 'plaintext',
41
+ 'raw_memory',
42
+ 'rawmemory',
43
+ 'memory'
44
+ ]));
45
+ const RAW_LOOKING_VALUE_RE = /(private\s+launch-code|must\s+not\s+leave\s+local\s+memory|raw[\s_-]*memory|plain[\s_-]*text|secret|password|api[\s_-]*key|body\s*:|content\s*:)/i;
46
+
47
+ export function createDesktopState(options = {}) {
48
+ const now = cleanString(options.now) || INITIAL_NOW;
49
+ const vaultId = cleanString(options.vault_id ?? options.vaultId);
50
+ return {
51
+ schema: DESKTOP_SCHEMA,
52
+ version: 1,
53
+ sequence: 0,
54
+ activeScreen: isKnownScreen(options.activeScreen) ? options.activeScreen : 'vault',
55
+ notice: 'Desktop shell state is operational evidence only. It is not cryptographic proof.',
56
+ vault: {
57
+ status: vaultId ? 'ready' : 'missing',
58
+ vault_id: vaultId,
59
+ active_set_root: cleanString(options.active_set_root ?? options.activeSetRoot),
60
+ receipt_log_root: cleanString(options.receipt_log_root ?? options.receiptLogRoot),
61
+ memory_count: 0,
62
+ deleted_count: 0,
63
+ receipt_count: 0,
64
+ created_at: vaultId ? now : '',
65
+ last_event_at: ''
66
+ },
67
+ mcp: {
68
+ status: 'stopped',
69
+ endpoint: cleanString(options.endpoint) || 'mcp://localhost/enigma',
70
+ transport: cleanString(options.transport) || 'stdio',
71
+ port: normalizePort(options.port),
72
+ started_at: '',
73
+ stopped_at: '',
74
+ client_count: 0
75
+ },
76
+ clients: [],
77
+ memories: [],
78
+ search: { query: '', results: [] },
79
+ verifier: {
80
+ status: 'idle',
81
+ evidence: [],
82
+ errors: [],
83
+ checked_at: '',
84
+ note: 'This screen inspects receipt JSON shape only. It does not perform cryptographic verification; run the offline verifier for proof.'
85
+ },
86
+ deletionEvidence: [],
87
+ importExport: {
88
+ import_status: 'idle',
89
+ import_errors: [],
90
+ exported_at: '',
91
+ exportBundle: null
92
+ },
93
+ mesh: {
94
+ status: 'offline',
95
+ peers: 0,
96
+ relays: 0,
97
+ last_witness_at: '',
98
+ note: 'Mesh status is local telemetry until matched to witness receipts.'
99
+ },
100
+ enterprise: {
101
+ status: 'not-configured',
102
+ tenant_id: cleanString(options.tenant_id ?? options.tenantId),
103
+ policy_id: cleanString(options.policy_id ?? options.policyId),
104
+ siem: 'disconnected',
105
+ note: 'Enterprise controls report configuration evidence, not memory custody proof.'
106
+ },
107
+ drafts: {
108
+ receiptInput: '',
109
+ importInput: '',
110
+ exportOutput: ''
111
+ }
112
+ };
113
+ }
114
+
115
+ export function desktopReducer(state = createDesktopState(), action = {}) {
116
+ const type = cleanString(action.type);
117
+ if (!type) return state;
118
+
119
+ switch (type) {
120
+ case 'desktop/select-screen':
121
+ return selectScreen(state, action.screen);
122
+ case 'mcp/start':
123
+ return startMcpState(state, action);
124
+ case 'mcp/stop':
125
+ return stopMcpState(state, action);
126
+ case 'vault/create':
127
+ return createVaultState(state, action);
128
+ case 'memory/remember':
129
+ return rememberMemoryState(state, action);
130
+ case 'memory/delete':
131
+ return deleteMemoryState(state, action);
132
+ case 'memory/search':
133
+ return searchMemoryState(state, action);
134
+ case 'receipts/verify':
135
+ return verifyReceiptState(state, action);
136
+ case 'client/connect':
137
+ return connectClientState(state, action);
138
+ case 'client/disconnect':
139
+ return disconnectClientState(state, action);
140
+ case 'bundle/import':
141
+ return importBundleState(state, action);
142
+ case 'bundle/export':
143
+ return exportBundleState(state, action);
144
+ case 'mesh/update':
145
+ return updateMeshState(state, action);
146
+ case 'enterprise/update':
147
+ return updateEnterpriseState(state, action);
148
+ case 'draft/set':
149
+ return setDraftState(state, action);
150
+ default:
151
+ return withNotice(state, `Unknown action \"${type}\" was rejected.`);
152
+ }
153
+ }
154
+
155
+ export function renderDesktopModel(state = createDesktopState()) {
156
+ const activeMemories = state.memories.filter((memory) => !memory.deleted);
157
+ const deletedMemories = state.memories.filter((memory) => memory.deleted);
158
+ const connectedClients = state.clients.filter((client) => client.status === 'connected');
159
+ const verifierOk = state.verifier.status === 'checked' && state.verifier.errors.length === 0;
160
+
161
+ return {
162
+ schema: 'enigma.desktop.render_model.v1',
163
+ activeScreen: state.activeScreen,
164
+ notice: state.notice,
165
+ navigation: DESKTOP_SCREENS.map((id) => ({ id, label: screenLabel(id), active: id === state.activeScreen })),
166
+ summary: {
167
+ vault: state.vault.status,
168
+ mcp: state.mcp.status,
169
+ clients: connectedClients.length,
170
+ memories: activeMemories.length,
171
+ deleted: deletedMemories.length,
172
+ verifier: verifierOk ? 'shape-clean' : state.verifier.status,
173
+ mesh: state.mesh.status,
174
+ enterprise: state.enterprise.status
175
+ },
176
+ screens: {
177
+ vault: {
178
+ title: 'Vault status',
179
+ status: state.vault.status,
180
+ vault_id: state.vault.vault_id,
181
+ active_set_root: state.vault.active_set_root,
182
+ receipt_log_root: state.vault.receipt_log_root,
183
+ metrics: [
184
+ { label: 'Active memories', value: activeMemories.length },
185
+ { label: 'Deleted memories', value: deletedMemories.length },
186
+ { label: 'Receipts observed', value: state.vault.receipt_count }
187
+ ],
188
+ memories: state.memories.map(renderMemoryRow)
189
+ },
190
+ mcp: {
191
+ title: 'MCP server status',
192
+ status: state.mcp.status,
193
+ endpoint: state.mcp.endpoint,
194
+ transport: state.mcp.transport,
195
+ port: state.mcp.port,
196
+ client_count: connectedClients.length,
197
+ honest_status: state.mcp.status === 'running' ? 'local server claimed running' : 'local server stopped'
198
+ },
199
+ clients: {
200
+ title: 'Client connections',
201
+ templates: CLIENT_TEMPLATES.map((template) => ({ ...template, connected: state.clients.some((client) => client.id === template.id && client.status === 'connected') })),
202
+ connected: state.clients.map((client) => ({ ...client })),
203
+ browser_bridge_status: BROWSER_BRIDGE_STATUS_COPY.slice()
204
+ },
205
+ importExport: {
206
+ title: 'Import / export',
207
+ import_status: state.importExport.import_status,
208
+ import_errors: state.importExport.import_errors.slice(),
209
+ exported_at: state.importExport.exported_at,
210
+ exportBundle: state.importExport.exportBundle
211
+ },
212
+ verifier: {
213
+ title: 'Receipt shape inspector',
214
+ status: state.verifier.status,
215
+ checked_at: state.verifier.checked_at,
216
+ note: state.verifier.note,
217
+ errors: state.verifier.errors.slice(),
218
+ evidence: state.verifier.evidence.map((item) => ({ ...item })),
219
+ body_fingerprint_note: BODY_FINGERPRINT_NOTE
220
+ },
221
+ deleteProve: {
222
+ title: 'Delete-and-prove flow',
223
+ note: 'Deletion evidence is queued until a verified deletion receipt is supplied.',
224
+ pending: state.deletionEvidence.filter((item) => item.status !== 'matched_receipt').map((item) => ({ ...item })),
225
+ completed: state.deletionEvidence.filter((item) => item.status === 'matched_receipt').map((item) => ({ ...item }))
226
+ },
227
+ mesh: {
228
+ title: 'Mesh status',
229
+ status: state.mesh.status,
230
+ peers: state.mesh.peers,
231
+ relays: state.mesh.relays,
232
+ last_witness_at: state.mesh.last_witness_at,
233
+ note: state.mesh.note
234
+ },
235
+ enterprise: {
236
+ title: 'Enterprise status',
237
+ status: state.enterprise.status,
238
+ tenant_id: state.enterprise.tenant_id,
239
+ policy_id: state.enterprise.policy_id,
240
+ siem: state.enterprise.siem,
241
+ note: state.enterprise.note
242
+ }
243
+ }
244
+ };
245
+ }
246
+
247
+ export function startMcp(options = {}) {
248
+ return { ...options, type: 'mcp/start' };
249
+ }
250
+
251
+ export function stopMcp(options = {}) {
252
+ return { ...options, type: 'mcp/stop' };
253
+ }
254
+
255
+ export function createVault(options = {}) {
256
+ return { ...options, type: 'vault/create' };
257
+ }
258
+
259
+ export function rememberMemory(memory = {}, options = {}) {
260
+ return { ...options, memory, type: 'memory/remember' };
261
+ }
262
+
263
+ export function deleteMemory(memoryId, options = {}) {
264
+ return { ...options, memory_id: memoryId, type: 'memory/delete' };
265
+ }
266
+
267
+ export function searchMemories(query, options = {}) {
268
+ return { ...options, query, type: 'memory/search' };
269
+ }
270
+
271
+ export function verifyReceipts(input = {}, options = {}) {
272
+ return { ...options, input, type: 'receipts/verify' };
273
+ }
274
+
275
+ export function connectClient(client = {}, options = {}) {
276
+ return { ...options, client, type: 'client/connect' };
277
+ }
278
+
279
+ export function disconnectClient(clientId, options = {}) {
280
+ return { ...options, client_id: clientId, type: 'client/disconnect' };
281
+ }
282
+
283
+ export function importBundle(bundle = {}, options = {}) {
284
+ return { ...options, bundle, type: 'bundle/import' };
285
+ }
286
+
287
+ export function exportBundle(options = {}) {
288
+ return { ...options, type: 'bundle/export' };
289
+ }
290
+
291
+ export function updateMeshStatus(mesh = {}, options = {}) {
292
+ return { ...options, mesh, type: 'mesh/update' };
293
+ }
294
+
295
+ export function updateEnterpriseStatus(enterprise = {}, options = {}) {
296
+ return { ...options, enterprise, type: 'enterprise/update' };
297
+ }
298
+
299
+ export function selectDesktopScreen(screen) {
300
+ return { screen, type: 'desktop/select-screen' };
301
+ }
302
+
303
+ export function setDesktopDraft(name, value) {
304
+ return { name, value, type: 'draft/set' };
305
+ }
306
+
307
+ export const desktopActions = Object.freeze({
308
+ startMcp,
309
+ stopMcp,
310
+ startMCP: startMcp,
311
+ stopMCP: stopMcp,
312
+ createVault,
313
+ rememberMemory,
314
+ remember: rememberMemory,
315
+ deleteMemory,
316
+ removeMemory: deleteMemory,
317
+ searchMemories,
318
+ searchMemory: searchMemories,
319
+ verifyReceipts,
320
+ verifyReceiptOutput: verifyReceipts,
321
+ connectClient,
322
+ connectDesktopClient: connectClient,
323
+ disconnectClient,
324
+ importBundle,
325
+ importDesktopBundle: importBundle,
326
+ exportBundle,
327
+ exportDesktopBundle: exportBundle,
328
+ updateMeshStatus,
329
+ updateEnterpriseStatus,
330
+ selectDesktopScreen,
331
+ setDesktopDraft
332
+ });
333
+
334
+ export const actions = desktopActions;
335
+
336
+ export const startMCP = startMcp;
337
+ export const stopMCP = stopMcp;
338
+ export const remember = rememberMemory;
339
+ export const removeMemory = deleteMemory;
340
+ export const searchMemory = searchMemories;
341
+ export const verifyReceiptOutput = verifyReceipts;
342
+ export const connectDesktopClient = connectClient;
343
+ export const importDesktopBundle = importBundle;
344
+ export const exportDesktopBundle = exportBundle;
345
+
346
+ export const desktopApi = Object.freeze({
347
+ createDesktopState,
348
+ desktopReducer,
349
+ renderDesktopModel,
350
+ desktopActions,
351
+ actions,
352
+ startMcp,
353
+ stopMcp,
354
+ startMCP,
355
+ stopMCP,
356
+ createVault,
357
+ rememberMemory,
358
+ remember,
359
+ deleteMemory,
360
+ removeMemory,
361
+ searchMemories,
362
+ searchMemory,
363
+ verifyReceipts,
364
+ verifyReceiptOutput,
365
+ connectClient,
366
+ connectDesktopClient,
367
+ disconnectClient,
368
+ importBundle,
369
+ importDesktopBundle,
370
+ exportBundle,
371
+ exportDesktopBundle
372
+ });
373
+
374
+ if (typeof globalThis !== 'undefined') {
375
+ globalThis.EnigmaDesktop = desktopApi;
376
+ }
377
+
378
+ function selectScreen(state, screen) {
379
+ if (!isKnownScreen(screen)) return withNotice(state, 'Unknown screen path rejected.');
380
+ return { ...state, activeScreen: screen, notice: 'Desktop shell state is operational evidence only. It is not cryptographic proof.' };
381
+ }
382
+
383
+ function startMcpState(state, action) {
384
+ const now = cleanString(action.now) || state.mcp.started_at || INITIAL_NOW;
385
+ const port = normalizePort(action.port ?? state.mcp.port);
386
+ return bump({
387
+ ...state,
388
+ activeScreen: 'mcp',
389
+ notice: 'MCP server marked running by local UI state. Confirm with server evidence before treating it as proof.',
390
+ mcp: {
391
+ ...state.mcp,
392
+ status: 'running',
393
+ endpoint: cleanString(action.endpoint) || state.mcp.endpoint,
394
+ transport: cleanString(action.transport) || state.mcp.transport,
395
+ port,
396
+ started_at: now,
397
+ stopped_at: '',
398
+ client_count: state.clients.filter((client) => client.status === 'connected').length
399
+ }
400
+ });
401
+ }
402
+
403
+ function stopMcpState(state, action) {
404
+ return bump({
405
+ ...state,
406
+ activeScreen: 'mcp',
407
+ notice: 'MCP server marked stopped in local UI state.',
408
+ mcp: {
409
+ ...state.mcp,
410
+ status: 'stopped',
411
+ stopped_at: cleanString(action.now) || INITIAL_NOW,
412
+ client_count: 0
413
+ },
414
+ clients: state.clients.map((client) => ({ ...client, status: 'disconnected', disconnected_at: cleanString(action.now) || INITIAL_NOW }))
415
+ });
416
+ }
417
+
418
+ function createVaultState(state, action) {
419
+ const now = cleanString(action.now) || INITIAL_NOW;
420
+ const seed = cleanString(action.vault_id ?? action.vaultId ?? action.name) || `vault-${state.sequence + 1}`;
421
+ const vaultId = `vault_${localFingerprint(seed).slice(0, 16)}`;
422
+ return bump({
423
+ ...state,
424
+ activeScreen: 'vault',
425
+ notice: 'Vault shell created. Receipts remain the source of proof.',
426
+ vault: {
427
+ ...state.vault,
428
+ status: 'ready',
429
+ vault_id: vaultId,
430
+ active_set_root: cleanString(action.active_set_root ?? action.activeSetRoot) || state.vault.active_set_root,
431
+ receipt_log_root: cleanString(action.receipt_log_root ?? action.receiptLogRoot) || state.vault.receipt_log_root,
432
+ created_at: now,
433
+ last_event_at: now
434
+ }
435
+ });
436
+ }
437
+
438
+ function rememberMemoryState(state, action) {
439
+ if (state.vault.status !== 'ready') return withNotice(state, 'Create a vault before remembering memory.');
440
+
441
+ const memory = normalizeMemory(action.memory, state, action);
442
+ const memories = upsertMemory(state.memories, memory);
443
+ return bump({
444
+ ...state,
445
+ activeScreen: 'vault',
446
+ notice: `Memory descriptor stored with a local fingerprint. Raw memory text is not displayed or exported by this shell. ${BODY_FINGERPRINT_NOTE}`,
447
+ memories,
448
+ vault: {
449
+ ...state.vault,
450
+ memory_count: memories.filter((item) => !item.deleted).length,
451
+ deleted_count: memories.filter((item) => item.deleted).length,
452
+ last_event_at: memory.created_at
453
+ }
454
+ });
455
+ }
456
+
457
+ function deleteMemoryState(state, action) {
458
+ const target = findMemory(state.memories, action.memory_id ?? action.memoryId ?? action.address);
459
+ if (!target) return withNotice(state, 'Delete request rejected because the memory address is unknown.');
460
+ if (target.deleted) return withNotice(state, 'Delete request ignored because the memory is already marked deleted.');
461
+
462
+ const now = cleanString(action.now) || INITIAL_NOW;
463
+ const evidence = {
464
+ schema: DELETE_EVIDENCE_SCHEMA,
465
+ evidence_id: `del_${localFingerprint(`${target.address}|${state.sequence + 1}|${now}`).slice(0, 20)}`,
466
+ vault_id: state.vault.vault_id,
467
+ memory_addr: target.address,
468
+ body_fingerprint: target.body_fingerprint,
469
+ requested_at: now,
470
+ status: 'pending_receipt',
471
+ receipt_id: cleanString(action.receipt_id ?? action.receiptId),
472
+ note: 'This is deletion evidence, not proof. Match it to a verified delete receipt.'
473
+ };
474
+
475
+ const memories = state.memories.map((memory) => memory.address === target.address ? { ...memory, deleted: true, deleted_at: now, delete_evidence_id: evidence.evidence_id } : memory);
476
+ return bump({
477
+ ...state,
478
+ activeScreen: 'delete-prove',
479
+ notice: 'Deletion evidence queued. Verification requires a matching receipt.',
480
+ memories,
481
+ deletionEvidence: state.deletionEvidence.concat(evidence),
482
+ vault: {
483
+ ...state.vault,
484
+ memory_count: memories.filter((item) => !item.deleted).length,
485
+ deleted_count: memories.filter((item) => item.deleted).length,
486
+ last_event_at: now
487
+ }
488
+ });
489
+ }
490
+
491
+ function searchMemoryState(state, action) {
492
+ const query = cleanString(action.query).toLowerCase();
493
+ const results = query
494
+ ? state.memories.filter((memory) => !memory.deleted && searchableMemoryText(memory).includes(query)).map((memory) => memory.address)
495
+ : [];
496
+ return {
497
+ ...state,
498
+ activeScreen: 'vault',
499
+ search: { query, results },
500
+ notice: query ? 'Search uses stored descriptors and fingerprints only, not raw memory plaintext.' : state.notice
501
+ };
502
+ }
503
+
504
+ function verifyReceiptState(state, action) {
505
+ const parsed = parseReceiptInput(action.input);
506
+ if (!parsed.ok) {
507
+ return bump({
508
+ ...state,
509
+ activeScreen: 'verifier',
510
+ notice: 'Receipt shape input was rejected.',
511
+ verifier: {
512
+ ...state.verifier,
513
+ status: 'error',
514
+ evidence: [],
515
+ errors: parsed.errors,
516
+ checked_at: cleanString(action.now) || INITIAL_NOW
517
+ }
518
+ });
519
+ }
520
+
521
+ const evidence = parsed.receipts.map((receipt, index) => receiptEvidence(receipt, index, action));
522
+ const errors = evidence.flatMap((item) => item.errors.map((error) => `${item.receipt_id || `receipt[${item.index}]`}: ${error}`));
523
+ const deletionEvidence = matchDeletionEvidence(state.deletionEvidence, evidence);
524
+ return bump({
525
+ ...state,
526
+ activeScreen: 'verifier',
527
+ notice: errors.length === 0 ? 'Receipt shape is inspectable. Offline verifier output is required before treating it as cryptographic proof.' : 'Receipt shape inspection found structural errors.',
528
+ verifier: {
529
+ ...state.verifier,
530
+ status: 'checked',
531
+ evidence,
532
+ errors,
533
+ checked_at: cleanString(action.now) || INITIAL_NOW
534
+ },
535
+ deletionEvidence,
536
+ vault: {
537
+ ...state.vault,
538
+ receipt_count: Math.max(state.vault.receipt_count, evidence.length)
539
+ }
540
+ });
541
+ }
542
+
543
+ function connectClientState(state, action) {
544
+ if (state.mcp.status !== 'running') return withNotice(state, 'Start MCP before connecting a client.');
545
+ const input = action.client ?? {};
546
+ const template = CLIENT_TEMPLATES.find((item) => item.id === input.id || item.name === input.name);
547
+ const id = cleanString(input.id) || template?.id || `client_${localFingerprint(cleanString(input.name) || `client-${state.clients.length + 1}`).slice(0, 12)}`;
548
+ const client = {
549
+ id,
550
+ name: cleanString(input.name) || template?.name || id,
551
+ kind: cleanString(input.kind) || template?.kind || 'mcp-client',
552
+ status: 'connected',
553
+ connected_at: cleanString(action.now) || INITIAL_NOW,
554
+ endpoint: state.mcp.endpoint,
555
+ capabilities: normalizeStringList(input.capabilities)
556
+ };
557
+ const clients = state.clients.filter((item) => item.id !== id).concat(client);
558
+ return bump({
559
+ ...state,
560
+ activeScreen: 'clients',
561
+ notice: `${client.name} connection recorded as local MCP evidence.`,
562
+ clients,
563
+ mcp: { ...state.mcp, client_count: clients.filter((item) => item.status === 'connected').length }
564
+ });
565
+ }
566
+
567
+ function disconnectClientState(state, action) {
568
+ const id = cleanString(action.client_id ?? action.clientId);
569
+ const now = cleanString(action.now) || INITIAL_NOW;
570
+ const clients = state.clients.map((client) => client.id === id ? { ...client, status: 'disconnected', disconnected_at: now } : client);
571
+ return bump({
572
+ ...state,
573
+ activeScreen: 'clients',
574
+ clients,
575
+ mcp: { ...state.mcp, client_count: clients.filter((item) => item.status === 'connected').length },
576
+ notice: id ? 'Client disconnected in local UI state.' : state.notice
577
+ });
578
+ }
579
+
580
+ function importBundleState(state, action) {
581
+ const parsed = parseBundle(action.bundle);
582
+ if (!parsed.ok) {
583
+ return bump({
584
+ ...state,
585
+ activeScreen: 'import-export',
586
+ notice: 'Import rejected. Unknown or unsafe bundle path failed closed.',
587
+ importExport: { ...state.importExport, import_status: 'rejected', import_errors: parsed.errors }
588
+ });
589
+ }
590
+
591
+ const safe = parsed.bundle;
592
+ const importedMemories = Array.isArray(safe.memories) ? safe.memories.map(importMemory).filter(Boolean) : [];
593
+ const importedDeletionEvidence = Array.isArray(safe.deletionEvidence) ? safe.deletionEvidence.map(importDeletionEvidence).filter(Boolean) : [];
594
+ const importedVerifierEvidence = Array.isArray(safe.verifierEvidence) ? safe.verifierEvidence.map(importVerifierEvidence).filter(Boolean) : [];
595
+ const memories = mergeMemories(state.memories, importedMemories);
596
+ return bump({
597
+ ...state,
598
+ activeScreen: 'import-export',
599
+ notice: `Bundle imported. Only sanitized descriptors, fingerprints, and evidence metadata were accepted. ${BODY_FINGERPRINT_NOTE}`,
600
+ memories,
601
+ deletionEvidence: state.deletionEvidence.concat(importedDeletionEvidence),
602
+ verifier: importedVerifierEvidence.length ? {
603
+ ...state.verifier,
604
+ status: 'checked',
605
+ evidence: state.verifier.evidence.concat(importedVerifierEvidence),
606
+ errors: state.verifier.errors.concat(importedVerifierEvidence.flatMap((item) => item.errors.map((error) => `${item.receipt_id || `receipt[${item.index}]`}: ${error}`)))
607
+ } : state.verifier,
608
+ importExport: { ...state.importExport, import_status: 'imported', import_errors: [] },
609
+ vault: {
610
+ ...state.vault,
611
+ status: state.vault.status === 'ready' ? 'ready' : (safe.vault?.vault_id ? 'ready' : state.vault.status),
612
+ vault_id: state.vault.vault_id || cleanString(safe.vault?.vault_id),
613
+ memory_count: memories.filter((item) => !item.deleted).length,
614
+ deleted_count: memories.filter((item) => item.deleted).length,
615
+ receipt_count: Math.max(state.vault.receipt_count, normalizeNonNegativeInteger(safe.vault?.receipt_count, 0))
616
+ }
617
+ });
618
+ }
619
+
620
+ function exportBundleState(state, action) {
621
+ const now = cleanString(action.now) || INITIAL_NOW;
622
+ const bundle = createExportBundle(state, now, action.scope);
623
+ return bump({
624
+ ...state,
625
+ activeScreen: 'import-export',
626
+ notice: 'Export prepared without raw memory plaintext.',
627
+ importExport: {
628
+ ...state.importExport,
629
+ exported_at: now,
630
+ exportBundle: bundle
631
+ },
632
+ drafts: {
633
+ ...state.drafts,
634
+ exportOutput: JSON.stringify(bundle, null, 2)
635
+ }
636
+ });
637
+ }
638
+
639
+ function updateMeshState(state, action) {
640
+ const mesh = action.mesh ?? {};
641
+ return bump({
642
+ ...state,
643
+ activeScreen: 'mesh',
644
+ mesh: {
645
+ ...state.mesh,
646
+ status: cleanString(mesh.status) || state.mesh.status,
647
+ peers: normalizeNonNegativeInteger(mesh.peers, state.mesh.peers),
648
+ relays: normalizeNonNegativeInteger(mesh.relays, state.mesh.relays),
649
+ last_witness_at: cleanString(mesh.last_witness_at ?? mesh.lastWitnessAt) || state.mesh.last_witness_at
650
+ },
651
+ notice: 'Mesh status updated as local telemetry.'
652
+ });
653
+ }
654
+
655
+ function updateEnterpriseState(state, action) {
656
+ const enterprise = action.enterprise ?? {};
657
+ return bump({
658
+ ...state,
659
+ activeScreen: 'enterprise',
660
+ enterprise: {
661
+ ...state.enterprise,
662
+ status: cleanString(enterprise.status) || state.enterprise.status,
663
+ tenant_id: cleanString(enterprise.tenant_id ?? enterprise.tenantId) || state.enterprise.tenant_id,
664
+ policy_id: cleanString(enterprise.policy_id ?? enterprise.policyId) || state.enterprise.policy_id,
665
+ siem: cleanString(enterprise.siem) || state.enterprise.siem
666
+ },
667
+ notice: 'Enterprise status updated as configuration evidence.'
668
+ });
669
+ }
670
+
671
+ function setDraftState(state, action) {
672
+ const name = cleanString(action.name);
673
+ if (!Object.prototype.hasOwnProperty.call(state.drafts, name)) return withNotice(state, 'Unknown draft field rejected.');
674
+ return { ...state, drafts: { ...state.drafts, [name]: String(action.value ?? '') } };
675
+ }
676
+
677
+ function normalizeMemory(input = {}, state, action) {
678
+ const now = cleanString(action.now) || INITIAL_NOW;
679
+ const descriptor = clamp(cleanString(input.descriptor ?? input.label ?? input.title) || `redacted memory ${state.memories.length + 1}`, 96);
680
+ const body = cleanString(input.body ?? input.text ?? input.content);
681
+ const tags = normalizeStringList(input.tags);
682
+ const source = cleanString(input.source) || 'desktop-shell';
683
+ const subject = cleanString(input.subject_id ?? input.subjectId) || 'local-user';
684
+ const tenant = cleanString(input.tenant_id ?? input.tenantId) || state.enterprise.tenant_id || 'local-tenant';
685
+ const fingerprint = localFingerprint(`${tenant}\u001f${subject}\u001f${descriptor}\u001f${body}\u001f${tags.join(',')}`);
686
+ const address = cleanString(input.address ?? input.memory_addr ?? input.memoryAddr) || `mem_${fingerprint.slice(0, 24)}`;
687
+ return {
688
+ address,
689
+ descriptor,
690
+ source,
691
+ tags,
692
+ tenant_id: tenant,
693
+ subject_id: subject,
694
+ body_fingerprint: `local:${fingerprint}`,
695
+ body_bytes: body.length,
696
+ deleted: false,
697
+ created_at: now,
698
+ receipt_id: cleanString(input.receipt_id ?? input.receiptId)
699
+ };
700
+ }
701
+
702
+ function upsertMemory(memories, next) {
703
+ let replaced = false;
704
+ const updated = memories.map((memory) => {
705
+ if (memory.address !== next.address) return memory;
706
+ replaced = true;
707
+ return { ...memory, ...next, created_at: memory.created_at || next.created_at };
708
+ });
709
+ return replaced ? updated : updated.concat(next);
710
+ }
711
+
712
+ function findMemory(memories, id) {
713
+ const key = cleanString(id);
714
+ if (!key) return null;
715
+ return memories.find((memory) => memory.address === key || memory.body_fingerprint === key || memory.receipt_id === key) || null;
716
+ }
717
+
718
+ function searchableMemoryText(memory) {
719
+ return `${memory.address} ${memory.descriptor} ${memory.source} ${memory.tags.join(' ')} ${memory.body_fingerprint}`.toLowerCase();
720
+ }
721
+
722
+ function parseReceiptInput(input) {
723
+ let value = input;
724
+ if (typeof input === 'string') {
725
+ const text = input.trim();
726
+ if (!text) return { ok: false, errors: ['receipt input is empty'] };
727
+ try {
728
+ value = JSON.parse(text);
729
+ } catch {
730
+ return { ok: false, errors: ['receipt input must be JSON'] };
731
+ }
732
+ }
733
+
734
+ const receipts = Array.isArray(value) ? value : Array.isArray(value?.receipts) ? value.receipts : value?.receipt ? [value.receipt] : isPlainObject(value) ? [value] : [];
735
+ if (receipts.length === 0) return { ok: false, errors: ['no receipts found'] };
736
+ return { ok: true, receipts };
737
+ }
738
+
739
+ function receiptEvidence(receipt, index, action) {
740
+ const errors = [];
741
+ if (!isPlainObject(receipt)) {
742
+ return {
743
+ schema: VERIFIER_EVIDENCE_SCHEMA,
744
+ index,
745
+ receipt_id: '',
746
+ ok: false,
747
+ errors: ['receipt must be an object'],
748
+ checked_at: cleanString(action.now) || INITIAL_NOW
749
+ };
750
+ }
751
+
752
+ const receiptId = cleanString(receipt.receipt_id ?? receipt.receiptId);
753
+ const operation = cleanString(receipt.operation);
754
+ const eventHash = cleanString(receipt.event_hash ?? receipt.eventHash);
755
+ const activeRoot = cleanString(receipt.active_set_root ?? receipt.activeSetRoot);
756
+ const logRoot = cleanString(receipt.receipt_log_root ?? receipt.receiptLogRoot);
757
+ const previous = cleanString(receipt.previous_receipt_hash ?? receipt.previousReceiptHash);
758
+ const signer = isPlainObject(receipt.signer) ? receipt.signer : {};
759
+ const signature = isPlainObject(receipt.signature) ? receipt.signature : {};
760
+
761
+ if (receipt.schema !== 'enigma.receipt.v1') errors.push('schema mismatch');
762
+ if (!receiptId) errors.push('receipt_id missing');
763
+ if (!operation) errors.push('operation missing');
764
+ if (!Number.isInteger(receipt.sequence) || receipt.sequence < 0) errors.push('sequence must be a non-negative integer');
765
+ if (!ROOT_RE.test(eventHash)) errors.push('event_hash must be sha256-prefixed');
766
+ if (!ROOT_RE.test(activeRoot)) errors.push('active_set_root must be sha256-prefixed');
767
+ if (!ROOT_RE.test(logRoot)) errors.push('receipt_log_root must be sha256-prefixed');
768
+ if (previous !== 'GENESIS' && !ROOT_RE.test(previous)) errors.push('previous_receipt_hash mismatch');
769
+ if (signer.alg !== 'Ed25519' || !cleanString(signer.key_id ?? signer.keyId)) errors.push('signer mismatch');
770
+ if (signature.alg !== 'Ed25519' || !SIGNATURE_RE.test(cleanString(signature.value))) errors.push('signature missing');
771
+
772
+ return {
773
+ schema: VERIFIER_EVIDENCE_SCHEMA,
774
+ index,
775
+ receipt_id: receiptId,
776
+ operation,
777
+ sequence: Number.isInteger(receipt.sequence) ? receipt.sequence : null,
778
+ event_hash: eventHash,
779
+ active_set_root: activeRoot,
780
+ receipt_log_root: logRoot,
781
+ previous_receipt_hash: previous,
782
+ signer_key_id: cleanString(signer.key_id ?? signer.keyId),
783
+ memory_addr: cleanString(receipt.memory_addr ?? receipt.memoryAddr),
784
+ source_addr: cleanString(receipt.source_addr ?? receipt.sourceAddr),
785
+ checked_at: cleanString(action.now) || INITIAL_NOW,
786
+ ok: errors.length === 0,
787
+ errors,
788
+ note: 'Structural receipt shape only; cryptographic signature verification is performed by the offline verifier.'
789
+ };
790
+ }
791
+
792
+ function matchDeletionEvidence(deletionEvidence, receiptEvidenceItems) {
793
+ if (deletionEvidence.length === 0 || receiptEvidenceItems.length === 0) return deletionEvidence;
794
+ return deletionEvidence.map((item) => {
795
+ if (item.status === 'matched_receipt') return item;
796
+ const match = receiptEvidenceItems.find((receipt) => receipt.ok && receipt.operation === 'delete' && receipt.memory_addr === item.memory_addr);
797
+ return match ? { ...item, status: 'matched_receipt', receipt_id: match.receipt_id } : item;
798
+ });
799
+ }
800
+
801
+ function parseBundle(bundle) {
802
+ let value = bundle;
803
+ if (typeof bundle === 'string') {
804
+ const text = bundle.trim();
805
+ if (!text) return { ok: false, errors: ['bundle input is empty'] };
806
+ try {
807
+ value = JSON.parse(text);
808
+ } catch {
809
+ return { ok: false, errors: ['bundle input must be JSON'] };
810
+ }
811
+ }
812
+ if (!isPlainObject(value)) return { ok: false, errors: ['bundle must be an object'] };
813
+ if (value.schema !== EXPORT_SCHEMA) return { ok: false, errors: ['bundle schema mismatch'] };
814
+ const unsafePaths = findForbiddenImportRawFields(value);
815
+ if (unsafePaths.length > 0) return { ok: false, errors: unsafePaths.map((path) => `forbidden raw import field: ${path}`) };
816
+ return { ok: true, bundle: value };
817
+ }
818
+
819
+ function importMemory(memory) {
820
+ if (!isPlainObject(memory)) return null;
821
+ const address = cleanString(memory.address ?? memory.memory_addr ?? memory.memoryAddr);
822
+ const bodyFingerprint = cleanString(memory.body_fingerprint ?? memory.bodyFingerprint);
823
+ if (!address || !bodyFingerprint || isRawLookingText(address) || isRawLookingText(bodyFingerprint)) return null;
824
+ return {
825
+ address,
826
+ descriptor: safeImportedText(memory.descriptor, 'imported memory', 96),
827
+ source: safeImportedText(memory.source, 'imported-bundle', 96),
828
+ tags: sanitizeImportedTags(memory.tags),
829
+ tenant_id: cleanString(memory.tenant_id ?? memory.tenantId),
830
+ subject_id: cleanString(memory.subject_id ?? memory.subjectId),
831
+ body_fingerprint: bodyFingerprint,
832
+ body_bytes: normalizeNonNegativeInteger(memory.body_bytes ?? memory.bodyBytes, 0),
833
+ deleted: Boolean(memory.deleted),
834
+ created_at: cleanString(memory.created_at ?? memory.createdAt),
835
+ deleted_at: cleanString(memory.deleted_at ?? memory.deletedAt),
836
+ receipt_id: cleanString(memory.receipt_id ?? memory.receiptId)
837
+ };
838
+ }
839
+
840
+ function importDeletionEvidence(item) {
841
+ if (!isPlainObject(item) || item.schema !== DELETE_EVIDENCE_SCHEMA) return null;
842
+ const memoryAddr = cleanString(item.memory_addr ?? item.memoryAddr);
843
+ if (!memoryAddr || isRawLookingText(memoryAddr)) return null;
844
+ const bodyFingerprint = cleanString(item.body_fingerprint ?? item.bodyFingerprint);
845
+ return {
846
+ schema: DELETE_EVIDENCE_SCHEMA,
847
+ evidence_id: cleanString(item.evidence_id ?? item.evidenceId) || `del_${localFingerprint(memoryAddr).slice(0, 20)}`,
848
+ vault_id: cleanString(item.vault_id ?? item.vaultId),
849
+ memory_addr: memoryAddr,
850
+ body_fingerprint: isRawLookingText(bodyFingerprint) ? '' : bodyFingerprint,
851
+ requested_at: cleanString(item.requested_at ?? item.requestedAt),
852
+ status: cleanString(item.status) || 'pending_receipt',
853
+ receipt_id: cleanString(item.receipt_id ?? item.receiptId),
854
+ note: 'Imported deletion evidence, not proof.'
855
+ };
856
+ }
857
+
858
+ function importVerifierEvidence(item) {
859
+ if (!isPlainObject(item) || item.schema !== VERIFIER_EVIDENCE_SCHEMA) return null;
860
+ return sanitizeVerifierEvidenceForExport(item);
861
+ }
862
+
863
+ function findForbiddenImportRawFields(bundle) {
864
+ const paths = [];
865
+ collectForbiddenImportRawFields(bundle.memories, 'memories', paths);
866
+ collectForbiddenImportRawFields(bundle.deletionEvidence, 'deletionEvidence', paths);
867
+ collectForbiddenImportRawFields(bundle.verifierEvidence, 'verifierEvidence', paths);
868
+ return paths;
869
+ }
870
+
871
+ function collectForbiddenImportRawFields(value, path, paths) {
872
+ if (Array.isArray(value)) {
873
+ value.forEach((item, index) => collectForbiddenImportRawFields(item, `${path}[${index}]`, paths));
874
+ return;
875
+ }
876
+ if (!isPlainObject(value)) return;
877
+ for (const [key, child] of Object.entries(value)) {
878
+ const normalizedKey = key.replace(/[-_]/g, '').toLowerCase();
879
+ if (FORBIDDEN_IMPORT_RAW_KEYS.has(key) || FORBIDDEN_IMPORT_RAW_KEYS.has(normalizedKey)) paths.push(`${path}.${key}`);
880
+ collectForbiddenImportRawFields(child, `${path}.${key}`, paths);
881
+ }
882
+ }
883
+
884
+ function safeImportedText(value, fallback, max) {
885
+ const text = clamp(cleanString(value), max);
886
+ return text && !isRawLookingText(text) ? text : fallback;
887
+ }
888
+
889
+ function sanitizeImportedTags(value) {
890
+ return normalizeStringList(value).filter((tag) => !isRawLookingText(tag));
891
+ }
892
+
893
+ function sanitizeDeletionEvidenceForExport(item) {
894
+ return {
895
+ schema: DELETE_EVIDENCE_SCHEMA,
896
+ evidence_id: cleanString(item.evidence_id ?? item.evidenceId),
897
+ vault_id: cleanString(item.vault_id ?? item.vaultId),
898
+ memory_addr: sanitizeEvidenceAddress(item.memory_addr ?? item.memoryAddr),
899
+ body_fingerprint: sanitizeEvidenceAddress(item.body_fingerprint ?? item.bodyFingerprint),
900
+ requested_at: cleanString(item.requested_at ?? item.requestedAt),
901
+ status: cleanString(item.status),
902
+ receipt_id: cleanString(item.receipt_id ?? item.receiptId),
903
+ note: 'Deletion evidence metadata only; not cryptographic proof.'
904
+ };
905
+ }
906
+
907
+ function sanitizeVerifierEvidenceForExport(item) {
908
+ return {
909
+ schema: VERIFIER_EVIDENCE_SCHEMA,
910
+ index: Number.isInteger(item.index) ? item.index : null,
911
+ receipt_id: cleanString(item.receipt_id ?? item.receiptId),
912
+ operation: cleanString(item.operation),
913
+ sequence: Number.isInteger(item.sequence) ? item.sequence : null,
914
+ event_hash: cleanString(item.event_hash ?? item.eventHash),
915
+ active_set_root: cleanString(item.active_set_root ?? item.activeSetRoot),
916
+ receipt_log_root: cleanString(item.receipt_log_root ?? item.receiptLogRoot),
917
+ previous_receipt_hash: cleanString(item.previous_receipt_hash ?? item.previousReceiptHash),
918
+ signer_key_id: cleanString(item.signer_key_id ?? item.signerKeyId),
919
+ memory_addr: sanitizeEvidenceAddress(item.memory_addr ?? item.memoryAddr),
920
+ source_addr: sanitizeEvidenceAddress(item.source_addr ?? item.sourceAddr),
921
+ checked_at: cleanString(item.checked_at ?? item.checkedAt),
922
+ ok: Boolean(item.ok),
923
+ errors: normalizeStringList(item.errors).map(sanitizeEvidenceAddress),
924
+ note: 'Structural receipt shape only; cryptographic signature verification is performed by the offline verifier.'
925
+ };
926
+ }
927
+
928
+ function sanitizeEvidenceAddress(value) {
929
+ const text = cleanString(value);
930
+ return isRawLookingText(text) ? IMPORT_REDACTED_TEXT : text;
931
+ }
932
+
933
+ function isRawLookingText(value) {
934
+ const text = cleanString(value);
935
+ return text ? RAW_LOOKING_VALUE_RE.test(text) : false;
936
+ }
937
+
938
+ function mergeMemories(current, imported) {
939
+ let next = current.slice();
940
+ for (const memory of imported) next = upsertMemory(next, memory);
941
+ return next;
942
+ }
943
+
944
+ function createExportBundle(state, now, scope) {
945
+ const includeClients = scope === 'all' || scope === 'clients';
946
+ return {
947
+ schema: EXPORT_SCHEMA,
948
+ exported_at: now,
949
+ notice: `This bundle intentionally excludes raw memory plaintext. It is evidence metadata, not proof. ${BODY_FINGERPRINT_NOTE}`,
950
+ vault: {
951
+ status: state.vault.status,
952
+ vault_id: state.vault.vault_id,
953
+ active_set_root: state.vault.active_set_root,
954
+ receipt_log_root: state.vault.receipt_log_root,
955
+ receipt_count: state.vault.receipt_count
956
+ },
957
+ memories: state.memories.map((memory) => ({
958
+ address: memory.address,
959
+ descriptor: safeImportedText(memory.descriptor, 'imported memory', 96),
960
+ source: safeImportedText(memory.source, 'imported-bundle', 96),
961
+ tags: sanitizeImportedTags(memory.tags),
962
+ tenant_id: memory.tenant_id,
963
+ subject_id: memory.subject_id,
964
+ body_fingerprint: isRawLookingText(memory.body_fingerprint) ? '' : memory.body_fingerprint,
965
+ body_bytes: memory.body_bytes,
966
+ deleted: memory.deleted,
967
+ created_at: memory.created_at,
968
+ deleted_at: memory.deleted_at,
969
+ receipt_id: memory.receipt_id
970
+ })),
971
+ deletionEvidence: state.deletionEvidence.map(sanitizeDeletionEvidenceForExport),
972
+ verifierEvidence: state.verifier.evidence.map(sanitizeVerifierEvidenceForExport),
973
+ mesh: { ...state.mesh },
974
+ enterprise: { ...state.enterprise },
975
+ clients: includeClients ? state.clients.map((client) => ({ ...client })) : []
976
+ };
977
+ }
978
+
979
+ function renderMemoryRow(memory) {
980
+ return {
981
+ address: memory.address,
982
+ descriptor: memory.descriptor,
983
+ source: memory.source,
984
+ tags: memory.tags.slice(),
985
+ body_fingerprint: memory.body_fingerprint,
986
+ deleted: memory.deleted,
987
+ receipt_id: memory.receipt_id
988
+ };
989
+ }
990
+
991
+ function bump(state) {
992
+ return { ...state, sequence: state.sequence + 1 };
993
+ }
994
+
995
+ function withNotice(state, notice) {
996
+ return { ...state, notice };
997
+ }
998
+
999
+ function isKnownScreen(screen) {
1000
+ return DESKTOP_SCREENS.includes(screen);
1001
+ }
1002
+
1003
+ function screenLabel(screen) {
1004
+ switch (screen) {
1005
+ case 'vault': return 'Vault';
1006
+ case 'mcp': return 'MCP server';
1007
+ case 'clients': return 'Clients';
1008
+ case 'import-export': return 'Import / export';
1009
+ case 'verifier': return 'Inspect receipt shape';
1010
+ case 'delete-prove': return 'Delete + prove';
1011
+ case 'mesh': return 'Mesh';
1012
+ case 'enterprise': return 'Enterprise';
1013
+ default: return screen;
1014
+ }
1015
+ }
1016
+
1017
+ function cleanString(value) {
1018
+ if (value === null || value === undefined) return '';
1019
+ return String(value).trim();
1020
+ }
1021
+
1022
+ function clamp(value, max) {
1023
+ const text = cleanString(value);
1024
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
1025
+ }
1026
+
1027
+ function normalizePort(value) {
1028
+ if (value === null || value === undefined || value === '') return null;
1029
+ const port = Number(value);
1030
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null;
1031
+ }
1032
+
1033
+ function normalizeNonNegativeInteger(value, fallback) {
1034
+ const number = Number(value);
1035
+ return Number.isInteger(number) && number >= 0 ? number : fallback;
1036
+ }
1037
+
1038
+ function normalizeStringList(value) {
1039
+ if (Array.isArray(value)) return value.map(cleanString).filter(Boolean);
1040
+ return cleanString(value).split(',').map((item) => item.trim()).filter(Boolean);
1041
+ }
1042
+
1043
+ function isPlainObject(value) {
1044
+ return value !== null && typeof value === 'object' && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
1045
+ }
1046
+
1047
+ function localFingerprint(value) {
1048
+ const text = String(value ?? '');
1049
+ let a = 0x811c9dc5;
1050
+ let b = 0x01000193;
1051
+ for (let index = 0; index < text.length; index += 1) {
1052
+ a ^= text.charCodeAt(index);
1053
+ a = Math.imul(a, 0x01000193) >>> 0;
1054
+ b ^= a + index;
1055
+ b = Math.imul(b, 0x85ebca6b) >>> 0;
1056
+ }
1057
+ return `${a.toString(16).padStart(8, '0')}${b.toString(16).padStart(8, '0')}${(a ^ b).toString(16).padStart(8, '0')}`;
1058
+ }
1059
+
1060
+ function escapeHtml(value) {
1061
+ return String(value ?? '').replace(/[&<>'"]/g, (char) => {
1062
+ switch (char) {
1063
+ case '&': return '&amp;';
1064
+ case '<': return '&lt;';
1065
+ case '>': return '&gt;';
1066
+ case "'": return '&#39;';
1067
+ case '"': return '&quot;';
1068
+ default: return char;
1069
+ }
1070
+ });
1071
+ }
1072
+
1073
+ function mountDesktopApp() {
1074
+ const root = document.querySelector('[data-enigma-desktop]');
1075
+ if (!root) return;
1076
+ let state = createDesktopState({ now: new Date().toISOString() });
1077
+
1078
+ const dispatch = (action) => {
1079
+ state = desktopReducer(state, action);
1080
+ render();
1081
+ };
1082
+
1083
+ const render = () => {
1084
+ const model = renderDesktopModel(state);
1085
+ root.innerHTML = renderHtml(model, state);
1086
+ wireDesktopEvents(root, state, dispatch);
1087
+ };
1088
+
1089
+ render();
1090
+ }
1091
+
1092
+ function renderHtml(model, state) {
1093
+ return `
1094
+ <header class="shell-hero">
1095
+ <div>
1096
+ <p class="eyebrow">Enigma desktop scaffold</p>
1097
+ <h1>Provider-neutral memory custody, receipts, and MCP control.</h1>
1098
+ <p>${escapeHtml(model.notice)}</p>
1099
+ </div>
1100
+ <dl class="summary-grid">
1101
+ ${renderMetric('Vault', model.summary.vault)}
1102
+ ${renderMetric('MCP', model.summary.mcp)}
1103
+ ${renderMetric('Clients', model.summary.clients)}
1104
+ ${renderMetric('Receipt inspector', model.summary.verifier)}
1105
+ </dl>
1106
+ </header>
1107
+ <nav class="screen-nav" aria-label="Desktop screens">
1108
+ ${model.navigation.map((item) => `<button class="screen-tab${item.active ? ' is-active' : ''}" type="button" data-screen="${escapeHtml(item.id)}">${escapeHtml(item.label)}</button>`).join('')}
1109
+ </nav>
1110
+ <main>
1111
+ ${renderVaultScreen(model.screens.vault, state)}
1112
+ ${renderMcpScreen(model.screens.mcp)}
1113
+ ${renderClientsScreen(model.screens.clients)}
1114
+ ${renderImportExportScreen(model.screens.importExport, state)}
1115
+ ${renderVerifierScreen(model.screens.verifier, state)}
1116
+ ${renderDeleteProveScreen(model.screens.deleteProve)}
1117
+ ${renderMeshScreen(model.screens.mesh)}
1118
+ ${renderEnterpriseScreen(model.screens.enterprise)}
1119
+ </main>`;
1120
+ }
1121
+
1122
+ function renderMetric(label, value) {
1123
+ return `<div><dt>${escapeHtml(label)}</dt><dd>${escapeHtml(value)}</dd></div>`;
1124
+ }
1125
+
1126
+ function renderScreen(id, title, body) {
1127
+ return `<section class="screen-card" id="screen-${escapeHtml(id)}"><div class="screen-heading"><p>${escapeHtml(screenLabel(id))}</p><h2>${escapeHtml(title)}</h2></div>${body}</section>`;
1128
+ }
1129
+
1130
+ function renderVaultScreen(screen, state) {
1131
+ const memories = screen.memories.length ? screen.memories.map((memory) => `
1132
+ <li class="memory-row${memory.deleted ? ' is-deleted' : ''}">
1133
+ <div><strong>${escapeHtml(memory.descriptor)}</strong><span>${escapeHtml(memory.address)}</span></div>
1134
+ <code>${escapeHtml(memory.body_fingerprint)}</code>
1135
+ <button type="button" data-delete-memory="${escapeHtml(memory.address)}" ${memory.deleted ? 'disabled' : ''}>Delete + prove</button>
1136
+ </li>`).join('') : '<li class="empty-row">No memory descriptors yet.</li>';
1137
+
1138
+ return renderScreen('vault', screen.title, `
1139
+ <div class="two-column">
1140
+ <div class="panel">
1141
+ <h3>Vault evidence</h3>
1142
+ <dl class="detail-list">
1143
+ ${renderMetric('Status', screen.status)}
1144
+ ${renderMetric('Vault ID', screen.vault_id || 'not created')}
1145
+ ${renderMetric('Active root', screen.active_set_root || 'not observed')}
1146
+ ${renderMetric('Receipt log root', screen.receipt_log_root || 'not observed')}
1147
+ </dl>
1148
+ <button type="button" data-action="create-vault">Create vault shell</button>
1149
+ </div>
1150
+ <form class="panel" data-form="remember">
1151
+ <h3>Remember</h3>
1152
+ <label>Descriptor <input name="descriptor" required placeholder="non-sensitive label"></label>
1153
+ <label>Memory text <textarea name="body" placeholder="fingerprinted locally; not displayed or exported"></textarea></label>
1154
+ <label>Tags <input name="tags" placeholder="project, source"></label>
1155
+ <button type="submit">Remember descriptor</button>
1156
+ </form>
1157
+ </div>
1158
+ <form class="inline-form" data-form="search">
1159
+ <label>Search descriptors <input name="query" value="${escapeHtml(state.search.query)}" placeholder="tag, address, fingerprint"></label>
1160
+ <button type="submit">Search</button>
1161
+ <span>${state.search.results.length ? `${state.search.results.length} result(s)` : 'Search excludes raw memory plaintext.'}</span>
1162
+ </form>
1163
+ <ul class="memory-list">${memories}</ul>`);
1164
+ }
1165
+
1166
+ function renderMcpScreen(screen) {
1167
+ return renderScreen('mcp', screen.title, `
1168
+ <div class="two-column">
1169
+ <div class="panel status-panel">
1170
+ <span class="status-pill">${escapeHtml(screen.status)}</span>
1171
+ <h3>${escapeHtml(screen.honest_status)}</h3>
1172
+ <p>MCP state here is a local control-plane claim. Server receipts and logs are the evidence.</p>
1173
+ <div class="button-row"><button type="button" data-action="start-mcp">Start MCP</button><button type="button" data-action="stop-mcp">Stop MCP</button></div>
1174
+ </div>
1175
+ <dl class="panel detail-list">
1176
+ ${renderMetric('Endpoint', screen.endpoint)}
1177
+ ${renderMetric('Transport', screen.transport)}
1178
+ ${renderMetric('Port', screen.port || 'n/a')}
1179
+ ${renderMetric('Connected clients', screen.client_count)}
1180
+ </dl>
1181
+ </div>`);
1182
+ }
1183
+
1184
+ function renderClientsScreen(screen) {
1185
+ const buttons = screen.templates.map((client) => `<button type="button" data-connect-client="${escapeHtml(client.id)}" ${client.connected ? 'disabled' : ''}>${client.connected ? 'Connected' : 'Connect'} ${escapeHtml(client.name)}</button>`).join('');
1186
+ const rows = screen.connected.length ? screen.connected.map((client) => `<li><strong>${escapeHtml(client.name)}</strong><span>${escapeHtml(client.status)} via ${escapeHtml(client.endpoint)}</span></li>`).join('') : '<li class="empty-row">No clients connected.</li>';
1187
+ const bridgeStatus = screen.browser_bridge_status.map((item) => `<li><span>${escapeHtml(item)}</span></li>`).join('');
1188
+ return renderScreen('clients', screen.title, `<div class="panel"><div class="button-grid">${buttons}</div></div><div class="panel"><h3>Browser bridge boundaries</h3><ul class="boundary-list">${bridgeStatus}</ul></div><ul class="connection-list">${rows}</ul>`);
1189
+ }
1190
+
1191
+ function renderImportExportScreen(screen, state) {
1192
+ return renderScreen('import-export', screen.title, `
1193
+ <div class="two-column">
1194
+ <form class="panel" data-form="import">
1195
+ <h3>Import safe bundle</h3>
1196
+ <textarea name="bundle" placeholder="Paste ${EXPORT_SCHEMA} JSON">${escapeHtml(state.drafts.importInput)}</textarea>
1197
+ <button type="submit">Import</button>
1198
+ <p>${escapeHtml(screen.import_status)} ${escapeHtml(screen.import_errors.join('; '))}</p>
1199
+ </form>
1200
+ <form class="panel" data-form="export">
1201
+ <h3>Export evidence metadata</h3>
1202
+ <button type="submit">Prepare export</button>
1203
+ <textarea readonly>${escapeHtml(state.drafts.exportOutput || '')}</textarea>
1204
+ </form>
1205
+ </div>`);
1206
+ }
1207
+
1208
+ function renderVerifierScreen(screen, state) {
1209
+ const evidence = screen.evidence.length ? screen.evidence.map((item) => `<li class="evidence-row${item.ok ? ' is-ok' : ' is-bad'}"><strong>${escapeHtml(item.receipt_id || `receipt[${item.index}]`)}</strong><span>${escapeHtml(item.operation || 'unknown operation')}</span><small>${escapeHtml(item.errors.length ? item.errors.join('; ') : item.note)}</small></li>`).join('') : '<li class="empty-row">No receipt evidence checked.</li>';
1210
+ return renderScreen('verifier', screen.title, `
1211
+ <form class="panel" data-form="verify">
1212
+ <h3>Inspect receipt shape</h3>
1213
+ <p>${escapeHtml(screen.note)} ${escapeHtml(screen.body_fingerprint_note)}</p>
1214
+ <textarea name="receipts" placeholder="Paste receipt JSON or { receipts: [...] }">${escapeHtml(state.drafts.receiptInput)}</textarea>
1215
+ <button type="submit">Inspect receipt shape</button>
1216
+ </form>
1217
+ <ul class="evidence-list">${evidence}</ul>`);
1218
+ }
1219
+
1220
+ function renderDeleteProveScreen(screen) {
1221
+ const pending = screen.pending.length ? screen.pending.map((item) => `<li><strong>${escapeHtml(item.memory_addr)}</strong><span>${escapeHtml(item.status)}</span><code>${escapeHtml(item.body_fingerprint)}</code></li>`).join('') : '<li class="empty-row">No pending deletion evidence.</li>';
1222
+ const completed = screen.completed.length ? screen.completed.map((item) => `<li><strong>${escapeHtml(item.memory_addr)}</strong><span>matched ${escapeHtml(item.receipt_id)}</span></li>`).join('') : '<li class="empty-row">No matched deletion receipts.</li>';
1223
+ return renderScreen('delete-prove', screen.title, `<p class="honesty-note">${escapeHtml(screen.note)}</p><div class="two-column"><div class="panel"><h3>Pending</h3><ul>${pending}</ul></div><div class="panel"><h3>Matched receipts</h3><ul>${completed}</ul></div></div>`);
1224
+ }
1225
+
1226
+ function renderMeshScreen(screen) {
1227
+ return renderScreen('mesh', screen.title, `<div class="panel status-panel"><span class="status-pill">${escapeHtml(screen.status)}</span><p>${escapeHtml(screen.note)}</p><dl class="detail-list">${renderMetric('Peers', screen.peers)}${renderMetric('Relays', screen.relays)}${renderMetric('Last witness', screen.last_witness_at || 'not observed')}</dl><button type="button" data-action="mesh-online">Mark local mesh online</button></div>`);
1228
+ }
1229
+
1230
+ function renderEnterpriseScreen(screen) {
1231
+ return renderScreen('enterprise', screen.title, `<div class="panel status-panel"><span class="status-pill">${escapeHtml(screen.status)}</span><p>${escapeHtml(screen.note)}</p><dl class="detail-list">${renderMetric('Tenant', screen.tenant_id || 'not configured')}${renderMetric('Policy', screen.policy_id || 'not configured')}${renderMetric('SIEM', screen.siem)}</dl><button type="button" data-action="enterprise-demo">Load enterprise demo status</button></div>`);
1232
+ }
1233
+
1234
+ function wireDesktopEvents(root, state, dispatch) {
1235
+ root.querySelectorAll('[data-screen]').forEach((button) => {
1236
+ button.addEventListener('click', () => dispatch(selectDesktopScreen(button.dataset.screen)));
1237
+ });
1238
+
1239
+ root.querySelectorAll('[data-action]').forEach((button) => {
1240
+ button.addEventListener('click', () => {
1241
+ const now = new Date().toISOString();
1242
+ switch (button.dataset.action) {
1243
+ case 'create-vault':
1244
+ dispatch(createVault({ now, name: 'desktop vault' }));
1245
+ break;
1246
+ case 'start-mcp':
1247
+ dispatch(startMcp({ now }));
1248
+ break;
1249
+ case 'stop-mcp':
1250
+ dispatch(stopMcp({ now }));
1251
+ break;
1252
+ case 'mesh-online':
1253
+ dispatch(updateMeshStatus({ status: 'online', peers: 3, relays: 1, last_witness_at: now }, { now }));
1254
+ break;
1255
+ case 'enterprise-demo':
1256
+ dispatch(updateEnterpriseStatus({ status: 'configured', tenant_id: 'tenant_local', policy_id: 'policy_desktop', siem: 'ready' }, { now }));
1257
+ break;
1258
+ default:
1259
+ break;
1260
+ }
1261
+ });
1262
+ });
1263
+
1264
+ root.querySelectorAll('[data-connect-client]').forEach((button) => {
1265
+ button.addEventListener('click', () => {
1266
+ const template = CLIENT_TEMPLATES.find((client) => client.id === button.dataset.connectClient);
1267
+ dispatch(connectClient(template, { now: new Date().toISOString() }));
1268
+ });
1269
+ });
1270
+
1271
+ root.querySelectorAll('[data-delete-memory]').forEach((button) => {
1272
+ button.addEventListener('click', () => dispatch(deleteMemory(button.dataset.deleteMemory, { now: new Date().toISOString() })));
1273
+ });
1274
+
1275
+ const rememberForm = root.querySelector('[data-form="remember"]');
1276
+ if (rememberForm) {
1277
+ rememberForm.addEventListener('submit', (event) => {
1278
+ event.preventDefault();
1279
+ const data = new FormData(rememberForm);
1280
+ dispatch(rememberMemory({ descriptor: data.get('descriptor'), body: data.get('body'), tags: data.get('tags') }, { now: new Date().toISOString() }));
1281
+ });
1282
+ }
1283
+
1284
+ const searchForm = root.querySelector('[data-form="search"]');
1285
+ if (searchForm) {
1286
+ searchForm.addEventListener('submit', (event) => {
1287
+ event.preventDefault();
1288
+ dispatch(searchMemories(new FormData(searchForm).get('query')));
1289
+ });
1290
+ }
1291
+
1292
+ const verifyForm = root.querySelector('[data-form="verify"]');
1293
+ if (verifyForm) {
1294
+ verifyForm.addEventListener('submit', (event) => {
1295
+ event.preventDefault();
1296
+ const text = String(new FormData(verifyForm).get('receipts') ?? '');
1297
+ dispatch(setDesktopDraft('receiptInput', text));
1298
+ dispatch(verifyReceipts(text, { now: new Date().toISOString() }));
1299
+ });
1300
+ }
1301
+
1302
+ const importForm = root.querySelector('[data-form="import"]');
1303
+ if (importForm) {
1304
+ importForm.addEventListener('submit', (event) => {
1305
+ event.preventDefault();
1306
+ const text = String(new FormData(importForm).get('bundle') ?? '');
1307
+ dispatch(setDesktopDraft('importInput', text));
1308
+ dispatch(importBundle(text, { now: new Date().toISOString() }));
1309
+ });
1310
+ }
1311
+
1312
+ const exportForm = root.querySelector('[data-form="export"]');
1313
+ if (exportForm) {
1314
+ exportForm.addEventListener('submit', (event) => {
1315
+ event.preventDefault();
1316
+ dispatch(exportBundle({ now: new Date().toISOString(), scope: 'all' }));
1317
+ });
1318
+ }
1319
+ }
1320
+
1321
+ if (typeof document !== 'undefined') {
1322
+ if (document.readyState === 'loading') {
1323
+ document.addEventListener('DOMContentLoaded', mountDesktopApp, { once: true });
1324
+ } else {
1325
+ mountDesktopApp();
1326
+ }
1327
+ }