persyst-mcp 1.0.0 → 1.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 +21 -0
- package/README.md +85 -62
- package/index.js +4 -1
- package/package.json +16 -2
- package/src/attestation.js +207 -0
- package/src/database.js +322 -28
- package/src/git.js +72 -11
- package/src/search.js +269 -49
- package/src/server.js +19 -5
- package/src/tools.js +362 -96
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Zayn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Local-first MCP memory server for coding agents.**
|
|
4
4
|
|
|
5
|
-
Persyst gives AI coding agents (Claude Code, Cursor, Aider, Windsurf) 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, 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.
|
|
6
6
|
|
|
7
7
|
## How It Works
|
|
8
8
|
|
|
@@ -14,59 +14,96 @@ Your AI Agent ←→ MCP (stdio) ←→ Persyst ←→ SQLite (local)
|
|
|
14
14
|
2. **Agent searches memories** → Persyst finds matches by both keywords AND meaning
|
|
15
15
|
3. **"dark mode" ↔ "night theme"** → Semantic search understands synonyms
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
> 🚨 **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
18
|
|
|
19
|
-
|
|
19
|
+
---
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
npm install -g persyst-mcp
|
|
23
|
-
```
|
|
21
|
+
## Quick Start
|
|
24
22
|
|
|
25
|
-
|
|
23
|
+
You don't need to install anything globally. You can run it instantly using `npx`:
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
### 1. Add to Claude Code or Claude Desktop
|
|
28
26
|
|
|
27
|
+
#### Claude Code (CLI)
|
|
28
|
+
Add this to your global configuration file located at `~/.claude.json`:
|
|
29
29
|
```json
|
|
30
30
|
{
|
|
31
31
|
"mcpServers": {
|
|
32
32
|
"persyst": {
|
|
33
|
-
"command": "
|
|
33
|
+
"command": "npx",
|
|
34
|
+
"args": ["-y", "persyst-mcp"]
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `get_memory` — Get a specific memory
|
|
45
|
-
- `update_memory` — Update a memory
|
|
46
|
-
- `delete_memory` — Remove a memory
|
|
47
|
-
- `get_recent_memories` — Latest memories
|
|
48
|
-
- `get_important_memories` — Most important memories
|
|
40
|
+
#### Claude Desktop
|
|
41
|
+
Add this to your Claude Desktop configuration file:
|
|
42
|
+
* **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
43
|
+
* **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
44
|
+
* **Linux**: `~/.config/Claude/claude_desktop_config.json`
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"persyst": {
|
|
50
|
+
"command": "npx",
|
|
51
|
+
"args": ["-y", "persyst-mcp"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
51
56
|
|
|
52
|
-
|
|
57
|
+
---
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
## Setup for Other Agents
|
|
55
60
|
|
|
61
|
+
### VS Code (Cline / Roo Code)
|
|
62
|
+
Add this configuration to your user settings under the MCP settings file (`cline_mcp_settings.json`):
|
|
56
63
|
```json
|
|
57
64
|
{
|
|
58
|
-
"
|
|
59
|
-
"
|
|
65
|
+
"mcpServers": {
|
|
66
|
+
"persyst": {
|
|
67
|
+
"command": "npx",
|
|
68
|
+
"args": ["-y", "persyst-mcp"]
|
|
69
|
+
}
|
|
60
70
|
}
|
|
61
71
|
}
|
|
62
72
|
```
|
|
63
73
|
|
|
64
|
-
###
|
|
74
|
+
### Cursor
|
|
75
|
+
Add Persyst in Cursor under **Settings → Features → MCP**:
|
|
76
|
+
1. Click **+ Add New MCP Server**
|
|
77
|
+
2. Name: `persyst`
|
|
78
|
+
3. Type: `stdio`
|
|
79
|
+
4. Command: `npx -y persyst-mcp`
|
|
65
80
|
|
|
81
|
+
### Aider
|
|
82
|
+
Start Aider from the command line passing the server command:
|
|
66
83
|
```bash
|
|
67
|
-
|
|
68
|
-
persyst-mcp &
|
|
84
|
+
aider --mcp-server persyst:npx -y persyst-mcp
|
|
69
85
|
```
|
|
86
|
+
Or append this to your `.aider.conf.yml` project file:
|
|
87
|
+
```yaml
|
|
88
|
+
mcp-server:
|
|
89
|
+
- name: persyst
|
|
90
|
+
command: npx -y persyst-mcp
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Antigravity
|
|
94
|
+
Add Persyst to your Antigravity agent configuration file at `~/.gemini/antigravity/mcp_config.json`:
|
|
95
|
+
```json
|
|
96
|
+
{
|
|
97
|
+
"mcpServers": {
|
|
98
|
+
"persyst": {
|
|
99
|
+
"command": "npx",
|
|
100
|
+
"args": ["-y", "persyst-mcp"]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
70
107
|
|
|
71
108
|
## Available Tools
|
|
72
109
|
|
|
@@ -76,10 +113,12 @@ persyst-mcp &
|
|
|
76
113
|
| `search_memories` | Hybrid keyword + semantic search | `query` (string), `limit` (number) |
|
|
77
114
|
| `get_memory` | Get memory by ID | `id` (number) |
|
|
78
115
|
| `update_memory` | Update memory content | `id` (number), `content` (string) |
|
|
79
|
-
| `delete_memory` | Delete a memory | `id` (number) |
|
|
116
|
+
| `delete_memory` | Delete a memory and clean up edges | `id` (number) |
|
|
80
117
|
| `get_recent_memories` | Get latest memories | `limit` (number) |
|
|
81
118
|
| `get_important_memories` | Get by importance score | `limit` (number) |
|
|
82
119
|
|
|
120
|
+
---
|
|
121
|
+
|
|
83
122
|
## How Search Works
|
|
84
123
|
|
|
85
124
|
Persyst uses **hybrid search** — combining two strategies:
|
|
@@ -89,28 +128,7 @@ Persyst uses **hybrid search** — combining two strategies:
|
|
|
89
128
|
|
|
90
129
|
Results from both are merged. Keyword matches get a score boost so exact matches rank higher, but semantic matches still surface related memories.
|
|
91
130
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
```
|
|
95
|
-
persyst/
|
|
96
|
-
├── index.js ← Entry point (starts MCP server)
|
|
97
|
-
├── src/
|
|
98
|
-
│ ├── server.js ← MCP server (stdio transport)
|
|
99
|
-
│ ├── database.js ← SQLite + schema + CRUD
|
|
100
|
-
│ ├── search.js ← Hybrid search engine
|
|
101
|
-
│ ├── embeddings.js ← Local embedding generation
|
|
102
|
-
│ └── tools.js ← 7 MCP tool definitions
|
|
103
|
-
├── test/
|
|
104
|
-
│ └── smoke.js ← End-to-end test
|
|
105
|
-
└── db/ ← Database files (gitignored)
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
## Data Storage
|
|
109
|
-
|
|
110
|
-
- Database location: `~/.persyst/persyst.db`
|
|
111
|
-
- All data stays on your machine
|
|
112
|
-
- No telemetry, no cloud calls, no API keys
|
|
113
|
-
- Works offline (airplane mode ✓)
|
|
131
|
+
---
|
|
114
132
|
|
|
115
133
|
## Tech Stack
|
|
116
134
|
|
|
@@ -121,21 +139,26 @@ persyst/
|
|
|
121
139
|
- **Embeddings:** @huggingface/transformers + all-MiniLM-L6-v2 (384-dim, ~50MB)
|
|
122
140
|
- **Protocol:** MCP over stdio
|
|
123
141
|
|
|
124
|
-
|
|
142
|
+
---
|
|
125
143
|
|
|
126
|
-
|
|
127
|
-
# Clone and install
|
|
128
|
-
git clone <repo-url>
|
|
129
|
-
cd persyst
|
|
130
|
-
npm install
|
|
144
|
+
## Troubleshooting
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
146
|
+
#### `better-sqlite3` installation fails
|
|
147
|
+
`better-sqlite3` compiles native C++ code on installation. Make sure you have python and C++ build tools installed on your system:
|
|
148
|
+
* **Windows:** Run `npm install --global windows-build-tools` or install Visual Studio Build Tools.
|
|
149
|
+
* **macOS/Linux:** Run `xcode-select --install` or install `build-essential`.
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
151
|
+
#### The agent is stuck or loading forever on startup
|
|
152
|
+
This is normal on the **very first run** because Persyst is downloading the ~50MB embedding model. Wait 30-60 seconds for it to complete. The next runs will be instant.
|
|
153
|
+
|
|
154
|
+
#### Command not found: `persyst-mcp`
|
|
155
|
+
Instead of running it globally, prefer using the `npx -y persyst-mcp` command in your agent configurations. It automatically installs and updates the server non-interactively.
|
|
156
|
+
|
|
157
|
+
#### Permission Denied
|
|
158
|
+
Do not run `npx` with `sudo`. If you run into permission issues, ensure your npm global prefix is owned by your user account.
|
|
159
|
+
|
|
160
|
+
---
|
|
138
161
|
|
|
139
162
|
## License
|
|
140
163
|
|
|
141
|
-
MIT
|
|
164
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "persyst-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Local-first MCP memory server with hybrid keyword + semantic search for coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"persyst-mcp": "index.js"
|
|
9
9
|
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"src/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
10
19
|
"scripts": {
|
|
11
20
|
"start": "node index.js",
|
|
12
21
|
"test": "node test/smoke.js",
|
|
@@ -20,7 +29,12 @@
|
|
|
20
29
|
"ai",
|
|
21
30
|
"agents",
|
|
22
31
|
"semantic-search",
|
|
23
|
-
"knowledge-graph"
|
|
32
|
+
"knowledge-graph",
|
|
33
|
+
"claude-code",
|
|
34
|
+
"cursor",
|
|
35
|
+
"aider",
|
|
36
|
+
"windsurf",
|
|
37
|
+
"memory-server"
|
|
24
38
|
],
|
|
25
39
|
"author": "Zayn",
|
|
26
40
|
"license": "MIT",
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attestation.js — Cryptographic Attestation Engine
|
|
3
|
+
*
|
|
4
|
+
* Implements Ed25519 signature generation and verification for search queries.
|
|
5
|
+
* Chains each attestation by linking to the hash of the previous one.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import crypto from 'crypto';
|
|
9
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import db, { getLastAttestation, insertAttestation, getAttestationById } from './database.js';
|
|
13
|
+
|
|
14
|
+
const KEYS_DIR = join(homedir(), '.persyst', 'keys');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize keypair if it doesn't already exist.
|
|
18
|
+
*/
|
|
19
|
+
export function initializeKeys() {
|
|
20
|
+
mkdirSync(KEYS_DIR, { recursive: true });
|
|
21
|
+
const pubPath = join(KEYS_DIR, 'public.pem');
|
|
22
|
+
const privPath = join(KEYS_DIR, 'private.pem');
|
|
23
|
+
|
|
24
|
+
if (!existsSync(pubPath) || !existsSync(privPath)) {
|
|
25
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
|
|
26
|
+
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
27
|
+
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
28
|
+
});
|
|
29
|
+
writeFileSync(pubPath, publicKey);
|
|
30
|
+
writeFileSync(privPath, privateKey);
|
|
31
|
+
console.error('[persyst] Generated new Ed25519 keypair for attestation');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read private key.
|
|
37
|
+
*/
|
|
38
|
+
function getPrivateKey() {
|
|
39
|
+
const privPath = join(KEYS_DIR, 'private.pem');
|
|
40
|
+
return readFileSync(privPath, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read public key.
|
|
45
|
+
*/
|
|
46
|
+
export function getPublicKey() {
|
|
47
|
+
const pubPath = join(KEYS_DIR, 'public.pem');
|
|
48
|
+
return readFileSync(pubPath, 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Generate a new attestation for search results.
|
|
53
|
+
*/
|
|
54
|
+
export function createAttestation(query, memories, agentId = null, sessionId = null) {
|
|
55
|
+
initializeKeys();
|
|
56
|
+
|
|
57
|
+
const attestationId = crypto.randomUUID();
|
|
58
|
+
const timestamp = new Date().toISOString();
|
|
59
|
+
|
|
60
|
+
// Map memories to {id, content_hash, score}
|
|
61
|
+
const memoriesRetrieved = memories.map(m => {
|
|
62
|
+
const contentHash = crypto.createHash('sha256').update(m.content).digest('hex');
|
|
63
|
+
return {
|
|
64
|
+
id: m.id,
|
|
65
|
+
content_hash: contentHash,
|
|
66
|
+
score: parseFloat(m.hybrid_score || 0)
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Fetch previous attestation hash for the hash chain
|
|
71
|
+
const lastAtt = getLastAttestation();
|
|
72
|
+
const previousHash = lastAtt ? lastAtt.hash : null;
|
|
73
|
+
|
|
74
|
+
// Construct document to sign (ordered keys to ensure canonical serialization)
|
|
75
|
+
const doc = {
|
|
76
|
+
attestation_id: attestationId,
|
|
77
|
+
query,
|
|
78
|
+
timestamp,
|
|
79
|
+
memories_retrieved: memoriesRetrieved,
|
|
80
|
+
agent_id: agentId || null,
|
|
81
|
+
session_id: sessionId || null,
|
|
82
|
+
previous_hash: previousHash
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const dataToSign = JSON.stringify(doc);
|
|
86
|
+
|
|
87
|
+
// Sign document using Ed25519
|
|
88
|
+
const privateKey = getPrivateKey();
|
|
89
|
+
const signature = crypto.sign(null, Buffer.from(dataToSign), {
|
|
90
|
+
key: privateKey,
|
|
91
|
+
type: 'pkcs8',
|
|
92
|
+
format: 'pem'
|
|
93
|
+
}).toString('hex');
|
|
94
|
+
|
|
95
|
+
// Construct full record and compute its hash
|
|
96
|
+
const fullAttestation = {
|
|
97
|
+
...doc,
|
|
98
|
+
signature
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const hash = crypto.createHash('sha256').update(JSON.stringify(fullAttestation)).digest('hex');
|
|
102
|
+
|
|
103
|
+
const record = {
|
|
104
|
+
...fullAttestation,
|
|
105
|
+
hash
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Persist to DB
|
|
109
|
+
insertAttestation(record);
|
|
110
|
+
|
|
111
|
+
return record;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Verify a single attestation record's signature and hash.
|
|
116
|
+
*/
|
|
117
|
+
export function verifyAttestationRecord(attestation) {
|
|
118
|
+
try {
|
|
119
|
+
const doc = {
|
|
120
|
+
attestation_id: attestation.attestation_id,
|
|
121
|
+
query: attestation.query,
|
|
122
|
+
timestamp: attestation.timestamp,
|
|
123
|
+
memories_retrieved: typeof attestation.memories_retrieved === 'string'
|
|
124
|
+
? JSON.parse(attestation.memories_retrieved)
|
|
125
|
+
: attestation.memories_retrieved,
|
|
126
|
+
agent_id: attestation.agent_id || null,
|
|
127
|
+
session_id: attestation.session_id || null,
|
|
128
|
+
previous_hash: attestation.previous_hash || null
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const dataToSign = JSON.stringify(doc);
|
|
132
|
+
const publicKey = getPublicKey();
|
|
133
|
+
|
|
134
|
+
// Verify signature
|
|
135
|
+
const isSignatureValid = crypto.verify(
|
|
136
|
+
null,
|
|
137
|
+
Buffer.from(dataToSign),
|
|
138
|
+
{
|
|
139
|
+
key: publicKey,
|
|
140
|
+
type: 'spki',
|
|
141
|
+
format: 'pem'
|
|
142
|
+
},
|
|
143
|
+
Buffer.from(attestation.signature, 'hex')
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (!isSignatureValid) {
|
|
147
|
+
return { valid: false, error: 'Signature verification failed' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Verify hash integrity
|
|
151
|
+
const fullRecord = {
|
|
152
|
+
...doc,
|
|
153
|
+
signature: attestation.signature
|
|
154
|
+
};
|
|
155
|
+
const computedHash = crypto.createHash('sha256').update(JSON.stringify(fullRecord)).digest('hex');
|
|
156
|
+
|
|
157
|
+
if (computedHash !== attestation.hash) {
|
|
158
|
+
return { valid: false, error: 'Attestation hash mismatch' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { valid: true };
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return { valid: false, error: err.message };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Verifies signature and chain integrity.
|
|
169
|
+
*/
|
|
170
|
+
export function verifyChainIntegrity(attestationId) {
|
|
171
|
+
const att = getAttestationById(attestationId);
|
|
172
|
+
if (!att) {
|
|
173
|
+
return { valid: false, error: `Attestation not found: ${attestationId}` };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const selfVerify = verifyAttestationRecord(att);
|
|
177
|
+
if (!selfVerify.valid) {
|
|
178
|
+
return selfVerify;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// If there's a previous link, check it
|
|
182
|
+
if (att.previous_hash) {
|
|
183
|
+
const db = getLastAttestation(); // Wait, we can run raw query on DB or use prepared stmt
|
|
184
|
+
const prevAtt = getAttestationByHash(att.previous_hash);
|
|
185
|
+
if (!prevAtt) {
|
|
186
|
+
return { valid: false, error: `Broken chain: Previous attestation with hash ${att.previous_hash} not found` };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (prevAtt.id >= att.id) {
|
|
190
|
+
return { valid: false, error: `Broken chain: Invalid sequence order` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const prevVerify = verifyAttestationRecord(prevAtt);
|
|
194
|
+
if (!prevVerify.valid) {
|
|
195
|
+
return { valid: false, error: `Broken chain: Previous link is invalid: ${prevVerify.error}` };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { valid: true, attestation: att };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Helper to fetch attestation by hash since it's not exposed globally.
|
|
204
|
+
*/
|
|
205
|
+
function getAttestationByHash(hash) {
|
|
206
|
+
return db.prepare('SELECT * FROM attestations WHERE hash = ?').get(hash) || null;
|
|
207
|
+
}
|