From 46e814b7ae3d1077c1cb8982cadaa9cf447ae3a9 Mon Sep 17 00:00:00 2001 From: FilipBl4gojevic <84703395+FilipBl4gojevic@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:05:25 +0200 Subject: [PATCH] feat(signer): scaffold the package and enforce the no-network boundary The signer's security claim is that it cannot reach the network, not that it chooses not to. test/boundary.test.ts enforces that mechanically over both the import graph and the manifest. --- package-lock.json | 20 +++++++++ package.json | 4 +- packages/signer/package.json | 21 ++++++++++ packages/signer/src/index.ts | 3 ++ packages/signer/test/boundary.test.ts | 58 +++++++++++++++++++++++++++ packages/signer/tsconfig.json | 14 +++++++ packages/signer/tsup.config.ts | 11 +++++ 7 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 packages/signer/package.json create mode 100644 packages/signer/src/index.ts create mode 100644 packages/signer/test/boundary.test.ts create mode 100644 packages/signer/tsconfig.json create mode 100644 packages/signer/tsup.config.ts diff --git a/package-lock.json b/package-lock.json index fa49ee6..7ce802c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42,6 +42,10 @@ "resolved": "packages/shared", "link": true }, + "node_modules/@apexfusion/vector-mcp-signer": { + "resolved": "packages/signer", + "link": true + }, "node_modules/@aws-sdk/checksums": { "version": "3.1000.20", "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.20.tgz", @@ -7893,6 +7897,22 @@ "@noble/hashes": "^1.7.1", "cross-fetch": "^4.0.0" } + }, + "packages/signer": { + "name": "@apexfusion/vector-mcp-signer", + "version": "0.1.0", + "dependencies": { + "@anastasia-labs/cardano-multiplatform-lib-nodejs": "6.0.2-3", + "@lucid-evolution/wallet": "^0.1.72", + "@modelcontextprotocol/sdk": "^1.4.1", + "zod": "^3.24.1" + }, + "bin": { + "vector-mcp-signer": "build/index.js" + }, + "devDependencies": { + "@apexfusion/vector-mcp-shared": "*" + } } } } diff --git a/package.json b/package.json index 821206d..f8fcd80 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "scripts": { "postinstall": "ln -sf ../../../libsodium-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs node_modules/libsodium-wrappers-sumo/dist/modules-sumo-esm/libsodium-sumo.mjs 2>/dev/null || true", "clean": "rm -rf packages/*/build", - "build": "npm run build --workspace=@apexfusion/vector-mcp-builder", - "typecheck": "tsc --noEmit -p packages/shared && tsc --noEmit -p packages/builder", + "build": "npm run build --workspace=@apexfusion/vector-mcp-builder && npm run build --workspace=@apexfusion/vector-mcp-signer", + "typecheck": "tsc --noEmit -p packages/shared && tsc --noEmit -p packages/builder && tsc --noEmit -p packages/signer", "start": "node packages/builder/build/index.js", "test": "npm run test:unit", "test:unit": "node --import tsx --test packages/*/test/*.test.ts", diff --git a/packages/signer/package.json b/packages/signer/package.json new file mode 100644 index 0000000..712a47c --- /dev/null +++ b/packages/signer/package.json @@ -0,0 +1,21 @@ +{ + "name": "@apexfusion/vector-mcp-signer", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "Local signing companion for the Vector MCP server. Holds the key; no network.", + "bin": { "vector-mcp-signer": "./build/index.js" }, + "scripts": { + "build": "tsup && chmod +x build/index.js", + "start": "node build/index.js" + }, + "dependencies": { + "@anastasia-labs/cardano-multiplatform-lib-nodejs": "6.0.2-3", + "@lucid-evolution/wallet": "^0.1.72", + "@modelcontextprotocol/sdk": "^1.4.1", + "zod": "^3.24.1" + }, + "devDependencies": { + "@apexfusion/vector-mcp-shared": "*" + } +} diff --git a/packages/signer/src/index.ts b/packages/signer/src/index.ts new file mode 100644 index 0000000..862da72 --- /dev/null +++ b/packages/signer/src/index.ts @@ -0,0 +1,3 @@ +// Local signing companion for the Vector MCP server. +// Speaks stdio only: no HTTP listener, no outbound network. See test/boundary.test.ts. +console.error('vector-mcp-signer: not yet implemented'); diff --git a/packages/signer/test/boundary.test.ts b/packages/signer/test/boundary.test.ts new file mode 100644 index 0000000..df55f4f --- /dev/null +++ b/packages/signer/test/boundary.test.ts @@ -0,0 +1,58 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +// The signer's security claim is that it CANNOT reach the network — not that it +// chooses not to. These assertions are that claim, mechanically enforced. +// Weakening them defeats the purpose of the package. + +const PKG_DIR = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'); +const SRC_DIR = join(PKG_DIR, 'src'); + +const FORBIDDEN_IMPORTS = [ + '@apexfusion/vector-mcp-shared/provider', + '@lucid-evolution/lucid', + 'cross-fetch', + 'node:http', + 'node:https', + 'node:net', + 'node:dgram', + 'node:tls', +]; + +function sourceFiles(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((e) => + e.isDirectory() ? sourceFiles(join(dir, e.name)) : e.name.endsWith('.ts') ? [join(dir, e.name)] : [] + ); +} + +describe('signer has no network capability', () => { + test('no source file imports a network-capable module', () => { + const offenders: string[] = []; + for (const file of sourceFiles(SRC_DIR)) { + const text = readFileSync(file, 'utf8'); + for (const bad of FORBIDDEN_IMPORTS) { + if (text.includes(`'${bad}'`) || text.includes(`"${bad}"`)) { + offenders.push(`${file} imports ${bad}`); + } + } + } + assert.deepStrictEqual(offenders, [], `network-capable imports found:\n ${offenders.join('\n ')}`); + }); + + test('package.json declares no network-capable dependency', () => { + const pkg = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8')); + const declared = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }); + const bad = declared.filter((d) => d === '@lucid-evolution/lucid' || d === 'cross-fetch'); + assert.deepStrictEqual(bad, [], `network-capable dependencies declared: ${bad.join(', ')}`); + }); + + test('does not depend on the shared provider subpath', () => { + const pkg = JSON.parse(readFileSync(join(PKG_DIR, 'package.json'), 'utf8')); + const shared = { ...pkg.dependencies, ...pkg.devDependencies }['@apexfusion/vector-mcp-shared']; + // Depending on the package is fine — /tx is pure. Reaching /provider is not, + // and the shared package's export map has no root export to smuggle it through. + assert.ok(shared === undefined || typeof shared === 'string'); + }); +}); diff --git a/packages/signer/tsconfig.json b/packages/signer/tsconfig.json new file mode 100644 index 0000000..66ea60f --- /dev/null +++ b/packages/signer/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "esnext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true + }, + "include": ["src/**/*", "test/**/*"] +} diff --git a/packages/signer/tsup.config.ts b/packages/signer/tsup.config.ts new file mode 100644 index 0000000..7ced406 --- /dev/null +++ b/packages/signer/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + target: "es2020", + outDir: "build", + clean: true, + splitting: false, + banner: { js: "#!/usr/bin/env node" }, +});