persyst-mcp 2.2.5 → 2.2.6

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/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # Persyst
2
2
 
3
- **Local-first MCP memory server for coding agents.**
3
+ **Local-first, compliance-grade MCP memory layer for regulated enterprise coding teams using AI assistants.**
4
4
 
5
- Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider, Windsurf, Antigravity) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — no cloud, no API keys, works offline.
5
+ Persyst gives AI coding agents (Claude Code, Cursor, VS Code, Aider, Continue.dev, Antigravity) persistent memory across sessions. It stores memories in a local SQLite database with hybrid keyword + semantic search — operating 100% offline with zero cloud egress.
6
+
7
+ ## Compliance-Grade Security Features
8
+
9
+ Persyst is built from the ground up for highly regulated enterprise environments (finance, healthcare, defense) subject to **SOC 2**, **HIPAA**, and the **EU AI Act**:
10
+
11
+ * **100% Data Residency (Zero-Egress)**: All vector calculations, full-text searches, and model inferences run locally on the developer's workstation. No database records or context data ever leave the local machine. Bypasses Business Associate Agreement (BAA) complexity for HIPAA.
12
+ * **Cryptographic Chain of Custody**: Every context retrieval generates an Ed25519 cryptographic signature sealing the query and retrieved memory hashes. Each attestation is chained to the previous one via SHA-256 hash chains, creating a tamper-evident audit ledger verifiable by security teams.
13
+ * **Automatic Secret Redaction**: Scans incoming log files and text writes to redact high-entropy secrets (API keys, JWTs, database strings, private keys) before they reach the persistent database.
14
+ * **Reactive File Watching**: Integrates `chokidar` for instant event-driven scanning of agent transcript folders, guaranteeing that your memories are synchronized immediately after each agent interaction.
15
+
16
+ *Read more in our compliance mapping guides:*
17
+ - [SOC 2 Type II Controls](file:///c:/Users/Super/Desktop/Peryst/compliance/SOC2-controls.md)
18
+ - [HIPAA Mapping & PHI Boundaries](file:///c:/Users/Super/Desktop/Peryst/compliance/HIPAA-mapping.md)
19
+ - [EU AI Act Article 13 Transparency](file:///c:/Users/Super/Desktop/Peryst/compliance/EU-AI-Act-Article13.md)
20
+ - [Compliance Audit Trail Sample](file:///c:/Users/Super/Desktop/Peryst/compliance/audit-trail-sample.md)
6
21
 
7
22
  ## How It Works
8
23
 
@@ -16,6 +31,14 @@ Your AI Agent ←→ MCP (stdio) ←→ Persyst ←→ SQLite (local)
16
31
 
17
32
  > 🚨 **First-Run Note**: On the first start, Persyst will automatically download the local embedding model (`all-MiniLM-L6-v2` ~50MB). This can take 30-60 seconds depending on your connection. The server will log `Loading embedding model...` and then proceed normally.
18
33
 
34
+ ### Passive Recording vs. Active Retrieval
35
+
36
+ > ⚠️ **Honest Note on Agent Integration**: Persyst operates in two complementary modes:
37
+ > 1. **Passive Recording**: The file watcher automatically extracts and saves memories from your agent conversation transcripts in the background.
38
+ > 2. **Active Retrieval**: The AI agent calls `search_memories` or `get_optimized_context` to fetch relevant context.
39
+ >
40
+ > The IDE itself does not automatically inject retrieved memories into prompt inputs unless configured to do so via workspace rules (e.g. `.cursorrules`, `.windsurfrules`, `.agents/AGENTS.md`) or custom system prompt builders. To ensure the agent utilizes its memory, make sure your agent instructions direct it to query the database.
41
+
19
42
  ---
20
43
 
21
44
  ## Quick Start
package/bin/init.js CHANGED
@@ -1,30 +1,33 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * persyst-init — Workspace rules generator for VS Code-based IDEs (Cursor, Windsurf, Antigravity)
4
+ * persyst-init — Workspace rules generator and global IDE configuration builder
5
5
  *
6
6
  * Usage:
7
7
  * npx persyst-mcp init
8
+ * npx persyst-mcp init --mcp cursor,aider
8
9
  *
9
10
  * What it does:
10
- * 1. Safely creates or appends system instructions to `.cursorrules`
11
- * 2. Safely creates or appends system instructions to `.windsurfrules`
12
- * 3. Creates a general `.persystrules.md` copy-pasteable guide
13
- * 4. Prints instructions on configuring MCP servers in Cursor/VS Code/Antigravity
14
- *
15
- * Design:
16
- * - Non-destructive: checks for existing content before appending to avoid duplication
17
- * - Idempotent: safe to run multiple times
18
- * - Localized: targets the current working directory (project root)
11
+ * 1. Safely creates or appends system instructions to `.cursorrules` and `.windsurfrules`
12
+ * 2. Creates a general `.persystrules.md` workspace guide
13
+ * 3. Configures Git post-commit hook for auto-ingestion
14
+ * 4. Generates cryptographic Ed25519 keys inside ~/.persyst
15
+ * 5. Automatically detects and configures global settings for Cursor, Aider, Claude Code, and Continue
19
16
  */
20
17
 
21
18
  import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
22
19
  import { join, resolve, dirname } from 'path';
20
+ import { homedir } from 'os';
23
21
  import { fileURLToPath } from 'url';
22
+ import { execSync } from 'child_process';
23
+ import { initializeKeys } from '../src/attestation.js';
24
24
 
25
25
  const __filename = fileURLToPath(import.meta.url);
26
26
  const __dirname = dirname(__filename);
27
27
 
28
+ const CONFIG_DIR = join(homedir(), '.persyst');
29
+ const PERSYST_DB = join(CONFIG_DIR, 'persyst.db');
30
+
28
31
  // ============================================================
29
32
  // SYSTEM INSTRUCTION CONTENT
30
33
  // ============================================================
@@ -46,6 +49,10 @@ You are integrated with Persyst, a local-first MCP memory server that stores use
46
49
  - Handle Contradictions: Persyst handles contradiction detection automatically. If a new fact contradicts an old memory, Persyst will flag it.
47
50
  - Quality Over Quantity: Do NOT store trivial facts, temporary conversation noise, or duplicate data. "Bad data is worse than no data". Only store long-term architecture decisions, project details, and explicit user preferences.
48
51
 
52
+ ## Explicit User Save Requests
53
+ - If the user explicitly asks you to remember, save, or keep a note of a fact (e.g., "Remember that John handles deployment", "remind me that staging is flaky"), call the \`add_memory\` tool immediately with that content.
54
+ - Bypassing Tech Filters: Explicit user requests bypass the programming keyword filters. Ensure they are captured verbatim.
55
+
49
56
  ## Mandatory Completion Checklist (HARD CONSTRAINT)
50
57
  Before writing your final response declaring a task, feature, or bug fix complete:
51
58
  1. Ask yourself: "Did I implement a feature, fix a bug, configure a tool, or discover a project rule?"
@@ -82,7 +89,7 @@ ${RULE_CONTENT.trim()}
82
89
  `;
83
90
 
84
91
  // ============================================================
85
- // HELPERS
92
+ // WORKSPACE HELPERS
86
93
  // ============================================================
87
94
 
88
95
  function setupRuleFile(filePath, fileName) {
@@ -104,33 +111,153 @@ function setupRuleFile(filePath, fileName) {
104
111
  }
105
112
 
106
113
  // ============================================================
107
- // MAIN
114
+ // GLOBAL CONFIG WRITERS
115
+ // ============================================================
116
+
117
+ function detectEditors() {
118
+ const editors = [];
119
+ const home = homedir();
120
+
121
+ // Cursor
122
+ const cursorDir = join(home, '.cursor');
123
+ const winCursorDir = join(home, 'AppData', 'Roaming', 'Cursor');
124
+ if (existsSync(cursorDir) || existsSync(winCursorDir) || existsSync('/Applications/Cursor.app') || existsSync(join(home, 'AppData', 'Local', 'Programs', 'cursor'))) {
125
+ editors.push('cursor');
126
+ }
127
+
128
+ // Aider
129
+ try {
130
+ execSync('aider --version', { stdio: 'ignore' });
131
+ editors.push('aider');
132
+ } catch (_) {}
133
+
134
+ // Claude Code
135
+ const claudeDir = join(home, '.claude');
136
+ if (existsSync(claudeDir) || existsSync('/Applications/Claude Code.app')) {
137
+ editors.push('claude-code');
138
+ }
139
+
140
+ // Continue.dev
141
+ const continueConfig = join(home, '.continue', 'config.json');
142
+ if (existsSync(continueConfig)) {
143
+ editors.push('continue');
144
+ }
145
+
146
+ return editors;
147
+ }
148
+
149
+ function writeCursorConfig() {
150
+ const cursorMcp = join(homedir(), '.cursor', 'mcp.json');
151
+ try {
152
+ const config = existsSync(cursorMcp) ? JSON.parse(readFileSync(cursorMcp, 'utf8')) : {};
153
+ config.mcpServers = config.mcpServers || {};
154
+ config.mcpServers.persyst = {
155
+ "command": "npx",
156
+ "args": ["-y", "persyst-mcp"],
157
+ "env": { "PERSYST_DB": PERSYST_DB }
158
+ };
159
+ mkdirSync(dirname(cursorMcp), { recursive: true });
160
+ writeFileSync(cursorMcp, JSON.stringify(config, null, 2));
161
+ console.log(' ✅ Cursor MCP config written to ~/.cursor/mcp.json');
162
+ } catch (err) {
163
+ console.error(` ✗ Failed to configure Cursor: ${err.message}`);
164
+ }
165
+ }
166
+
167
+ function writeAiderConfig() {
168
+ const aiderYml = join(homedir(), '.aider.conf.yml');
169
+ try {
170
+ let content = '';
171
+ if (existsSync(aiderYml)) {
172
+ content = readFileSync(aiderYml, 'utf8');
173
+ }
174
+ if (!content.includes('persyst')) {
175
+ content += `\n# Persyst MCP integration\nmcp:\n - name: persyst\n cmd: npx\n args: ["-y", "persyst-mcp"]\n env:\n PERSYST_DB: ${PERSYST_DB}\n`;
176
+ writeFileSync(aiderYml, content);
177
+ console.log(' ✅ Aider MCP config appended to ~/.aider.conf.yml');
178
+ } else {
179
+ console.log(' ℹ️ Aider already has Persyst configured (skipped).');
180
+ }
181
+ } catch (err) {
182
+ console.error(` ✗ Failed to configure Aider: ${err.message}`);
183
+ }
184
+ }
185
+
186
+ function writeClaudeCodeConfig() {
187
+ const claudeJson = join(homedir(), '.claude.json');
188
+ try {
189
+ const config = existsSync(claudeJson) ? JSON.parse(readFileSync(claudeJson, 'utf8')) : {};
190
+ config.mcpServers = config.mcpServers || {};
191
+ config.mcpServers.persyst = {
192
+ "command": "npx",
193
+ "args": ["-y", "persyst-mcp"],
194
+ "env": { "PERSYST_DB": PERSYST_DB }
195
+ };
196
+ writeFileSync(claudeJson, JSON.stringify(config, null, 2));
197
+ console.log(' ✅ Claude Code MCP config written to ~/.claude.json');
198
+ } catch (err) {
199
+ console.error(` ✗ Failed to configure Claude Code: ${err.message}`);
200
+ }
201
+ }
202
+
203
+ function writeContinueConfig() {
204
+ const continueConfig = join(homedir(), '.continue', 'config.json');
205
+ try {
206
+ const config = existsSync(continueConfig) ? JSON.parse(readFileSync(continueConfig, 'utf8')) : {};
207
+ config.mcpServers = config.mcpServers || [];
208
+ // Remove existing persyst entry
209
+ config.mcpServers = config.mcpServers.filter(s => s.name !== 'persyst');
210
+ config.mcpServers.push({
211
+ "name": "persyst",
212
+ "command": "npx",
213
+ "args": ["-y", "persyst-mcp"],
214
+ "env": { "PERSYST_DB": PERSYST_DB }
215
+ });
216
+ mkdirSync(dirname(continueConfig), { recursive: true });
217
+ writeFileSync(continueConfig, JSON.stringify(config, null, 2));
218
+ console.log(' ✅ Continue.dev MCP config written to ~/.continue/config.json');
219
+ } catch (err) {
220
+ console.error(` ✗ Failed to configure Continue.dev: ${err.message}`);
221
+ }
222
+ }
223
+
224
+ // ============================================================
225
+ // MAIN RUNNER
108
226
  // ============================================================
109
227
 
110
228
  function run() {
111
229
  console.log('');
112
- console.log(' 🧠 Persyst — Workspace Rules Setup');
113
- console.log(' ════════════════════════════════════');
230
+ console.log(' 🧠 Persyst — Workspace & Editor Setup');
231
+ console.log(' ══════════════════════════════════════');
114
232
  console.log('');
115
233
 
116
234
  const cwd = process.cwd();
117
235
  console.log(` 📁 Target workspace: ${cwd}`);
118
- console.log('');
119
236
 
120
- // 1. Create/Append Cursor Rules
237
+ // 1. Initialize local configuration folder and attestations
238
+ console.log(' ⚙️ Initializing keypairs & DB folders...');
239
+ mkdirSync(CONFIG_DIR, { recursive: true });
240
+ initializeKeys();
241
+ console.log(' ✅ Cryptographic keypairs generated');
242
+
243
+ // 2. Local workspace configurations
244
+ console.log('');
245
+ console.log(' 📄 Initializing workspace rule files...');
246
+
121
247
  const cursorRulesPath = join(cwd, '.cursorrules');
122
248
  setupRuleFile(cursorRulesPath, '.cursorrules');
123
249
 
124
- // 2. Create/Append Windsurf Rules
125
250
  const windsurfRulesPath = join(cwd, '.windsurfrules');
126
251
  setupRuleFile(windsurfRulesPath, '.windsurfrules');
127
252
 
128
- // 3. Create General Guide File
253
+ const clineRulesPath = join(cwd, '.clinerules');
254
+ setupRuleFile(clineRulesPath, '.clinerules');
255
+
129
256
  const generalGuidePath = join(cwd, '.persystrules.md');
130
257
  writeFileSync(generalGuidePath, GENERAL_GUIDE.trim() + '\n', 'utf8');
131
258
  console.log(' ✅ Created .persystrules.md (General Guide)');
132
259
 
133
- // 4. Configure Git post-commit hook for automatic commit ingestion
260
+ // 3. Git post-commit hook
134
261
  const gitDir = join(cwd, '.git');
135
262
  if (existsSync(gitDir)) {
136
263
  const hooksDir = join(gitDir, 'hooks');
@@ -159,22 +286,31 @@ fi
159
286
  console.log(' ✅ Configured Git post-commit hook for auto-ingestion');
160
287
  }
161
288
 
162
- // 5. Print Success & Configuration Help
289
+ // 4. Global editor configurations
163
290
  console.log('');
164
- console.log(' ════════════════════════════════════');
165
- console.log(' ✅ Rules and Git hooks initialization complete!');
291
+ console.log(' 💻 Initializing global IDE configurations...');
292
+
293
+ const args = process.argv.slice(2);
294
+ const mcpFlag = args.find(a => a.startsWith('--mcp='));
295
+ const requestedEditors = mcpFlag ? mcpFlag.split('=')[1].split(',') : [];
296
+
297
+ const editors = requestedEditors.length > 0 ? requestedEditors : detectEditors();
298
+ console.log(` Detected editors/environments: ${editors.join(', ') || 'none'}`);
299
+
300
+ if (editors.includes('cursor')) writeCursorConfig();
301
+ if (editors.includes('aider')) writeAiderConfig();
302
+ if (editors.includes('claude-code')) writeClaudeCodeConfig();
303
+ if (editors.includes('continue')) writeContinueConfig();
304
+
305
+ // 5. Final self-test and notes
166
306
  console.log('');
167
- console.log(' To connect the memory server to Cursor, Antigravity, or VS Code:');
168
- console.log(' 1. Open your IDE Settings -> MCP (Model Context Protocol).');
169
- console.log(' 2. Add a new command server:');
170
- console.log(' • Name: persyst');
171
- console.log(' • Command: npx');
172
- console.log(' • Arguments: -y persyst-mcp');
307
+ console.log(' ══════════════════════════════════════');
308
+ console.log(' 🎉 Setup complete! Persyst is fully configured.');
173
309
  console.log('');
174
- console.log(' The rules we generated will guide the AI agents in this workspace to:');
175
- console.log(' Proactively search memory before answering prompts.');
176
- console.log(' Log milestone achievements and user preferences.');
177
- console.log(' Keep the memory clean ("no bad data").');
310
+ console.log(' Next steps:');
311
+ console.log(' 1. Restart your editor to load the new MCP configurations.');
312
+ console.log(' 2. Test gateway connection:');
313
+ console.log(' curl http://127.0.0.1:4321/health');
178
314
  console.log('');
179
315
  }
180
316
 
package/bin/mcp.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from '../src/server.js';
3
+
4
+ startServer().catch(err => {
5
+ console.error('❌ Persyst failed to start:', err.message);
6
+ process.exit(1);
7
+ });
package/index.js CHANGED
@@ -39,22 +39,42 @@ if (process.versions.bun && !process.env.PERSYST_RUN_BY_NODE) {
39
39
 
40
40
  // Fix PATH on Windows if running in environments like Qwen Desktop that override PATH
41
41
  if (process.platform === 'win32') {
42
- const nodeBin = 'C:\\Program Files\\nodejs';
43
- const gitBin = 'C:\\Program Files\\Git\\cmd';
44
- const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
45
-
46
42
  const currentPath = process.env.PATH || '';
47
43
  const paths = currentPath.split(';');
48
-
49
- if (!paths.includes(nodeBin)) paths.push(nodeBin);
50
- if (!paths.includes(gitBin)) paths.push(gitBin);
51
-
52
- // Make sure system folders are there
44
+
45
+ // Dynamically find node and git on PATH using where.exe
46
+ const { execFileSync } = await import('child_process');
47
+ for (const cmd of ['node', 'git']) {
48
+ try {
49
+ const result = execFileSync('where.exe', [cmd], { encoding: 'utf8', timeout: 2000 });
50
+ const binDir = result.trim().split('\r\n')[0].trim();
51
+ if (binDir) {
52
+ const dir = binDir.substring(0, binDir.lastIndexOf('\\'));
53
+ if (dir && !paths.some(p => p.toLowerCase() === dir.toLowerCase())) {
54
+ paths.push(dir);
55
+ }
56
+ }
57
+ } catch {
58
+ // where.exe failed — fall back to common paths
59
+ if (cmd === 'node') {
60
+ for (const p of ['C:\\Program Files\\nodejs', process.env.NVM_SYMLINK, `${process.env.USERPROFILE}\\AppData\\Roaming\\nvm\\v20.11.0`].filter(Boolean)) {
61
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
62
+ }
63
+ } else if (cmd === 'git') {
64
+ for (const p of ['C:\\Program Files\\Git\\cmd', 'C:\\Program Files\\Git\\bin', `${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`].filter(Boolean)) {
65
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
66
+ }
67
+ }
68
+ }
69
+ }
70
+
71
+ // Ensure system folders are present
72
+ const systemBin = 'C:\\WINDOWS\\system32;C:\\WINDOWS';
53
73
  const sysPaths = systemBin.split(';');
54
74
  sysPaths.forEach(p => {
55
- if (!paths.includes(p)) paths.push(p);
75
+ if (!paths.some(ex => ex.toLowerCase() === p.toLowerCase())) paths.push(p);
56
76
  });
57
-
77
+
58
78
  process.env.PATH = paths.join(';');
59
79
  }
60
80
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "persyst-mcp",
3
- "version": "2.2.5",
4
- "description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
3
+ "version": "2.2.6",
4
+ "description": "Local-first, compliance-grade MCP memory layer with hybrid keyword + semantic search for coding agents",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -12,15 +12,10 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
- "persyst-mcp": "index.js",
16
- "persyst-setup": "bin/setup.js",
17
- "persyst-aider": "bin/aider.js",
18
- "persyst-init": "bin/init.js",
19
- "persyst-ingest": "bin/ingest.js",
20
- "persyst-extract": "bin/extract.js",
21
- "persyst-worker": "bin/extract-worker.js",
22
- "persyst-export": "bin/export.js",
23
- "persyst-import": "bin/import.js"
15
+ "persyst-mcp": "./bin/mcp.js",
16
+ "persyst-setup": "./bin/setup.js",
17
+ "persyst-init": "./bin/init.js",
18
+ "persyst": "./index.js"
24
19
  },
25
20
  "engines": {
26
21
  "node": ">=18.0.0"
@@ -9,7 +9,7 @@ import crypto from 'crypto';
9
9
  import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import { homedir } from 'os';
12
- import db, { getLastAttestation, insertAttestation, getAttestationById } from './database.js';
12
+ import db, { stmts, getLastAttestation, insertAttestation, getAttestationById } from './database.js';
13
13
 
14
14
  const KEYS_DIR = join(homedir(), '.persyst', 'keys');
15
15
 
@@ -69,7 +69,7 @@ export function createAttestation(query, memories, agentId = null, sessionId = n
69
69
  return {
70
70
  id: m.id,
71
71
  content_hash: contentHash,
72
- score: parseFloat(scoreVal)
72
+ score: Math.round(parseFloat(scoreVal) * 10000)
73
73
  };
74
74
  });
75
75
 
@@ -135,6 +135,22 @@ export function verifyAttestationRecord(attestation) {
135
135
  };
136
136
 
137
137
  const dataToSign = JSON.stringify(doc);
138
+ const fullRecord = {
139
+ ...doc,
140
+ signature: attestation.signature
141
+ };
142
+ const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
143
+
144
+ // Check hash first — if it matches, doc reconstruction is correct
145
+ const hashMatch = computedHash === attestation.hash;
146
+ if (!hashMatch) {
147
+ console.error('[persyst-attest] HASH MISMATCH for', attestation.attestation_id);
148
+ console.error('[persyst-attest] stored hash:', attestation.hash);
149
+ console.error('[persyst-attest] computed hash:', computedHash);
150
+ console.error('[persyst-attest] doc:', JSON.stringify(doc));
151
+ return { valid: false, error: 'Attestation hash mismatch' };
152
+ }
153
+
138
154
  const publicKey = getPublicKey();
139
155
 
140
156
  // Verify signature
@@ -150,20 +166,14 @@ export function verifyAttestationRecord(attestation) {
150
166
  );
151
167
 
152
168
  if (!isSignatureValid) {
169
+ console.error('[persyst-attest] SIG VERIFY FAIL for', attestation.attestation_id);
170
+ console.error('[persyst-attest] Hash matches but signature invalid');
171
+ console.error('[persyst-attest] dataToSign:', dataToSign);
172
+ console.error('[persyst-attest] signature:', attestation.signature);
173
+ console.error('[persyst-attest] public key:', publicKey);
153
174
  return { valid: false, error: 'Signature verification failed' };
154
175
  }
155
176
 
156
- // Verify hash integrity
157
- const fullRecord = {
158
- ...doc,
159
- signature: attestation.signature
160
- };
161
- const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
162
-
163
- if (computedHash !== attestation.hash) {
164
- return { valid: false, error: 'Attestation hash mismatch' };
165
- }
166
-
167
177
  return { valid: true };
168
178
  } catch (err) {
169
179
  return { valid: false, error: err.message };
@@ -171,7 +181,10 @@ export function verifyAttestationRecord(attestation) {
171
181
  }
172
182
 
173
183
  /**
174
- * Verifies signature and chain integrity.
184
+ * Iteratively verifies signature and chain integrity.
185
+ * Walks backwards from the target attestation to the genesis link,
186
+ * confirming each previous_hash matches the predecessor's actual hash
187
+ * and that sequence order strictly increases.
175
188
  */
176
189
  export function verifyChainIntegrity(attestationId) {
177
190
  const att = getAttestationById(attestationId);
@@ -184,29 +197,37 @@ export function verifyChainIntegrity(attestationId) {
184
197
  return selfVerify;
185
198
  }
186
199
 
187
- // If there's a previous link, check it
188
- if (att.previous_hash) {
189
- const prevAtt = getAttestationByHash(att.previous_hash);
200
+ // Iterative chain walk no recursion, no stack overflow risk
201
+ const MAX_CHAIN_DEPTH = 10000;
202
+ let current = att;
203
+ let depth = 0;
204
+
205
+ while (current.previous_hash) {
206
+ if (depth >= MAX_CHAIN_DEPTH) {
207
+ return { valid: false, error: 'Broken chain: chain length exceeds maximum' };
208
+ }
209
+
210
+ const prevAtt = stmts.getAttestationByHash.get(current.previous_hash);
190
211
  if (!prevAtt) {
191
- return { valid: false, error: `Broken chain: Previous attestation with hash ${att.previous_hash} not found` };
212
+ return { valid: false, error: `Broken chain: Previous attestation with hash ${current.previous_hash} not found` };
192
213
  }
193
214
 
194
- if (prevAtt.id >= att.id) {
195
- return { valid: false, error: `Broken chain: Invalid sequence order` };
215
+ if (prevAtt.hash !== current.previous_hash) {
216
+ return { valid: false, error: 'Broken chain: previous_hash does not match predecessor hash' };
217
+ }
218
+
219
+ if (prevAtt.id >= current.id) {
220
+ return { valid: false, error: 'Broken chain: Invalid sequence order' };
196
221
  }
197
222
 
198
223
  const prevVerify = verifyAttestationRecord(prevAtt);
199
224
  if (!prevVerify.valid) {
200
225
  return { valid: false, error: `Broken chain: Previous link is invalid: ${prevVerify.error}` };
201
226
  }
202
- }
203
227
 
204
- return { valid: true, attestation: att };
205
- }
228
+ current = prevAtt;
229
+ depth++;
230
+ }
206
231
 
207
- /**
208
- * Helper to fetch attestation by hash since it's not exposed globally.
209
- */
210
- function getAttestationByHash(hash) {
211
- return db.prepare('SELECT * FROM attestations WHERE hash = ?').get(hash) || null;
232
+ return { valid: true, attestation: current };
212
233
  }