@nodatachat/mcp 0.8.0 → 1.0.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/dist/index.js CHANGED
@@ -1,843 +1,108 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * NoData MCP Server
4
- *
5
- * Two modes — the credential you give it decides what it can do:
6
- *
7
- * ADMIN MODE (--api-key sk_live_...)
8
- * Manage governance + blind-relay from inside any MCP client:
9
- * nodata_register_table · nodata_grant · nodata_revoke
10
- * nodata_encrypt · nodata_decrypt · nodata_deliver · nodata_evidence
11
- *
12
- * AGENT MODE (--grant-token ndca-...)
13
- * This is the "permission management for language models" artifact.
14
- * You hand an AI agent a grant token; this server then exposes tools
15
- * scoped to exactly that grant:
16
- * nodata_decide — ask "may I reach this?" and get a decision + proof,
17
- * never any data (plan before you read).
18
- * nodata_retrieve — get prompt-ready context filtered to what you may
19
- * see, with an accounting of what was withheld.
20
- * nodata_read — read governed data (denied columns never decrypt).
21
- * nodata_use — invoke a secret-blind capability: the Capsule injects
22
- * the sealed credential server-side and returns only the
23
- * result; the agent never receives the secret.
24
- * Every decide/retrieve/read/invoke emits a receipt. Point any MCP client
25
- * (Claude, an agent runtime) at it: it physically cannot exceed its grant.
26
- *
27
- * Usage:
28
- * nodata-mcp --api-key sk_live_xxx
29
- * nodata-mcp --grant-token ndca-xxx [--base-url https://www.nodatacapsule.com]
30
- */
31
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
32
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33
- import { z } from "zod";
34
- import { homedir, hostname } from "node:os";
35
- import { join } from "node:path";
36
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
37
- import { spawn } from "node:child_process";
38
- import { generateKeyPairSync, createPrivateKey, sign, randomBytes } from "node:crypto";
39
- const VERSION = "0.8.0";
40
- // ---------------------------------------------------------------------------
41
- // Local credential store — "register once, autoload forever"
42
- // ---------------------------------------------------------------------------
43
- // This is the apply-side of "registration ≠ login": you register ONCE (in the
44
- // browser, where the recovery phrase + email verify happen), nodata_connect
45
- // saves an identity here, and every later start reads it back — no re-register,
46
- // no re-typing the launch command. The file is chmod 600.
47
- //
48
- // Two identity shapes live here:
49
- // · device — a DEVICE PRIVATE KEY (the preferred, hardened path). No reusable
50
- // bearer is stored; each start proves possession by signing a fresh nonce
51
- // (POST /api/v1/attest) and gets a 15-min token. A stolen file is a
52
- // revocable device key, not a replayable credential.
53
- // · api_key — the org key at rest (the pre-attest fallback). Still supported
54
- // for a backend that predates the attest endpoints; it is the standard
55
- // CLI-login posture (gh/npm/vercel store a token the same way).
56
- // nodata_connect writes `device` when enrollment succeeds and falls back to
57
- // `api_key` otherwise. See docs/ATTEST-FABRIC-IDENTITY-SPEC.md.
58
- const CRED_DIR = join(homedir(), ".nodata");
59
- const CRED_FILE = join(CRED_DIR, "credentials.json");
60
- function readStoredCredential() {
61
- try {
62
- if (!existsSync(CRED_FILE))
63
- return null;
64
- const parsed = JSON.parse(readFileSync(CRED_FILE, "utf8"));
65
- if (!parsed)
66
- return null;
67
- const hasDevice = parsed.device && typeof parsed.device.private_key_pkcs8_b64 === "string";
68
- const hasKey = typeof parsed.api_key === "string" && parsed.api_key;
69
- return hasDevice || hasKey ? parsed : null;
70
- }
71
- catch {
72
- // A corrupt or unreadable store must never crash startup — fall through to
73
- // setup mode, where nodata_connect can rewrite it.
74
- return null;
75
- }
76
- }
77
- function writeStoredCredential(cred) {
78
- mkdirSync(CRED_DIR, { recursive: true });
79
- const payload = JSON.stringify({ ...cred, saved_at: new Date().toISOString() }, null, 2);
80
- writeFileSync(CRED_FILE, payload, { mode: 0o600 });
81
- // writeFileSync's mode is ignored if the file already existed; enforce it.
82
- try {
83
- chmodSync(CRED_FILE, 0o600);
84
- }
85
- catch {
86
- /* platforms without POSIX perms (Windows) — best effort */
87
- }
88
- }
89
- const USAGE = `nodata-mcp v${VERSION} — NoData MCP server\n\n` +
90
- " ADMIN mode: nodata-mcp --api-key sk_live_...\n" +
91
- " AGENT mode: nodata-mcp --grant-token ndca-...\n" +
92
- " Options: --base-url <url> (default https://www.nodatacapsule.com)\n" +
93
- " --help · --version\n\n" +
94
- "Env vars: NODATA_API_KEY · NODATA_GRANT_TOKEN · NODATA_BASE_URL\n" +
95
- "Get a free API key: https://www.nodatacapsule.com/developers/get-started\n" +
96
- "Mint a grant token: POST /api/v1/governance/grant (or the nodata_grant tool).";
97
- function parseArgs(argv) {
98
- let apiKey = "";
99
- let grantToken = "";
100
- let baseUrl = "https://www.nodatacapsule.com";
101
- for (let i = 0; i < argv.length; i++) {
102
- if (argv[i] === "--api-key" && argv[i + 1]) {
103
- apiKey = argv[++i];
104
- }
105
- else if (argv[i] === "--grant-token" && argv[i + 1]) {
106
- grantToken = argv[++i];
107
- }
108
- else if (argv[i] === "--base-url" && argv[i + 1]) {
109
- baseUrl = argv[++i];
110
- }
111
- else if (argv[i] === "--help" || argv[i] === "-h") {
112
- console.log(USAGE);
113
- process.exit(0);
114
- }
115
- else if (argv[i] === "--version" || argv[i] === "-v") {
116
- console.log(VERSION);
117
- process.exit(0);
118
- }
119
- else if (argv[i].startsWith("--")) {
120
- // A typo like --apikey silently degrading to setup mode is worse than noise.
121
- console.error(`nodata-mcp: unknown option '${argv[i]}' (see --help)`);
122
- }
123
- }
124
- let credentialSource = apiKey || grantToken ? "flag" : "none";
125
- if (!apiKey && process.env.NODATA_API_KEY) {
126
- apiKey = process.env.NODATA_API_KEY;
127
- credentialSource = "env";
128
- }
129
- if (!grantToken && process.env.NODATA_GRANT_TOKEN) {
130
- grantToken = process.env.NODATA_GRANT_TOKEN;
131
- if (credentialSource === "none")
132
- credentialSource = "env";
133
- }
134
- if (process.env.NODATA_BASE_URL)
135
- baseUrl = process.env.NODATA_BASE_URL;
136
- // Last resort: what nodata_connect saved. An explicit flag or env always wins,
137
- // so this only fires on a bare `npx @nodatachat/mcp` — the "autoload forever"
138
- // path after a one-time connect. A stored DEVICE (attest) is preferred over a
139
- // stored api_key.
140
- let device;
141
- if (!apiKey && !grantToken) {
142
- const stored = readStoredCredential();
143
- if (stored) {
144
- // Honor a stored base_url only if nothing more explicit set one.
145
- if (!process.env.NODATA_BASE_URL && baseUrl === "https://www.nodatacapsule.com" && stored.base_url) {
146
- baseUrl = stored.base_url;
147
- }
148
- if (stored.device) {
149
- device = stored.device;
150
- credentialSource = "stored";
151
- }
152
- else if (stored.api_key) {
153
- apiKey = stored.api_key;
154
- credentialSource = "stored";
155
- }
156
- }
157
- }
158
- baseUrl = baseUrl.replace(/\/+$/, "");
159
- return { apiKey, grantToken, baseUrl, credentialSource, device };
160
- }
161
- // Without a deadline a dead network turns a tool call into an indefinite hang
162
- // inside the MCP client; 30s comfortably covers a slow decide/retrieve.
163
- const REQUEST_TIMEOUT_MS = 30_000;
164
- async function doFetch(url, init) {
165
- let res;
166
- try {
167
- res = await fetch(url, { ...init, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
168
- }
169
- catch (err) {
170
- const timedOut = err instanceof Error && err.name === "TimeoutError";
171
- return {
172
- ok: false,
173
- status: 0,
174
- body: {
175
- error: timedOut
176
- ? `Request timed out after ${REQUEST_TIMEOUT_MS / 1000}s: ${url}`
177
- : `Network error reaching ${url}: ${err instanceof Error ? err.message : String(err)}`,
178
- },
179
- };
180
- }
181
- let parsed;
182
- try {
183
- parsed = await res.json();
184
- }
185
- catch {
186
- parsed = { error: `Non-JSON response (HTTP ${res.status})` };
187
- }
188
- return { ok: res.ok, status: res.status, body: parsed };
189
- }
190
- async function resolveBearer(bearer) {
191
- if (typeof bearer === "string")
192
- return { token: bearer };
193
- try {
194
- return { token: await bearer() };
195
- }
196
- catch (err) {
197
- return {
198
- error: { ok: false, status: 0, body: { error: `Could not obtain an access token: ${err instanceof Error ? err.message : String(err)}` } },
199
- };
200
- }
201
- }
202
- async function apiPost(baseUrl, bearer, path, body) {
203
- const r = await resolveBearer(bearer);
204
- if ("error" in r)
205
- return r.error;
206
- return doFetch(`${baseUrl}${path}`, {
207
- method: "POST",
208
- headers: {
209
- "Content-Type": "application/json",
210
- Authorization: `Bearer ${r.token}`,
211
- },
212
- body: JSON.stringify(body),
213
- });
214
- }
215
- async function apiGet(baseUrl, bearer, path, params) {
216
- const r = await resolveBearer(bearer);
217
- if ("error" in r)
218
- return r.error;
219
- const url = new URL(`${baseUrl}${path}`);
220
- for (const [key, val] of Object.entries(params)) {
221
- if (val !== undefined && val !== "")
222
- url.searchParams.set(key, val);
223
- }
224
- return doFetch(url.toString(), {
225
- method: "GET",
226
- headers: { Authorization: `Bearer ${r.token}` },
227
- });
228
- }
229
- function formatResult(resp) {
230
- const text = JSON.stringify(resp.body, null, 2);
231
- if (!resp.ok) {
232
- return {
233
- content: [{ type: "text", text: `Error (HTTP ${resp.status}):\n${text}` }],
234
- isError: true,
235
- };
236
- }
237
- return { content: [{ type: "text", text }] };
238
- }
239
- /** Decode the handle out of an agent grant token (ndca-<jwt>). */
240
- function handleFromGrant(token) {
241
- try {
242
- const jwt = token.startsWith("ndca-") ? token.slice(5) : token;
243
- const payload = jwt.split(".")[1];
244
- if (!payload)
245
- return null;
246
- const json = JSON.parse(Buffer.from(payload.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8"));
247
- return json.handle ?? null;
248
- }
249
- catch {
250
- return null;
251
- }
252
- }
253
- // ---------------------------------------------------------------------------
254
- // Device attestation — the hardened credential path ("registration ≠ login")
255
- // ---------------------------------------------------------------------------
256
- // The client holds an Ed25519 PRIVATE key that never leaves this machine. Each
257
- // session it signs a fresh server nonce (POST /api/v1/attest) and gets a 15-min
258
- // token the governance surface already accepts — no reusable key at rest.
259
- // Reuses the shipped enrolled-agent challenge model server-side.
260
- const ATTEST_REFRESH_BUFFER_MS = 5 * 60_000; // refresh 5m before the 15m token expires
261
- function generateDeviceKeypair() {
262
- const { publicKey, privateKey } = generateKeyPairSync("ed25519");
263
- const spki = publicKey.export({ format: "der", type: "spki" });
264
- const pkcs8 = privateKey.export({ format: "der", type: "pkcs8" });
265
- return { publicKeyHex: spki.subarray(-32).toString("hex"), privateKeyPkcs8B64: pkcs8.toString("base64") };
266
- }
267
- function signChallenge(privateKeyPkcs8B64, challenge) {
268
- const key = createPrivateKey({ key: Buffer.from(privateKeyPkcs8B64, "base64"), format: "der", type: "pkcs8" });
269
- return sign(null, Buffer.from(challenge, "utf8"), key).toString("base64");
270
- }
271
- /** A stable, valid device handle derived from the machine (host + short random). */
272
- function defaultDeviceHandle() {
273
- const host = hostname().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 20) || "device";
274
- const head = /^[a-z]/.test(host) ? host : `d-${host}`;
275
- return `${head}-${randomBytes(3).toString("hex")}`.slice(0, 32);
276
- }
277
- /** Enroll a device public key under the org that owns `apiKey`; returns its org_id. */
278
- async function enrollDevice(baseUrl, apiKey, handle, label, publicKeyHex) {
279
- const resp = await apiPost(baseUrl, apiKey, "/api/v1/attest/devices", {
280
- handle,
281
- label,
282
- ed25519_pubkey_hex: publicKeyHex,
283
- });
284
- if (resp.ok) {
285
- const orgId = resp.body.org_id ?? "";
286
- return orgId ? { ok: true, orgId } : { ok: false, status: resp.status, error: "enroll response missing org_id" };
287
- }
288
- return { ok: false, status: resp.status, error: resp.body?.error ?? `HTTP ${resp.status}` };
289
- }
290
- /** One attest round: challenge → sign → token. Throws with a clear message on failure. */
291
- async function attestOnce(baseUrl, device) {
292
- // Both attest endpoints are public (the signature is the auth), so the bearer is unused.
293
- const ch = await apiPost(baseUrl, "", "/api/v1/attest/challenge", { org_id: device.org_id, handle: device.handle });
294
- if (!ch.ok)
295
- throw new Error(`challenge: ${ch.body?.error ?? `HTTP ${ch.status}`}`);
296
- const { challenge, nonce } = ch.body;
297
- if (!challenge || !nonce)
298
- throw new Error("challenge response malformed");
299
- const signature = signChallenge(device.private_key_pkcs8_b64, challenge);
300
- const at = await apiPost(baseUrl, "", "/api/v1/attest", { org_id: device.org_id, handle: device.handle, nonce, signature });
301
- if (!at.ok)
302
- throw new Error(`attest: ${at.body?.error ?? `HTTP ${at.status}`}`);
303
- const body = at.body;
304
- if (!body.allowed || !body.access_token)
305
- throw new Error(`attestation rejected: ${body.reason ?? "denied"}`);
306
- return {
307
- token: body.access_token,
308
- expiresAtMs: body.expires_at ? Date.parse(body.expires_at) : Date.now() + 15 * 60_000,
309
- };
310
- }
311
- /** A Bearer that attests lazily and refreshes before the token lapses. */
312
- function deviceTokenProvider(baseUrl, device) {
313
- let cached = null;
314
- return async () => {
315
- if (!cached || Date.now() >= cached.expiresAtMs - ATTEST_REFRESH_BUFFER_MS) {
316
- cached = await attestOnce(baseUrl, device);
317
- }
318
- return cached.token;
319
- };
320
- }
321
- // ---------------------------------------------------------------------------
322
- // Tool registration
323
- // ---------------------------------------------------------------------------
324
- function registerAdminTools(server, baseUrl, apiKey) {
325
- // ── Governance ──────────────────────────────────────────────────────
326
- server.registerTool("nodata_register_table", {
327
- 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.",
328
- inputSchema: {
329
- table_name: z.string().describe("Your SQL table name"),
330
- encrypted_columns: z.array(z.string()).describe("Columns stored as <col>_ct/_iv/_tag triplets"),
331
- plaintext_columns: z.array(z.string()).optional().describe("Plaintext metadata/filter columns"),
332
- classification_column: z.string().optional().describe("Plaintext column carrying the classification value"),
333
- },
334
- }, async ({ table_name, encrypted_columns, plaintext_columns, classification_column }) => {
335
- const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/tables", {
336
- table_name,
337
- encrypted_columns,
338
- plaintext_columns: plaintext_columns ?? [],
339
- classification_column,
340
- });
341
- return formatResult(resp);
342
- });
343
- server.registerTool("nodata_grant", {
344
- 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.",
345
- inputSchema: {
346
- agent: z.string().describe("Agent label (becomes its handle)"),
347
- table: z.string().optional().describe("Target table (default agent_demo_records)"),
348
- columns: z.array(z.string()).describe("Columns the agent may read"),
349
- where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe("Equality filter (plaintext cols only)"),
350
- classifications: z.array(z.string()).optional().describe("Allowed classification values; empty = no gating"),
351
- ttl_seconds: z.number().optional().describe("Token lifetime (default 30d, max 1y)"),
352
- },
353
- }, async ({ agent, table, columns, where, classifications, ttl_seconds }) => {
354
- const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/grant", {
355
- agent,
356
- table,
357
- columns,
358
- where,
359
- classifications,
360
- ttl_seconds,
361
- });
362
- return formatResult(resp);
363
- });
364
- server.registerTool("nodata_revoke", {
365
- 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.",
366
- inputSchema: {
367
- handle: z.string().optional().describe("Agent handle to revoke"),
368
- jti: z.string().optional().describe("Specific credential jti to revoke"),
369
- reason: z.string().optional(),
370
- },
371
- }, async ({ handle, jti, reason }) => {
372
- const resp = await apiPost(baseUrl, apiKey, "/api/v1/governance/revoke", { handle, jti, reason });
373
- return formatResult(resp);
374
- });
375
- // ── Blind relay (kept from v0.1) ────────────────────────────────────
376
- server.registerTool("nodata_encrypt", {
377
- 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.",
378
- inputSchema: {
379
- field: z.string(),
380
- value: z.string(),
381
- context: z.string().optional(),
382
- },
383
- }, async ({ field, value, context }) => {
384
- const body = { field, value };
385
- if (context)
386
- body.context = context;
387
- return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/encrypt", body));
388
- });
389
- server.registerTool("nodata_decrypt", {
390
- description: "Decrypt a Capsule ciphertext back to plaintext (aes256gcm:v1:iv:cipher:key format).",
391
- inputSchema: {
392
- ciphertext: z.string(),
393
- field: z.string().optional(),
394
- requester: z.string().optional(),
395
- },
396
- }, async ({ ciphertext, field, requester }) => {
397
- const body = { ciphertext };
398
- if (field)
399
- body.field = field;
400
- if (requester)
401
- body.requester = requester;
402
- return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/decrypt", body));
403
- });
404
- server.registerTool("nodata_deliver", {
405
- description: "Create a burn-after-read secure link for delivering sensitive content. Returns a view URL and OTP.",
406
- inputSchema: {
407
- content: z.string(),
408
- to: z.string().optional(),
409
- maxViews: z.number().optional(),
410
- ttl: z.string().optional(),
411
- burn: z.boolean().optional(),
412
- },
413
- }, async ({ content, to, maxViews, ttl, burn }) => {
414
- const body = { content };
415
- if (to)
416
- body.to = to;
417
- if (maxViews !== undefined)
418
- body.maxViews = maxViews;
419
- if (ttl)
420
- body.ttl = ttl;
421
- if (burn !== undefined)
422
- body.burn = burn;
423
- return formatResult(await apiPost(baseUrl, apiKey, "/api/v1/deliver", body));
424
- });
425
- server.registerTool("nodata_evidence", {
426
- description: "Retrieve the audit trail of governance/encryption events. Metadata only — never content, keys, or plaintext.",
427
- inputSchema: {
428
- from: z.string().optional(),
429
- to: z.string().optional(),
430
- action: z.string().optional(),
431
- limit: z.number().optional(),
432
- },
433
- }, async ({ from, to, action, limit }) => {
434
- const params = {
435
- from,
436
- to,
437
- action,
438
- limit: limit !== undefined ? String(limit) : undefined,
439
- };
440
- return formatResult(await apiGet(baseUrl, apiKey, "/api/v1/evidence", params));
441
- });
442
- }
443
- function registerAgentTool(server, baseUrl, grantToken) {
444
- const handle = handleFromGrant(grantToken);
445
- if (!handle) {
446
- // decide/retrieve are bearer-only and still work; read/use need the handle.
447
- // Say so at startup instead of surprising the agent mid-conversation.
448
- console.error("nodata-mcp: warning — could not derive an agent handle from the grant token " +
449
- "(expected ndca-<jwt> with a 'handle' claim). nodata_decide/nodata_retrieve will work; " +
450
- "nodata_read/nodata_use will fail until a valid grant token is provided.");
451
- }
452
- // ── Access Compute · the "may I?" oracle (P0) ───────────────────────
453
- // Returns ONLY a decision + a signed proof, never any content. A planner
454
- // calls this to learn what it can reach BEFORE moving data: allow, degrade
455
- // (the reachable subset), or deny — with an access-distance cost. Bearer-only
456
- // endpoint (no handle in the path).
457
- server.registerTool("nodata_decide", {
458
- 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.",
459
- inputSchema: {
460
- table: z.string().describe("Table to check (must be in your grant)"),
461
- columns: z.array(z.string()).describe("Columns you want to reach"),
462
- where: z
463
- .record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
464
- .optional()
465
- .describe("Equality filter (readable columns only)"),
466
- },
467
- }, async ({ table, columns, where }) => {
468
- const resp = await apiPost(baseUrl, grantToken, "/api/access/decide", {
469
- resource: { table, columns, where },
470
- intent: "read",
471
- });
472
- return formatResult(resp);
473
- });
474
- // ── Authorized Context · access-aware RAG (P1) ──────────────────────
475
- // Runs the same decision, then returns prompt-ready context filtered to
476
- // exactly the allowed columns (denied columns are never decrypted), plus an
477
- // accounting of what was withheld and a signed proof.
478
- server.registerTool("nodata_retrieve", {
479
- 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.",
480
- inputSchema: {
481
- table: z.string().describe("Table to retrieve from (must be in your grant)"),
482
- columns: z.array(z.string()).describe("Columns to include in context"),
483
- where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
484
- top_k: z.number().optional().describe("Max rows (default 50, max 200)"),
485
- },
486
- }, async ({ table, columns, where, top_k }) => {
487
- const resp = await apiPost(baseUrl, grantToken, "/api/access/retrieve", {
488
- resource: { table, columns, where },
489
- top_k,
490
- });
491
- return formatResult(resp);
492
- });
493
- server.registerTool("nodata_read", {
494
- 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.",
495
- inputSchema: {
496
- table: z.string().describe("Table to read (must be in your grant)"),
497
- columns: z.array(z.string()).describe("Columns to read (must be in your grant)"),
498
- where: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional(),
499
- limit: z.number().optional().describe("Max rows (default 50, max 200)"),
500
- },
501
- }, async ({ table, columns, where, limit }) => {
502
- if (!handle) {
503
- return {
504
- content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
505
- isError: true,
506
- };
507
- }
508
- const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/read`, {
509
- table,
510
- columns,
511
- where,
512
- limit,
513
- });
514
- return formatResult(resp);
515
- });
516
- // ── Secret-blind action (AI Access Plane) ───────────────────────────
517
- // The headline of the plane: INVOKE a pre-registered capability. The
518
- // Capsule injects the sealed secret server-side and returns ONLY the
519
- // result — you (the agent / model) never receive the key. Deny-by-default:
520
- // you can invoke only capabilities your grant lists. Every invoke emits a
521
- // signed receipt proving you acted and never held the secret.
522
- server.registerTool("nodata_use", {
523
- 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.",
524
- inputSchema: {
525
- capability: z.string().describe("Capability name to invoke (must be in your grant, e.g. 'deploy-prod')"),
526
- params: z.record(z.string(), z.unknown()).optional().describe("Allowlisted params; unknown keys are dropped server-side"),
527
- resource_key: z.string().optional().describe("Optional folder/resource the action targets (enforced against corpus scope)"),
528
- },
529
- }, async ({ capability, params, resource_key }) => {
530
- if (!handle) {
531
- return {
532
- content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
533
- isError: true,
534
- };
535
- }
536
- const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/use`, {
537
- capability,
538
- params,
539
- resource_key,
540
- });
541
- return formatResult(resp);
542
- });
543
- // ── Code plane · fix code without exposure, over the Fabric ─────────
544
- // The agent drives the blind advisor on a code capsule. It sees only a
545
- // content-blind projection (or, on an in-boundary model, a scoped slice that
546
- // never egresses); the human sees only the changed hunks. Every turn returns
547
- // an exposure proof + a receipt. Authorised by the capsule owner listing this
548
- // agent's kid in scope.fabric_developers; deny-by-default.
549
- server.registerTool("nodata_code_advise", {
550
- 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.",
551
- inputSchema: {
552
- capsule_id: z.string().describe("The code capsule to advise on (you must be granted on it)"),
553
- question: z.string().optional().describe("What to fix / improve, in natural language"),
554
- note: z.string().optional().describe("Extra context for the advisor"),
555
- error: z
556
- .object({
557
- message: z.string().optional(),
558
- stack: z.string().optional(),
559
- kind: z.string().optional(),
560
- })
561
- .optional()
562
- .describe("An error the code produced, to guide the fix"),
563
- steer: z.string().optional().describe("A steer on a refine turn (e.g. 'no, round half-up')"),
564
- prior_diff: z.string().optional().describe("The previously proposed diff, to refine"),
565
- },
566
- }, async ({ capsule_id, question, note, error, steer, prior_diff }) => {
567
- if (!handle) {
568
- return {
569
- content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
570
- isError: true,
571
- };
572
- }
573
- const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/advise`, { question, note, error, steer, prior_diff });
574
- return formatResult(resp);
575
- });
576
- server.registerTool("nodata_code_patch", {
577
- 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.",
578
- inputSchema: {
579
- capsule_id: z.string().describe("The code capsule to patch (you must be granted on it)"),
580
- summary: z.string().optional().describe("Short human-readable summary of the fix"),
581
- diff: z.string().optional().describe("A raw unified diff (advanced)"),
582
- replace: z
583
- .object({ path: z.string(), content: z.string() })
584
- .optional()
585
- .describe("Whole-file replacement; the diff is synthesized server-side against sealed source"),
586
- },
587
- }, async ({ capsule_id, summary, diff, replace }) => {
588
- if (!handle) {
589
- return {
590
- content: [{ type: "text", text: "Error: could not derive agent handle from grant token." }],
591
- isError: true,
592
- };
593
- }
594
- const resp = await apiPost(baseUrl, grantToken, `/api/agents/${encodeURIComponent(handle)}/code-capsule/${encodeURIComponent(capsule_id)}/patch`, { summary, diff, replace });
595
- return formatResult(resp);
596
- });
597
- }
598
- const START_ACHIEVEMENT = "WHAT NODATA DOES — in one line:\n" +
599
- " Give any agent real power over your data, without the power to leak it.\n" +
600
- " (Possession is not permission: it can hold the key and still not read what you didn't allow.)\n";
601
- // Setup mode: NOT connected yet. The one job here is to switch the tools on, so
602
- // this is a numbered do-this-exactly list — three steps, not a catalogue. The
603
- // last line answers the two questions everyone asks: "how do I activate?" (/mcp)
604
- // and "what about next time?" (nothing).
605
- const SETUP_STEPS = "TURN THE DATA TOOLS ON — 3 steps, about a minute, once:\n\n" +
606
- " 1. GET A KEY call nodata_connect (no arguments)\n" +
607
- " -> opens the signup in your browser: email + company (~30s), a 12-word\n" +
608
- " recovery phrase to keep, and an ORG KEY shown ONCE. Copy that key.\n\n" +
609
- " 2. SAVE IT call nodata_connect again, this time with\n" +
610
- " api_key: \"<the key you copied>\"\n" +
611
- " -> saved on this machine; you never paste it again.\n\n" +
612
- " 3. ACTIVATE type /mcp (or restart Claude Code) so the tools load.\n\n" +
613
- "COMING BACK LATER? Nothing to redo — it starts ready. If the tools look missing, just /mcp.\n";
614
- // Admin mode: connected. Frame the whole product as the developer's real journey
615
- // — SEE -> CHOOSE -> PROVE — each step a plain-language ask mapped to a tool.
616
- 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" +
617
- " 1. SEE what's sensitive open https://www.nodatacapsule.com/ai-folder\n" +
618
- " -> pick a folder; it shows what an AI could read. Nothing uploads.\n\n" +
619
- " 2. CHOOSE what to release say \"let <agent> read name + plan on <table>, hide the rest\"\n" +
620
- " -> I run nodata_register_table + nodata_grant; the agent gets a token.\n\n" +
621
- " 3. PROVE & control the agent now reads ONLY those columns — the rest never decrypt.\n" +
622
- " \"what did it see?\" -> nodata_evidence · \"cut it off\" -> nodata_revoke\n\n" +
623
- "Also: a secret even NoData can't read -> nodata_encrypt, then hand it over once -> nodata_deliver.\n";
624
- // The agent-scoped reader IS the governed agent: it holds only decide/retrieve/read/use,
625
- // so its steps name those verbs (pointing it at admin tools it lacks would dead-end it).
626
- const START_OUTCOMES_AGENT = "WHAT YOU CAN DO — scoped to your grant; you reach only what it allows, the rest never decrypts:\n\n" +
627
- " 1. PLAN nodata_decide \"may I reach X?\" -> allow / partial / deny + a signed proof. No data moves.\n" +
628
- " 2. PULL nodata_retrieve prompt-ready context, allowed fields only, with a receipt.\n" +
629
- " (nodata_read for raw governed rows.)\n" +
630
- " 3. ACT nodata_use run a capability with a secret injected server-side — you never see it.\n";
631
- const START_UNSTUCK = "STUCK? There's always a next step:\n" +
632
- " - Don't see the nodata_* tools? They load when the client starts — type /mcp or restart.\n" +
633
- " (Expected, not an error.)\n" +
634
- " - No MCP at all? The same governance runs from one curl, or the guided page:\n" +
635
- " https://www.nodatacapsule.com/developers/get-started\n";
636
- function startHeader(mode) {
637
- if (mode === "setup") {
638
- return "You're connected to NoData — but there's NO key yet, so the data tools are still off.\n\n";
639
- }
640
- if (mode === "agent") {
641
- return "You're connected to NoData, scoped to one grant token.\n\n";
642
- }
643
- return "You're connected to NoData (admin mode). You don't memorize commands — just ask in plain words.\n\n";
644
- }
645
- function getStartedText(mode) {
646
- // Setup leads with the 3 connect steps (the only thing that matters when off);
647
- // admin/agent lead with the journey they can act on right now.
648
- if (mode === "setup") {
649
- return startHeader(mode) + START_ACHIEVEMENT + "\n" + SETUP_STEPS + "\n" + START_UNSTUCK;
650
- }
651
- const outcomes = mode === "agent" ? START_OUTCOMES_AGENT : START_OUTCOMES_ADMIN;
652
- return startHeader(mode) + START_ACHIEVEMENT + "\n" + outcomes + "\n" + START_UNSTUCK;
653
- }
654
- function registerGetStarted(server, mode) {
655
- server.registerTool("nodata_get_started", {
656
- description: "Call this FIRST (no arguments). NoData in one line: what you can achieve — an agent that " +
657
- "cannot leak, a look at what is already exposed, a secret we cannot read — with the single " +
658
- "next step for each, and how to recover if the tools or MCP seem stuck. If the user asks what " +
659
- "NoData can do, how to start, or why the tools are missing, call this and relay it.",
660
- inputSchema: {},
661
- }, async () => ({ content: [{ type: "text", text: getStartedText(mode) }] }));
662
- }
663
- // ---------------------------------------------------------------------------
664
- // nodata_connect — setup-mode "get switched on without re-typing the command"
665
- // ---------------------------------------------------------------------------
666
- // "Registration ≠ login." Call once with no args → open the real signup in the
667
- // browser (email + company, the 12-word recovery, the email verify — the parts
668
- // a headless mint would skip). It shows the org key once; call again with that
669
- // key → it is saved to ~/.nodata and every later start autoloads it. First run:
670
- // register + one paste. Every run after: nothing. The only residual is one /mcp,
671
- // because MCP clients read their tool list at startup (a client fact, not ours).
672
- const REGISTER_URL_PATH = "/capsule-api/register";
673
- function openInBrowser(url) {
674
- const isWin = process.platform === "win32";
675
- const cmd = process.platform === "darwin" ? "open" : isWin ? "cmd" : "xdg-open";
676
- const args = isWin ? ["/c", "start", "", url] : [url];
677
- try {
678
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
679
- // A missing opener (headless box, no xdg-open) must never surface as a tool
680
- // error — the URL is always printed too, so the flow proceeds regardless.
681
- child.on("error", () => { });
682
- child.unref();
683
- return true;
684
- }
685
- catch {
686
- return false;
687
- }
688
- }
689
- function registerConnectTool(server, baseUrl) {
690
- server.registerTool("nodata_connect", {
691
- description: "Switch NoData's data tools on. Step 1: call with NO arguments to open the free signup in a " +
692
- "browser (email + company, ~30s — also where your 12-word recovery phrase + email verification " +
693
- "happen); it shows an ORG KEY once. Step 2: call this tool again with that key as `api_key` and " +
694
- "it is saved on this machine, so every later start comes up ready with nothing to retype. " +
695
- "Step 3: type /mcp (or restart) so the client loads the tools.",
696
- inputSchema: {
697
- api_key: z
698
- .string()
699
- .optional()
700
- .describe("The org key shown once at signup (ndp_… or sk_…). Omit on the first call to open signup."),
701
- },
702
- }, async ({ api_key }) => {
703
- const key = (api_key ?? "").trim();
704
- // First call (no key): open the real signup and tell them to come back with it.
705
- if (!key) {
706
- const url = baseUrl + REGISTER_URL_PATH;
707
- const opened = openInBrowser(url);
708
- return {
709
- content: [
710
- {
711
- type: "text",
712
- 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 ") +
713
- url +
714
- "\n\n" +
715
- "There, about a minute:\n" +
716
- " - email + company (a personal email is fine for the sandbox)\n" +
717
- " - a 12-WORD RECOVERY PHRASE — save it; it is the only way back into your org\n" +
718
- " - your ORG KEY, shown ONCE — copy it\n\n" +
719
- "STEP 2 — come back and call this tool again with the key:\n" +
720
- ' nodata_connect api_key: "<the key you copied>"\n' +
721
- "and I'll save it so you never paste it again. (Then STEP 3: type /mcp to activate.)",
722
- },
723
- ],
724
- };
725
- }
726
- // Shape check — don't persist something that clearly isn't a NoData key.
727
- if (!/^(ndp_|sk_)/.test(key)) {
728
- return {
729
- content: [
730
- {
731
- type: "text",
732
- text: "That doesn't look like a NoData org key (it should start with ndp_ or sk_).\n" +
733
- 'Copy the key shown once on the signup page and try again: nodata_connect api_key: "ndp_…".',
734
- },
735
- ],
736
- };
737
- }
738
- // Preferred path: enroll a DEVICE KEY so no reusable key is stored. Generate
739
- // a keypair, register its PUBLIC half under the org (authenticated by the key
740
- // the user just pasted), keep only the PRIVATE half. Then every start attests.
741
- const { publicKeyHex, privateKeyPkcs8B64 } = generateDeviceKeypair();
742
- const handle = defaultDeviceHandle();
743
- const label = hostname() || "this device";
744
- const enrolled = await enrollDevice(baseUrl, key, handle, label, publicKeyHex);
745
- if (enrolled.ok) {
746
- try {
747
- writeStoredCredential({
748
- device: {
749
- org_id: enrolled.orgId,
750
- handle,
751
- label,
752
- private_key_pkcs8_b64: privateKeyPkcs8B64,
753
- public_key_hex: publicKeyHex,
754
- },
755
- base_url: baseUrl,
756
- });
757
- }
758
- catch (e) {
759
- return {
760
- content: [{ type: "text", text: "Enrolled the device, but couldn't save it locally: " + (e instanceof Error ? e.message : String(e)) }],
761
- };
762
- }
763
- return {
764
- content: [
765
- {
766
- type: "text",
767
- text: `STEP 2 done — this device is enrolled as "${label}" (${handle}), and only its PRIVATE key is\n` +
768
- "saved at ~/.nodata/credentials.json (readable only by you). The org key you pasted was NOT\n" +
769
- "stored: every start now proves possession by signing a fresh challenge and gets a 15-minute token.\n\n" +
770
- "STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
771
- },
772
- ],
773
- };
774
- }
775
- // Fallback: a backend that predates the attest endpoints. Store the key at
776
- // rest (the Part-2 path) so onboarding still works end to end.
777
- const attestUnavailable = enrolled.status === 404 || enrolled.status === 0;
778
- try {
779
- writeStoredCredential({ api_key: key, key_prefix: key.slice(0, 12), base_url: baseUrl });
780
- }
781
- catch (e) {
782
- return {
783
- content: [
784
- {
785
- type: "text",
786
- text: "Couldn't save locally: " +
787
- (e instanceof Error ? e.message : String(e)) +
788
- "\nAs a fallback, pass it on launch: claude mcp add nodata --env NODATA_API_KEY=<your key> -- npx @nodatachat/mcp",
789
- },
790
- ],
791
- };
792
- }
793
- return {
794
- content: [
795
- {
796
- type: "text",
797
- text: (attestUnavailable
798
- ? "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). "
799
- : `STEP 2 done — saved your key on this machine (device enrollment didn't succeed: ${enrolled.error}). `) +
800
- "Every future start loads it automatically — no re-register, no re-paste.\n\n" +
801
- "STEP 3 — type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.",
802
- },
803
- ],
804
- };
805
- });
806
- }
807
- // ---------------------------------------------------------------------------
808
- // Server setup
809
- // ---------------------------------------------------------------------------
810
- async function main() {
811
- const { apiKey, grantToken, baseUrl, credentialSource, device } = parseArgs(process.argv.slice(2));
812
- const mode = grantToken ? "agent" : apiKey || device ? "admin" : "setup";
813
- const server = new McpServer({
814
- name: mode === "agent" ? "NoData (agent-scoped)" : mode === "setup" ? "NoData (setup)" : "NoData",
815
- version: VERSION,
816
- });
817
- // A stored device (attest) is preferred: the admin tools get a token PROVIDER
818
- // that signs a fresh challenge per session instead of a static key.
819
- if (device)
820
- registerAdminTools(server, baseUrl, deviceTokenProvider(baseUrl, device));
821
- else if (apiKey)
822
- registerAdminTools(server, baseUrl, apiKey);
823
- if (grantToken)
824
- registerAgentTool(server, baseUrl, grantToken);
825
- registerGetStarted(server, mode);
826
- if (mode === "setup") {
827
- registerConnectTool(server, baseUrl);
828
- console.error("nodata-mcp: no credential given — running in setup mode (data tools disabled).\n" +
829
- "Call nodata_connect (opens signup, enrolls this device), or pass --api-key / --grant-token. See --help.");
830
- }
831
- else if (device) {
832
- console.error(`nodata-mcp: loaded device identity "${device.label}" (${device.handle}) — attest mode; tokens are minted per session.`);
833
- }
834
- else if (credentialSource === "stored") {
835
- console.error("nodata-mcp: loaded saved credential from ~/.nodata/credentials.json (admin mode).");
836
- }
837
- const transport = new StdioServerTransport();
838
- await server.connect(transport);
839
- }
840
- main().catch((err) => {
841
- console.error("Fatal error:", err);
842
- process.exit(1);
843
- });
2
+ /*! @nodatachat/mcp 1.0.0 | Copyright 2026 Capsule Ltd. | FSL-1.1-ALv2 | see LICENSE.md */
3
+ import{McpServer as gt}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as mt}from"@modelcontextprotocol/sdk/server/stdio.js";import{z as c}from"zod";import{readdirSync as _e,readFileSync as ve,statSync as J}from"node:fs";import{join as ct}from"node:path";var ie=[{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"}],ae=/node_modules\/|\.next\/|\.test\.|\.spec\.|__tests__\/|\.env\.example$|\.nodata-proof\.json$|\.git\//,Oe=[{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 De(e,t){let n=o=>o.replace(/^[^0-9]*/,"").split(".").map(a=>parseInt(a,10)||0),i=n(e),r=n(t);for(let o=0;o<3;o++){if((i[o]||0)<(r[o]||0))return!0;if((i[o]||0)>(r[o]||0))return!1}return!1}function oe(e){let t=[],n;try{n=JSON.parse(e)}catch{return t}let i={...n.dependencies??{},...n.devDependencies??{}};for(let[r,o]of Object.entries(i)){let a=(o||"").replace(/^[\^~>=<\s]+/,""),s=Oe.find(l=>l.name===r&&De(a,l.below));s&&t.push({name:r,version:a,vuln:s})}return t}var Le=[{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"}],je=/^(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 W(e){if(!e)return null;let t=e.trim();if(je.test(t)||/^name$/i.test(t))return null;for(let n of[...Fe,...Le])if(n.pattern.test(t))return{type:n.type,label:n.label};return null}var G={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 B(e){let t=String(e??"").replace(/\D/g,"");if(t.length===0||t.length>9)return!1;let n=t.padStart(9,"0"),i=0;for(let r=0;r<9;r++){let o=parseInt(n[r]??"0",10)*(r%2+1);o>9&&(o-=9),i+=o}return i%10===0&&parseInt(n,10)>0}var F=/(?:\+972[\s-]?|0)(?:5[0-9]|[2-4]|[89])(?:[\s-]?\d){7}/g;var j=/\bIL\d{2}\s?(?:\d{4}\s?){4}\d{3}\b/g;function Me(e){let t=String(e??"").replace(/\D/g,"");if(t.length<12||t.length>19)return!1;let n=0,i=!1;for(let r=t.length-1;r>=0;r-=1){let o=parseInt(t[r]??"0",10);i&&(o*=2,o>9&&(o-=9)),n+=o,i=!i}return n%10===0}function Be(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),i=0;for(let r of n){let o=r>="A"&&r<="Z"?String(r.charCodeAt(0)-55):r;for(let a of o)i=(i*10+Number(a))%97}return i===1}var H="(?<![\\d.,])",V="(?![\\d.,])",Ue=[{subkind:"israeli_id",severity:"high",pattern:new RegExp(`${H}\\d{9}${V}`,"g"),validate:B,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(`${H}(?:\\d[ -]?){12,18}\\d${V}`,"g"),validate:Me,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:Be,hint:"Bank account in IBAN form (mod-97 verified) in plaintext."},{subkind:"israeli_phone",severity:"medium",pattern:new RegExp(`${H}(?:\\+972[- ]?|0)(?:5\\d|[2-489])[- ]?\\d{3}[- ]?\\d{4}${V}`,"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 se(e){let t=[];for(let n of Ue){n.pattern.lastIndex=0;let i=new Set,r,o=0;for(;(r=n.pattern.exec(e))!==null&&(o+=1,!(o>2e5));){let a=r[0];if(n.validate(a)&&i.add(a.replace(/[\s-]/g,"")),i.size>=1e5)break}i.size>0&&t.push({subkind:n.subkind,severity:n.severity,hint:n.hint,count:i.size})}return t}function Z(e,t){let n=[],i="",r=!1;for(let o=0;o<e.length;o++){let a=e[o];r?a==='"'&&e[o+1]==='"'?(i+='"',o++):a==='"'?r=!1:i+=a:a==='"'?r=!0:a===t?(n.push(i),i=""):i+=a}return n.push(i),n}import{inflateRawSync as Je}from"node:zlib";import{inflateSync as ze}from"node:zlib";var Y=new Set([".pdf"]),ce=8*1024*1024;function Ke(e){let t=e.toString("latin1"),n=new Map,i=/(\d+)\s+(\d+)\s+obj\b/g,r;for(;(r=i.exec(t))!==null;){let o=Number(r[1]),a=i.lastIndex,s=t.indexOf("endobj",a);if(s<0)continue;let l=t.indexOf("stream",a),d,p=null;if(l>=0&&l<s){d=t.slice(a,l);let f=l+6;t[f]==="\r"&&(f+=1),t[f]===`
4
+ `&&(f+=1);let S=t.indexOf("endstream",f);S>f&&(p=e.subarray(f,S))}else d=t.slice(a,s);n.set(o,{num:o,dict:d,stream:p})}return n}function He(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 i=[],r=0,o=0;for(let a of t){if(a==="z"&&o===0){i.push(0,0,0,0);continue}let s=a.charCodeAt(0)-33;if(s<0||s>84)return null;r=r*85+s,o+=1,o===5&&(i.push(r>>>24&255,r>>>16&255,r>>>8&255,r&255),r=0,o=0)}if(o>0){for(let s=o;s<5;s+=1)r=r*85+84;let a=[r>>>24&255,r>>>16&255,r>>>8&255,r&255];for(let s=0;s<o-1;s+=1)i.push(a[s]??0)}return Buffer.from(i)}function Ve(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 We(e){let t=/\/Filter\s*(\[[^\]]*\]|\/\w+)/.exec(e);return!t||!t[1]?[]:[...t[1].matchAll(/\/(\w+)/g)].map(n=>n[1]??"")}function de(e){if(!e.stream)return null;let t=e.stream;for(let n of We(e.dict)){let i=null;if(n==="FlateDecode")try{i=ze(t)}catch{return null}else if(n==="ASCII85Decode")i=He(t);else if(n==="ASCIIHexDecode")i=Ve(t);else return null;if(i===null)return null;t=i}return t.toString("latin1")}function Ge(e){let t=new Map,n=1,i=/begincodespacerange([\s\S]*?)endcodespacerange/.exec(e);if(i&&i[1]){let l=/<([0-9A-Fa-f]+)>/.exec(i[1]);l&&l[1]&&l[1].length>=4&&(n=2)}let r=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,a;for(;(a=o.exec(e))!==null;){let l=/<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]*)>/g,d;for(;(d=l.exec(a[1]??""))!==null;)t.set(parseInt(d[1]??"0",16),r(d[2]??""))}let s=/beginbfrange([\s\S]*?)endbfrange/g;for(;(a=s.exec(e))!==null;){let l=a[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),S=parseInt(p[2]??"0",16),P=parseInt((p[3]??"0").slice(0,4)||"0",16);if(!(S-f>65535))for(let R=f;R<=S;R+=1)t.set(R,String.fromCharCode(P+(R-f)))}}return{map:t,bytes:n}}function Ze(e){let t=new Map,n=new Map,i=[];for(let r of e.values()){let o=/\/Font\s*<<([\s\S]*?)>>/.exec(r.dict);o?.[1]&&i.push(o[1]);let a=/\/Font\s+(\d+)\s+\d+\s+R/.exec(r.dict);if(a){let s=e.get(Number(a[1]));s&&i.push(s.dict)}}for(let r of i){let o=/\/([^\s/<>[\]()]+)\s+(\d+)\s+\d+\s+R/g,a;for(;(a=o.exec(r))!==null;)n.set(a[1]??"",Number(a[2]))}for(let[r,o]of n){let a=e.get(o);if(!a)continue;let s=/\/ToUnicode\s+(\d+)\s+\d+\s+R/.exec(a.dict);if(!s)continue;let l=e.get(Number(s[1]));if(!l)continue;let d=de(l);d&&t.set(r,Ge(d))}return t}function pe(e){let t=[];for(let n=0;n<e.length;n+=1){let i=e[n];if(i!=="\\"){t.push(i.charCodeAt(0)&255);continue}let r=e[n+1];if(r===void 0)break;if(r>="0"&&r<="7"){let a="",s=n+1;for(;s<e.length&&a.length<3&&e[s]>="0"&&e[s]<="7";)a+=e[s],s+=1;t.push(parseInt(a,8)&255),n=s-1;continue}let o={n:10,r:13,t:9,b:8,f:12};t.push(o[r]??r.charCodeAt(0)&255),n+=1}return t}function ue(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 le(e,t){let n=ue(e);if(n!==null)return n;if(!t)return e.map(r=>String.fromCharCode(r)).join("");let i="";if(t.bytes===2)for(let r=0;r+1<e.length;r+=2){let o=(e[r]??0)<<8|(e[r+1]??0);i+=t.map.get(o)??""}else for(let r of e)i+=t.map.get(r)??String.fromCharCode(r);return i}function Ye(e,t){let n="",i,r=/\/([^\s/<>[\]()]+)\s+[\d.]+\s+Tf|\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>|TJ|Tj|'|"/g,o,a="";for(;(o=r.exec(e))!==null;){let s=o[0];if(s.endsWith("Tf")){i=t.get(o[1]??"");continue}if(s==="TJ"||s==="Tj"||s==="'"||s==='"'){a&&(n+=a+`
5
+ `,a="");continue}if(s.startsWith("("))a+=le(pe(s.slice(1,-1)),i);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));a+=le(d,i)}}return a&&(n+=a+`
6
+ `),n}function Xe(e,t){let n=e.toString("latin1"),i=/\/Info\s+(\d+)\s+\d+\s+R/.exec(n),r=i?t.get(Number(i[1]))?.dict:void 0;if(!r)return"";let o=[],a=/\/(Author|Title|Subject|Keywords|Creator|Producer)\s*(\((?:\\.|[^\\)])*\)|<[0-9A-Fa-f\s]*>)/g,s;for(;(s=a.exec(r))!==null;){let l=s[2]??"";if(l.startsWith("(")){let d=pe(l.slice(1,-1));o.push(ue(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 fe(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=Ke(e);if(n.size===0)return null;let i=Ze(n),r=[];r.push(Xe(e,n));let o=0,a=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=de(l);if(d===null||!/(TJ|Tj)\b/.test(d))continue;let p=Ye(d,i);if(p.trim().length!==0){if(a+=1,o+p.length>ce){r.push(p.slice(0,Math.max(0,ce-o))),s=!0;break}r.push(p),o+=p.length}}return a===0?null:{text:r.join(`
8
+ `),truncated:s,streams:a}}var he=new Set([".xlsx",".xlsm",".docx",".pptx"]),X=8*1024*1024,qe=101010256,Qe=33639248,et=67324752;function tt(e){let t=Math.min(e.length,67584),n=-1;for(let a=e.length-22;a>=e.length-t&&a>=0;a-=1)if(e.readUInt32LE(a)===qe){n=a;break}if(n<0)return null;let i=e.readUInt16LE(n+10),r=e.readUInt32LE(n+16);if(r>=e.length)return null;let o=[];for(let a=0;a<i&&!(r+46>e.length||e.readUInt32LE(r)!==Qe);a+=1){let s=e.readUInt16LE(r+10),l=e.readUInt32LE(r+20),d=e.readUInt32LE(r+24),p=e.readUInt16LE(r+28),f=e.readUInt16LE(r+30),S=e.readUInt16LE(r+32),P=e.readUInt32LE(r+42),R=e.subarray(r+46,r+46+p).toString("utf8");o.push({name:R,method:s,compSize:l,uncompSize:d,localHeaderOffset:P}),r+=46+p+f+S}return o}function nt(e,t){let n=t.localHeaderOffset;if(n+30>e.length||e.readUInt32LE(n)!==et)return null;let i=e.readUInt16LE(n+26),r=e.readUInt16LE(n+28),o=n+30+i+r,a=o+t.compSize;if(a>e.length)return null;let s=e.subarray(o,a);try{if(t.method===0)return s.toString("utf8");if(t.method===8)return Je(s).toString("utf8")}catch{return null}return null}var rt={"&amp;":"&","&lt;":"<","&gt;":">","&quot;":'"',"&apos;":"'"};function ge(e){return e.replace(/&(amp|lt|gt|quot|apos);/g,t=>rt[t]??t).replace(/&#(\d+);/g,(t,n)=>{let i=Number(n);return i>0&&i<1114112?String.fromCodePoint(i):""}).replace(/&#x([0-9a-fA-F]+);/g,(t,n)=>{let i=parseInt(n,16);return i>0&&i<1114112?String.fromCodePoint(i):""})}function it(e){return ge(e.replace(/<[^>]*>/g,`
9
+ `)).replace(/\n{2,}/g,`
10
+ `)}function at(e,t,n){let i=new RegExp(`<${t}\\b[^>]*\\b${n}="([^"]*)"`,"g"),r=[],o;for(;(o=i.exec(e))!==null;)o[1]&&r.push(ge(o[1]));return r}function ot(e,t){let n=[];for(let i of t)(i==="docProps/core.xml"||i==="docProps/app.xml")&&n.push(i);if(e===".xlsx"||e===".xlsm")for(let i of t)(i==="xl/sharedStrings.xml"||i==="xl/workbook.xml"||/^xl\/worksheets\/sheet\d+\.xml$/.test(i)||/^xl\/comments\d*\.xml$/.test(i))&&n.push(i);else if(e===".docx")for(let i of t)(i==="word/document.xml"||/^word\/(header|footer|footnotes|endnotes|comments)\d*\.xml$/.test(i))&&n.push(i);else if(e===".pptx")for(let i of t)/^ppt\/(slides|notesSlides)\/(slide|notesSlide)\d+\.xml$/.test(i)&&n.push(i);return n}function st(e,t){let n=tt(e);if(!n||n.length===0)return null;let i=new Map(n.map(d=>[d.name,d])),r=ot(t,n.map(d=>d.name));if(r.length===0)return null;let o=[],a=[],s=0,l=!1;for(let d of r){if(s>=X){l=!0;break}let p=i.get(d);if(!p)continue;let f=nt(e,p);if(f===null)continue;let S=it(f);if(d==="xl/workbook.xml"){let P=at(f,"sheet","name");P.length&&(S+=`
11
+ `+P.join(`
12
+ `))}s+S.length>X&&(S=S.slice(0,Math.max(0,X-s)),l=!0),S.trim().length!==0&&(o.push(S),a.push(d),s+=S.length)}return o.length===0?null:{text:o.join(`
13
+ `),truncated:l,parts:a}}function me(e,t){if(he.has(t))return st(e,t);if(Y.has(t)){let n=fe(e);return n?{text:n.text,truncated:n.truncated,parts:[`pdf:${n.streams}-streams`]}:null}return null}var ye=new Set([...he,...Y]);var q=10,M=5e3,lt=64*1024*1024,Q=32*1024,dt=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"]),pt=new Set(["node_modules",".git",".hg",".svn"]),ut=[{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."}],ft=[{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 ht(e){let t=e.lastIndexOf(".");return t<0?"":e.slice(t).toLowerCase()}async function ee(e){let t=Date.now(),n=[],i=[],r=0,o=0,a={hit_file_cap:!1,hit_depth_cap:!1,max_files:M,max_depth:q,skipped_by_extension:0,documents_extracted:0,documents_truncated:0,documents_unreadable:0,files_too_large:0,files_sampled_only:0,sample_bytes:Q};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,k){let m=s(y);m!==""&&i.push({target_id:e.target_id,path:m,kind:g,size_bytes:h,modified_at:v?v.toISOString():null,child_count:k})}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,k){n.push({target_id:e.target_id,relative_path:y,finding_kind:g,finding_subkind:v,severity:h,remediation_hint:k})}function f(y,g){if(r+=1,r>M){a.hit_file_cap=!0;return}let h=s(y);for(let u of ft)u.re.test(g)&&d(h,u);let v=ht(g);if(dt.has(v)){a.skipped_by_extension+=1;return}let k=ye.has(v),m=0;try{m=J(y).size}catch{return}if(k&&m>lt){a.files_too_large+=1;return}let x;try{let u=ve(y,{flag:"r"});x=k?u:u.subarray(0,Q)}catch{return}o+=1;let T;if(k){let u=me(x,v);if(!u){a.documents_unreadable+=1;return}T=u.text,a.documents_extracted+=1,u.truncated&&(a.documents_truncated+=1)}else{let u=0;for(let _=0;_<x.length;_+=1)x[_]===0&&(u+=1);if(x.length>0&&u/x.length>.05)return;T=x.toString("utf8"),m>Q&&(a.files_sampled_only+=1)}let w=new Set;for(let u of ut)if(u.re.test(T)){let _=`${u.subkind}::${h}`;w.has(_)||(w.add(_),d(h,u))}for(let u of se(T)){let _=`validated::${u.subkind}::${h}`;w.has(_)||(w.add(_),p(h,"pii_unencrypted",u.severity,u.subkind,`${u.hint} ${u.count} distinct value${u.count===1?"":"s"} in this file.`))}if(!ae.test(h)){let u=T.split(`
14
+ `),_=Math.min(u.length,2e3);for(let b=0;b<_;b++){let E=u[b];if(!E)continue;let $=E.trimStart();if(!($.startsWith("//")||$.startsWith("/*")||$.startsWith("*")||$.startsWith("#"))&&!/process\.env/.test(E)&&!/\$\{[A-Z_]+/.test(E)){for(let C of ie)if(C.pattern.test(E)){let N=`secret::${C.id}::${h}::${b+1}`;if(w.has(N))break;w.add(N),p(h,"hardcoded_secret",C.severity,C.id,`Looks like a ${C.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=ve(y,"utf8"),_=oe(u);for(let b of _){let E=`vuln::${b.name}::${h}`;w.has(E)||(w.add(E),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=Z(_[0]??"",u),E=_.slice(1,51).map($=>Z($??"",u));b.forEach(($,C)=>{let N=($??"").trim();if(!N)return;let O=W(N)?.type??null;if(!O){let D=[];for(let Ne of E){let K=Ne[C];if(K&&K.trim().length>0&&D.push(K),D.length>=50)break}let L=P(D);L&&(O=L)}if(O){let D=G[O]??"medium",L=`header::${h}::${N}::${O}`;if(w.has(L))return;w.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 E=W(b);if(E){let $=G[E.type]??"medium",C=`jsonkey::${h}::${b}::${E.type}`;if(w.has(C))continue;w.add(C),p(h,"pii_header_match",$,`${E.type}:${b}`,`JSON key "${b}" looks like ${E.type} data.`)}}}catch{}/\.(txt|log|md|csv|tsv|json|jsonl|xml|html?|yaml|yml|sql)$/i.test(g)&&S(T,h,w)}function S(y,g,h){let v=y.split(`
15
+ `),k=Math.min(v.length,1e3),m=-1,x=-1,T=-1;for(let w=0;w<k;w++){let u=v[w]??"";if(T<0){let _=u.match(/\b\d{9}\b/);_&&B(_[0])&&(T=w+1)}if(m<0&&(F.lastIndex=0,F.test(u)&&(m=w+1)),x<0&&(j.lastIndex=0,j.test(u)&&(x=w+1)),T>0&&m>0&&x>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}.`)),x>0&&!h.has(`iliban::${g}`)&&(h.add(`iliban::${g}`),p(g,"israeli_iban_value","critical",`near_line:${x}`,`Israeli IBAN (IL prefix) detected near line ${x}.`))}function P(y){if(y.length===0)return null;let g=0,h=0,v=0;for(let m of y)typeof m=="string"&&(B(m)&&g++,F.lastIndex=0,F.test(m)&&h++,j.lastIndex=0,j.test(m)&&v++);let k=y.length;return g/k>=.3?"id":h/k>=.3?"phone":v/k>=.3?"financial":null}function R(y,g){if(g>q){a.hit_depth_cap=!0;return}if(r>M){a.hit_file_cap=!0;return}let h;try{h=_e(y)}catch{return}for(let v of h){if(r>M)return;let k=ct(y,v),m;try{m=J(k)}catch{continue}if(!m.isSymbolicLink())if(m.isDirectory()){if(pt.has(v))continue;let x=null;try{x=_e(k).length}catch{x=null}l(k,"folder",null,m.mtime??null,x),R(k,g+1)}else m.isFile()&&(l(k,"file",m.size,m.mtime??null,null),f(k,v))}}let z;try{z=J(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:M,max_depth:q}),R(e.target_path,0),{target_id:e.target_id,target_label:e.target_label,target_path:e.target_path,files_walked:r,files_inspected:o,duration_ms:Date.now()-t,findings:n,inventory:i,coverage:a}):{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:a,error:"target_path_not_found_or_not_directory"}}var te={info(){}};var be={critical:4,high:3,medium:2,low:1,info:0};function ne(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??[],i={};for(let s of n)i[s.severity]=(i[s.severity]??0)+1;let r=["critical","high","medium","low","info"].filter(s=>i[s]).map(s=>`${i[s]} ${s}`).join(" \xB7 ");if(t.push(`EXPOSURE ${n.length} findings${r?" \u2014 "+r:""}
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)=>(be[p.severity]??0)-(be[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,a=[];return o?.hit_file_cap&&a.push(`stopped at the ${o.max_files}-file cap`),o?.hit_depth_cap&&a.push(`folders deeper than ${o.max_depth} levels were not descended`),o?.files_too_large&&a.push(`${o.files_too_large} file(s) too large to read`),o?.documents_unreadable&&a.push(`${o.documents_unreadable} document(s) unreadable`),a.length&&t.push(`COVERAGE ${a.join("; ")}.`),t.join(`
21
+ `)}import{homedir as yt,hostname as Ae}from"node:os";import{join as Ie,resolve as _t,basename as vt}from"node:path";import{existsSync as $e,readFileSync as bt,writeFileSync as kt,mkdirSync as wt,chmodSync as xt}from"node:fs";import{spawn as St}from"node:child_process";import{generateKeyPairSync as Et,createPrivateKey as Tt,sign as At,randomBytes as It}from"node:crypto";var re="1.0.0",Ce=Ie(yt(),".nodata"),U=Ie(Ce,"credentials.json");function $t(){try{if(!$e(U))return null;let e=JSON.parse(bt(U,"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 ke(e){wt(Ce,{recursive:!0});let t=JSON.stringify({...e,saved_at:new Date().toISOString()},null,2);kt(U,t,{mode:384});try{xt(U,384)}catch{}}var Ct=`nodata-mcp v${re} \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 Rt(e){let t="",n="",i="https://www.nodatacapsule.com";for(let a=0;a<e.length;a++)e[a]==="--api-key"&&e[a+1]?t=e[++a]:e[a]==="--grant-token"&&e[a+1]?n=e[++a]:e[a]==="--base-url"&&e[a+1]?i=e[++a]:e[a]==="--help"||e[a]==="-h"?(console.log(Ct),process.exit(0)):e[a]==="--version"||e[a]==="-v"?(console.log(re),process.exit(0)):e[a].startsWith("--")&&console.error(`nodata-mcp: unknown option '${e[a]}' (see --help)`);let r=t||n?"flag":"none";!t&&process.env.NODATA_API_KEY&&(t=process.env.NODATA_API_KEY,r="env"),!n&&process.env.NODATA_GRANT_TOKEN&&(n=process.env.NODATA_GRANT_TOKEN,r==="none"&&(r="env")),process.env.NODATA_BASE_URL&&(i=process.env.NODATA_BASE_URL);let o;if(!t&&!n){let a=$t();a&&(!process.env.NODATA_BASE_URL&&i==="https://www.nodatacapsule.com"&&a.base_url&&(i=a.base_url),a.device?(o=a.device,r="stored"):a.api_key&&(t=a.api_key,r="stored"))}return i=i.replace(/\/+$/,""),{apiKey:t,grantToken:n,baseUrl:i,credentialSource:r,device:o}}var we=3e4;async function Re(e,t){let n;try{n=await fetch(e,{...t,signal:AbortSignal.timeout(we)})}catch(r){return{ok:!1,status:0,body:{error:r instanceof Error&&r.name==="TimeoutError"?`Request timed out after ${we/1e3}s: ${e}`:`Network error reaching ${e}: ${r instanceof Error?r.message:String(r)}`}}}let i;try{i=await n.json()}catch{i={error:`Non-JSON response (HTTP ${n.status})`}}return{ok:n.ok,status:n.status,body:i}}async function Pe(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,i){let r=await Pe(t);return"error"in r?r.error:Re(`${e}${n}`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r.token}`},body:JSON.stringify(i)})}async function Pt(e,t,n,i){let r=await Pe(t);if("error"in r)return r.error;let o=new URL(`${e}${n}`);for(let[a,s]of Object.entries(i))s!==void 0&&s!==""&&o.searchParams.set(a,s);return Re(o.toString(),{method:"GET",headers:{Authorization:`Bearer ${r.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){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 Ot=5*6e4;function Dt(){let{publicKey:e,privateKey:t}=Et("ed25519"),n=e.export({format:"der",type:"spki"}),i=t.export({format:"der",type:"pkcs8"});return{publicKeyHex:n.subarray(-32).toString("hex"),privateKeyPkcs8B64:i.toString("base64")}}function Lt(e,t){let n=Tt({key:Buffer.from(e,"base64"),format:"der",type:"pkcs8"});return At(null,Buffer.from(t,"utf8"),n).toString("base64")}function Ft(){let e=Ae().toLowerCase().replace(/[^a-z0-9-]+/g,"-").replace(/^-+|-+$/g,"").slice(0,20)||"device";return`${/^[a-z]/.test(e)?e:`d-${e}`}-${It(3).toString("hex")}`.slice(0,32)}async function jt(e,t,n,i,r){let o=await A(e,t,"/api/v1/attest/devices",{handle:n,label:i,ed25519_pubkey_hex:r});if(o.ok){let a=o.body.org_id??"";return a?{ok:!0,orgId:a}:{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 Mt(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:i,nonce:r}=n.body;if(!i||!r)throw new Error("challenge response malformed");let o=Lt(t.private_key_pkcs8_b64,i),a=await A(e,"","/api/v1/attest",{org_id:t.org_id,handle:t.handle,nonce:r,signature:o});if(!a.ok)throw new Error(`attest: ${a.body?.error??`HTTP ${a.status}`}`);let s=a.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 Bt(e,t){let n=null;return async()=>((!n||Date.now()>=n.expiresAtMs-Ot)&&(n=await Mt(e,t)),n.token)}function xe(e,t,n){e.registerTool("nodata_register_table",{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.",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:i,encrypted_columns:r,plaintext_columns:o,classification_column:a})=>{let s=await A(t,n,"/api/v1/governance/tables",{table_name:i,encrypted_columns:r,plaintext_columns:o??[],classification_column:a});return I(s)}),e.registerTool("nodata_grant",{description:"Issue an 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.",inputSchema:{agent:c.string().describe("Agent label (becomes its handle)"),table:c.string().optional().describe("Target table (default agent_demo_records)"),columns:c.array(c.string()).describe("Columns the agent may read"),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:i,table:r,columns:o,where:a,classifications:s,ttl_seconds:l})=>{let d=await A(t,n,"/api/v1/governance/grant",{agent:i,table:r,columns:o,where:a,classifications:s,ttl_seconds:l});return I(d)}),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:i,jti:r,reason:o})=>{let a=await A(t,n,"/api/v1/governance/revoke",{handle:i,jti:r,reason:o});return I(a)}),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:i,value:r,context:o})=>{let a={field:i,value:r};return o&&(a.context=o),I(await A(t,n,"/api/v1/encrypt",a))}),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:i,field:r,requester:o})=>{let a={ciphertext:i};return r&&(a.field=r),o&&(a.requester=o),I(await A(t,n,"/api/v1/decrypt",a))}),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:i,to:r,maxViews:o,ttl:a,burn:s})=>{let l={content:i};return r&&(l.to=r),o!==void 0&&(l.maxViews=o),a&&(l.ttl=a),s!==void 0&&(l.burn=s),I(await A(t,n,"/api/v1/deliver",l))}),e.registerTool("nodata_evidence",{description:"Retrieve the audit trail of governance/encryption events. Metadata only \u2014 never content, keys, or plaintext.",inputSchema:{from:c.string().optional(),to:c.string().optional(),action:c.string().optional(),limit:c.number().optional()}},async({from:i,to:r,action:o,limit:a})=>{let s={from:i,to:r,action:o,limit:a!==void 0?String(a):void 0};return I(await Pt(t,n,"/api/v1/evidence",s))})}function Ut(e,t,n){let i=Nt(n);i||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:r,columns:o,where:a})=>{let s=await A(t,n,"/api/access/decide",{resource:{table:r,columns:o,where:a},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:r,columns:o,where:a,top_k:s})=>{let l=await A(t,n,"/api/access/retrieve",{resource:{table:r,columns:o,where:a},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:r,columns:o,where:a,limit:s})=>{if(!i)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(i)}/read`,{table:r,columns:o,where:a,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:r,params:o,resource_key:a})=>{if(!i)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(i)}/use`,{capability:r,params:o,resource_key:a});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:r,question:o,note:a,error:s,steer:l,prior_diff:d})=>{if(!i)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(i)}/code-capsule/${encodeURIComponent(r)}/advise`,{question:o,note:a,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:r,summary:o,diff:a,replace:s})=>{if(!i)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(i)}/code-capsule/${encodeURIComponent(r)}/patch`,{summary:o,diff:a,replace:s});return I(l)})}var Se=`WHAT NODATA DOES \u2014 in one line:
32
+ Give any agent real power over your data, without the power to leak it.
33
+ (Possession is not permission: it can hold the key and still not read what you didn't allow.)
34
+ `,zt=`TURN THE DATA TOOLS ON \u2014 3 steps, about a minute, once:
35
+
36
+ 1. GET A KEY call nodata_connect (no arguments)
37
+ -> opens the signup in your browser: email + company (~30s), a 12-word
38
+ recovery phrase to keep, and an ORG KEY shown ONCE. Copy that key.
39
+
40
+ 2. SAVE IT call nodata_connect again, this time with
41
+ api_key: "<the key you copied>"
42
+ -> saved on this machine; you never paste it again.
43
+
44
+ 3. ACTIVATE type /mcp (or restart Claude Code) so the tools load.
45
+
46
+ COMING BACK LATER? Nothing to redo \u2014 it starts ready. If the tools look missing, just /mcp.
47
+ `,Kt=`NOW \u2014 TURN YOUR DATA INTO AN AGENT THAT CAN'T LEAK, in 3 asks (just say them in plain words):
48
+
49
+ 1. SEE what's sensitive run nodata_scan_folder \u2014 scans a local folder right here, nothing
50
+ uploads (or open https://www.nodatacapsule.com/ai-folder in a browser).
51
+
52
+ 2. CHOOSE what to release say "let <agent> read name + plan on <table>, hide the rest"
53
+ -> I run nodata_register_table + nodata_grant; the agent gets a token.
54
+
55
+ 3. PROVE & control the agent now reads ONLY those columns \u2014 the rest never decrypt.
56
+ "what did it see?" -> nodata_evidence \xB7 "cut it off" -> nodata_revoke
57
+
58
+ Also: a secret even NoData can't read -> nodata_encrypt, then hand it over once -> nodata_deliver.
59
+ `,Ht=`WHAT YOU CAN DO \u2014 scoped to your grant; you reach only what it allows, the rest never decrypts:
60
+
61
+ 1. PLAN nodata_decide "may I reach X?" -> allow / partial / deny + a signed proof. No data moves.
62
+ 2. PULL nodata_retrieve prompt-ready context, allowed fields only, with a receipt.
63
+ (nodata_read for raw governed rows.)
64
+ 3. ACT nodata_use run a capability with a secret injected server-side \u2014 you never see it.
65
+ `,Ee=`STUCK? There's always a next step:
66
+ - Don't see the nodata_* tools? They load when the client starts \u2014 type /mcp or restart.
67
+ (Expected, not an error.)
68
+ - No MCP at all? The same governance runs from one curl, or the guided page:
69
+ https://www.nodatacapsule.com/developers/get-started
70
+ `;function Te(e){return e==="setup"?`You're connected to NoData \u2014 but there's NO key yet, so the data tools are still off.
71
+
72
+ `:e==="agent"?`You're connected to NoData, scoped to one grant token.
73
+
74
+ `:`You're connected to NoData (admin mode). You don't memorize commands \u2014 just ask in plain words.
75
+
76
+ `}function Vt(e){if(e==="setup")return Te(e)+Se+`
77
+ `+zt+`
78
+ `+Ee;let t=e==="agent"?Ht:Kt;return Te(e)+Se+`
79
+ `+t+`
80
+ `+Ee}function Wt(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:Vt(t)}]}))}var Gt="/capsule-api/register";function Zt(e){let t=process.platform==="win32",n=process.platform==="darwin"?"open":t?"cmd":"xdg-open",i=t?["/c","start","",e]:[e];try{let r=St(n,i,{stdio:"ignore",detached:!0});return r.on("error",()=>{}),r.unref(),!0}catch{return!1}}function Yt(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 i=(n??"").trim();if(!i){let p=t+Gt;return{content:[{type:"text",text:(Zt(p)?`STEP 1 of 3 \u2014 opened the NoData signup in your browser:
81
+ `:`STEP 1 of 3 \u2014 open the NoData signup in your browser:
82
+ `)+p+`
83
+
84
+ There, about a minute:
85
+ - email + company (a personal email is fine for the sandbox)
86
+ - a 12-WORD RECOVERY PHRASE \u2014 save it; it is the only way back into your org
87
+ - your ORG KEY, shown ONCE \u2014 copy it
88
+
89
+ STEP 2 \u2014 come back and call this tool again with the key:
90
+ nodata_connect api_key: "<the key you copied>"
91
+ and I'll save it so you never paste it again. (Then STEP 3: type /mcp to activate.)`}]}}if(!/^(ndp_|sk_)/.test(i))return{content:[{type:"text",text:`That doesn't look like a NoData org key (it should start with ndp_ or sk_).
92
+ Copy the key shown once on the signup page and try again: nodata_connect api_key: "ndp_\u2026".`}]};let{publicKeyHex:r,privateKeyPkcs8B64:o}=Dt(),a=Ft(),s=Ae()||"this device",l=await jt(t,i,a,s,r);if(l.ok){try{ke({device:{org_id:l.orgId,handle:a,label:s,private_key_pkcs8_b64:o,public_key_hex:r},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}" (${a}), and only its PRIVATE key is
93
+ saved at ~/.nodata/credentials.json (readable only by you). The org key you pasted was NOT
94
+ stored: every start now proves possession by signing a fresh challenge and gets a 15-minute token.
95
+
96
+ 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{ke({api_key:i,key_prefix:i.slice(0,12),base_url:t})}catch(p){return{content:[{type:"text",text:"Couldn't save locally: "+(p instanceof Error?p.message:String(p))+`
97
+ 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
98
+ 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.
99
+
100
+ STEP 3 \u2014 type /mcp (or restart Claude Code) and the admin tools appear. Next time: nothing to do.`}]}})}function Xt(e){let t=ne(e);return e.error?t:t+`
101
+
102
+ GOVERN what it surfaced \u2014 same journey, inside this command:
103
+ \xB7 CHOOSE what an agent may read: nodata_register_table then nodata_grant (denied columns never decrypt)
104
+ \xB7 PROVE every access afterwards: nodata_evidence
105
+
106
+ Want a standalone deep scan any time? npx @nodatachat/nodata scan <folder> (this same engine).`}function Jt(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()||".",i=_t(n);if(!$e(i))return{content:[{type:"text",text:`No folder found at: ${i}
107
+ Pass an existing path, e.g. nodata_scan_folder path: "."`}]};try{let r=await ee({target_id:"mcp",target_label:vt(i),target_path:i,log:te});return{content:[{type:"text",text:Xt(r)}]}}catch(r){return{content:[{type:"text",text:`Scan failed: ${r instanceof Error?r.message:String(r)}`}],isError:!0}}})}async function qt(){let{apiKey:e,grantToken:t,baseUrl:n,credentialSource:i,device:r}=Rt(process.argv.slice(2)),o=t?"agent":e||r?"admin":"setup",a=new gt({name:o==="agent"?"NoData (agent-scoped)":o==="setup"?"NoData (setup)":"NoData",version:re});r?xe(a,n,Bt(n,r)):e&&xe(a,n,e),t&&Ut(a,n,t),Wt(a,o),o!=="agent"&&Jt(a),o==="setup"?(Yt(a,n),console.error(`nodata-mcp: no credential given \u2014 running in setup mode (data tools disabled).
108
+ Call nodata_connect (opens signup, enrolls this device), or pass --api-key / --grant-token. See --help.`)):r?console.error(`nodata-mcp: loaded device identity "${r.label}" (${r.handle}) \u2014 attest mode; tokens are minted per session.`):i==="stored"&&console.error("nodata-mcp: loaded saved credential from ~/.nodata/credentials.json (admin mode).");let s=new mt;await a.connect(s)}qt().catch(e=>{console.error("Fatal error:",e),process.exit(1)});