@nodatachat/mcp 0.9.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.md +105 -0
- package/README.md +163 -151
- package/dist/index.js +108 -909
- package/package.json +49 -44
- package/dist/index.d.ts +0 -31
package/dist/index.js
CHANGED
|
@@ -1,910 +1,109 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
* NoData MCP Server
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
grantToken = argv[++i];
|
|
111
|
-
}
|
|
112
|
-
else if (argv[i] === "--base-url" && argv[i + 1]) {
|
|
113
|
-
baseUrl = argv[++i];
|
|
114
|
-
}
|
|
115
|
-
else if (argv[i] === "--help" || argv[i] === "-h") {
|
|
116
|
-
console.log(USAGE);
|
|
117
|
-
process.exit(0);
|
|
118
|
-
}
|
|
119
|
-
else if (argv[i] === "--version" || argv[i] === "-v") {
|
|
120
|
-
console.log(VERSION);
|
|
121
|
-
process.exit(0);
|
|
122
|
-
}
|
|
123
|
-
else if (argv[i].startsWith("--")) {
|
|
124
|
-
// A typo like --apikey silently degrading to setup mode is worse than noise.
|
|
125
|
-
console.error(`nodata-mcp: unknown option '${argv[i]}' (see --help)`);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
let credentialSource = apiKey || grantToken ? "flag" : "none";
|
|
129
|
-
if (!apiKey && process.env.NODATA_API_KEY) {
|
|
130
|
-
apiKey = process.env.NODATA_API_KEY;
|
|
131
|
-
credentialSource = "env";
|
|
132
|
-
}
|
|
133
|
-
if (!grantToken && process.env.NODATA_GRANT_TOKEN) {
|
|
134
|
-
grantToken = process.env.NODATA_GRANT_TOKEN;
|
|
135
|
-
if (credentialSource === "none")
|
|
136
|
-
credentialSource = "env";
|
|
137
|
-
}
|
|
138
|
-
if (process.env.NODATA_BASE_URL)
|
|
139
|
-
baseUrl = process.env.NODATA_BASE_URL;
|
|
140
|
-
// Last resort: what nodata_connect saved. An explicit flag or env always wins,
|
|
141
|
-
// so this only fires on a bare `npx @nodatachat/mcp` — the "autoload forever"
|
|
142
|
-
// path after a one-time connect. A stored DEVICE (attest) is preferred over a
|
|
143
|
-
// stored api_key.
|
|
144
|
-
let device;
|
|
145
|
-
if (!apiKey && !grantToken) {
|
|
146
|
-
const stored = readStoredCredential();
|
|
147
|
-
if (stored) {
|
|
148
|
-
// Honor a stored base_url only if nothing more explicit set one.
|
|
149
|
-
if (!process.env.NODATA_BASE_URL && baseUrl === "https://www.nodatacapsule.com" && stored.base_url) {
|
|
150
|
-
baseUrl = stored.base_url;
|
|
151
|
-
}
|
|
152
|
-
if (stored.device) {
|
|
153
|
-
device = stored.device;
|
|
154
|
-
credentialSource = "stored";
|
|
155
|
-
}
|
|
156
|
-
else if (stored.api_key) {
|
|
157
|
-
apiKey = stored.api_key;
|
|
158
|
-
credentialSource = "stored";
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
163
|
-
return { apiKey, grantToken, baseUrl, credentialSource, device };
|
|
164
|
-
}
|
|
165
|
-
// Without a deadline a dead network turns a tool call into an indefinite hang
|
|
166
|
-
// inside the MCP client; 30s comfortably covers a slow decide/retrieve.
|
|
167
|
-
const REQUEST_TIMEOUT_MS = 30_000;
|
|
168
|
-
async function doFetch(url, init) {
|
|
169
|
-
let res;
|
|
170
|
-
try {
|
|
171
|
-
res = await fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
172
|
-
}
|
|
173
|
-
catch (err) {
|
|
174
|
-
const timedOut = err instanceof Error && err.name === "TimeoutError";
|
|
175
|
-
return {
|
|
176
|
-
ok: false,
|
|
177
|
-
status: 0,
|
|
178
|
-
body: {
|
|
179
|
-
error: timedOut
|
|
180
|
-
? `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s: ${url}`
|
|
181
|
-
: `Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}`,
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
let parsed;
|
|
186
|
-
try {
|
|
187
|
-
parsed = await res.json();
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
parsed = { error: `Non-JSON response (HTTP ${res.status})` };
|
|
191
|
-
}
|
|
192
|
-
return { ok: res.ok, status: res.status, body: parsed };
|
|
193
|
-
}
|
|
194
|
-
async function resolveBearer(bearer) {
|
|
195
|
-
if (typeof bearer === "string")
|
|
196
|
-
return { token: bearer };
|
|
197
|
-
try {
|
|
198
|
-
return { token: await bearer() };
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
return {
|
|
202
|
-
error: { ok: false, status: 0, body: { error: `Could not obtain an access token: ${err instanceof Error ? err.message : String(err)}` } },
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
async function apiPost(baseUrl, bearer, path, body) {
|
|
207
|
-
const r = await resolveBearer(bearer);
|
|
208
|
-
if ("error" in r)
|
|
209
|
-
return r.error;
|
|
210
|
-
return doFetch(`${baseUrl}${path}`, {
|
|
211
|
-
method: "POST",
|
|
212
|
-
headers: {
|
|
213
|
-
"Content-Type": "application/json",
|
|
214
|
-
Authorization: `Bearer ${r.token}`,
|
|
215
|
-
},
|
|
216
|
-
body: JSON.stringify(body),
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
async function apiGet(baseUrl, bearer, path, params) {
|
|
220
|
-
const r = await resolveBearer(bearer);
|
|
221
|
-
if ("error" in r)
|
|
222
|
-
return r.error;
|
|
223
|
-
const url = new URL(`${baseUrl}${path}`);
|
|
224
|
-
for (const [key, val] of Object.entries(params)) {
|
|
225
|
-
if (val !== undefined && val !== "")
|
|
226
|
-
url.searchParams.set(key, val);
|
|
227
|
-
}
|
|
228
|
-
return doFetch(url.toString(), {
|
|
229
|
-
method: "GET",
|
|
230
|
-
headers: { Authorization: `Bearer ${r.token}` },
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
function formatResult(resp) {
|
|
234
|
-
const text = JSON.stringify(resp.body, null, 2);
|
|
235
|
-
if (!resp.ok) {
|
|
236
|
-
return {
|
|
237
|
-
content: [{ type: "text", text: `Error (HTTP ${resp.status}):\n${text}` }],
|
|
238
|
-
isError: true,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
return { content: [{ type: "text", text }] };
|
|
242
|
-
}
|
|
243
|
-
/** Decode the handle out of an agent grant token (ndca-<jwt>). */
|
|
244
|
-
function handleFromGrant(token) {
|
|
245
|
-
try {
|
|
246
|
-
const jwt = token.startsWith("ndca-") ? token.slice(5) : token;
|
|
247
|
-
const payload = jwt.split(".")[1];
|
|
248
|
-
if (!payload)
|
|
249
|
-
return null;
|
|
250
|
-
const json = JSON.parse(Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
|
|
251
|
-
return json.handle ?? null;
|
|
252
|
-
}
|
|
253
|
-
catch {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
// ---------------------------------------------------------------------------
|
|
258
|
-
// Device attestation — the hardened credential path ("registration ≠ login")
|
|
259
|
-
// ---------------------------------------------------------------------------
|
|
260
|
-
// The client holds an Ed25519 PRIVATE key that never leaves this machine. Each
|
|
261
|
-
// session it signs a fresh server nonce (POST /api/v1/attest) and gets a 15-min
|
|
262
|
-
// token the governance surface already accepts — no reusable key at rest.
|
|
263
|
-
// Reuses the shipped enrolled-agent challenge model server-side.
|
|
264
|
-
const ATTEST_REFRESH_BUFFER_MS = 5 * 60_000; // refresh 5m before the 15m token expires
|
|
265
|
-
function generateDeviceKeypair() {
|
|
266
|
-
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
267
|
-
const spki = publicKey.export({ format: "der", type: "spki" });
|
|
268
|
-
const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" });
|
|
269
|
-
return { publicKeyHex: spki.subarray(-32).toString("hex"), privateKeyPkcs8B64: pkcs8.toString("base64") };
|
|
270
|
-
}
|
|
271
|
-
function signChallenge(privateKeyPkcs8B64, challenge) {
|
|
272
|
-
const key = createPrivateKey({ key: Buffer.from(privateKeyPkcs8B64, "base64"), format: "der", type: "pkcs8" });
|
|
273
|
-
return sign(null, Buffer.from(challenge, "utf8"), key).toString("base64");
|
|
274
|
-
}
|
|
275
|
-
/** A stable, valid device handle derived from the machine (host + short random). */
|
|
276
|
-
function defaultDeviceHandle() {
|
|
277
|
-
const host = hostname().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20) || "device";
|
|
278
|
-
const head = /^[a-z]/.test(host) ? host : `d-${host}`;
|
|
279
|
-
return `${head}-${randomBytes(3).toString("hex")}`.slice(0, 32);
|
|
280
|
-
}
|
|
281
|
-
/** Enroll a device public key under the org that owns `apiKey`; returns its org_id. */
|
|
282
|
-
async function enrollDevice(baseUrl, apiKey, handle, label, publicKeyHex) {
|
|
283
|
-
const resp = await apiPost(baseUrl, apiKey, "/api/v1/attest/devices", {
|
|
284
|
-
handle,
|
|
285
|
-
label,
|
|
286
|
-
ed25519_pubkey_hex: publicKeyHex,
|
|
287
|
-
});
|
|
288
|
-
if (resp.ok) {
|
|
289
|
-
const orgId = resp.body.org_id ?? "";
|
|
290
|
-
return orgId ? { ok: true, orgId } : { ok: false, status: resp.status, error: "enroll response missing org_id" };
|
|
291
|
-
}
|
|
292
|
-
return { ok: false, status: resp.status, error: resp.body?.error ?? `HTTP ${resp.status}` };
|
|
293
|
-
}
|
|
294
|
-
/** One attest round: challenge → sign → token. Throws with a clear message on failure. */
|
|
295
|
-
async function attestOnce(baseUrl, device) {
|
|
296
|
-
// Both attest endpoints are public (the signature is the auth), so the bearer is unused.
|
|
297
|
-
const ch = await apiPost(baseUrl, "", "/api/v1/attest/challenge", { org_id: device.org_id, handle: device.handle });
|
|
298
|
-
if (!ch.ok)
|
|
299
|
-
throw new Error(`challenge: ${ch.body?.error ?? `HTTP ${ch.status}`}`);
|
|
300
|
-
const { challenge, nonce } = ch.body;
|
|
301
|
-
if (!challenge || !nonce)
|
|
302
|
-
throw new Error("challenge response malformed");
|
|
303
|
-
const signature = signChallenge(device.private_key_pkcs8_b64, challenge);
|
|
304
|
-
const at = await apiPost(baseUrl, "", "/api/v1/attest", { org_id: device.org_id, handle: device.handle, nonce, signature });
|
|
305
|
-
if (!at.ok)
|
|
306
|
-
throw new Error(`attest: ${at.body?.error ?? `HTTP ${at.status}`}`);
|
|
307
|
-
const body = at.body;
|
|
308
|
-
if (!body.allowed || !body.access_token)
|
|
309
|
-
throw new Error(`attestation rejected: ${body.reason ?? "denied"}`);
|
|
310
|
-
return {
|
|
311
|
-
token: body.access_token,
|
|
312
|
-
expiresAtMs: body.expires_at ? Date.parse(body.expires_at) : Date.now() + 15 * 60_000,
|
|
313
|
-
};
|
|
314
|
-
}
|
|
315
|
-
/** A Bearer that attests lazily and refreshes before the token lapses. */
|
|
316
|
-
function deviceTokenProvider(baseUrl, device) {
|
|
317
|
-
let cached = null;
|
|
318
|
-
return async () => {
|
|
319
|
-
if (!cached || Date.now() >= cached.expiresAtMs - ATTEST_REFRESH_BUFFER_MS) {
|
|
320
|
-
cached = await attestOnce(baseUrl, device);
|
|
321
|
-
}
|
|
322
|
-
return cached.token;
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
// ---------------------------------------------------------------------------
|
|
326
|
-
// Tool registration
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
function registerAdminTools(server, baseUrl, apiKey) {
|
|
329
|
-
// ── Governance ──────────────────────────────────────────────────────
|
|
330
|
-
server.registerTool("nodata_register_table", {
|
|
331
|
-
description: "Register one of your own tables as AI-agent-readable ('connect your data'). Declares which columns are encrypted (per-column key release) and which are plaintext metadata. After this, grants can target the table.",
|
|
332
|
-
inputSchema: {
|
|
333
|
-
table_name: z.string().describe("Your SQL table name"),
|
|
334
|
-
encrypted_columns: z.array(z.string()).describe("Columns stored as <col>_ct/_iv/_tag triplets"),
|
|
335
|
-
plaintext_columns: z.array(z.string()).optional().describe("Plaintext metadata/filter columns"),
|
|
336
|
-
classification_column: z.string().optional().describe("Plaintext column carrying the classification value"),
|
|
337
|
-
},
|
|
338
|
-
}, async ({ table_name, encrypted_columns, plaintext_columns, classification_column }) => {
|
|
339
|
-
const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/tables", {
|
|
340
|
-
table_name,
|
|
341
|
-
encrypted_columns,
|
|
342
|
-
plaintext_columns: plaintext_columns ?? [],
|
|
343
|
-
classification_column,
|
|
344
|
-
});
|
|
345
|
-
return formatResult(resp);
|
|
346
|
-
});
|
|
347
|
-
server.registerTool("nodata_grant", {
|
|
348
|
-
description: "Issue an AI agent a claim-scoped grant over your data — claims, not blanket access. Returns a grant_token (shown once) the agent presents to read. Denied columns never decrypt.",
|
|
349
|
-
inputSchema: {
|
|
350
|
-
agent: z.string().describe("Agent label (becomes its handle)"),
|
|
351
|
-
table: z.string().optional().describe("Target table (default agent_demo_records)"),
|
|
352
|
-
columns: z.array(z.string()).describe("Columns the agent may read"),
|
|
353
|
-
where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Equality filter (plaintext cols only)"),
|
|
354
|
-
classifications: z.array(z.string()).optional().describe("Allowed classification values; empty = no gating"),
|
|
355
|
-
ttl_seconds: z.number().optional().describe("Token lifetime (default 30d, max 1y)"),
|
|
356
|
-
},
|
|
357
|
-
}, async ({ agent, table, columns, where, classifications, ttl_seconds }) => {
|
|
358
|
-
const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/grant", {
|
|
359
|
-
agent,
|
|
360
|
-
table,
|
|
361
|
-
columns,
|
|
362
|
-
where,
|
|
363
|
-
classifications,
|
|
364
|
-
ttl_seconds,
|
|
365
|
-
});
|
|
366
|
-
return formatResult(resp);
|
|
367
|
-
});
|
|
368
|
-
server.registerTool("nodata_revoke", {
|
|
369
|
-
description: "Revoke an agent's grant by handle (kills all its active credentials) or by a specific jti. Revocation is mathematics — the next read 401s and any cached data is governance-dead.",
|
|
370
|
-
inputSchema: {
|
|
371
|
-
handle: z.string().optional().describe("Agent handle to revoke"),
|
|
372
|
-
jti: z.string().optional().describe("Specific credential jti to revoke"),
|
|
373
|
-
reason: z.string().optional(),
|
|
374
|
-
},
|
|
375
|
-
}, async ({ handle, jti, reason }) => {
|
|
376
|
-
const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/revoke", { handle, jti, reason });
|
|
377
|
-
return formatResult(resp);
|
|
378
|
-
});
|
|
379
|
-
// ── Blind relay (kept from v0.1) ────────────────────────────────────
|
|
380
|
-
server.registerTool("nodata_encrypt", {
|
|
381
|
-
description: "Encrypt a sensitive field value using Capsule Blind Relay. Returns ciphertext. NoData stores nothing — the key is embedded in the ciphertext for YOU to store.",
|
|
382
|
-
inputSchema: {
|
|
383
|
-
field: z.string(),
|
|
384
|
-
value: z.string(),
|
|
385
|
-
context: z.string().optional(),
|
|
386
|
-
},
|
|
387
|
-
}, async ({ field, value, context }) => {
|
|
388
|
-
const body = { field, value };
|
|
389
|
-
if (context)
|
|
390
|
-
body.context = context;
|
|
391
|
-
return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/encrypt", body));
|
|
392
|
-
});
|
|
393
|
-
server.registerTool("nodata_decrypt", {
|
|
394
|
-
description: "Decrypt a Capsule ciphertext back to plaintext (aes256gcm:v1:iv:cipher:key format).",
|
|
395
|
-
inputSchema: {
|
|
396
|
-
ciphertext: z.string(),
|
|
397
|
-
field: z.string().optional(),
|
|
398
|
-
requester: z.string().optional(),
|
|
399
|
-
},
|
|
400
|
-
}, async ({ ciphertext, field, requester }) => {
|
|
401
|
-
const body = { ciphertext };
|
|
402
|
-
if (field)
|
|
403
|
-
body.field = field;
|
|
404
|
-
if (requester)
|
|
405
|
-
body.requester = requester;
|
|
406
|
-
return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/decrypt", body));
|
|
407
|
-
});
|
|
408
|
-
server.registerTool("nodata_deliver", {
|
|
409
|
-
description: "Create a burn-after-read secure link for delivering sensitive content. Returns a view URL and OTP.",
|
|
410
|
-
inputSchema: {
|
|
411
|
-
content: z.string(),
|
|
412
|
-
to: z.string().optional(),
|
|
413
|
-
maxViews: z.number().optional(),
|
|
414
|
-
ttl: z.string().optional(),
|
|
415
|
-
burn: z.boolean().optional(),
|
|
416
|
-
},
|
|
417
|
-
}, async ({ content, to, maxViews, ttl, burn }) => {
|
|
418
|
-
const body = { content };
|
|
419
|
-
if (to)
|
|
420
|
-
body.to = to;
|
|
421
|
-
if (maxViews !== undefined)
|
|
422
|
-
body.maxViews = maxViews;
|
|
423
|
-
if (ttl)
|
|
424
|
-
body.ttl = ttl;
|
|
425
|
-
if (burn !== undefined)
|
|
426
|
-
body.burn = burn;
|
|
427
|
-
return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/deliver", body));
|
|
428
|
-
});
|
|
429
|
-
server.registerTool("nodata_evidence", {
|
|
430
|
-
description: "Retrieve the audit trail of governance/encryption events. Metadata only — never content, keys, or plaintext.",
|
|
431
|
-
inputSchema: {
|
|
432
|
-
from: z.string().optional(),
|
|
433
|
-
to: z.string().optional(),
|
|
434
|
-
action: z.string().optional(),
|
|
435
|
-
limit: z.number().optional(),
|
|
436
|
-
},
|
|
437
|
-
}, async ({ from, to, action, limit }) => {
|
|
438
|
-
const params = {
|
|
439
|
-
from,
|
|
440
|
-
to,
|
|
441
|
-
action,
|
|
442
|
-
limit: limit !== undefined ? String(limit) : undefined,
|
|
443
|
-
};
|
|
444
|
-
return formatResult(await apiGet(baseUrl, apiKey, "/api/v1/evidence", params));
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
function registerAgentTool(server, baseUrl, grantToken) {
|
|
448
|
-
const handle = handleFromGrant(grantToken);
|
|
449
|
-
if (!handle) {
|
|
450
|
-
// decide/retrieve are bearer-only and still work; read/use need the handle.
|
|
451
|
-
// Say so at startup instead of surprising the agent mid-conversation.
|
|
452
|
-
console.error("nodata-mcp: warning — could not derive an agent handle from the grant token " +
|
|
453
|
-
"(expected ndca-<jwt> with a 'handle' claim). nodata_decide/nodata_retrieve will work; " +
|
|
454
|
-
"nodata_read/nodata_use will fail until a valid grant token is provided.");
|
|
455
|
-
}
|
|
456
|
-
// ── Access Compute · the "may I?" oracle (P0) ───────────────────────
|
|
457
|
-
// Returns ONLY a decision + a signed proof, never any content. A planner
|
|
458
|
-
// calls this to learn what it can reach BEFORE moving data: allow, degrade
|
|
459
|
-
// (the reachable subset), or deny — with an access-distance cost. Bearer-only
|
|
460
|
-
// endpoint (no handle in the path).
|
|
461
|
-
server.registerTool("nodata_decide", {
|
|
462
|
-
description: "Ask whether this agent MAY reach a resource, WITHOUT reading it. Returns a decision (allow / degrade / deny), the reachable column subset, an access-distance cost, and a signed proof — never any data. Use it to plan: check reachability before you retrieve. Hard-scoped to this server's grant token.",
|
|
463
|
-
inputSchema: {
|
|
464
|
-
table: z.string().describe("Table to check (must be in your grant)"),
|
|
465
|
-
columns: z.array(z.string()).describe("Columns you want to reach"),
|
|
466
|
-
where: z
|
|
467
|
-
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
468
|
-
.optional()
|
|
469
|
-
.describe("Equality filter (readable columns only)"),
|
|
470
|
-
},
|
|
471
|
-
}, async ({ table, columns, where }) => {
|
|
472
|
-
const resp = await apiPost(baseUrl, grantToken, "/api/access/decide", {
|
|
473
|
-
resource: { table, columns, where },
|
|
474
|
-
intent: "read",
|
|
475
|
-
});
|
|
476
|
-
return formatResult(resp);
|
|
477
|
-
});
|
|
478
|
-
// ── Authorized Context · access-aware RAG (P1) ──────────────────────
|
|
479
|
-
// Runs the same decision, then returns prompt-ready context filtered to
|
|
480
|
-
// exactly the allowed columns (denied columns are never decrypted), plus an
|
|
481
|
-
// accounting of what was withheld and a signed proof.
|
|
482
|
-
server.registerTool("nodata_retrieve", {
|
|
483
|
-
description: "Retrieve prompt-ready context for a resource, filtered to exactly what this agent may see. Denied columns are never decrypted; the response includes an accounting of what was withheld (columns + estimated tokens saved) and a signed proof. Prefer this over a raw read when assembling an LLM prompt. Hard-scoped to this server's grant token.",
|
|
484
|
-
inputSchema: {
|
|
485
|
-
table: z.string().describe("Table to retrieve from (must be in your grant)"),
|
|
486
|
-
columns: z.array(z.string()).describe("Columns to include in context"),
|
|
487
|
-
where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
488
|
-
top_k: z.number().optional().describe("Max rows (default 50, max 200)"),
|
|
489
|
-
},
|
|
490
|
-
}, async ({ table, columns, where, top_k }) => {
|
|
491
|
-
const resp = await apiPost(baseUrl, grantToken, "/api/access/retrieve", {
|
|
492
|
-
resource: { table, columns, where },
|
|
493
|
-
top_k,
|
|
494
|
-
});
|
|
495
|
-
return formatResult(resp);
|
|
496
|
-
});
|
|
497
|
-
server.registerTool("nodata_read", {
|
|
498
|
-
description: "Read governed data within this agent's grant. You may only request columns your grant allows — denied columns never decrypt server-side, and every read emits a cryptographic receipt. This tool is hard-scoped to the grant token this server was started with.",
|
|
499
|
-
inputSchema: {
|
|
500
|
-
table: z.string().describe("Table to read (must be in your grant)"),
|
|
501
|
-
columns: z.array(z.string()).describe("Columns to read (must be in your grant)"),
|
|
502
|
-
where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
|
|
503
|
-
limit: z.number().optional().describe("Max rows (default 50, max 200)"),
|
|
504
|
-
},
|
|
505
|
-
}, async ({ table, columns, where, limit }) => {
|
|
506
|
-
if (!handle) {
|
|
507
|
-
return {
|
|
508
|
-
content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
|
|
509
|
-
isError: true,
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/read`, {
|
|
513
|
-
table,
|
|
514
|
-
columns,
|
|
515
|
-
where,
|
|
516
|
-
limit,
|
|
517
|
-
});
|
|
518
|
-
return formatResult(resp);
|
|
519
|
-
});
|
|
520
|
-
// ── Secret-blind action (AI Access Plane) ───────────────────────────
|
|
521
|
-
// The headline of the plane: INVOKE a pre-registered capability. The
|
|
522
|
-
// Capsule injects the sealed secret server-side and returns ONLY the
|
|
523
|
-
// result — you (the agent / model) never receive the key. Deny-by-default:
|
|
524
|
-
// you can invoke only capabilities your grant lists. Every invoke emits a
|
|
525
|
-
// signed receipt proving you acted and never held the secret.
|
|
526
|
-
server.registerTool("nodata_use", {
|
|
527
|
-
description: "Invoke a pre-registered capability (deploy, send, charge, query, …) WITHOUT receiving the secret. The Capsule injects the sealed credential server-side and returns only the result. You may invoke only capabilities your grant allows; the secret never reaches you. Hard-scoped to the grant token this server was started with.",
|
|
528
|
-
inputSchema: {
|
|
529
|
-
capability: z.string().describe("Capability name to invoke (must be in your grant, e.g. 'deploy-prod')"),
|
|
530
|
-
params: z.record(z.string(), z.unknown()).optional().describe("Allowlisted params; unknown keys are dropped server-side"),
|
|
531
|
-
resource_key: z.string().optional().describe("Optional folder/resource the action targets (enforced against corpus scope)"),
|
|
532
|
-
},
|
|
533
|
-
}, async ({ capability, params, resource_key }) => {
|
|
534
|
-
if (!handle) {
|
|
535
|
-
return {
|
|
536
|
-
content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
|
|
537
|
-
isError: true,
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/use`, {
|
|
541
|
-
capability,
|
|
542
|
-
params,
|
|
543
|
-
resource_key,
|
|
544
|
-
});
|
|
545
|
-
return formatResult(resp);
|
|
546
|
-
});
|
|
547
|
-
// ── Code plane · fix code without exposure, over the Fabric ─────────
|
|
548
|
-
// The agent drives the blind advisor on a code capsule. It sees only a
|
|
549
|
-
// content-blind projection (or, on an in-boundary model, a scoped slice that
|
|
550
|
-
// never egresses); the human sees only the changed hunks. Every turn returns
|
|
551
|
-
// an exposure proof + a receipt. Authorised by the capsule owner listing this
|
|
552
|
-
// agent's kid in scope.fabric_developers; deny-by-default.
|
|
553
|
-
server.registerTool("nodata_code_advise", {
|
|
554
|
-
description: "Ask the blind code advisor to diagnose/fix a bug in a code capsule WITHOUT seeing its source. You receive guidance + a targeted diff + an exposure proof (source never left the boundary); the source is never returned to you. Requires the capsule owner to have granted this agent (scope.fabric_developers). Hard-scoped to this server's grant token.",
|
|
555
|
-
inputSchema: {
|
|
556
|
-
capsule_id: z.string().describe("The code capsule to advise on (you must be granted on it)"),
|
|
557
|
-
question: z.string().optional().describe("What to fix / improve, in natural language"),
|
|
558
|
-
note: z.string().optional().describe("Extra context for the advisor"),
|
|
559
|
-
error: z
|
|
560
|
-
.object({
|
|
561
|
-
message: z.string().optional(),
|
|
562
|
-
stack: z.string().optional(),
|
|
563
|
-
kind: z.string().optional(),
|
|
564
|
-
})
|
|
565
|
-
.optional()
|
|
566
|
-
.describe("An error the code produced, to guide the fix"),
|
|
567
|
-
steer: z.string().optional().describe("A steer on a refine turn (e.g. 'no, round half-up')"),
|
|
568
|
-
prior_diff: z.string().optional().describe("The previously proposed diff, to refine"),
|
|
569
|
-
},
|
|
570
|
-
}, async ({ capsule_id, question, note, error, steer, prior_diff }) => {
|
|
571
|
-
if (!handle) {
|
|
572
|
-
return {
|
|
573
|
-
content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
|
|
574
|
-
isError: true,
|
|
575
|
-
};
|
|
576
|
-
}
|
|
577
|
-
const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/advise`, { question, note, error, steer, prior_diff });
|
|
578
|
-
return formatResult(resp);
|
|
579
|
-
});
|
|
580
|
-
server.registerTool("nodata_code_patch", {
|
|
581
|
-
description: "Submit a fix to a code capsule as a SIGNED, queued patch WITHOUT seeing its source. Provide a raw unified `diff`, or a whole-file `replace` ({path, content}) that is diffed server-side against the sealed source. The source is never persisted or returned; you get back a patch id, the diff hash, and a receipt. Requires a capsule grant (scope.fabric_developers). Hard-scoped to this server's grant token.",
|
|
582
|
-
inputSchema: {
|
|
583
|
-
capsule_id: z.string().describe("The code capsule to patch (you must be granted on it)"),
|
|
584
|
-
summary: z.string().optional().describe("Short human-readable summary of the fix"),
|
|
585
|
-
diff: z.string().optional().describe("A raw unified diff (advanced)"),
|
|
586
|
-
replace: z
|
|
587
|
-
.object({ path: z.string(), content: z.string() })
|
|
588
|
-
.optional()
|
|
589
|
-
.describe("Whole-file replacement; the diff is synthesized server-side against sealed source"),
|
|
590
|
-
},
|
|
591
|
-
}, async ({ capsule_id, summary, diff, replace }) => {
|
|
592
|
-
if (!handle) {
|
|
593
|
-
return {
|
|
594
|
-
content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
|
|
595
|
-
isError: true,
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/patch`, { summary, diff, replace });
|
|
599
|
-
return formatResult(resp);
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
const START_ACHIEVEMENT = "WHAT NODATA DOES — in one line:\n" +
|
|
603
|
-
" Give any agent real power over your data, without the power to leak it.\n" +
|
|
604
|
-
" (Possession is not permission: it can hold the key and still not read what you didn't allow.)\n";
|
|
605
|
-
// Setup mode: NOT connected yet. The one job here is to switch the tools on, so
|
|
606
|
-
// this is a numbered do-this-exactly list — three steps, not a catalogue. The
|
|
607
|
-
// last line answers the two questions everyone asks: "how do I activate?" (/mcp)
|
|
608
|
-
// and "what about next time?" (nothing).
|
|
609
|
-
const SETUP_STEPS = "TURN THE DATA TOOLS ON — 3 steps, about a minute, once:\n\n" +
|
|
610
|
-
" 1. GET A KEY call nodata_connect (no arguments)\n" +
|
|
611
|
-
" -> opens the signup in your browser: email + company (~30s), a 12-word\n" +
|
|
612
|
-
" recovery phrase to keep, and an ORG KEY shown ONCE. Copy that key.\n\n" +
|
|
613
|
-
" 2. SAVE IT call nodata_connect again, this time with\n" +
|
|
614
|
-
" api_key: \"<the key you copied>\"\n" +
|
|
615
|
-
" -> saved on this machine; you never paste it again.\n\n" +
|
|
616
|
-
" 3. ACTIVATE type /mcp (or restart Claude Code) so the tools load.\n\n" +
|
|
617
|
-
"COMING BACK LATER? Nothing to redo — it starts ready. If the tools look missing, just /mcp.\n";
|
|
618
|
-
// Admin mode: connected. Frame the whole product as the developer's real journey
|
|
619
|
-
// — SEE -> CHOOSE -> PROVE — each step a plain-language ask mapped to a tool.
|
|
620
|
-
const START_OUTCOMES_ADMIN = "NOW — TURN YOUR DATA INTO AN AGENT THAT CAN'T LEAK, in 3 asks (just say them in plain words):\n\n" +
|
|
621
|
-
" 1. SEE what's sensitive run nodata_scan_folder — scans a local folder right here, nothing\n" +
|
|
622
|
-
" uploads (or open https://www.nodatacapsule.com/ai-folder in a browser).\n\n" +
|
|
623
|
-
" 2. CHOOSE what to release say \"let <agent> read name + plan on <table>, hide the rest\"\n" +
|
|
624
|
-
" -> I run nodata_register_table + nodata_grant; the agent gets a token.\n\n" +
|
|
625
|
-
" 3. PROVE & control the agent now reads ONLY those columns — the rest never decrypt.\n" +
|
|
626
|
-
" \"what did it see?\" -> nodata_evidence · \"cut it off\" -> nodata_revoke\n\n" +
|
|
627
|
-
"Also: a secret even NoData can't read -> nodata_encrypt, then hand it over once -> nodata_deliver.\n";
|
|
628
|
-
// The agent-scoped reader IS the governed agent: it holds only decide/retrieve/read/use,
|
|
629
|
-
// so its steps name those verbs (pointing it at admin tools it lacks would dead-end it).
|
|
630
|
-
const START_OUTCOMES_AGENT = "WHAT YOU CAN DO — scoped to your grant; you reach only what it allows, the rest never decrypts:\n\n" +
|
|
631
|
-
" 1. PLAN nodata_decide \"may I reach X?\" -> allow / partial / deny + a signed proof. No data moves.\n" +
|
|
632
|
-
" 2. PULL nodata_retrieve prompt-ready context, allowed fields only, with a receipt.\n" +
|
|
633
|
-
" (nodata_read for raw governed rows.)\n" +
|
|
634
|
-
" 3. ACT nodata_use run a capability with a secret injected server-side — you never see it.\n";
|
|
635
|
-
const START_UNSTUCK = "STUCK? There's always a next step:\n" +
|
|
636
|
-
" - Don't see the nodata_* tools? They load when the client starts — type /mcp or restart.\n" +
|
|
637
|
-
" (Expected, not an error.)\n" +
|
|
638
|
-
" - No MCP at all? The same governance runs from one curl, or the guided page:\n" +
|
|
639
|
-
" https://www.nodatacapsule.com/developers/get-started\n";
|
|
640
|
-
function startHeader(mode) {
|
|
641
|
-
if (mode === "setup") {
|
|
642
|
-
return "You're connected to NoData — but there's NO key yet, so the data tools are still off.\n\n";
|
|
643
|
-
}
|
|
644
|
-
if (mode === "agent") {
|
|
645
|
-
return "You're connected to NoData, scoped to one grant token.\n\n";
|
|
646
|
-
}
|
|
647
|
-
return "You're connected to NoData (admin mode). You don't memorize commands — just ask in plain words.\n\n";
|
|
648
|
-
}
|
|
649
|
-
function getStartedText(mode) {
|
|
650
|
-
// Setup leads with the 3 connect steps (the only thing that matters when off);
|
|
651
|
-
// admin/agent lead with the journey they can act on right now.
|
|
652
|
-
if (mode === "setup") {
|
|
653
|
-
return startHeader(mode) + START_ACHIEVEMENT + "\n" + SETUP_STEPS + "\n" + START_UNSTUCK;
|
|
654
|
-
}
|
|
655
|
-
const outcomes = mode === "agent" ? START_OUTCOMES_AGENT : START_OUTCOMES_ADMIN;
|
|
656
|
-
return startHeader(mode) + START_ACHIEVEMENT + "\n" + outcomes + "\n" + START_UNSTUCK;
|
|
657
|
-
}
|
|
658
|
-
function registerGetStarted(server, mode) {
|
|
659
|
-
server.registerTool("nodata_get_started", {
|
|
660
|
-
description: "Call this FIRST (no arguments). NoData in one line: what you can achieve — an agent that " +
|
|
661
|
-
"cannot leak, a look at what is already exposed, a secret we cannot read — with the single " +
|
|
662
|
-
"next step for each, and how to recover if the tools or MCP seem stuck. If the user asks what " +
|
|
663
|
-
"NoData can do, how to start, or why the tools are missing, call this and relay it.",
|
|
664
|
-
inputSchema: {},
|
|
665
|
-
}, async () => ({ content: [{ type: "text", text: getStartedText(mode) }] }));
|
|
666
|
-
}
|
|
667
|
-
// ---------------------------------------------------------------------------
|
|
668
|
-
// nodata_connect — setup-mode "get switched on without re-typing the command"
|
|
669
|
-
// ---------------------------------------------------------------------------
|
|
670
|
-
// "Registration ≠ login." Call once with no args → open the real signup in the
|
|
671
|
-
// browser (email + company, the 12-word recovery, the email verify — the parts
|
|
672
|
-
// a headless mint would skip). It shows the org key once; call again with that
|
|
673
|
-
// key → it is saved to ~/.nodata and every later start autoloads it. First run:
|
|
674
|
-
// register + one paste. Every run after: nothing. The only residual is one /mcp,
|
|
675
|
-
// because MCP clients read their tool list at startup (a client fact, not ours).
|
|
676
|
-
const REGISTER_URL_PATH = "/capsule-api/register";
|
|
677
|
-
function openInBrowser(url) {
|
|
678
|
-
const isWin = process.platform === "win32";
|
|
679
|
-
const cmd = process.platform === "darwin" ? "open" : isWin ? "cmd" : "xdg-open";
|
|
680
|
-
const args = isWin ? ["/c", "start", "", url] : [url];
|
|
681
|
-
try {
|
|
682
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
683
|
-
// A missing opener (headless box, no xdg-open) must never surface as a tool
|
|
684
|
-
// error — the URL is always printed too, so the flow proceeds regardless.
|
|
685
|
-
child.on("error", () => { });
|
|
686
|
-
child.unref();
|
|
687
|
-
return true;
|
|
688
|
-
}
|
|
689
|
-
catch {
|
|
690
|
-
return false;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
function registerConnectTool(server, baseUrl) {
|
|
694
|
-
server.registerTool("nodata_connect", {
|
|
695
|
-
description: "Switch NoData's data tools on. Step 1: call with NO arguments to open the free signup in a " +
|
|
696
|
-
"browser (email + company, ~30s — also where your 12-word recovery phrase + email verification " +
|
|
697
|
-
"happen); it shows an ORG KEY once. Step 2: call this tool again with that key as `api_key` and " +
|
|
698
|
-
"it is saved on this machine, so every later start comes up ready with nothing to retype. " +
|
|
699
|
-
"Step 3: type /mcp (or restart) so the client loads the tools.",
|
|
700
|
-
inputSchema: {
|
|
701
|
-
api_key: z
|
|
702
|
-
.string()
|
|
703
|
-
.optional()
|
|
704
|
-
.describe("The org key shown once at signup (ndp_… or sk_…). Omit on the first call to open signup."),
|
|
705
|
-
},
|
|
706
|
-
}, async ({ api_key }) => {
|
|
707
|
-
const key = (api_key ?? "").trim();
|
|
708
|
-
// First call (no key): open the real signup and tell them to come back with it.
|
|
709
|
-
if (!key) {
|
|
710
|
-
const url = baseUrl + REGISTER_URL_PATH;
|
|
711
|
-
const opened = openInBrowser(url);
|
|
712
|
-
return {
|
|
713
|
-
content: [
|
|
714
|
-
{
|
|
715
|
-
type: "text",
|
|
716
|
-
text: (opened ? "STEP 1 of 3 — opened the NoData signup in your browser:\n " : "STEP 1 of 3 — open the NoData signup in your browser:\n ") +
|
|
717
|
-
url +
|
|
718
|
-
"\n\n" +
|
|
719
|
-
"There, about a minute:\n" +
|
|
720
|
-
" - email + company (a personal email is fine for the sandbox)\n" +
|
|
721
|
-
" - a 12-WORD RECOVERY PHRASE — save it; it is the only way back into your org\n" +
|
|
722
|
-
" - your ORG KEY, shown ONCE — copy it\n\n" +
|
|
723
|
-
"STEP 2 — come back and call this tool again with the key:\n" +
|
|
724
|
-
' nodata_connect api_key: "<the key you copied>"\n' +
|
|
725
|
-
"and I'll save it so you never paste it again. (Then STEP 3: type /mcp to activate.)",
|
|
726
|
-
},
|
|
727
|
-
],
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
// Shape check — don't persist something that clearly isn't a NoData key.
|
|
731
|
-
if (!/^(ndp_|sk_)/.test(key)) {
|
|
732
|
-
return {
|
|
733
|
-
content: [
|
|
734
|
-
{
|
|
735
|
-
type: "text",
|
|
736
|
-
text: "That doesn't look like a NoData org key (it should start with ndp_ or sk_).\n" +
|
|
737
|
-
'Copy the key shown once on the signup page and try again: nodata_connect api_key: "ndp_…".',
|
|
738
|
-
},
|
|
739
|
-
],
|
|
740
|
-
};
|
|
741
|
-
}
|
|
742
|
-
// Preferred path: enroll a DEVICE KEY so no reusable key is stored. Generate
|
|
743
|
-
// a keypair, register its PUBLIC half under the org (authenticated by the key
|
|
744
|
-
// the user just pasted), keep only the PRIVATE half. Then every start attests.
|
|
745
|
-
const { publicKeyHex, privateKeyPkcs8B64 } = generateDeviceKeypair();
|
|
746
|
-
const handle = defaultDeviceHandle();
|
|
747
|
-
const label = hostname() || "this device";
|
|
748
|
-
const enrolled = await enrollDevice(baseUrl, key, handle, label, publicKeyHex);
|
|
749
|
-
if (enrolled.ok) {
|
|
750
|
-
try {
|
|
751
|
-
writeStoredCredential({
|
|
752
|
-
device: {
|
|
753
|
-
org_id: enrolled.orgId,
|
|
754
|
-
handle,
|
|
755
|
-
label,
|
|
756
|
-
private_key_pkcs8_b64: privateKeyPkcs8B64,
|
|
757
|
-
public_key_hex: publicKeyHex,
|
|
758
|
-
},
|
|
759
|
-
base_url: baseUrl,
|
|
760
|
-
});
|
|
761
|
-
}
|
|
762
|
-
catch (e) {
|
|
763
|
-
return {
|
|
764
|
-
content: [{ type: "text", text: "Enrolled the device, but couldn't save it locally: " + (e instanceof Error ? e.message : String(e)) }],
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
return {
|
|
768
|
-
content: [
|
|
769
|
-
{
|
|
770
|
-
type: "text",
|
|
771
|
-
text: `STEP 2 done — this device is enrolled as "${label}" (${handle}), and only its PRIVATE key is\n` +
|
|
772
|
-
"saved at ~/.nodata/credentials.json (readable only by you). The org key you pasted was NOT\n" +
|
|
773
|
-
"stored: every start now proves possession by signing a fresh challenge and gets a 15-minute token.\n\n" +
|
|
774
|
-
"STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
|
|
775
|
-
},
|
|
776
|
-
],
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
// Fallback: a backend that predates the attest endpoints. Store the key at
|
|
780
|
-
// rest (the Part-2 path) so onboarding still works end to end.
|
|
781
|
-
const attestUnavailable = enrolled.status === 404 || enrolled.status === 0;
|
|
782
|
-
try {
|
|
783
|
-
writeStoredCredential({ api_key: key, key_prefix: key.slice(0, 12), base_url: baseUrl });
|
|
784
|
-
}
|
|
785
|
-
catch (e) {
|
|
786
|
-
return {
|
|
787
|
-
content: [
|
|
788
|
-
{
|
|
789
|
-
type: "text",
|
|
790
|
-
text: "Couldn't save locally: " +
|
|
791
|
-
(e instanceof Error ? e.message : String(e)) +
|
|
792
|
-
"\nAs a fallback, pass it on launch: claude mcp add nodata --env NODATA_API_KEY=<your key> -- npx @nodatachat/mcp",
|
|
793
|
-
},
|
|
794
|
-
],
|
|
795
|
-
};
|
|
796
|
-
}
|
|
797
|
-
return {
|
|
798
|
-
content: [
|
|
799
|
-
{
|
|
800
|
-
type: "text",
|
|
801
|
-
text: (attestUnavailable
|
|
802
|
-
? "STEP 2 done — saved your key on this machine (device attestation isn't available on this\nserver yet, so this is the key-at-rest path). "
|
|
803
|
-
: `STEP 2 done — saved your key on this machine (device enrollment didn't succeed: ${enrolled.error}). `) +
|
|
804
|
-
"Every future start loads it automatically — no re-register, no re-paste.\n\n" +
|
|
805
|
-
"STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
|
|
806
|
-
},
|
|
807
|
-
],
|
|
808
|
-
};
|
|
809
|
-
});
|
|
810
|
-
}
|
|
811
|
-
// ---------------------------------------------------------------------------
|
|
812
|
-
// nodata_scan_folder — SEE what's sensitive, locally, before you govern it
|
|
813
|
-
// ---------------------------------------------------------------------------
|
|
814
|
-
// The journey's first step ("SEE") brought inside the one command. It runs our
|
|
815
|
-
// deep content scanner (the published @nodatachat/folder-scan — the same detection
|
|
816
|
-
// engine the NoData agent uses: 24 secret patterns + checksum-validated PII, and
|
|
817
|
-
// PDF/Office text extraction) ENTIRELY on this machine — nothing uploads — over a
|
|
818
|
-
// folder the developer names, then points at register_table + grant for what it
|
|
819
|
-
// surfaces. Local compute needs no credential, so this is offered even in setup
|
|
820
|
-
// mode: scan before you even sign up.
|
|
821
|
-
function formatScanReport(r) {
|
|
822
|
-
// The neutral report comes from the scanner package (shared with its CLI);
|
|
823
|
-
// the MCP only adds the governance bridge — turn what the scan surfaced into
|
|
824
|
-
// the next step of the same journey.
|
|
825
|
-
const base = formatFolderScan(r);
|
|
826
|
-
if (r.error)
|
|
827
|
-
return base;
|
|
828
|
-
return (base +
|
|
829
|
-
"\n\nGOVERN what it surfaced — same journey, inside this command:\n" +
|
|
830
|
-
" · CHOOSE what an agent may read: nodata_register_table then nodata_grant (denied columns never decrypt)\n" +
|
|
831
|
-
" · PROVE every access afterwards: nodata_evidence\n" +
|
|
832
|
-
"\nWant a standalone deep scan any time? npx @nodatachat/folder-scan (this same engine).");
|
|
833
|
-
}
|
|
834
|
-
function registerScanFolderTool(server) {
|
|
835
|
-
server.registerTool("nodata_scan_folder", {
|
|
836
|
-
description: "SEE what's sensitive before you govern it. Scans a LOCAL folder for hardcoded secrets and unencrypted PII (checksum-validated — Israeli ID / Luhn credit-card / IBAN) with NoData's deep content scanner, reading PDFs and Office files too, ENTIRELY on this machine — nothing uploads, no credential needed. Returns the findings by file and severity plus the first fix, then points at nodata_register_table + nodata_grant to govern what it found. If the user asks to scan / audit / check a folder or repo for sensitive data, secrets, or exposure, call this.",
|
|
837
|
-
inputSchema: {
|
|
838
|
-
path: z
|
|
839
|
-
.string()
|
|
840
|
-
.optional()
|
|
841
|
-
.describe("Folder to scan (absolute or relative). Default: the directory the server runs in."),
|
|
842
|
-
},
|
|
843
|
-
}, async ({ path: scanPath }) => {
|
|
844
|
-
const dir = (scanPath ?? ".").trim() || ".";
|
|
845
|
-
const absDir = resolve(dir);
|
|
846
|
-
if (!existsSync(absDir)) {
|
|
847
|
-
return {
|
|
848
|
-
content: [{ type: "text", text: `No folder found at: ${absDir}\nPass an existing path, e.g. nodata_scan_folder path: "."` }],
|
|
849
|
-
};
|
|
850
|
-
}
|
|
851
|
-
try {
|
|
852
|
-
// Deep scanner: async, takes an injected logger. silentLogger keeps the
|
|
853
|
-
// MCP's stdout (JSON-RPC) clean; the scan never writes to stdout itself.
|
|
854
|
-
const report = await runFolderScan({
|
|
855
|
-
target_id: "mcp",
|
|
856
|
-
target_label: basename(absDir),
|
|
857
|
-
target_path: absDir,
|
|
858
|
-
log: silentLogger,
|
|
859
|
-
});
|
|
860
|
-
return { content: [{ type: "text", text: formatScanReport(report) }] };
|
|
861
|
-
}
|
|
862
|
-
catch (e) {
|
|
863
|
-
return {
|
|
864
|
-
content: [{ type: "text", text: `Scan failed: ${e instanceof Error ? e.message : String(e)}` }],
|
|
865
|
-
isError: true,
|
|
866
|
-
};
|
|
867
|
-
}
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
// ---------------------------------------------------------------------------
|
|
871
|
-
// Server setup
|
|
872
|
-
// ---------------------------------------------------------------------------
|
|
873
|
-
async function main() {
|
|
874
|
-
const { apiKey, grantToken, baseUrl, credentialSource, device } = parseArgs(process.argv.slice(2));
|
|
875
|
-
const mode = grantToken ? "agent" : apiKey || device ? "admin" : "setup";
|
|
876
|
-
const server = new McpServer({
|
|
877
|
-
name: mode === "agent" ? "NoData (agent-scoped)" : mode === "setup" ? "NoData (setup)" : "NoData",
|
|
878
|
-
version: VERSION,
|
|
879
|
-
});
|
|
880
|
-
// A stored device (attest) is preferred: the admin tools get a token PROVIDER
|
|
881
|
-
// that signs a fresh challenge per session instead of a static key.
|
|
882
|
-
if (device)
|
|
883
|
-
registerAdminTools(server, baseUrl, deviceTokenProvider(baseUrl, device));
|
|
884
|
-
else if (apiKey)
|
|
885
|
-
registerAdminTools(server, baseUrl, apiKey);
|
|
886
|
-
if (grantToken)
|
|
887
|
-
registerAgentTool(server, baseUrl, grantToken);
|
|
888
|
-
registerGetStarted(server, mode);
|
|
889
|
-
// The local exposure scan needs no credential — offer it wherever the owner is
|
|
890
|
-
// driving (setup, before signup, or admin), just not to a scoped agent.
|
|
891
|
-
if (mode !== "agent")
|
|
892
|
-
registerScanFolderTool(server);
|
|
893
|
-
if (mode === "setup") {
|
|
894
|
-
registerConnectTool(server, baseUrl);
|
|
895
|
-
console.error("nodata-mcp: no credential given — running in setup mode (data tools disabled).\n" +
|
|
896
|
-
"Call nodata_connect (opens signup, enrolls this device), or pass --api-key / --grant-token. See --help.");
|
|
897
|
-
}
|
|
898
|
-
else if (device) {
|
|
899
|
-
console.error(`nodata-mcp: loaded device identity "${device.label}" (${device.handle}) — attest mode; tokens are minted per session.`);
|
|
900
|
-
}
|
|
901
|
-
else if (credentialSource === "stored") {
|
|
902
|
-
console.error("nodata-mcp: loaded saved credential from ~/.nodata/credentials.json (admin mode).");
|
|
903
|
-
}
|
|
904
|
-
const transport = new StdioServerTransport();
|
|
905
|
-
await server.connect(transport);
|
|
906
|
-
}
|
|
907
|
-
main().catch((err) => {
|
|
908
|
-
console.error("Fatal error:", err);
|
|
909
|
-
process.exit(1);
|
|
910
|
-
});
|
|
2
|
+
/*! @nodatachat/mcp 1.1.0 | Copyright 2026 Capsule Ltd. | FSL-1.1-ALv2 | see LICENSE.md */
|
|
3
|
+
import{McpServer as _t}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as vt}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as c}from"zod";import{readdirSync as be,readFileSync as Se,statSync as Q}from"node:fs";import{join as pt}from"node:path";var oe=[{id:"aws-access-key",name:"AWS Access Key",pattern:/(?:^|[^A-Z0-9])AKIA[A-Z2-7]{16}(?:[^A-Z2-7]|$)/,severity:"critical"},{id:"aws-secret-key",name:"AWS Secret Key",pattern:/(?:aws_secret_access_key|aws_secret_key|secret_access_key)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/,severity:"critical"},{id:"gcp-api-key",name:"GCP API Key",pattern:/AIza[0-9A-Za-z_-]{35}/,severity:"critical"},{id:"gcp-service-account",name:"GCP Service Account",pattern:/"type"\s*:\s*"service_account"/,severity:"critical"},{id:"github-token",name:"GitHub Token",pattern:/gh[pousr]_[0-9a-zA-Z]{36,}/,severity:"critical"},{id:"github-fine-grained",name:"GitHub Fine-Grained Token",pattern:/github_pat_[0-9a-zA-Z_]{22,}/,severity:"critical"},{id:"gitlab-token",name:"GitLab Token",pattern:/glpat-[0-9a-zA-Z_-]{20,}/,severity:"critical"},{id:"stripe-secret",name:"Stripe Secret Key",pattern:/sk_live_[0-9a-zA-Z]{24,}/,severity:"critical"},{id:"stripe-restricted",name:"Stripe Restricted Key",pattern:/rk_live_[0-9a-zA-Z]{24,}/,severity:"critical"},{id:"slack-token",name:"Slack Token",pattern:/xox[baprs]-[0-9a-zA-Z-]{10,}/,severity:"high"},{id:"slack-webhook",name:"Slack Webhook",pattern:/hooks\.slack\.com\/services\/T[0-9A-Z]{8,}\/B[0-9A-Z]{8,}\/[0-9a-zA-Z]{24}/,severity:"high"},{id:"twilio-api-key",name:"Twilio API Key",pattern:/SK[0-9a-fA-F]{32}/,severity:"high"},{id:"sendgrid-api-key",name:"SendGrid API Key",pattern:/SG\.[0-9a-zA-Z_-]{22}\.[0-9a-zA-Z_-]{43}/,severity:"critical"},{id:"postgres-uri",name:"PostgreSQL Connection String",pattern:/postgres(?:ql)?:\/\/[^\s'"]{10,}/,severity:"critical"},{id:"mongodb-uri",name:"MongoDB Connection String",pattern:/mongodb(?:\+srv)?:\/\/[^\s'"]{10,}/,severity:"critical"},{id:"redis-uri",name:"Redis Connection String",pattern:/redis(?:s)?:\/\/[^\s'"]{10,}/,severity:"high"},{id:"openai-key",name:"OpenAI API Key",pattern:/sk-[0-9a-zA-Z]{20,}T3BlbkFJ[0-9a-zA-Z]{20,}/,severity:"critical"},{id:"anthropic-key",name:"Anthropic API Key",pattern:/sk-ant-[0-9a-zA-Z_-]{40,}/,severity:"critical"},{id:"firebase-key",name:"Firebase API Key",pattern:/AIza[0-9A-Za-z_-]{35}/,severity:"high"},{id:"supabase-key",name:"Supabase Service Key",pattern:/eyJ[0-9a-zA-Z_-]{100,}/,severity:"critical"},{id:"private-key",name:"Private Key",pattern:/-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,severity:"critical"},{id:"jwt-secret",name:"JWT/Generic Secret",pattern:/(?:jwt_secret|JWT_SECRET|secret_key|SECRET_KEY|api_secret|API_SECRET)\s*[=:]\s*['"][^'"]{8,}['"]/,severity:"high"},{id:"password-in-code",name:"Password in Code",pattern:/(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]/,severity:"high"},{id:"bearer-token",name:"Bearer Token in Code",pattern:/['"]Bearer\s+[0-9a-zA-Z._-]{20,}['"]/,severity:"high"}],se=/node_modules\/|\.next\/|\.test\.|\.spec\.|__tests__\/|\.env\.example$|\.nodata-proof\.json$|\.git\//,Ue=[{name:"lodash",below:"4.17.21",severity:"critical",cve:"CVE-2021-23337",desc:"Command injection via template"},{name:"minimist",below:"1.2.6",severity:"critical",cve:"CVE-2021-44906",desc:"Prototype pollution"},{name:"json5",below:"2.2.2",severity:"high",cve:"CVE-2022-46175",desc:"Prototype pollution"},{name:"express",below:"4.19.2",severity:"high",cve:"CVE-2024-29041",desc:"Open redirect"},{name:"path-to-regexp",below:"6.3.0",severity:"high",cve:"CVE-2024-45296",desc:"ReDoS"},{name:"sanitize-html",below:"2.12.1",severity:"high",cve:"CVE-2024-21501",desc:"XSS bypass"},{name:"dompurify",below:"3.0.9",severity:"high",cve:"CVE-2024-45801",desc:"mXSS"},{name:"jsonwebtoken",below:"9.0.0",severity:"critical",cve:"CVE-2022-23529",desc:"Key confusion attack"},{name:"jose",below:"4.15.5",severity:"high",cve:"CVE-2024-28176",desc:"DoS via JWE"},{name:"axios",below:"1.6.0",severity:"high",cve:"CVE-2023-45857",desc:"CSRF cookie leak"},{name:"node-fetch",below:"2.6.7",severity:"high",cve:"CVE-2022-0235",desc:"Header leak on redirect"},{name:"serialize-javascript",below:"6.0.2",severity:"critical",cve:"CVE-2024-11831",desc:"XSS via serialization"},{name:"xml2js",below:"0.6.0",severity:"high",cve:"CVE-2023-0842",desc:"Prototype pollution"},{name:"next",below:"14.1.1",severity:"high",cve:"CVE-2024-34350",desc:"SSRF via headers"},{name:"sharp",below:"0.32.6",severity:"high",cve:"CVE-2023-4863",desc:"libwebp heap overflow"},{name:"multer",below:"1.4.5",severity:"high",cve:"CVE-2022-24434",desc:"Prototype pollution"},{name:"tough-cookie",below:"4.1.3",severity:"medium",cve:"CVE-2023-26136",desc:"Prototype pollution"},{name:"semver",below:"7.5.4",severity:"medium",cve:"CVE-2022-25883",desc:"ReDoS"},{name:"ip",below:"2.0.1",severity:"critical",cve:"CVE-2024-29415",desc:"SSRF bypass via octal IP"},{name:"tar",below:"6.2.1",severity:"high",cve:"CVE-2024-28863",desc:"DoS via path traversal"}];function Be(e,t){let n=o=>o.replace(/^[^0-9]*/,"").split(".").map(r=>parseInt(r,10)||0),a=n(e),i=n(t);for(let o=0;o<3;o++){if((a[o]||0)<(i[o]||0))return!0;if((a[o]||0)>(i[o]||0))return!1}return!1}function ce(e){let t=[],n;try{n=JSON.parse(e)}catch{return t}let a={...n.dependencies??{},...n.devDependencies??{}};for(let[i,o]of Object.entries(a)){let r=(o||"").replace(/^[\^~>=<\s]+/,""),s=Ue.find(l=>l.name===i&&Be(r,l.below));s&&t.push({name:i,version:r,vuln:s})}return t}var je=[{pattern:/^(full_?name|first_?name|last_?name|spouse_?name|beneficiary_?name)$/i,type:"name",label:"Person name"},{pattern:/^(email|e_?mail|email_?address)$/i,type:"email",label:"Email address"},{pattern:/^(phone|mobile|telephone|phone_?number|cell_?phone)$/i,type:"phone",label:"Phone number"},{pattern:/^(id_?number|identity|ssn|social_?security|national_?id|teudat_?zehut)$/i,type:"id",label:"ID number"},{pattern:/^(address|street_?address|home_?address)$/i,type:"address",label:"Address"},{pattern:/^(date_?of_?birth|dob|birth_?date|birthday)$/i,type:"dob",label:"Date of birth"},{pattern:/^(bank_?account|iban|account_?number|bank_?details|routing_?number)$/i,type:"financial",label:"Bank account"},{pattern:/^(credit_?card|card_?number|cvv|cc_?number)$/i,type:"financial",label:"Credit card"},{pattern:/^(salary|income|commission|premium|compensation)$/i,type:"financial",label:"Salary / income"},{pattern:/^(medical_?info|health_?conditions|diagnosis|medication|prescription)$/i,type:"medical",label:"Medical"},{pattern:/^(passport|passport_?number|passport_?id)$/i,type:"id",label:"Passport"},{pattern:/^(driver_?license|drivers_?license|dl_?number)$/i,type:"id",label:"Driver license"},{pattern:/^(tax_?id|tin|vat_?number|ein)$/i,type:"financial",label:"Tax ID"},{pattern:/^(insurance_?number|policy_?number|member_?id)$/i,type:"id",label:"Insurance/Policy"},{pattern:/^(biometric|fingerprint|face_?id|retina)$/i,type:"biometric",label:"Biometric"},{pattern:/^(latitude|longitude|lat|lng|geo_?location|coordinates)$/i,type:"location",label:"Geolocation"}],Fe=[{pattern:/^(?:שם|שם_מלא|שם פרטי|שם_פרטי|שם משפחה|שם_משפחה|שם הלקוח|שם בן זוג|שם המבוטח|שם המוטב|שם הילד|שם הקטין)$/i,type:"name",label:"\u05E9\u05DD \u05D0\u05D3\u05DD"},{pattern:/^(?:ת"ז|ת\.ז|ת ז|תז|תעודת זהות|תעודת_זהות|מספר זהות|מס' זהות|teudat_zehut|tz|israeli_id|il_id)$/i,type:"id",label:"\u05EA\u05E2\u05D5\u05D3\u05EA \u05D6\u05D4\u05D5\u05EA"},{pattern:/^(?:דרכון|מספר דרכון|רישיון נהיגה|רישיון_נהיגה|רשיון נהיגה)$/i,type:"id",label:"\u05D3\u05E8\u05DB\u05D5\u05DF/\u05E8\u05D9\u05E9\u05D9\u05D5\u05DF"},{pattern:/^(?:מייל|דוא"ל|דוא_ל|דואר אלקטרוני|אימייל|כתובת מייל)$/i,type:"email",label:"\u05D3\u05D5\u05D0\u05E8 \u05D0\u05DC\u05E7\u05D8\u05E8\u05D5\u05E0\u05D9"},{pattern:/^(?:טלפון|טל'|נייד|פלאפון|סלולרי|מספר טלפון|טלפון נייד)$/i,type:"phone",label:"\u05DE\u05E1\u05E4\u05E8 \u05D8\u05DC\u05E4\u05D5\u05DF"},{pattern:/^(?:כתובת|כתובת מגורים|רחוב|מס בית|עיר|ישוב|יישוב|מיקוד|כתובת מלאה|מען)$/i,type:"address",label:"\u05DB\u05EA\u05D5\u05D1\u05EA"},{pattern:/^(?:תאריך לידה|ת לידה|תאריך הולדת|יום הולדת|גיל)$/i,type:"dob",label:"\u05EA\u05D0\u05E8\u05D9\u05DA \u05DC\u05D9\u05D3\u05D4"},{pattern:/^(?:חשבון בנק|מספר חשבון|מס חשבון|בנק|סניף|iban|iban_il)$/i,type:"financial",label:"\u05D7\u05E9\u05D1\u05D5\u05DF \u05D1\u05E0\u05E7"},{pattern:/^(?:כרטיס אשראי|מספר אשראי|cvv|אשראי)$/i,type:"financial",label:"\u05DB\u05E8\u05D8\u05D9\u05E1 \u05D0\u05E9\u05E8\u05D0\u05D9"},{pattern:/^(?:שכר|משכורת|הכנסה|פרמיה|שכר ברוטו|שכר נטו|תגמול|בונוס|פיצוי)$/i,type:"financial",label:"\u05E9\u05DB\u05E8/\u05D4\u05DB\u05E0\u05E1\u05D4"},{pattern:/^(?:אבחון|אבחנה|מחלה|תרופה|מרשם|בדיקה רפואית|מצב רפואי|חיסון|רגישות|אלרגיה|סוג דם)$/i,type:"medical",label:"\u05DE\u05D9\u05D3\u05E2 \u05E8\u05E4\u05D5\u05D0\u05D9"},{pattern:/^(?:מספר פוליסה|מס פוליסה|פוליסה|מספר חבר|מספר ביטוח)$/i,type:"id",label:"\u05E4\u05D5\u05DC\u05D9\u05E1\u05D4/\u05D7\u05D1\u05E8\u05D5\u05EA"},{pattern:/^(?:שם הילד|תעודת זהות של ילד|ילדים|קטין|מספר ילדים|תעודת לידה)$/i,type:"minor_data",label:"\u05DE\u05D9\u05D3\u05E2 \u05E2\u05DC \u05E7\u05D8\u05D9\u05DF"},{pattern:/^(?:דת|לאום|מין|נטייה מינית|השקפה פוליטית|מצב משפחתי|עדה|מוצא)$/i,type:"sensitive_personal",label:"\u05DE\u05D9\u05D3\u05E2 \u05D0\u05D9\u05E9\u05D9 \u05E8\u05D2\u05D9\u05E9"},{pattern:/^(?:מספר רכב|מס רכב|לוחית רישוי|מספר רישוי)$/i,type:"tracking",label:"\u05DE\u05E1\u05E4\u05E8 \u05E8\u05DB\u05D1"}],Me=/^(id|uuid|created_?at|updated_?at|deleted_?at|status|type|category|description|title|slug|url|path|key|value|enabled|active|public|version|metadata|config|settings|data|content|note|comment|label|color|icon)$/i;function G(e){if(!e)return null;let t=e.trim();if(Me.test(t)||/^name$/i.test(t))return null;for(let n of[...Fe,...je])if(n.pattern.test(t))return{type:n.type,label:n.label};return null}var Z={id:"critical",financial:"critical",medical:"critical",biometric:"critical",minor_data:"critical",sensitive_personal:"critical",email:"high",phone:"high",name:"high",address:"high",dob:"high",tracking:"medium",location:"medium"};function F(e){let t=String(e??"").replace(/\D/g,"");if(t.length===0||t.length>9)return!1;let n=t.padStart(9,"0"),a=0;for(let i=0;i<9;i++){let o=parseInt(n[i]??"0",10)*(i%2+1);o>9&&(o-=9),a+=o}return a%10===0&&parseInt(n,10)>0}var U=/(?:\+972[\s-]?|0)(?:5[0-9]|[2-4]|[89])(?:[\s-]?\d){7}/g;var B=/\bIL\d{2}\s?(?:\d{4}\s?){4}\d{3}\b/g;function Ke(e){let t=String(e??"").replace(/\D/g,"");if(t.length<12||t.length>19)return!1;let n=0,a=!1;for(let i=t.length-1;i>=0;i-=1){let o=parseInt(t[i]??"0",10);a&&(o*=2,o>9&&(o-=9)),n+=o,a=!a}return n%10===0}function He(e){let t=String(e??"").replace(/\s/g,"").toUpperCase();if(!/^[A-Z]{2}\d{2}[A-Z0-9]{10,30}$/.test(t))return!1;let n=t.slice(4)+t.slice(0,4),a=0;for(let i of n){let o=i>="A"&&i<="Z"?String(i.charCodeAt(0)-55):i;for(let r of o)a=(a*10+Number(r))%97}return a===1}var W="(?<![\\d.,])",Y="(?![\\d.,])",ze=[{subkind:"israeli_id",severity:"high",pattern:new RegExp(`${W}\\d{9}${Y}`,"g"),validate:F,hint:"Israeli national ID (check digit verified) in plaintext. Encrypt the column or mask it at rest."},{subkind:"credit_card",severity:"critical",pattern:new RegExp(`${W}(?:\\d[ -]?){12,18}\\d${Y}`,"g"),validate:Ke,hint:"Payment card number (Luhn verified) in plaintext. This is PCI scope."},{subkind:"iban",severity:"high",pattern:/\b[A-Z]{2}\d{2}[A-Z0-9]{10,30}\b/g,validate:He,hint:"Bank account in IBAN form (mod-97 verified) in plaintext."},{subkind:"israeli_phone",severity:"medium",pattern:new RegExp(`${W}(?:\\+972[- ]?|0)(?:5\\d|[2-489])[- ]?\\d{3}[- ]?\\d{4}${Y}`,"g"),validate:e=>{let t=e.replace(/\D/g,"").replace(/^972/,"0");return t.length===10&&t.startsWith("0")},hint:"Israeli phone number in plaintext."}];function le(e){let t=[];for(let n of ze){n.pattern.lastIndex=0;let a=new Set,i,o=0;for(;(i=n.pattern.exec(e))!==null&&(o+=1,!(o>2e5));){let r=i[0];if(n.validate(r)&&a.add(r.replace(/[\s-]/g,"")),a.size>=1e5)break}a.size>0&&t.push({subkind:n.subkind,severity:n.severity,hint:n.hint,count:a.size})}return t}function X(e,t){let n=[],a="",i=!1;for(let o=0;o<e.length;o++){let r=e[o];i?r==='"'&&e[o+1]==='"'?(a+='"',o++):r==='"'?i=!1:a+=r:r==='"'?i=!0:r===t?(n.push(a),a=""):a+=r}return n.push(a),n}import{inflateRawSync as et}from"node:zlib";import{inflateSync as Ve}from"node:zlib";var J=new Set([".pdf"]),de=8*1024*1024;function We(e){let t=e.toString("latin1"),n=new Map,a=/(\d+)\s+(\d+)\s+obj\b/g,i;for(;(i=a.exec(t))!==null;){let o=Number(i[1]),r=a.lastIndex,s=t.indexOf("endobj",r);if(s<0)continue;let l=t.indexOf("stream",r),d,p=null;if(l>=0&&l<s){d=t.slice(r,l);let f=l+6;t[f]==="\r"&&(f+=1),t[f]===`
|
|
4
|
+
`&&(f+=1);let E=t.indexOf("endstream",f);E>f&&(p=e.subarray(f,E))}else d=t.slice(r,s);n.set(o,{num:o,dict:d,stream:p})}return n}function Ye(e){let t=e.toString("latin1").replace(/\s/g,"");t.startsWith("<~")&&(t=t.slice(2));let n=t.indexOf("~>");n>=0&&(t=t.slice(0,n));let a=[],i=0,o=0;for(let r of t){if(r==="z"&&o===0){a.push(0,0,0,0);continue}let s=r.charCodeAt(0)-33;if(s<0||s>84)return null;i=i*85+s,o+=1,o===5&&(a.push(i>>>24&255,i>>>16&255,i>>>8&255,i&255),i=0,o=0)}if(o>0){for(let s=o;s<5;s+=1)i=i*85+84;let r=[i>>>24&255,i>>>16&255,i>>>8&255,i&255];for(let s=0;s<o-1;s+=1)a.push(r[s]??0)}return Buffer.from(a)}function Ge(e){let t=e.toString("latin1").replace(/\s/g,"").split(">")[0]??"",n=t.length%2?t+"0":t;return/^[0-9A-Fa-f]*$/.test(n)?Buffer.from(n,"hex"):null}function Ze(e){let t=/\/Filter\s*(\[[^\]]*\]|\/\w+)/.exec(e);return!t||!t[1]?[]:[...t[1].matchAll(/\/(\w+)/g)].map(n=>n[1]??"")}function ue(e){if(!e.stream)return null;let t=e.stream;for(let n of Ze(e.dict)){let a=null;if(n==="FlateDecode")try{a=Ve(t)}catch{return null}else if(n==="ASCII85Decode")a=Ye(t);else if(n==="ASCIIHexDecode")a=Ge(t);else return null;if(a===null)return null;t=a}return t.toString("latin1")}function Xe(e){let t=new Map,n=1,a=/begincodespacerange([\s\S]*?)endcodespacerange/.exec(e);if(a&&a[1]){let l=/<([0-9A-Fa-f]+)>/.exec(a[1]);l&&l[1]&&l[1].length>=4&&(n=2)}let i=l=>{let d="";for(let p=0;p+3<l.length+1;p+=4){let f=parseInt(l.slice(p,p+4),16);Number.isFinite(f)&&f>0&&(d+=String.fromCharCode(f))}return d},o=/beginbfchar([\s\S]*?)endbfchar/g,r;for(;(r=o.exec(e))!==null;){let l=/<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]*)>/g,d;for(;(d=l.exec(r[1]??""))!==null;)t.set(parseInt(d[1]??"0",16),i(d[2]??""))}let s=/beginbfrange([\s\S]*?)endbfrange/g;for(;(r=s.exec(e))!==null;){let l=r[1]??"",d=/<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]*)>/g,p;for(;(p=d.exec(l))!==null;){let f=parseInt(p[1]??"0",16),E=parseInt(p[2]??"0",16),P=parseInt((p[3]??"0").slice(0,4)||"0",16);if(!(E-f>65535))for(let R=f;R<=E;R+=1)t.set(R,String.fromCharCode(P+(R-f)))}}return{map:t,bytes:n}}function Je(e){let t=new Map,n=new Map,a=[];for(let i of e.values()){let o=/\/Font\s*<<([\s\S]*?)>>/.exec(i.dict);o?.[1]&&a.push(o[1]);let r=/\/Font\s+(\d+)\s+\d+\s+R/.exec(i.dict);if(r){let s=e.get(Number(r[1]));s&&a.push(s.dict)}}for(let i of a){let o=/\/([^\s/<>[\]()]+)\s+(\d+)\s+\d+\s+R/g,r;for(;(r=o.exec(i))!==null;)n.set(r[1]??"",Number(r[2]))}for(let[i,o]of n){let r=e.get(o);if(!r)continue;let s=/\/ToUnicode\s+(\d+)\s+\d+\s+R/.exec(r.dict);if(!s)continue;let l=e.get(Number(s[1]));if(!l)continue;let d=ue(l);d&&t.set(i,Xe(d))}return t}function fe(e){let t=[];for(let n=0;n<e.length;n+=1){let a=e[n];if(a!=="\\"){t.push(a.charCodeAt(0)&255);continue}let i=e[n+1];if(i===void 0)break;if(i>="0"&&i<="7"){let r="",s=n+1;for(;s<e.length&&r.length<3&&e[s]>="0"&&e[s]<="7";)r+=e[s],s+=1;t.push(parseInt(r,8)&255),n=s-1;continue}let o={n:10,r:13,t:9,b:8,f:12};t.push(o[i]??i.charCodeAt(0)&255),n+=1}return t}function he(e){if(e.length<2||e[0]!==254||e[1]!==255)return null;let t="";for(let n=2;n+1<e.length;n+=2)t+=String.fromCharCode((e[n]??0)<<8|(e[n+1]??0));return t}function pe(e,t){let n=he(e);if(n!==null)return n;if(!t)return e.map(i=>String.fromCharCode(i)).join("");let a="";if(t.bytes===2)for(let i=0;i+1<e.length;i+=2){let o=(e[i]??0)<<8|(e[i+1]??0);a+=t.map.get(o)??""}else for(let i of e)a+=t.map.get(i)??String.fromCharCode(i);return a}function qe(e,t){let n="",a,i=/\/([^\s/<>[\]()]+)\s+[\d.]+\s+Tf|\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>|TJ|Tj|'|"/g,o,r="";for(;(o=i.exec(e))!==null;){let s=o[0];if(s.endsWith("Tf")){a=t.get(o[1]??"");continue}if(s==="TJ"||s==="Tj"||s==="'"||s==='"'){r&&(n+=r+`
|
|
5
|
+
`,r="");continue}if(s.startsWith("("))r+=pe(fe(s.slice(1,-1)),a);else if(s.startsWith("<")){let l=s.slice(1,-1).replace(/\s+/g,""),d=[];for(let p=0;p+1<l.length;p+=2)d.push(parseInt(l.slice(p,p+2),16));r+=pe(d,a)}}return r&&(n+=r+`
|
|
6
|
+
`),n}function Qe(e,t){let n=e.toString("latin1"),a=/\/Info\s+(\d+)\s+\d+\s+R/.exec(n),i=a?t.get(Number(a[1]))?.dict:void 0;if(!i)return"";let o=[],r=/\/(Author|Title|Subject|Keywords|Creator|Producer)\s*(\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>)/g,s;for(;(s=r.exec(i))!==null;){let l=s[2]??"";if(l.startsWith("(")){let d=fe(l.slice(1,-1));o.push(he(d)??d.map(p=>String.fromCharCode(p)).join(""))}else{let d=l.slice(1,-1).replace(/\s+/g,""),p="";if(d.toUpperCase().startsWith("FEFF"))for(let f=4;f+3<d.length+1;f+=4)p+=String.fromCharCode(parseInt(d.slice(f,f+4),16));else for(let f=0;f+1<d.length;f+=2)p+=String.fromCharCode(parseInt(d.slice(f,f+2),16));o.push(p)}}return o.join(`
|
|
7
|
+
`)}function ge(e){if(!e.subarray(0,8).toString("latin1").startsWith("%PDF-")||e.toString("latin1",0,Math.min(e.length,4096)).includes("/Encrypt"))return null;let n=We(e);if(n.size===0)return null;let a=Je(n),i=[];i.push(Qe(e,n));let o=0,r=0,s=!1;for(let l of n.values()){if(!l.stream||/\/Subtype|\/Type\s*\/(XObject|Font|Metadata)|\/FontFile/.test(l.dict))continue;let d=ue(l);if(d===null||!/(TJ|Tj)\b/.test(d))continue;let p=qe(d,a);if(p.trim().length!==0){if(r+=1,o+p.length>de){i.push(p.slice(0,Math.max(0,de-o))),s=!0;break}i.push(p),o+=p.length}}return r===0?null:{text:i.join(`
|
|
8
|
+
`),truncated:s,streams:r}}var me=new Set([".xlsx",".xlsm",".docx",".pptx"]),q=8*1024*1024,tt=101010256,nt=33639248,rt=67324752;function it(e){let t=Math.min(e.length,67584),n=-1;for(let r=e.length-22;r>=e.length-t&&r>=0;r-=1)if(e.readUInt32LE(r)===tt){n=r;break}if(n<0)return null;let a=e.readUInt16LE(n+10),i=e.readUInt32LE(n+16);if(i>=e.length)return null;let o=[];for(let r=0;r<a&&!(i+46>e.length||e.readUInt32LE(i)!==nt);r+=1){let s=e.readUInt16LE(i+10),l=e.readUInt32LE(i+20),d=e.readUInt32LE(i+24),p=e.readUInt16LE(i+28),f=e.readUInt16LE(i+30),E=e.readUInt16LE(i+32),P=e.readUInt32LE(i+42),R=e.subarray(i+46,i+46+p).toString("utf8");o.push({name:R,method:s,compSize:l,uncompSize:d,localHeaderOffset:P}),i+=46+p+f+E}return o}function at(e,t){let n=t.localHeaderOffset;if(n+30>e.length||e.readUInt32LE(n)!==rt)return null;let a=e.readUInt16LE(n+26),i=e.readUInt16LE(n+28),o=n+30+a+i,r=o+t.compSize;if(r>e.length)return null;let s=e.subarray(o,r);try{if(t.method===0)return s.toString("utf8");if(t.method===8)return et(s).toString("utf8")}catch{return null}return null}var ot={"&":"&","<":"<",">":">",""":'"',"'":"'"};function ye(e){return e.replace(/&(amp|lt|gt|quot|apos);/g,t=>ot[t]??t).replace(/&#(\d+);/g,(t,n)=>{let a=Number(n);return a>0&&a<1114112?String.fromCodePoint(a):""}).replace(/&#x([0-9a-fA-F]+);/g,(t,n)=>{let a=parseInt(n,16);return a>0&&a<1114112?String.fromCodePoint(a):""})}function st(e){return ye(e.replace(/<[^>]*>/g,`
|
|
9
|
+
`)).replace(/\n{2,}/g,`
|
|
10
|
+
`)}function ct(e,t,n){let a=new RegExp(`<${t}\\b[^>]*\\b${n}="([^"]*)"`,"g"),i=[],o;for(;(o=a.exec(e))!==null;)o[1]&&i.push(ye(o[1]));return i}function lt(e,t){let n=[];for(let a of t)(a==="docProps/core.xml"||a==="docProps/app.xml")&&n.push(a);if(e===".xlsx"||e===".xlsm")for(let a of t)(a==="xl/sharedStrings.xml"||a==="xl/workbook.xml"||/^xl\/worksheets\/sheet\d+\.xml$/.test(a)||/^xl\/comments\d*\.xml$/.test(a))&&n.push(a);else if(e===".docx")for(let a of t)(a==="word/document.xml"||/^word\/(header|footer|footnotes|endnotes|comments)\d*\.xml$/.test(a))&&n.push(a);else if(e===".pptx")for(let a of t)/^ppt\/(slides|notesSlides)\/(slide|notesSlide)\d+\.xml$/.test(a)&&n.push(a);return n}function dt(e,t){let n=it(e);if(!n||n.length===0)return null;let a=new Map(n.map(d=>[d.name,d])),i=lt(t,n.map(d=>d.name));if(i.length===0)return null;let o=[],r=[],s=0,l=!1;for(let d of i){if(s>=q){l=!0;break}let p=a.get(d);if(!p)continue;let f=at(e,p);if(f===null)continue;let E=st(f);if(d==="xl/workbook.xml"){let P=ct(f,"sheet","name");P.length&&(E+=`
|
|
11
|
+
`+P.join(`
|
|
12
|
+
`))}s+E.length>q&&(E=E.slice(0,Math.max(0,q-s)),l=!0),E.trim().length!==0&&(o.push(E),r.push(d),s+=E.length)}return o.length===0?null:{text:o.join(`
|
|
13
|
+
`),truncated:l,parts:r}}function _e(e,t){if(me.has(t))return dt(e,t);if(J.has(t)){let n=ge(e);return n?{text:n.text,truncated:n.truncated,parts:[`pdf:${n.streams}-streams`]}:null}return null}var ve=new Set([...me,...J]);var ee=10,j=5e3,ut=64*1024*1024,te=32*1024,ft=new Set([".exe",".dll",".so",".dylib",".msi",".bin",".iso",".zip",".tar",".gz",".7z",".rar",".png",".jpg",".jpeg",".gif",".bmp",".tif",".tiff",".webp",".ico",".mp3",".mp4",".mov",".avi",".mkv",".wav",".flac",".ogg",".psd",".ai",".fla",".dwg",".dxf",".sqlite",".sqlite3",".db",".pyc",".class"]),ht=new Set(["node_modules",".git",".hg",".svn"]),gt=[{re:/\b\d{3}-\d{2}-\d{4}\b/,kind:"pii_unencrypted",subkind:"ssn",severity:"high",hint:"US Social Security Number pattern in plaintext."},{re:/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,kind:"pii_unencrypted",subkind:"email",severity:"low",hint:"Email addresses in plaintext."},{re:/(passport|teudat.?zehut|(?<![א-ת])תעודת.?זהות(?![א-ת])|(?<![א-ת])ת["'׳״]?\.?ז(?![א-ת]))/i,kind:"pii_unencrypted",subkind:"passport_keyword",severity:"medium",hint:"Filename / content references passport / national-ID keyword."}],mt=[{re:/(salary|payroll|משכורת|ssn|social.?security|passport|דרכון|תעודת.?זהות)/i,kind:"pii_unencrypted",subkind:"sensitive_filename",severity:"medium",hint:"Filename suggests sensitive PII content."},{re:/\.(env|env\.local|pem|key|p12|pfx)$/i,kind:"pii_unencrypted",subkind:"secret_file",severity:"high",hint:"File extension implies secret / credential bundle in plaintext."}];function yt(e){let t=e.lastIndexOf(".");return t<0?"":e.slice(t).toLowerCase()}async function ne(e){let t=Date.now(),n=[],a=[],i=0,o=0,r={hit_file_cap:!1,hit_depth_cap:!1,max_files:j,max_depth:ee,skipped_by_extension:0,documents_extracted:0,documents_truncated:0,documents_unreadable:0,files_too_large:0,files_sampled_only:0,sample_bytes:te};function s(y){return(y.startsWith(e.target_path)?y.slice(e.target_path.length).replace(/^[\\/]+/,""):y).replace(/\\/g,"/")}function l(y,g,h,v,S){let m=s(y);m!==""&&a.push({target_id:e.target_id,path:m,kind:g,size_bytes:h,modified_at:v?v.toISOString():null,child_count:S})}function d(y,g){n.push({target_id:e.target_id,relative_path:y,finding_kind:g.kind,finding_subkind:g.subkind,severity:g.severity,remediation_hint:g.hint})}function p(y,g,h,v,S){n.push({target_id:e.target_id,relative_path:y,finding_kind:g,finding_subkind:v,severity:h,remediation_hint:S})}function f(y,g){if(i+=1,i>j){r.hit_file_cap=!0;return}let h=s(y);for(let u of mt)u.re.test(g)&&d(h,u);let v=yt(g);if(ft.has(v)){r.skipped_by_extension+=1;return}let S=ve.has(v),m=0;try{m=Q(y).size}catch{return}if(S&&m>ut){r.files_too_large+=1;return}let w;try{let u=Se(y,{flag:"r"});w=S?u:u.subarray(0,te)}catch{return}o+=1;let T;if(S){let u=_e(w,v);if(!u){r.documents_unreadable+=1;return}T=u.text,r.documents_extracted+=1,u.truncated&&(r.documents_truncated+=1)}else{let u=0;for(let _=0;_<w.length;_+=1)w[_]===0&&(u+=1);if(w.length>0&&u/w.length>.05)return;T=w.toString("utf8"),m>te&&(r.files_sampled_only+=1)}let k=new Set;for(let u of gt)if(u.re.test(T)){let _=`${u.subkind}::${h}`;k.has(_)||(k.add(_),d(h,u))}for(let u of le(T)){let _=`validated::${u.subkind}::${h}`;k.has(_)||(k.add(_),p(h,"pii_unencrypted",u.severity,u.subkind,`${u.hint} ${u.count} distinct value${u.count===1?"":"s"} in this file.`))}if(!se.test(h)){let u=T.split(`
|
|
14
|
+
`),_=Math.min(u.length,2e3);for(let b=0;b<_;b++){let x=u[b];if(!x)continue;let C=x.trimStart();if(!(C.startsWith("//")||C.startsWith("/*")||C.startsWith("*")||C.startsWith("#"))&&!/process\.env/.test(x)&&!/\$\{[A-Z_]+/.test(x)){for(let $ of oe)if($.pattern.test(x)){let N=`secret::${$.id}::${h}::${b+1}`;if(k.has(N))break;k.add(N),p(h,"hardcoded_secret",$.severity,$.id,`Looks like a ${$.name} hardcoded near line ${b+1}. Move to an env var or secret manager.`);break}}}}if(g==="package.json"&&!h.includes("node_modules"))try{let u=Se(y,"utf8"),_=ce(u);for(let b of _){let x=`vuln::${b.name}::${h}`;k.has(x)||(k.add(x),p(h,"vulnerable_dependency",b.vuln.severity,b.name,`${b.name}@${b.version} is below ${b.vuln.below} (${b.vuln.cve} \xB7 ${b.vuln.desc}). Bump to ${b.vuln.below} or higher.`))}}catch{}if(v===".csv"||v===".tsv"){let u=v===".tsv"?" ":",",_=T.split(/\r?\n/);if(_.length>0){let b=X(_[0]??"",u),x=_.slice(1,51).map(C=>X(C??"",u));b.forEach((C,$)=>{let N=(C??"").trim();if(!N)return;let O=G(N)?.type??null;if(!O){let D=[];for(let Le of x){let V=Le[$];if(V&&V.trim().length>0&&D.push(V),D.length>=50)break}let L=P(D);L&&(O=L)}if(O){let D=Z[O]??"medium",L=`header::${h}::${N}::${O}`;if(k.has(L))return;k.add(L),p(h,"pii_header_match",D,`${O}:${N}`,`Column "${N}" looks like ${O} data. Wrap with envelope or move to a vault-backed reference.`)}})}}if(v===".json"&&!g.endsWith(".nodata-proof.json"))try{let u=JSON.parse(T),_=Array.isArray(u)?u[0]:u;if(_&&typeof _=="object")for(let b of Object.keys(_)){let x=G(b);if(x){let C=Z[x.type]??"medium",$=`jsonkey::${h}::${b}::${x.type}`;if(k.has($))continue;k.add($),p(h,"pii_header_match",C,`${x.type}:${b}`,`JSON key "${b}" looks like ${x.type} data.`)}}}catch{}/\.(txt|log|md|csv|tsv|json|jsonl|xml|html?|yaml|yml|sql)$/i.test(g)&&E(T,h,k)}function E(y,g,h){let v=y.split(`
|
|
15
|
+
`),S=Math.min(v.length,1e3),m=-1,w=-1,T=-1;for(let k=0;k<S;k++){let u=v[k]??"";if(T<0){let _=u.match(/\b\d{9}\b/);_&&F(_[0])&&(T=k+1)}if(m<0&&(U.lastIndex=0,U.test(u)&&(m=k+1)),w<0&&(B.lastIndex=0,B.test(u)&&(w=k+1)),T>0&&m>0&&w>0)break}T>0&&!h.has(`ilid::${g}`)&&(h.add(`ilid::${g}`),p(g,"israeli_id_value","critical",`near_line:${T}`,`Israeli ID with valid Luhn checksum detected near line ${T}. Strong signal that real customer IDs are in plaintext.`)),m>0&&!h.has(`ilphone::${g}`)&&(h.add(`ilphone::${g}`),p(g,"israeli_phone_value","high",`near_line:${m}`,`Israeli phone number detected near line ${m}.`)),w>0&&!h.has(`iliban::${g}`)&&(h.add(`iliban::${g}`),p(g,"israeli_iban_value","critical",`near_line:${w}`,`Israeli IBAN (IL prefix) detected near line ${w}.`))}function P(y){if(y.length===0)return null;let g=0,h=0,v=0;for(let m of y)typeof m=="string"&&(F(m)&&g++,U.lastIndex=0,U.test(m)&&h++,B.lastIndex=0,B.test(m)&&v++);let S=y.length;return g/S>=.3?"id":h/S>=.3?"phone":v/S>=.3?"financial":null}function R(y,g){if(g>ee){r.hit_depth_cap=!0;return}if(i>j){r.hit_file_cap=!0;return}let h;try{h=be(y)}catch{return}for(let v of h){if(i>j)return;let S=pt(y,v),m;try{m=Q(S)}catch{continue}if(!m.isSymbolicLink())if(m.isDirectory()){if(ht.has(v))continue;let w=null;try{w=be(S).length}catch{w=null}l(S,"folder",null,m.mtime??null,w),R(S,g+1)}else m.isFile()&&(l(S,"file",m.size,m.mtime??null,null),f(S,v))}}let z;try{z=Q(e.target_path).isDirectory()}catch{z=!1}return z?(e.log.info("folder-scanner: starting",{target_label:e.target_label,target_path:e.target_path,max_files:j,max_depth:ee}),R(e.target_path,0),{target_id:e.target_id,target_label:e.target_label,target_path:e.target_path,files_walked:i,files_inspected:o,duration_ms:Date.now()-t,findings:n,inventory:a,coverage:r}):{target_id:e.target_id,target_label:e.target_label,target_path:e.target_path,files_walked:0,files_inspected:0,duration_ms:Date.now()-t,findings:[],inventory:[],coverage:r,error:"target_path_not_found_or_not_directory"}}var re={info(){}};var ke={critical:4,high:3,medium:2,low:1,info:0};function ie(e){let t=[];if(t.push(`Scanned "${e.target_label||e.target_path}" on this machine \u2014 nothing left your computer.
|
|
16
|
+
`),e.error)return t.push(`Could not scan: ${e.error}`),t.join(`
|
|
17
|
+
`);t.push(`WALKED ${e.files_walked} files (${e.files_inspected} inspected) in ${e.duration_ms}ms`);let n=e.findings??[],a={};for(let s of n)a[s.severity]=(a[s.severity]??0)+1;let i=["critical","high","medium","low","info"].filter(s=>a[s]).map(s=>`${a[s]} ${s}`).join(" \xB7 ");if(t.push(`EXPOSURE ${n.length} findings${i?" \u2014 "+i:""}
|
|
18
|
+
`),n.length===0)t.push(`No hardcoded secrets or unencrypted PII found in the files scanned. Clean.
|
|
19
|
+
`);else{let s=[...n].sort((d,p)=>(ke[p.severity]??0)-(ke[d.severity]??0));t.push("TOP EXPOSURES:");for(let d of s.slice(0,8))t.push(` \u2717 [${d.severity.toUpperCase()}] ${d.finding_subkind||d.finding_kind} \u2014 ${d.relative_path}`);s.length>8&&t.push(` \u2026 and ${s.length-8} more.`),t.push("");let l=s.find(d=>d.remediation_hint);l?.remediation_hint&&t.push(`FIRST FIX ${l.remediation_hint}
|
|
20
|
+
`)}let o=e.coverage,r=[];return o?.hit_file_cap&&r.push(`stopped at the ${o.max_files}-file cap`),o?.hit_depth_cap&&r.push(`folders deeper than ${o.max_depth} levels were not descended`),o?.files_too_large&&r.push(`${o.files_too_large} file(s) too large to read`),o?.documents_unreadable&&r.push(`${o.documents_unreadable} document(s) unreadable`),r.length&&t.push(`COVERAGE ${r.join("; ")}.`),t.join(`
|
|
21
|
+
`)}import{homedir as bt,hostname as $e}from"node:os";import{join as Re,resolve as M,basename as St}from"node:path";import{existsSync as K,readFileSync as Pe,writeFileSync as kt,mkdirSync as wt,chmodSync as Et}from"node:fs";import{spawn as xt}from"node:child_process";import{generateKeyPairSync as At,createPrivateKey as Tt,sign as It,randomBytes as Ct}from"node:crypto";var ae="1.1.0",Ne=Re(bt(),".nodata"),H=Re(Ne,"credentials.json");function $t(){try{if(!K(H))return null;let e=JSON.parse(Pe(H,"utf8"));if(!e)return null;let t=e.device&&typeof e.device.private_key_pkcs8_b64=="string",n=typeof e.api_key=="string"&&e.api_key;return t||n?e:null}catch{return null}}function we(e){wt(Ne,{recursive:!0});let t=JSON.stringify({...e,saved_at:new Date().toISOString()},null,2);kt(H,t,{mode:384});try{Et(H,384)}catch{}}var Rt=`nodata-mcp v${ae} \u2014 NoData MCP server
|
|
22
|
+
|
|
23
|
+
ADMIN mode: nodata-mcp --api-key sk_live_...
|
|
24
|
+
AGENT mode: nodata-mcp --grant-token ndca-...
|
|
25
|
+
Options: --base-url <url> (default https://www.nodatacapsule.com)
|
|
26
|
+
--help \xB7 --version
|
|
27
|
+
|
|
28
|
+
Env vars: NODATA_API_KEY \xB7 NODATA_GRANT_TOKEN \xB7 NODATA_BASE_URL
|
|
29
|
+
Get a free API key: https://www.nodatacapsule.com/developers/get-started
|
|
30
|
+
Mint a grant token: POST /api/v1/governance/grant (or the nodata_grant tool).`;function Pt(e){let t="",n="",a="https://www.nodatacapsule.com";for(let r=0;r<e.length;r++)e[r]==="--api-key"&&e[r+1]?t=e[++r]:e[r]==="--grant-token"&&e[r+1]?n=e[++r]:e[r]==="--base-url"&&e[r+1]?a=e[++r]:e[r]==="--help"||e[r]==="-h"?(console.log(Rt),process.exit(0)):e[r]==="--version"||e[r]==="-v"?(console.log(ae),process.exit(0)):e[r].startsWith("--")&&console.error(`nodata-mcp: unknown option '${e[r]}' (see --help)`);let i=t||n?"flag":"none";!t&&process.env.NODATA_API_KEY&&(t=process.env.NODATA_API_KEY,i="env"),!n&&process.env.NODATA_GRANT_TOKEN&&(n=process.env.NODATA_GRANT_TOKEN,i==="none"&&(i="env")),process.env.NODATA_BASE_URL&&(a=process.env.NODATA_BASE_URL);let o;if(!t&&!n){let r=$t();r&&(!process.env.NODATA_BASE_URL&&a==="https://www.nodatacapsule.com"&&r.base_url&&(a=r.base_url),r.device?(o=r.device,i="stored"):r.api_key&&(t=r.api_key,i="stored"))}return a=a.replace(/\/+$/,""),{apiKey:t,grantToken:n,baseUrl:a,credentialSource:i,device:o}}var Ee=3e4;async function Oe(e,t){let n;try{n=await fetch(e,{...t,signal:AbortSignal.timeout(Ee)})}catch(i){return{ok:!1,status:0,body:{error:i instanceof Error&&i.name==="TimeoutError"?`Request timed out after ${Ee/1e3}s: ${e}`:`Network error reaching ${e}: ${i instanceof Error?i.message:String(i)}`}}}let a;try{a=await n.json()}catch{a={error:`Non-JSON response (HTTP ${n.status})`}}return{ok:n.ok,status:n.status,body:a}}async function De(e){if(typeof e=="string")return{token:e};try{return{token:await e()}}catch(t){return{error:{ok:!1,status:0,body:{error:`Could not obtain an access token: ${t instanceof Error?t.message:String(t)}`}}}}}async function A(e,t,n,a){let i=await De(t);return"error"in i?i.error:Oe(`${e}${n}`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${i.token}`},body:JSON.stringify(a)})}async function xe(e,t,n,a){let i=await De(t);if("error"in i)return i.error;let o=new URL(`${e}${n}`);for(let[r,s]of Object.entries(a))s!==void 0&&s!==""&&o.searchParams.set(r,s);return Oe(o.toString(),{method:"GET",headers:{Authorization:`Bearer ${i.token}`}})}function I(e){let t=JSON.stringify(e.body,null,2);return e.ok?{content:[{type:"text",text:t}]}:{content:[{type:"text",text:`Error (HTTP ${e.status}):
|
|
31
|
+
${t}`}],isError:!0}}function Nt(e){let t=e?[M(e)]:[M(".env.local"),M(".env")];if(e&&!K(t[0]))return{error:`env file not found: ${t[0]}`};let n={},a=[];for(let o of t)if(K(o)){a.push(o);for(let r of Pe(o,"utf8").split(/\r?\n/)){let s=r.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);!s||s[1]in n||(n[s[1]]=s[2].replace(/^(['"])(.*)\1$/,"$2").trim())}}let i=o=>{for(let r of o){let s=n[r]??process.env[r];if(s)return{name:r,value:s}}};return{url:i(["SUPABASE_URL","NEXT_PUBLIC_SUPABASE_URL","VITE_SUPABASE_URL","EXPO_PUBLIC_SUPABASE_URL"]),anon:i(["SUPABASE_ANON_KEY","NEXT_PUBLIC_SUPABASE_ANON_KEY","VITE_SUPABASE_ANON_KEY","EXPO_PUBLIC_SUPABASE_ANON_KEY","SUPABASE_PUBLISHABLE_KEY","NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY"]),jwt:i(["SUPABASE_JWT_SECRET","JWT_SECRET"]),files:a}}function Ot(e){try{let n=(e.startsWith("ndca-")?e.slice(5):e).split(".")[1];return n?JSON.parse(Buffer.from(n.replace(/-/g,"+").replace(/_/g,"/"),"base64").toString("utf8")).handle??null:null}catch{return null}}var Dt=5*6e4;function Lt(){let{publicKey:e,privateKey:t}=At("ed25519"),n=e.export({format:"der",type:"spki"}),a=t.export({format:"der",type:"pkcs8"});return{publicKeyHex:n.subarray(-32).toString("hex"),privateKeyPkcs8B64:a.toString("base64")}}function Ut(e,t){let n=Tt({key:Buffer.from(e,"base64"),format:"der",type:"pkcs8"});return It(null,Buffer.from(t,"utf8"),n).toString("base64")}function Bt(){let e=$e().toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,20)||"device";return`${/^[a-z]/.test(e)?e:`d-${e}`}-${Ct(3).toString("hex")}`.slice(0,32)}async function jt(e,t,n,a,i){let o=await A(e,t,"/api/v1/attest/devices",{handle:n,label:a,ed25519_pubkey_hex:i});if(o.ok){let r=o.body.org_id??"";return r?{ok:!0,orgId:r}:{ok:!1,status:o.status,error:"enroll response missing org_id"}}return{ok:!1,status:o.status,error:o.body?.error??`HTTP ${o.status}`}}async function Ft(e,t){let n=await A(e,"","/api/v1/attest/challenge",{org_id:t.org_id,handle:t.handle});if(!n.ok)throw new Error(`challenge: ${n.body?.error??`HTTP ${n.status}`}`);let{challenge:a,nonce:i}=n.body;if(!a||!i)throw new Error("challenge response malformed");let o=Ut(t.private_key_pkcs8_b64,a),r=await A(e,"","/api/v1/attest",{org_id:t.org_id,handle:t.handle,nonce:i,signature:o});if(!r.ok)throw new Error(`attest: ${r.body?.error??`HTTP ${r.status}`}`);let s=r.body;if(!s.allowed||!s.access_token)throw new Error(`attestation rejected: ${s.reason??"denied"}`);return{token:s.access_token,expiresAtMs:s.expires_at?Date.parse(s.expires_at):Date.now()+15*6e4}}function Mt(e,t){let n=null;return async()=>((!n||Date.now()>=n.expiresAtMs-Dt)&&(n=await Ft(e,t)),n.token)}function Ae(e,t,n){e.registerTool("nodata_register_table",{description:"Register one of your own tables as AI-agent-readable. This writes POLICY only: it declares which columns are sensitive (released per-column only if their ciphertext exists) and which are plaintext metadata. It does NOT encrypt or change anything in the user's database \u2014 never tell the user their data is now encrypted. Relay the response's `message` and `next_steps` verbatim. After this, grants can target the table.",inputSchema:{table_name:c.string().describe("Your SQL table name"),encrypted_columns:c.array(c.string()).describe("Columns stored as <col>_ct/_iv/_tag triplets"),plaintext_columns:c.array(c.string()).optional().describe("Plaintext metadata/filter columns"),classification_column:c.string().optional().describe("Plaintext column carrying the classification value")}},async({table_name:a,encrypted_columns:i,plaintext_columns:o,classification_column:r})=>{let s=await A(t,n,"/api/v1/governance/tables",{table_name:a,encrypted_columns:i,plaintext_columns:o??[],classification_column:r});return I(s)}),e.registerTool("nodata_connect_source",{description:"Connect the user's own Supabase project as the data source for their registered tables, so granted agents can read metadata through NoData. Reads the project URL, anon key and JWT secret from the user's LOCAL env file (e.g. .env.local) and sends them directly to NoData \u2014 never ask the user to paste a key or secret into the chat, and never print them. Only allowed_tables are readable (deny-by-default).",inputSchema:{allowed_tables:c.array(c.string()).describe("Tables agents may read through this source"),env_file:c.string().optional().describe("Path to the env file holding the Supabase keys (default: .env.local, then .env in the working directory)"),source_url:c.string().optional().describe("Override the project URL (https://<ref>.supabase.co) if the env file lacks it")}},async({allowed_tables:a,env_file:i,source_url:o})=>{let r=Nt(i);if("error"in r)return{content:[{type:"text",text:`Error: ${r.error}`}],isError:!0};let s=o??r.url?.value,l=[s?null:"project URL (SUPABASE_URL / NEXT_PUBLIC_SUPABASE_URL)",r.anon?null:"anon key (SUPABASE_ANON_KEY / NEXT_PUBLIC_SUPABASE_ANON_KEY)",r.jwt?null:"JWT secret (SUPABASE_JWT_SECRET \xB7 Supabase \u2192 Settings \u2192 API \u2192 JWT secret)"].filter(Boolean);if(l.length>0)return{content:[{type:"text",text:`Not sent. Missing in ${r.files.join(", ")||"the environment"}: ${l.join("; ")}.
|
|
32
|
+
Ask the user to add them to that file themselves (not in this chat), then call this tool again.`}],isError:!0};let d=await A(t,n,"/api/connector/secrets",{source_url:s,allowed_tables:a,anon_key:r.anon.value,jwt_secret:r.jwt.value}),p={read_from:r.files,variables_used:[r.url?.name,r.anon.name,r.jwt.name].filter(Boolean)};return I({...d,body:{...d.body,...p}})}),e.registerTool("nodata_grant",{description:"Issue ONE AI agent a claim-scoped grant over your data \u2014 claims, not blanket access. Returns a grant_token (shown once) the agent presents to read. Denied columns never decrypt. For one agent over several tables pass `tables` in a single call \u2014 do NOT call this once per table, that creates a separate agent each time.",inputSchema:{agent:c.string().describe("Agent label (becomes its handle)"),tables:c.array(c.object({table:c.string(),columns:c.array(c.string()).describe("Columns the agent may read in this table"),where:c.record(c.string(),c.union([c.string(),c.number(),c.boolean()])).optional()})).optional().describe("Several tables for the same agent. When set, table/columns/where are ignored."),table:c.string().optional().describe("Single target table (default agent_demo_records)"),columns:c.array(c.string()).optional().describe("Columns the agent may read (single-table form)"),where:c.record(c.string(),c.union([c.string(),c.number(),c.boolean()])).optional().describe("Equality filter (plaintext cols only)"),classifications:c.array(c.string()).optional().describe("Allowed classification values; empty = no gating"),ttl_seconds:c.number().optional().describe("Token lifetime (default 30d, max 1y)")}},async({agent:a,tables:i,table:o,columns:r,where:s,classifications:l,ttl_seconds:d})=>{let p=await A(t,n,"/api/v1/governance/grant",{agent:a,...i?{tables:i}:{table:o,columns:r,where:s},classifications:l,ttl_seconds:d});return I(p)}),e.registerTool("nodata_revoke",{description:"Revoke an agent's grant by handle (kills all its active credentials) or by a specific jti. Revocation is mathematics \u2014 the next read 401s and any cached data is governance-dead.",inputSchema:{handle:c.string().optional().describe("Agent handle to revoke"),jti:c.string().optional().describe("Specific credential jti to revoke"),reason:c.string().optional()}},async({handle:a,jti:i,reason:o})=>{let r=await A(t,n,"/api/v1/governance/revoke",{handle:a,jti:i,reason:o});return I(r)}),e.registerTool("nodata_encrypt",{description:"Encrypt a sensitive field value using Capsule Blind Relay. Returns ciphertext. NoData stores nothing \u2014 the key is embedded in the ciphertext for YOU to store.",inputSchema:{field:c.string(),value:c.string(),context:c.string().optional()}},async({field:a,value:i,context:o})=>{let r={field:a,value:i};return o&&(r.context=o),I(await A(t,n,"/api/v1/encrypt",r))}),e.registerTool("nodata_decrypt",{description:"Decrypt a Capsule ciphertext back to plaintext (aes256gcm:v1:iv:cipher:key format).",inputSchema:{ciphertext:c.string(),field:c.string().optional(),requester:c.string().optional()}},async({ciphertext:a,field:i,requester:o})=>{let r={ciphertext:a};return i&&(r.field=i),o&&(r.requester=o),I(await A(t,n,"/api/v1/decrypt",r))}),e.registerTool("nodata_deliver",{description:"Create a burn-after-read secure link for delivering sensitive content. Returns a view URL and OTP.",inputSchema:{content:c.string(),to:c.string().optional(),maxViews:c.number().optional(),ttl:c.string().optional(),burn:c.boolean().optional()}},async({content:a,to:i,maxViews:o,ttl:r,burn:s})=>{let l={content:a};return i&&(l.to=i),o!==void 0&&(l.maxViews=o),r&&(l.ttl=r),s!==void 0&&(l.burn=s),I(await A(t,n,"/api/v1/deliver",l))}),e.registerTool("nodata_evidence",{description:"Field-level encrypt/decrypt/deliver events (field_access_log). Requires the Engine scope on the key. For the signed decision-receipt chain \u2014 grants, agent reads, denials \u2014 use nodata_proofs instead, which every org key can call. Metadata only.",inputSchema:{from:c.string().optional(),to:c.string().optional(),action:c.string().optional(),limit:c.number().optional()}},async({from:a,to:i,action:o,limit:r})=>{let s={from:a,to:i,action:o,limit:r!==void 0?String(r):void 0};return I(await xe(t,n,"/api/v1/evidence",s))}),e.registerTool("nodata_proofs",{description:"List your organization's signed proofs: every grant, agent read and denial, newest first. Content-free (labels, hashes, timestamps). Each proof can be verified by anyone, without an account, by POSTing its receipt_id to /api/access/verify. The response's `plan` says which proof types the user's plan includes and which need an upgrade (with the plan name) \u2014 relay it as is.",inputSchema:{receipt_id:c.string().optional().describe("Fetch one proof by id"),surface:c.string().optional().describe("Filter, e.g. 'governance.grant', 'agent.read'"),decision:c.string().optional().describe("Filter: 'grant' | 'allow' | 'deny' | 'revoke'"),from:c.string().optional().describe("ISO timestamp"),to:c.string().optional().describe("ISO timestamp"),limit:c.number().optional().describe("1..1000, default 50")}},async({receipt_id:a,surface:i,decision:o,from:r,to:s,limit:l})=>I(await xe(t,n,"/api/v1/proofs",{receipt_id:a,surface:i,decision:o,from:r,to:s,limit:l!==void 0?String(l):void 0})))}function Kt(e,t,n){let a=Ot(n);a||console.error("nodata-mcp: warning \u2014 could not derive an agent handle from the grant token (expected ndca-<jwt> with a 'handle' claim). nodata_decide/nodata_retrieve will work; nodata_read/nodata_use will fail until a valid grant token is provided."),e.registerTool("nodata_decide",{description:"Ask whether this agent MAY reach a resource, WITHOUT reading it. Returns a decision (allow / degrade / deny), the reachable column subset, an access-distance cost, and a signed proof \u2014 never any data. Use it to plan: check reachability before you retrieve. Hard-scoped to this server's grant token.",inputSchema:{table:c.string().describe("Table to check (must be in your grant)"),columns:c.array(c.string()).describe("Columns you want to reach"),where:c.record(c.string(),c.union([c.string(),c.number(),c.boolean()])).optional().describe("Equality filter (readable columns only)")}},async({table:i,columns:o,where:r})=>{let s=await A(t,n,"/api/access/decide",{resource:{table:i,columns:o,where:r},intent:"read"});return I(s)}),e.registerTool("nodata_retrieve",{description:"Retrieve prompt-ready context for a resource, filtered to exactly what this agent may see. Denied columns are never decrypted; the response includes an accounting of what was withheld (columns + estimated tokens saved) and a signed proof. Prefer this over a raw read when assembling an LLM prompt. Hard-scoped to this server's grant token.",inputSchema:{table:c.string().describe("Table to retrieve from (must be in your grant)"),columns:c.array(c.string()).describe("Columns to include in context"),where:c.record(c.string(),c.union([c.string(),c.number(),c.boolean()])).optional(),top_k:c.number().optional().describe("Max rows (default 50, max 200)")}},async({table:i,columns:o,where:r,top_k:s})=>{let l=await A(t,n,"/api/access/retrieve",{resource:{table:i,columns:o,where:r},top_k:s});return I(l)}),e.registerTool("nodata_read",{description:"Read governed data within this agent's grant. You may only request columns your grant allows \u2014 denied columns never decrypt server-side, and every read emits a cryptographic receipt. This tool is hard-scoped to the grant token this server was started with.",inputSchema:{table:c.string().describe("Table to read (must be in your grant)"),columns:c.array(c.string()).describe("Columns to read (must be in your grant)"),where:c.record(c.string(),c.union([c.string(),c.number(),c.boolean()])).optional(),limit:c.number().optional().describe("Max rows (default 50, max 200)")}},async({table:i,columns:o,where:r,limit:s})=>{if(!a)return{content:[{type:"text",text:"Error: could not derive agent handle from grant token."}],isError:!0};let l=await A(t,n,`/api/agents/${encodeURIComponent(a)}/read`,{table:i,columns:o,where:r,limit:s});return I(l)}),e.registerTool("nodata_use",{description:"Invoke a pre-registered capability (deploy, send, charge, query, \u2026) WITHOUT receiving the secret. The Capsule injects the sealed credential server-side and returns only the result. You may invoke only capabilities your grant allows; the secret never reaches you. Hard-scoped to the grant token this server was started with.",inputSchema:{capability:c.string().describe("Capability name to invoke (must be in your grant, e.g. 'deploy-prod')"),params:c.record(c.string(),c.unknown()).optional().describe("Allowlisted params; unknown keys are dropped server-side"),resource_key:c.string().optional().describe("Optional folder/resource the action targets (enforced against corpus scope)")}},async({capability:i,params:o,resource_key:r})=>{if(!a)return{content:[{type:"text",text:"Error: could not derive agent handle from grant token."}],isError:!0};let s=await A(t,n,`/api/agents/${encodeURIComponent(a)}/use`,{capability:i,params:o,resource_key:r});return I(s)}),e.registerTool("nodata_code_advise",{description:"Ask the blind code advisor to diagnose/fix a bug in a code capsule WITHOUT seeing its source. You receive guidance + a targeted diff + an exposure proof (source never left the boundary); the source is never returned to you. Requires the capsule owner to have granted this agent (scope.fabric_developers). Hard-scoped to this server's grant token.",inputSchema:{capsule_id:c.string().describe("The code capsule to advise on (you must be granted on it)"),question:c.string().optional().describe("What to fix / improve, in natural language"),note:c.string().optional().describe("Extra context for the advisor"),error:c.object({message:c.string().optional(),stack:c.string().optional(),kind:c.string().optional()}).optional().describe("An error the code produced, to guide the fix"),steer:c.string().optional().describe("A steer on a refine turn (e.g. 'no, round half-up')"),prior_diff:c.string().optional().describe("The previously proposed diff, to refine")}},async({capsule_id:i,question:o,note:r,error:s,steer:l,prior_diff:d})=>{if(!a)return{content:[{type:"text",text:"Error: could not derive agent handle from grant token."}],isError:!0};let p=await A(t,n,`/api/agents/${encodeURIComponent(a)}/code-capsule/${encodeURIComponent(i)}/advise`,{question:o,note:r,error:s,steer:l,prior_diff:d});return I(p)}),e.registerTool("nodata_code_patch",{description:"Submit a fix to a code capsule as a SIGNED, queued patch WITHOUT seeing its source. Provide a raw unified `diff`, or a whole-file `replace` ({path, content}) that is diffed server-side against the sealed source. The source is never persisted or returned; you get back a patch id, the diff hash, and a receipt. Requires a capsule grant (scope.fabric_developers). Hard-scoped to this server's grant token.",inputSchema:{capsule_id:c.string().describe("The code capsule to patch (you must be granted on it)"),summary:c.string().optional().describe("Short human-readable summary of the fix"),diff:c.string().optional().describe("A raw unified diff (advanced)"),replace:c.object({path:c.string(),content:c.string()}).optional().describe("Whole-file replacement; the diff is synthesized server-side against sealed source")}},async({capsule_id:i,summary:o,diff:r,replace:s})=>{if(!a)return{content:[{type:"text",text:"Error: could not derive agent handle from grant token."}],isError:!0};let l=await A(t,n,`/api/agents/${encodeURIComponent(a)}/code-capsule/${encodeURIComponent(i)}/patch`,{summary:o,diff:r,replace:s});return I(l)})}var Te=`WHAT NODATA DOES \u2014 in one line:
|
|
33
|
+
Give any agent real power over your data, without the power to leak it.
|
|
34
|
+
(Possession is not permission: it can hold the key and still not read what you didn't allow.)
|
|
35
|
+
`,Ht=`TURN THE DATA TOOLS ON \u2014 3 steps, about a minute, once:
|
|
36
|
+
|
|
37
|
+
1. GET A KEY call nodata_connect (no arguments)
|
|
38
|
+
-> opens the signup in your browser: email + company (~30s), a 12-word
|
|
39
|
+
recovery phrase to keep, and an ORG KEY shown ONCE. Copy that key.
|
|
40
|
+
|
|
41
|
+
2. SAVE IT call nodata_connect again, this time with
|
|
42
|
+
api_key: "<the key you copied>"
|
|
43
|
+
-> saved on this machine; you never paste it again.
|
|
44
|
+
|
|
45
|
+
3. ACTIVATE type /mcp (or restart Claude Code) so the tools load.
|
|
46
|
+
|
|
47
|
+
COMING BACK LATER? Nothing to redo \u2014 it starts ready. If the tools look missing, just /mcp.
|
|
48
|
+
`,zt=`NOW \u2014 TURN YOUR DATA INTO AN AGENT THAT CAN'T LEAK, in 3 asks (just say them in plain words):
|
|
49
|
+
|
|
50
|
+
1. SEE what's sensitive run nodata_scan_folder \u2014 scans a local folder right here, nothing
|
|
51
|
+
uploads (or open https://www.nodatacapsule.com/ai-folder in a browser).
|
|
52
|
+
|
|
53
|
+
2. CHOOSE what to release say "let <agent> read name + plan on <table>, hide the rest"
|
|
54
|
+
-> I run nodata_register_table + nodata_grant; the agent gets a token.
|
|
55
|
+
|
|
56
|
+
3. PROVE & control the agent now reads ONLY those columns \u2014 the rest never decrypt.
|
|
57
|
+
"what did it see?" -> nodata_evidence \xB7 "cut it off" -> nodata_revoke
|
|
58
|
+
|
|
59
|
+
Also: a secret even NoData can't read -> nodata_encrypt, then hand it over once -> nodata_deliver.
|
|
60
|
+
`,Vt=`WHAT YOU CAN DO \u2014 scoped to your grant; you reach only what it allows, the rest never decrypts:
|
|
61
|
+
|
|
62
|
+
1. PLAN nodata_decide "may I reach X?" -> allow / partial / deny + a signed proof. No data moves.
|
|
63
|
+
2. PULL nodata_retrieve prompt-ready context, allowed fields only, with a receipt.
|
|
64
|
+
(nodata_read for raw governed rows.)
|
|
65
|
+
3. ACT nodata_use run a capability with a secret injected server-side \u2014 you never see it.
|
|
66
|
+
`,Ie=`STUCK? There's always a next step:
|
|
67
|
+
- Don't see the nodata_* tools? They load when the client starts \u2014 type /mcp or restart.
|
|
68
|
+
(Expected, not an error.)
|
|
69
|
+
- No MCP at all? The same governance runs from one curl, or the guided page:
|
|
70
|
+
https://www.nodatacapsule.com/developers/get-started
|
|
71
|
+
`;function Ce(e){return e==="setup"?`You're connected to NoData \u2014 but there's NO key yet, so the data tools are still off.
|
|
72
|
+
|
|
73
|
+
`:e==="agent"?`You're connected to NoData, scoped to one grant token.
|
|
74
|
+
|
|
75
|
+
`:`You're connected to NoData (admin mode). You don't memorize commands \u2014 just ask in plain words.
|
|
76
|
+
|
|
77
|
+
`}function Wt(e){if(e==="setup")return Ce(e)+Te+`
|
|
78
|
+
`+Ht+`
|
|
79
|
+
`+Ie;let t=e==="agent"?Vt:zt;return Ce(e)+Te+`
|
|
80
|
+
`+t+`
|
|
81
|
+
`+Ie}function Yt(e,t){e.registerTool("nodata_get_started",{description:"Call this FIRST (no arguments). NoData in one line: what you can achieve \u2014 an agent that cannot leak, a look at what is already exposed, a secret we cannot read \u2014 with the single next step for each, and how to recover if the tools or MCP seem stuck. If the user asks what NoData can do, how to start, or why the tools are missing, call this and relay it.",inputSchema:{}},async()=>({content:[{type:"text",text:Wt(t)}]}))}var Gt="/capsule-api/register";function Zt(e){let t=process.platform==="win32",n=process.platform==="darwin"?"open":t?"cmd":"xdg-open",a=t?["/c","start","",e]:[e];try{let i=xt(n,a,{stdio:"ignore",detached:!0});return i.on("error",()=>{}),i.unref(),!0}catch{return!1}}function Xt(e,t){e.registerTool("nodata_connect",{description:"Switch NoData's data tools on. Step 1: call with NO arguments to open the free signup in a browser (email + company, ~30s \u2014 also where your 12-word recovery phrase + email verification happen); it shows an ORG KEY once. Step 2: call this tool again with that key as `api_key` and it is saved on this machine, so every later start comes up ready with nothing to retype. Step 3: type /mcp (or restart) so the client loads the tools.",inputSchema:{api_key:c.string().optional().describe("The org key shown once at signup (ndp_\u2026 or sk_\u2026). Omit on the first call to open signup.")}},async({api_key:n})=>{let a=(n??"").trim();if(!a){let p=t+Gt;return{content:[{type:"text",text:(Zt(p)?`STEP 1 of 3 \u2014 opened the NoData signup in your browser:
|
|
82
|
+
`:`STEP 1 of 3 \u2014 open the NoData signup in your browser:
|
|
83
|
+
`)+p+`
|
|
84
|
+
|
|
85
|
+
There, about a minute:
|
|
86
|
+
- email + company (a personal email is fine for the sandbox)
|
|
87
|
+
- a 12-WORD RECOVERY PHRASE \u2014 save it; it is the only way back into your org
|
|
88
|
+
- your ORG KEY, shown ONCE \u2014 copy it
|
|
89
|
+
|
|
90
|
+
STEP 2 \u2014 come back and call this tool again with the key:
|
|
91
|
+
nodata_connect api_key: "<the key you copied>"
|
|
92
|
+
and I'll save it so you never paste it again. (Then STEP 3: type /mcp to activate.)`}]}}if(!/^(ndp_|sk_)/.test(a))return{content:[{type:"text",text:`That doesn't look like a NoData org key (it should start with ndp_ or sk_).
|
|
93
|
+
Copy the key shown once on the signup page and try again: nodata_connect api_key: "ndp_\u2026".`}]};let{publicKeyHex:i,privateKeyPkcs8B64:o}=Lt(),r=Bt(),s=$e()||"this device",l=await jt(t,a,r,s,i);if(l.ok){try{we({device:{org_id:l.orgId,handle:r,label:s,private_key_pkcs8_b64:o,public_key_hex:i},base_url:t})}catch(p){return{content:[{type:"text",text:"Enrolled the device, but couldn't save it locally: "+(p instanceof Error?p.message:String(p))}]}}return{content:[{type:"text",text:`STEP 2 done \u2014 this device is enrolled as "${s}" (${r}), and only its PRIVATE key is
|
|
94
|
+
saved at ~/.nodata/credentials.json (readable only by you). The org key you pasted was NOT
|
|
95
|
+
stored: every start now proves possession by signing a fresh challenge and gets a 15-minute token.
|
|
96
|
+
|
|
97
|
+
STEP 3 \u2014 type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.`}]}}let d=l.status===404||l.status===0;try{we({api_key:a,key_prefix:a.slice(0,12),base_url:t})}catch(p){return{content:[{type:"text",text:"Couldn't save locally: "+(p instanceof Error?p.message:String(p))+`
|
|
98
|
+
As a fallback, pass it on launch: claude mcp add nodata --env NODATA_API_KEY=<your key> -- npx @nodatachat/mcp`}]}}return{content:[{type:"text",text:(d?`STEP 2 done \u2014 saved your key on this machine (device attestation isn't available on this
|
|
99
|
+
server yet, so this is the key-at-rest path). `:`STEP 2 done \u2014 saved your key on this machine (device enrollment didn't succeed: ${l.error}). `)+`Every future start loads it automatically \u2014 no re-register, no re-paste.
|
|
100
|
+
|
|
101
|
+
STEP 3 \u2014 type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.`}]}})}function Jt(e){let t=ie(e);return e.error?t:t+`
|
|
102
|
+
|
|
103
|
+
GOVERN what it surfaced \u2014 same journey, inside this command:
|
|
104
|
+
\xB7 CHOOSE what an agent may read: nodata_register_table then nodata_grant (denied columns never decrypt)
|
|
105
|
+
\xB7 PROVE every access afterwards: nodata_evidence
|
|
106
|
+
|
|
107
|
+
Want a standalone deep scan any time? npx @nodatachat/nodata scan <folder> (this same engine).`}function qt(e){e.registerTool("nodata_scan_folder",{description:"SEE what's sensitive before you govern it. Scans a LOCAL folder for hardcoded secrets and unencrypted PII (checksum-validated \u2014 Israeli ID / Luhn credit-card / IBAN) with NoData's deep content scanner, reading PDFs and Office files too, ENTIRELY on this machine \u2014 nothing uploads, no credential needed. Returns the findings by file and severity plus the first fix, then points at nodata_register_table + nodata_grant to govern what it found. If the user asks to scan / audit / check a folder or repo for sensitive data, secrets, or exposure, call this.",inputSchema:{path:c.string().optional().describe("Folder to scan (absolute or relative). Default: the directory the server runs in.")}},async({path:t})=>{let n=(t??".").trim()||".",a=M(n);if(!K(a))return{content:[{type:"text",text:`No folder found at: ${a}
|
|
108
|
+
Pass an existing path, e.g. nodata_scan_folder path: "."`}]};try{let i=await ne({target_id:"mcp",target_label:St(a),target_path:a,log:re});return{content:[{type:"text",text:Jt(i)}]}}catch(i){return{content:[{type:"text",text:`Scan failed: ${i instanceof Error?i.message:String(i)}`}],isError:!0}}})}async function Qt(){let{apiKey:e,grantToken:t,baseUrl:n,credentialSource:a,device:i}=Pt(process.argv.slice(2)),o=t?"agent":e||i?"admin":"setup",r=new _t({name:o==="agent"?"NoData (agent-scoped)":o==="setup"?"NoData (setup)":"NoData",version:ae});i?Ae(r,n,Mt(n,i)):e&&Ae(r,n,e),t&&Kt(r,n,t),Yt(r,o),o!=="agent"&&qt(r),o==="setup"?(Xt(r,n),console.error(`nodata-mcp: no credential given \u2014 running in setup mode (data tools disabled).
|
|
109
|
+
Call nodata_connect (opens signup, enrolls this device), or pass --api-key / --grant-token. See --help.`)):i?console.error(`nodata-mcp: loaded device identity "${i.label}" (${i.handle}) \u2014 attest mode; tokens are minted per session.`):a==="stored"&&console.error("nodata-mcp: loaded saved credential from ~/.nodata/credentials.json (admin mode).");let s=new vt;await r.connect(s)}Qt().catch(e=>{console.error("Fatal error:",e),process.exit(1)});
|