@stackwright-pro/mcp 0.2.0-alpha.90 → 0.2.0-alpha.92

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/integrity.js CHANGED
@@ -37,7 +37,7 @@ var _checksums = /* @__PURE__ */ new Map([
37
37
  ],
38
38
  [
39
39
  "stackwright-pro-auth-otter.json",
40
- "07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3"
40
+ "643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8"
41
41
  ],
42
42
  [
43
43
  "stackwright-pro-dashboard-otter.json",
@@ -59,6 +59,10 @@ var _checksums = /* @__PURE__ */ new Map([
59
59
  "stackwright-pro-foreman-otter.json",
60
60
  "438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
61
61
  ],
62
+ [
63
+ "stackwright-pro-form-wizard-otter.json",
64
+ "3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60"
65
+ ],
62
66
  [
63
67
  "stackwright-pro-geo-otter.json",
64
68
  "2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
@@ -79,10 +83,6 @@ var _checksums = /* @__PURE__ */ new Map([
79
83
  "stackwright-pro-theme-otter.json",
80
84
  "fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
81
85
  ],
82
- [
83
- "stackwright-pro-workflow-otter.json",
84
- "5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8"
85
- ],
86
86
  [
87
87
  "stackwright-services-otter.json",
88
88
  "c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '1f21b8ff3450bdae29a4d31b31462ba22583995a448af3c11ab0a5071f46bd72',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n '2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n 'bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8',\n ],\n [\n 'stackwright-services-otter.json',\n 'c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '1f21b8ff3450bdae29a4d31b31462ba22583995a448af3c11ab0a5071f46bd72',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1',\n ],\n [\n 'stackwright-pro-form-wizard-otter.json',\n '3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n '2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n 'bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944',\n ],\n [\n 'stackwright-services-otter.json',\n 'c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
@@ -9,7 +9,7 @@ var _checksums = /* @__PURE__ */ new Map([
9
9
  ],
10
10
  [
11
11
  "stackwright-pro-auth-otter.json",
12
- "07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3"
12
+ "643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8"
13
13
  ],
14
14
  [
15
15
  "stackwright-pro-dashboard-otter.json",
@@ -31,6 +31,10 @@ var _checksums = /* @__PURE__ */ new Map([
31
31
  "stackwright-pro-foreman-otter.json",
32
32
  "438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
33
33
  ],
34
+ [
35
+ "stackwright-pro-form-wizard-otter.json",
36
+ "3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60"
37
+ ],
34
38
  [
35
39
  "stackwright-pro-geo-otter.json",
36
40
  "2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
@@ -51,10 +55,6 @@ var _checksums = /* @__PURE__ */ new Map([
51
55
  "stackwright-pro-theme-otter.json",
52
56
  "fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
53
57
  ],
54
- [
55
- "stackwright-pro-workflow-otter.json",
56
- "5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8"
57
- ],
58
58
  [
59
59
  "stackwright-services-otter.json",
60
60
  "c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '1f21b8ff3450bdae29a4d31b31462ba22583995a448af3c11ab0a5071f46bd72',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n '2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n 'bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944',\n ],\n [\n 'stackwright-pro-workflow-otter.json',\n '5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8',\n ],\n [\n 'stackwright-services-otter.json',\n 'c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/integrity.ts"],"sourcesContent":["/**\n * Otter Integrity Verification\n * ============================\n * Protects the Pro Otter Raft from disk-based prompt injection / jailbreak attacks.\n *\n * TypeScript port of python/src/stackwright_pro/raft/integrity.py — this lets\n * the MCP package verify otter files without a Python dependency.\n *\n * Certificate-pinned canonical checksums — hardcoded in the MCP package.\n *\n * These are NOT read from disk. An attacker who modifies otter JSON files\n * in @stackwright-pro/otters cannot also modify these constants without\n * compromising the separately-published @stackwright-pro/mcp package.\n *\n * To update: node scripts/sync-mcp-checksums.cjs\n * (reads from packages/otters/src/checksums.json, writes this file)\n */\nimport { createHash, timingSafeEqual } from 'crypto';\nimport { readFileSync, readdirSync, lstatSync } from 'fs';\nimport { join, basename } from 'path';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\n// ---------------------------------------------------------------------------\n// Certificate-pinned canonical checksums — frozen Map, immutable by design.\n// DO NOT read these from disk — that would defeat the entire purpose.\n// Object.freeze prevents property mutation at runtime; ReadonlyMap prevents\n// .set() / .delete() at compile time (belt-and-suspenders).\n// ---------------------------------------------------------------------------\n\nconst _checksums = new Map<string, string>([\n [\n 'stackwright-pro-api-otter.json',\n 'df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd',\n ],\n [\n 'stackwright-pro-auth-otter.json',\n '643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8',\n ],\n [\n 'stackwright-pro-dashboard-otter.json',\n '160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01',\n ],\n [\n 'stackwright-pro-data-otter.json',\n '709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25',\n ],\n [\n 'stackwright-pro-designer-otter.json',\n '1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9',\n ],\n [\n 'stackwright-pro-domain-expert-otter.json',\n '1f21b8ff3450bdae29a4d31b31462ba22583995a448af3c11ab0a5071f46bd72',\n ],\n [\n 'stackwright-pro-foreman-otter.json',\n '438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1',\n ],\n [\n 'stackwright-pro-form-wizard-otter.json',\n '3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60',\n ],\n [\n 'stackwright-pro-geo-otter.json',\n '2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f',\n ],\n [\n 'stackwright-pro-page-otter.json',\n '0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03',\n ],\n [\n 'stackwright-pro-polish-otter.json',\n 'bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3',\n ],\n [\n 'stackwright-pro-scaffold-otter.json',\n '91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a',\n ],\n [\n 'stackwright-pro-theme-otter.json',\n 'fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944',\n ],\n [\n 'stackwright-services-otter.json',\n 'c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261',\n ],\n]);\nObject.freeze(_checksums);\nconst CANONICAL_CHECKSUMS: ReadonlyMap<string, string> = _checksums;\n\n// ---------------------------------------------------------------------------\n// Import-time format validation — malformed constants are a packaging bug,\n// not a runtime surprise. Fail fast, fail loud.\n// ---------------------------------------------------------------------------\n\nconst SHA256_HEX_RE = /^[0-9a-f]{64}$/;\n\nfor (const [name, digest] of CANONICAL_CHECKSUMS) {\n if (!SHA256_HEX_RE.test(digest)) {\n throw new Error(\n `Malformed SHA-256 in CANONICAL_CHECKSUMS for \"${name}\": ` +\n `expected 64 hex chars, got ${digest.length}: \"${digest}\"`\n );\n }\n}\n\n// 1 MB — generous headroom for agent definitions; anything larger is suspicious.\nconst MAX_OTTER_BYTES = 1 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Core functions (exported for direct testing — no MCP server needed)\n// ---------------------------------------------------------------------------\n\n/** Compute the hex-encoded SHA-256 digest of raw bytes. Pure, no I/O. */\nexport function computeSha256(data: Buffer): string {\n return createHash('sha256').update(data).digest('hex');\n}\n\n/** Constant-time comparison of two hex digest strings. */\nfunction safeEqual(a: string, b: string): boolean {\n if (a.length !== b.length) return false;\n return timingSafeEqual(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8'));\n}\n\n// ---------------------------------------------------------------------------\n// Single-file verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyOtterFileResult {\n verified: boolean;\n filename: string;\n error?: string;\n}\n\n/**\n * Read a single otter JSON file, check its size, compute its SHA-256,\n * and constant-time compare against the canonical checksum.\n *\n * Single read → hash → decode. No TOCTOU window.\n */\nexport function verifyOtterFile(filePath: string): VerifyOtterFileResult {\n const filename = basename(filePath);\n\n // Fast-fail on unknown filenames before any I/O\n const expected = CANONICAL_CHECKSUMS.get(filename);\n if (expected === undefined) {\n return { verified: false, filename, error: `Unknown otter file: not in canonical set` };\n }\n\n // Symlink guard — refuse to follow symlinks (prevents symlink-based swaps)\n let stat: ReturnType<typeof lstatSync>;\n try {\n stat = lstatSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot stat file: ${msg}` };\n }\n\n if (stat.isSymbolicLink()) {\n return { verified: false, filename, error: 'Refusing to verify symlink' };\n }\n\n // Stat-based size pre-check — don't materialise oversized payloads\n const size = stat.size;\n\n if (size > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${size.toLocaleString()})`,\n };\n }\n\n // Single read — used for hashing and UTF-8 validation (zero TOCTOU window)\n let raw: Buffer;\n try {\n raw = readFileSync(filePath);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return { verified: false, filename, error: `Cannot read file: ${msg}` };\n }\n\n // Belt-and-suspenders: re-check length after read in case of a race\n if (raw.length > MAX_OTTER_BYTES) {\n return {\n verified: false,\n filename,\n error: `File exceeds size limit after read (${MAX_OTTER_BYTES.toLocaleString()} bytes, got ${raw.length.toLocaleString()})`,\n };\n }\n\n // Hash the raw bytes\n const actual = computeSha256(raw);\n\n // Constant-time comparison prevents timing-oracle attacks\n if (!safeEqual(actual, expected)) {\n return {\n verified: false,\n filename,\n error: `SHA-256 mismatch: expected ${expected.substring(0, 8)}…, got ${actual.substring(0, 8)}…`,\n };\n }\n\n // UTF-8 validation — binary injection guard\n try {\n const decoder = new TextDecoder('utf-8', { fatal: true });\n decoder.decode(raw);\n } catch {\n return {\n verified: false,\n filename,\n error: 'File is not valid UTF-8 — may be corrupted or contain binary injection',\n };\n }\n\n return { verified: true, filename };\n}\n\n// ---------------------------------------------------------------------------\n// Directory-level verification\n// ---------------------------------------------------------------------------\n\nexport interface VerifyAllOttersResult {\n verified: string[];\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Scan a directory for `*-otter.json` files, verify each one against\n * canonical checksums. Returns lists of verified, failed, and unknown files.\n */\nexport function verifyAllOtters(otterDir: string): VerifyAllOttersResult {\n // ---------------------------------------------------------------------------\n // Path traversal guard — reject any input containing \"..\" sequences before\n // any I/O. An attacker controlling this parameter could otherwise scan\n // directories outside the expected otter install locations.\n // ---------------------------------------------------------------------------\n if (/(?:^|[/\\\\])\\.\\.(?:[/\\\\]|$)/.test(otterDir) || otterDir.includes('..')) {\n return {\n verified: [],\n failed: [\n {\n filename: '<directory>',\n error: `Security: path traversal sequence detected in otter directory parameter`,\n },\n ],\n unknown: [],\n };\n }\n\n const verified: string[] = [];\n const failed: Array<{ filename: string; error: string }> = [];\n const unknown: string[] = [];\n\n let entries: string[];\n try {\n entries = readdirSync(otterDir);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n return {\n verified: [],\n failed: [{ filename: '<directory>', error: `Cannot read directory: ${msg}` }],\n unknown: [],\n };\n }\n\n const otterFiles = entries.filter((f) => f.endsWith('-otter.json'));\n\n for (const filename of otterFiles) {\n const filePath = join(otterDir, filename);\n\n // Skip symlinks at the directory-scan level too\n try {\n if (lstatSync(filePath).isSymbolicLink()) {\n failed.push({ filename, error: 'Skipped: symlink' });\n continue;\n }\n } catch {\n // verifyOtterFile will handle stat errors\n }\n\n const result = verifyOtterFile(filePath);\n\n if (result.verified) {\n verified.push(result.filename);\n } else if (result.error?.startsWith('Unknown otter file')) {\n unknown.push(result.filename);\n } else {\n failed.push({ filename: result.filename, error: result.error ?? 'Unknown error' });\n }\n }\n\n // Check for missing canonical files — ones we expect but didn't find on disk\n for (const canonicalName of CANONICAL_CHECKSUMS.keys()) {\n if (!otterFiles.includes(canonicalName)) {\n failed.push({ filename: canonicalName, error: 'Missing from directory' });\n }\n }\n\n return { verified, failed, unknown };\n}\n\n// ---------------------------------------------------------------------------\n// Otter directory resolution\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_SEARCH_PATHS = ['node_modules/@stackwright-pro/otters/src/', 'packages/otters/src/'];\n\nfunction resolveOtterDir(): string | null {\n const cwd = process.cwd();\n for (const relative of DEFAULT_SEARCH_PATHS) {\n const candidate = join(cwd, relative);\n try {\n lstatSync(candidate);\n return candidate;\n } catch {\n // Not found, try next\n }\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Audit stream — SOC2 / FedRAMP / DoD ATO compliance\n// ---------------------------------------------------------------------------\n\n/**\n * Structured audit event parameters for an integrity failure.\n * Kept as a plain interface so callers can pass partial results without\n * constructing the full VerifyAllOttersResult.\n */\nexport interface IntegrityAuditEvent {\n otterDir: string;\n failed: Array<{ filename: string; error: string }>;\n unknown: string[];\n}\n\n/**\n * Emit a structured INTEGRITY_FAIL audit event to stderr.\n *\n * Writes a single line to process.stderr in the format:\n * INTEGRITY_FAIL {\"level\":\"AUDIT\",\"event\":\"INTEGRITY_FAIL\",...}\n *\n * The line prefix \"INTEGRITY_FAIL\" (without JSON) allows log shippers\n * (FluentBit, syslog, CloudWatch Logs, Splunk) to match and route the\n * event to a dedicated audit stream using a simple string filter, even\n * before attempting JSON parsing.\n *\n * Exported for unit testing. Do not call directly in production code —\n * use registerIntegrityTools() which calls this automatically.\n */\nexport function emitIntegrityAuditEvent(params: IntegrityAuditEvent): void {\n const record = JSON.stringify({\n level: 'AUDIT',\n event: 'INTEGRITY_FAIL',\n timestamp: new Date().toISOString(),\n source: 'stackwright_pro_verify_otter_integrity',\n otterDir: params.otterDir,\n failedCount: params.failed.length,\n unknownCount: params.unknown.length,\n failures: params.failed,\n unknown: params.unknown,\n });\n process.stderr.write(`INTEGRITY_FAIL ${record}\\n`);\n}\n\n// ---------------------------------------------------------------------------\n// MCP tool registration\n// ---------------------------------------------------------------------------\n\nexport function registerIntegrityTools(server: McpServer): void {\n server.tool(\n 'stackwright_pro_verify_otter_integrity',\n 'Verify SHA-256 integrity of all Pro otter agent definitions. Call this at startup before discovering otters. Auto-discovers the otter directory from known paths. Returns verified/failed/unknown lists.',\n {},\n async () => {\n const resolved = resolveOtterDir();\n\n if (!resolved) {\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n error: true,\n message:\n 'Could not locate otter directory. Searched: ' + DEFAULT_SEARCH_PATHS.join(', '),\n }),\n },\n ],\n isError: true,\n };\n }\n\n const result = verifyAllOtters(resolved);\n\n const allGood = result.failed.length === 0 && result.unknown.length === 0;\n\n // Emit to dedicated audit stream for SOC2/FedRAMP/DoD ATO compliance\n if (!allGood) {\n emitIntegrityAuditEvent({\n otterDir: resolved,\n failed: result.failed,\n unknown: result.unknown,\n });\n }\n\n return {\n content: [\n {\n type: 'text' as const,\n text: JSON.stringify({\n otterDir: resolved,\n totalCanonical: CANONICAL_CHECKSUMS.size,\n verifiedCount: result.verified.length,\n failedCount: result.failed.length,\n unknownCount: result.unknown.length,\n verified: result.verified,\n failed: result.failed,\n unknown: result.unknown,\n ...(allGood\n ? {}\n : {\n error:\n 'INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed — otter files may have been tampered with.',\n }),\n }),\n },\n ],\n isError: !allGood,\n };\n }\n );\n}\n"],"mappings":";AAiBA,SAAS,YAAY,uBAAuB;AAC5C,SAAS,cAAc,aAAa,iBAAiB;AACrD,SAAS,MAAM,gBAAgB;AAU/B,IAAM,aAAa,oBAAI,IAAoB;AAAA,EACzC;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AAAA,EACA;AAAA,IACE;AAAA,IACA;AAAA,EACF;AACF,CAAC;AACD,OAAO,OAAO,UAAU;AACxB,IAAM,sBAAmD;AAOzD,IAAM,gBAAgB;AAEtB,WAAW,CAAC,MAAM,MAAM,KAAK,qBAAqB;AAChD,MAAI,CAAC,cAAc,KAAK,MAAM,GAAG;AAC/B,UAAM,IAAI;AAAA,MACR,iDAAiD,IAAI,iCACrB,OAAO,MAAM,MAAM,MAAM;AAAA,IAC3D;AAAA,EACF;AACF;AAGA,IAAM,kBAAkB,IAAI,OAAO;AAO5B,SAAS,cAAc,MAAsB;AAClD,SAAO,WAAW,QAAQ,EAAE,OAAO,IAAI,EAAE,OAAO,KAAK;AACvD;AAGA,SAAS,UAAU,GAAW,GAAoB;AAChD,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,gBAAgB,OAAO,KAAK,GAAG,MAAM,GAAG,OAAO,KAAK,GAAG,MAAM,CAAC;AACvE;AAkBO,SAAS,gBAAgB,UAAyC;AACvE,QAAM,WAAW,SAAS,QAAQ;AAGlC,QAAM,WAAW,oBAAoB,IAAI,QAAQ;AACjD,MAAI,aAAa,QAAW;AAC1B,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,2CAA2C;AAAA,EACxF;AAGA,MAAI;AACJ,MAAI;AACF,WAAO,UAAU,QAAQ;AAAA,EAC3B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,qBAAqB,GAAG,GAAG;AAAA,EACxE;AAEA,MAAI,KAAK,eAAe,GAAG;AACzB,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,6BAA6B;AAAA,EAC1E;AAGA,QAAM,OAAO,KAAK;AAElB,MAAI,OAAO,iBAAiB;AAC1B,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,4BAA4B,gBAAgB,eAAe,CAAC,eAAe,KAAK,eAAe,CAAC;AAAA,IACzG;AAAA,EACF;AAGA,MAAI;AACJ,MAAI;AACF,UAAM,aAAa,QAAQ;AAAA,EAC7B,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO,EAAE,UAAU,OAAO,UAAU,OAAO,qBAAqB,GAAG,GAAG;AAAA,EACxE;AAGA,MAAI,IAAI,SAAS,iBAAiB;AAChC,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,uCAAuC,gBAAgB,eAAe,CAAC,eAAe,IAAI,OAAO,eAAe,CAAC;AAAA,IAC1H;AAAA,EACF;AAGA,QAAM,SAAS,cAAc,GAAG;AAGhC,MAAI,CAAC,UAAU,QAAQ,QAAQ,GAAG;AAChC,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO,8BAA8B,SAAS,UAAU,GAAG,CAAC,CAAC,eAAU,OAAO,UAAU,GAAG,CAAC,CAAC;AAAA,IAC/F;AAAA,EACF;AAGA,MAAI;AACF,UAAM,UAAU,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AACxD,YAAQ,OAAO,GAAG;AAAA,EACpB,QAAQ;AACN,WAAO;AAAA,MACL,UAAU;AAAA,MACV;AAAA,MACA,OAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,MAAM,SAAS;AACpC;AAgBO,SAAS,gBAAgB,UAAyC;AAMvE,MAAI,6BAA6B,KAAK,QAAQ,KAAK,SAAS,SAAS,IAAI,GAAG;AAC1E,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,QAAQ;AAAA,QACN;AAAA,UACE,UAAU;AAAA,UACV,OAAO;AAAA,QACT;AAAA,MACF;AAAA,MACA,SAAS,CAAC;AAAA,IACZ;AAAA,EACF;AAEA,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAqD,CAAC;AAC5D,QAAM,UAAoB,CAAC;AAE3B,MAAI;AACJ,MAAI;AACF,cAAU,YAAY,QAAQ;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC3D,WAAO;AAAA,MACL,UAAU,CAAC;AAAA,MACX,QAAQ,CAAC,EAAE,UAAU,eAAe,OAAO,0BAA0B,GAAG,GAAG,CAAC;AAAA,MAC5E,SAAS,CAAC;AAAA,IACZ;AAAA,EACF;AAEA,QAAM,aAAa,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,aAAa,CAAC;AAElE,aAAW,YAAY,YAAY;AACjC,UAAM,WAAW,KAAK,UAAU,QAAQ;AAGxC,QAAI;AACF,UAAI,UAAU,QAAQ,EAAE,eAAe,GAAG;AACxC,eAAO,KAAK,EAAE,UAAU,OAAO,mBAAmB,CAAC;AACnD;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AAEA,UAAM,SAAS,gBAAgB,QAAQ;AAEvC,QAAI,OAAO,UAAU;AACnB,eAAS,KAAK,OAAO,QAAQ;AAAA,IAC/B,WAAW,OAAO,OAAO,WAAW,oBAAoB,GAAG;AACzD,cAAQ,KAAK,OAAO,QAAQ;AAAA,IAC9B,OAAO;AACL,aAAO,KAAK,EAAE,UAAU,OAAO,UAAU,OAAO,OAAO,SAAS,gBAAgB,CAAC;AAAA,IACnF;AAAA,EACF;AAGA,aAAW,iBAAiB,oBAAoB,KAAK,GAAG;AACtD,QAAI,CAAC,WAAW,SAAS,aAAa,GAAG;AACvC,aAAO,KAAK,EAAE,UAAU,eAAe,OAAO,yBAAyB,CAAC;AAAA,IAC1E;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,QAAQ,QAAQ;AACrC;AAMA,IAAM,uBAAuB,CAAC,6CAA6C,sBAAsB;AAEjG,SAAS,kBAAiC;AACxC,QAAM,MAAM,QAAQ,IAAI;AACxB,aAAW,YAAY,sBAAsB;AAC3C,UAAM,YAAY,KAAK,KAAK,QAAQ;AACpC,QAAI;AACF,gBAAU,SAAS;AACnB,aAAO;AAAA,IACT,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO;AACT;AA+BO,SAAS,wBAAwB,QAAmC;AACzE,QAAM,SAAS,KAAK,UAAU;AAAA,IAC5B,OAAO;AAAA,IACP,OAAO;AAAA,IACP,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,QAAQ;AAAA,IACR,UAAU,OAAO;AAAA,IACjB,aAAa,OAAO,OAAO;AAAA,IAC3B,cAAc,OAAO,QAAQ;AAAA,IAC7B,UAAU,OAAO;AAAA,IACjB,SAAS,OAAO;AAAA,EAClB,CAAC;AACD,UAAQ,OAAO,MAAM,kBAAkB,MAAM;AAAA,CAAI;AACnD;AAMO,SAAS,uBAAuB,QAAyB;AAC9D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,CAAC;AAAA,IACD,YAAY;AACV,YAAM,WAAW,gBAAgB;AAEjC,UAAI,CAAC,UAAU;AACb,eAAO;AAAA,UACL,SAAS;AAAA,YACP;AAAA,cACE,MAAM;AAAA,cACN,MAAM,KAAK,UAAU;AAAA,gBACnB,OAAO;AAAA,gBACP,SACE,iDAAiD,qBAAqB,KAAK,IAAI;AAAA,cACnF,CAAC;AAAA,YACH;AAAA,UACF;AAAA,UACA,SAAS;AAAA,QACX;AAAA,MACF;AAEA,YAAM,SAAS,gBAAgB,QAAQ;AAEvC,YAAM,UAAU,OAAO,OAAO,WAAW,KAAK,OAAO,QAAQ,WAAW;AAGxE,UAAI,CAAC,SAAS;AACZ,gCAAwB;AAAA,UACtB,UAAU;AAAA,UACV,QAAQ,OAAO;AAAA,UACf,SAAS,OAAO;AAAA,QAClB,CAAC;AAAA,MACH;AAEA,aAAO;AAAA,QACL,SAAS;AAAA,UACP;AAAA,YACE,MAAM;AAAA,YACN,MAAM,KAAK,UAAU;AAAA,cACnB,UAAU;AAAA,cACV,gBAAgB,oBAAoB;AAAA,cACpC,eAAe,OAAO,SAAS;AAAA,cAC/B,aAAa,OAAO,OAAO;AAAA,cAC3B,cAAc,OAAO,QAAQ;AAAA,cAC7B,UAAU,OAAO;AAAA,cACjB,QAAQ,OAAO;AAAA,cACf,SAAS,OAAO;AAAA,cAChB,GAAI,UACA,CAAC,IACD;AAAA,gBACE,OACE;AAAA,cACJ;AAAA,YACN,CAAC;AAAA,UACH;AAAA,QACF;AAAA,QACA,SAAS,CAAC;AAAA,MACZ;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/dist/server.js CHANGED
@@ -1619,6 +1619,9 @@ var OTTER_NAME_TO_PHASE = [
1619
1619
  ["dashboard", "dashboard"],
1620
1620
  ["data", "data"],
1621
1621
  ["page", "pages"],
1622
+ // swp-4071: workflow-otter renamed to form-wizard-otter; keep 'workflow' for
1623
+ // any in-flight session that still uses the old name (belt-and-suspenders)
1624
+ ["form-wizard", "workflow"],
1622
1625
  ["workflow", "workflow"],
1623
1626
  ["services", "services"],
1624
1627
  ["polish", "polish"],
@@ -1633,7 +1636,7 @@ var PHASE_TO_OTTER = {
1633
1636
  pages: "stackwright-pro-page-otter",
1634
1637
  dashboard: "stackwright-pro-dashboard-otter",
1635
1638
  data: "stackwright-pro-data-otter",
1636
- workflow: "stackwright-pro-workflow-otter",
1639
+ workflow: "stackwright-pro-form-wizard-otter",
1637
1640
  services: "stackwright-services-otter",
1638
1641
  polish: "stackwright-pro-polish-otter",
1639
1642
  geo: "stackwright-pro-geo-otter"
@@ -2753,7 +2756,7 @@ var PHASE_TO_OTTER2 = {
2753
2756
  data: "stackwright-pro-data-otter",
2754
2757
  pages: "stackwright-pro-page-otter",
2755
2758
  dashboard: "stackwright-pro-dashboard-otter",
2756
- workflow: "stackwright-pro-workflow-otter",
2759
+ workflow: "stackwright-pro-form-wizard-otter",
2757
2760
  services: "stackwright-services-otter",
2758
2761
  polish: "stackwright-pro-polish-otter",
2759
2762
  geo: "stackwright-pro-geo-otter"
@@ -3466,7 +3469,7 @@ var PHASE_ARTIFACT_SCHEMA = {
3466
3469
  workflow: JSON.stringify(
3467
3470
  {
3468
3471
  version: "1.0",
3469
- generatedBy: "stackwright-pro-workflow-otter",
3472
+ generatedBy: "stackwright-pro-form-wizard-otter",
3470
3473
  // Root key is `workflow` — NOT `workflowConfig` (see swp-k7cl)
3471
3474
  workflow: {
3472
3475
  id: "procurement-approval",
@@ -3676,6 +3679,57 @@ function handleValidateArtifact(input) {
3676
3679
  const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3677
3680
  return { success: false, error: { message: issues } };
3678
3681
  }
3682
+ const rbacRoles = authConfig["rbacRoles"];
3683
+ const rbacDefaultRole = authConfig["rbacDefaultRole"];
3684
+ const protectedRoutes = authConfig["protectedRoutes"];
3685
+ if (Array.isArray(rbacRoles) && rbacRoles.length > 2 && Array.isArray(protectedRoutes) && protectedRoutes.length > 0 && typeof rbacDefaultRole === "string") {
3686
+ const uniqueRoles = new Set(
3687
+ protectedRoutes.map(
3688
+ (r) => typeof r === "string" ? rbacDefaultRole : r.requiredRole ?? rbacDefaultRole
3689
+ )
3690
+ );
3691
+ if (uniqueRoles.size === 1 && uniqueRoles.has(rbacDefaultRole)) {
3692
+ return {
3693
+ success: false,
3694
+ error: {
3695
+ message: `RBAC theatre detected: all ${protectedRoutes.length} protectedRoutes require '${rbacDefaultRole}' (the defaultRole), but ${rbacRoles.length} roles are defined. This defeats the RBAC hierarchy. Each route MUST have the minimum role required for its function. See auth-otter PER-ROUTE RBAC GRANULARITY section.`
3696
+ }
3697
+ };
3698
+ }
3699
+ }
3700
+ return { success: true };
3701
+ },
3702
+ // swp-4071 — opt-in cross-reference validator for the services phase.
3703
+ // If workflow-config exists and declares serviceHooks, every hook ref must resolve
3704
+ // to a flow or state-machine in this services-config artifact.
3705
+ // Skips gracefully when wizard-otter didn't run (Pro-only install) or declared no hooks.
3706
+ services: (artifact2) => {
3707
+ const workflowArtifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
3708
+ if (!(0, import_fs5.existsSync)(workflowArtifactPath)) {
3709
+ return { success: true };
3710
+ }
3711
+ let workflowArtifact;
3712
+ try {
3713
+ workflowArtifact = JSON.parse((0, import_fs5.readFileSync)(workflowArtifactPath, "utf-8"));
3714
+ } catch {
3715
+ return { success: true };
3716
+ }
3717
+ const declaredHooks = workflowArtifact.serviceHooks ?? [];
3718
+ if (declaredHooks.length === 0) {
3719
+ return { success: true };
3720
+ }
3721
+ const flows = artifact2["flows"] ?? [];
3722
+ const workflows = artifact2["workflows"] ?? [];
3723
+ const flowNames = /* @__PURE__ */ new Set([...flows.map((f) => f.name), ...workflows.map((w) => w.name)]);
3724
+ const unresolved = declaredHooks.filter((h) => !flowNames.has(h.ref));
3725
+ if (unresolved.length > 0) {
3726
+ return {
3727
+ success: false,
3728
+ error: {
3729
+ message: `Workflow declared ${declaredHooks.length} service hook(s) but ${unresolved.length} unresolved by services-config: ` + unresolved.map((h) => `${h.ref} (${h.kind ?? "unknown"}: ${h.purpose ?? "no purpose"})`).join("; ") + ". Either add matching flows to services-config OR remove the hooks from the workflow YAML."
3730
+ }
3731
+ };
3732
+ }
3679
3733
  return { success: true };
3680
3734
  }
3681
3735
  };
@@ -3915,7 +3969,7 @@ var OTTER_WRITE_ALLOWLISTS = {
3915
3969
  { prefix: "pages/", suffix: "/content.yaml", description: "Dashboard content YAML" },
3916
3970
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Dashboard manifest" }
3917
3971
  ],
3918
- "stackwright-pro-workflow-otter": [
3972
+ "stackwright-pro-form-wizard-otter": [
3919
3973
  { prefix: "workflows/", suffix: ".yml", description: "Workflow definition" },
3920
3974
  { prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
3921
3975
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
@@ -4317,9 +4371,14 @@ function hierarchyToYaml(hierarchy, indent) {
4317
4371
  function rolesToYaml(roles, indent) {
4318
4372
  return roles.map((r) => `${indent}- ${r}`).join("\n");
4319
4373
  }
4320
- function routesToYaml(routes, defaultRole, indent) {
4321
- return routes.map((r) => `${indent}- pattern: ${r}
4322
- ${indent} requiredRole: ${defaultRole}`).join("\n");
4374
+ function normalizeRoutes(routes, defaultRole) {
4375
+ return routes.map(
4376
+ (r) => typeof r === "string" ? { pattern: r, requiredRole: defaultRole } : { pattern: r.pattern, requiredRole: r.requiredRole ?? defaultRole }
4377
+ );
4378
+ }
4379
+ function routesToYaml(routes, indent) {
4380
+ return routes.map((r) => `${indent}- pattern: ${r.pattern}
4381
+ ${indent} requiredRole: ${r.requiredRole}`).join("\n");
4323
4382
  }
4324
4383
  function detectNextMajorVersion(cwd) {
4325
4384
  try {
@@ -4363,9 +4422,10 @@ function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy
4363
4422
  enabled: ${auditEnabled},
4364
4423
  retentionDays: ${auditRetentionDays},
4365
4424
  },`;
4366
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4425
+ const patterns = protectedRoutes.map((r) => r.pattern);
4426
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4367
4427
  const configBlock = `export const config = {
4368
- matcher: ${JSON.stringify(protectedRoutes)},
4428
+ matcher: ${JSON.stringify(patterns)},
4369
4429
  };`;
4370
4430
  if (method === "cac") {
4371
4431
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
@@ -4448,9 +4508,10 @@ function generateProxyContent(method, params, roles, defaultRole, hierarchy, aud
4448
4508
  enabled: ${auditEnabled},
4449
4509
  retentionDays: ${auditRetentionDays},
4450
4510
  },`;
4451
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4511
+ const patterns = protectedRoutes.map((r) => r.pattern);
4512
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4452
4513
  const configBlock = `export const config = {
4453
- matcher: ${JSON.stringify(protectedRoutes)},
4514
+ matcher: ${JSON.stringify(patterns)},
4454
4515
  };`;
4455
4516
  if (method === "cac") {
4456
4517
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
@@ -4559,10 +4620,7 @@ ${hierarchyToYaml(hierarchy, " ")}`;
4559
4620
  const auditSection = ` audit:
4560
4621
  enabled: ${auditEnabled}
4561
4622
  retentionDays: ${auditRetentionDays}`;
4562
- const routesSection = ` protectedRoutes:
4563
- ${routesToYaml(protectedRoutes, " ", defaultRole)}`.replace(/\n\s+,/g, "");
4564
- const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4565
- requiredRole: ${defaultRole}`).join("\n");
4623
+ const routeLines = routesToYaml(protectedRoutes, " ");
4566
4624
  const providerLine = params.provider ? ` provider: ${params.provider}
4567
4625
  ` : "";
4568
4626
  if (method === "cac") {
@@ -4628,9 +4686,10 @@ function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hi
4628
4686
  enabled: ${auditEnabled},
4629
4687
  retentionDays: ${auditRetentionDays},
4630
4688
  },`;
4631
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4689
+ const patterns = protectedRoutes.map((r) => r.pattern);
4690
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4632
4691
  const configBlock = `export const config = {
4633
- matcher: ${JSON.stringify(protectedRoutes)},
4692
+ matcher: ${JSON.stringify(patterns)},
4634
4693
  };`;
4635
4694
  const devHeader = [
4636
4695
  "// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
@@ -4717,9 +4776,10 @@ function generateDevOnlyProxyContent(method, params, roles, defaultRole, hierarc
4717
4776
  enabled: ${auditEnabled},
4718
4777
  retentionDays: ${auditRetentionDays},
4719
4778
  },`;
4720
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4779
+ const patterns = protectedRoutes.map((r) => r.pattern);
4780
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4721
4781
  const configBlock = `export const config = {
4722
- matcher: ${JSON.stringify(protectedRoutes)},
4782
+ matcher: ${JSON.stringify(patterns)},
4723
4783
  };`;
4724
4784
  const devHeader = [
4725
4785
  "// proxy.ts -- generated by @stackwright-pro/auth (Next.js >=16, dev-only mock)",
@@ -4806,8 +4866,8 @@ ${hierarchyToYaml(hierarchy, " ")}`;
4806
4866
  const auditSection = ` audit:
4807
4867
  enabled: ${auditEnabled}
4808
4868
  retentionDays: ${auditRetentionDays}`;
4809
- const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4810
- requiredRole: ${defaultRole}`).join("\n");
4869
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r.pattern}
4870
+ requiredRole: ${r.requiredRole}`).join("\n");
4811
4871
  const providerLine = params.provider ? ` provider: ${params.provider}
4812
4872
  ` : "";
4813
4873
  if (method === "cac") {
@@ -4940,6 +5000,7 @@ async function configureAuthHandler(params, cwd) {
4940
5000
  } = params;
4941
5001
  const roles = rbacRoles;
4942
5002
  const defaultRole = params.rbacDefaultRole ?? roles[roles.length - 1];
5003
+ const normalizedRoutes = normalizeRoutes(protectedRoutes, defaultRole);
4943
5004
  const hierarchy = buildHierarchy(roles);
4944
5005
  const detectedVersion = params.nextMajorVersion ?? detectNextMajorVersion(cwd);
4945
5006
  const useProxy = (detectedVersion ?? 0) >= 16;
@@ -4976,7 +5037,7 @@ async function configureAuthHandler(params, cwd) {
4976
5037
  hierarchy,
4977
5038
  auditEnabled,
4978
5039
  auditRetentionDays,
4979
- protectedRoutes
5040
+ normalizedRoutes
4980
5041
  ) : generateDevOnlyMiddlewareContent(
4981
5042
  effectiveMethod,
4982
5043
  params,
@@ -4985,7 +5046,7 @@ async function configureAuthHandler(params, cwd) {
4985
5046
  hierarchy,
4986
5047
  auditEnabled,
4987
5048
  auditRetentionDays,
4988
- protectedRoutes
5049
+ normalizedRoutes
4989
5050
  ) : useProxy ? generateProxyContent(
4990
5051
  effectiveMethod,
4991
5052
  params,
@@ -4994,7 +5055,7 @@ async function configureAuthHandler(params, cwd) {
4994
5055
  hierarchy,
4995
5056
  auditEnabled,
4996
5057
  auditRetentionDays,
4997
- protectedRoutes
5058
+ normalizedRoutes
4998
5059
  ) : generateMiddlewareContent(
4999
5060
  effectiveMethod,
5000
5061
  params,
@@ -5003,7 +5064,7 @@ async function configureAuthHandler(params, cwd) {
5003
5064
  hierarchy,
5004
5065
  auditEnabled,
5005
5066
  auditRetentionDays,
5006
- protectedRoutes
5067
+ normalizedRoutes
5007
5068
  );
5008
5069
  (0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, conventionFile), authFileContent, "utf8");
5009
5070
  filesWritten.push(conventionFile);
@@ -5101,7 +5162,7 @@ async function configureAuthHandler(params, cwd) {
5101
5162
  hierarchy,
5102
5163
  auditEnabled,
5103
5164
  auditRetentionDays,
5104
- protectedRoutes,
5165
+ normalizedRoutes,
5105
5166
  useProxy
5106
5167
  ) : generateYamlBlock(
5107
5168
  effectiveMethod,
@@ -5111,7 +5172,7 @@ async function configureAuthHandler(params, cwd) {
5111
5172
  hierarchy,
5112
5173
  auditEnabled,
5113
5174
  auditRetentionDays,
5114
- protectedRoutes,
5175
+ normalizedRoutes,
5115
5176
  useProxy
5116
5177
  );
5117
5178
  const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
@@ -5200,8 +5261,15 @@ function registerAuthTools(server2) {
5200
5261
  // Audit
5201
5262
  auditEnabled: boolCoerce(import_zod15.z.boolean().optional()),
5202
5263
  auditRetentionDays: numCoerce(import_zod15.z.number().int().positive().optional()),
5203
- // Routes
5204
- protectedRoutes: jsonCoerce(import_zod15.z.array(import_zod15.z.string()).optional()),
5264
+ // Routes — accepts bare strings (backward-compat) or per-route objects (swp-owu0)
5265
+ protectedRoutes: jsonCoerce(
5266
+ import_zod15.z.array(
5267
+ import_zod15.z.union([
5268
+ import_zod15.z.string(),
5269
+ import_zod15.z.object({ pattern: import_zod15.z.string(), requiredRole: import_zod15.z.string().optional() })
5270
+ ])
5271
+ ).optional()
5272
+ ),
5205
5273
  // Injection for tests
5206
5274
  _cwd: import_zod15.z.string().optional(),
5207
5275
  devOnly: boolCoerce(import_zod15.z.boolean().optional()),
@@ -5226,7 +5294,7 @@ var _checksums = /* @__PURE__ */ new Map([
5226
5294
  ],
5227
5295
  [
5228
5296
  "stackwright-pro-auth-otter.json",
5229
- "07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3"
5297
+ "643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8"
5230
5298
  ],
5231
5299
  [
5232
5300
  "stackwright-pro-dashboard-otter.json",
@@ -5248,6 +5316,10 @@ var _checksums = /* @__PURE__ */ new Map([
5248
5316
  "stackwright-pro-foreman-otter.json",
5249
5317
  "438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
5250
5318
  ],
5319
+ [
5320
+ "stackwright-pro-form-wizard-otter.json",
5321
+ "3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60"
5322
+ ],
5251
5323
  [
5252
5324
  "stackwright-pro-geo-otter.json",
5253
5325
  "2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
@@ -5268,10 +5340,6 @@ var _checksums = /* @__PURE__ */ new Map([
5268
5340
  "stackwright-pro-theme-otter.json",
5269
5341
  "fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
5270
5342
  ],
5271
- [
5272
- "stackwright-pro-workflow-otter.json",
5273
- "5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8"
5274
- ],
5275
5343
  [
5276
5344
  "stackwright-services-otter.json",
5277
5345
  "c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
@@ -5915,7 +5983,7 @@ function buildTypeSchemaSummary() {
5915
5983
  "TransitionConditionSchema",
5916
5984
  "PersistenceSchema"
5917
5985
  ],
5918
- otter: "stackwright-pro-workflow-otter",
5986
+ otter: "stackwright-pro-form-wizard-otter",
5919
5987
  artifactKey: "workflowConfig"
5920
5988
  },
5921
5989
  auth: {
@@ -6487,7 +6555,7 @@ var package_default = {
6487
6555
  "test:coverage": "vitest run --coverage"
6488
6556
  },
6489
6557
  name: "@stackwright-pro/mcp",
6490
- version: "0.2.0-alpha.90",
6558
+ version: "0.2.0-alpha.92",
6491
6559
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
6492
6560
  license: "SEE LICENSE IN LICENSE",
6493
6561
  main: "./dist/server.js",