@cybedefend/vibedefend 1.5.0 → 1.6.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.
Files changed (246) hide show
  1. package/README.md +67 -2
  2. package/dist/api/gateway-request.js +49 -0
  3. package/dist/api/gateway-request.js.map +1 -0
  4. package/dist/api/projects.js +67 -0
  5. package/dist/api/projects.js.map +1 -1
  6. package/dist/api/repository-link.js +157 -0
  7. package/dist/api/repository-link.js.map +1 -0
  8. package/dist/auth/auth-store.js +107 -25
  9. package/dist/auth/auth-store.js.map +1 -1
  10. package/dist/auth/auth.js +21 -0
  11. package/dist/auth/auth.js.map +1 -1
  12. package/dist/clients/cursor.js +5 -1
  13. package/dist/clients/cursor.js.map +1 -1
  14. package/dist/clients/vscode.js +38 -29
  15. package/dist/clients/vscode.js.map +1 -1
  16. package/dist/commands/mcp/allow.js +219 -0
  17. package/dist/commands/mcp/allow.js.map +1 -0
  18. package/dist/commands/mcp/connect.js +337 -0
  19. package/dist/commands/mcp/connect.js.map +1 -0
  20. package/dist/commands/mcp/index.js +170 -0
  21. package/dist/commands/mcp/index.js.map +1 -0
  22. package/dist/commands/mcp/proxy.js +264 -0
  23. package/dist/commands/mcp/proxy.js.map +1 -0
  24. package/dist/commands/mcp/scan.js +349 -0
  25. package/dist/commands/mcp/scan.js.map +1 -0
  26. package/dist/commands/mcp/sync.js +215 -0
  27. package/dist/commands/mcp/sync.js.map +1 -0
  28. package/dist/config.js +6 -0
  29. package/dist/config.js.map +1 -1
  30. package/dist/diagnostics.js +78 -12
  31. package/dist/diagnostics.js.map +1 -1
  32. package/dist/doctor.js +19 -0
  33. package/dist/doctor.js.map +1 -1
  34. package/dist/guards-evaluator/analysis-types.js +27 -0
  35. package/dist/guards-evaluator/analysis-types.js.map +1 -0
  36. package/dist/guards-evaluator/command-analysis.js +572 -0
  37. package/dist/guards-evaluator/command-analysis.js.map +1 -0
  38. package/dist/guards-evaluator/command-view.js +586 -0
  39. package/dist/guards-evaluator/command-view.js.map +1 -0
  40. package/dist/guards-evaluator/env.js +11 -0
  41. package/dist/guards-evaluator/env.js.map +1 -0
  42. package/dist/guards-evaluator/evaluate.js +91 -41
  43. package/dist/guards-evaluator/evaluate.js.map +1 -1
  44. package/dist/guards-evaluator/index.js +2 -1
  45. package/dist/guards-evaluator/index.js.map +1 -1
  46. package/dist/guards-evaluator/inline-code.js +259 -0
  47. package/dist/guards-evaluator/inline-code.js.map +1 -0
  48. package/dist/guards-evaluator/normalize.js +716 -0
  49. package/dist/guards-evaluator/normalize.js.map +1 -0
  50. package/dist/guards-evaluator/preprocess.js +13 -5
  51. package/dist/guards-evaluator/preprocess.js.map +1 -1
  52. package/dist/guards-evaluator/redact.js +19 -0
  53. package/dist/guards-evaluator/redact.js.map +1 -1
  54. package/dist/guards-evaluator/regex.js +25 -3
  55. package/dist/guards-evaluator/regex.js.map +1 -1
  56. package/dist/guards-evaluator/safe-commands.js +155 -0
  57. package/dist/guards-evaluator/safe-commands.js.map +1 -0
  58. package/dist/guards-evaluator/triage.js +315 -0
  59. package/dist/guards-evaluator/triage.js.map +1 -0
  60. package/dist/hook-runner.js +24679 -5079
  61. package/dist/hooks/install.js +38 -0
  62. package/dist/hooks/install.js.map +1 -1
  63. package/dist/hooks/runtime/config.js +38 -0
  64. package/dist/hooks/runtime/config.js.map +1 -1
  65. package/dist/hooks/runtime/emit.js +94 -61
  66. package/dist/hooks/runtime/emit.js.map +1 -1
  67. package/dist/hooks/runtime/fetch-rules.js +22 -7
  68. package/dist/hooks/runtime/fetch-rules.js.map +1 -1
  69. package/dist/hooks/runtime/gateway-http-error.js +34 -0
  70. package/dist/hooks/runtime/gateway-http-error.js.map +1 -0
  71. package/dist/hooks/runtime/guard-rules-cache.js +139 -17
  72. package/dist/hooks/runtime/guard-rules-cache.js.map +1 -1
  73. package/dist/hooks/runtime/guard-violations-buffer.js +321 -48
  74. package/dist/hooks/runtime/guard-violations-buffer.js.map +1 -1
  75. package/dist/hooks/runtime/judge-cache.js +6 -0
  76. package/dist/hooks/runtime/judge-cache.js.map +1 -1
  77. package/dist/hooks/runtime/judge-triage.js +193 -0
  78. package/dist/hooks/runtime/judge-triage.js.map +1 -0
  79. package/dist/hooks/runtime/judge.js +7 -1
  80. package/dist/hooks/runtime/judge.js.map +1 -1
  81. package/dist/hooks/runtime/pre-compact.js +12 -6
  82. package/dist/hooks/runtime/pre-compact.js.map +1 -1
  83. package/dist/hooks/runtime/resolve.js +90 -8
  84. package/dist/hooks/runtime/resolve.js.map +1 -1
  85. package/dist/hooks/runtime/retry-policy.js +57 -0
  86. package/dist/hooks/runtime/retry-policy.js.map +1 -0
  87. package/dist/hooks/runtime/review-sessions.js +74 -0
  88. package/dist/hooks/runtime/review-sessions.js.map +1 -0
  89. package/dist/hooks/runtime/script-resolve.js +695 -0
  90. package/dist/hooks/runtime/script-resolve.js.map +1 -0
  91. package/dist/hooks/runtime/session-activation.js +5 -1
  92. package/dist/hooks/runtime/session-activation.js.map +1 -1
  93. package/dist/hooks/runtime/session-review.js +163 -45
  94. package/dist/hooks/runtime/session-review.js.map +1 -1
  95. package/dist/hooks/runtime/session-start.js +2 -1
  96. package/dist/hooks/runtime/session-start.js.map +1 -1
  97. package/dist/hooks/runtime/session-writes.js +232 -0
  98. package/dist/hooks/runtime/session-writes.js.map +1 -0
  99. package/dist/hooks/runtime/sniff.js +26 -7
  100. package/dist/hooks/runtime/sniff.js.map +1 -1
  101. package/dist/hooks/runtime/target-resolve.js +336 -0
  102. package/dist/hooks/runtime/target-resolve.js.map +1 -0
  103. package/dist/hooks/runtime/unmapped-tools.js +93 -0
  104. package/dist/hooks/runtime/unmapped-tools.js.map +1 -0
  105. package/dist/hooks/runtime/usage-block.js +4 -0
  106. package/dist/hooks/runtime/usage-block.js.map +1 -1
  107. package/dist/hooks/runtime/user-prompt-submit.js +2 -1
  108. package/dist/hooks/runtime/user-prompt-submit.js.map +1 -1
  109. package/dist/hooks/runtime/wire-text.js +41 -0
  110. package/dist/hooks/runtime/wire-text.js.map +1 -0
  111. package/dist/hooks/runtime/work-dir.js +84 -0
  112. package/dist/hooks/runtime/work-dir.js.map +1 -0
  113. package/dist/index.js +45 -12
  114. package/dist/index.js.map +1 -1
  115. package/dist/install.js +174 -11
  116. package/dist/install.js.map +1 -1
  117. package/dist/link.js +74 -48
  118. package/dist/link.js.map +1 -1
  119. package/dist/login.js +77 -11
  120. package/dist/login.js.map +1 -1
  121. package/dist/mcp/auth/agent-sign-ins.js +174 -0
  122. package/dist/mcp/auth/agent-sign-ins.js.map +1 -0
  123. package/dist/mcp/auth/callback.js +200 -0
  124. package/dist/mcp/auth/callback.js.map +1 -0
  125. package/dist/mcp/auth/connect.js +392 -0
  126. package/dist/mcp/auth/connect.js.map +1 -0
  127. package/dist/mcp/auth/discovery.js +399 -0
  128. package/dist/mcp/auth/discovery.js.map +1 -0
  129. package/dist/mcp/auth/oauth.js +326 -0
  130. package/dist/mcp/auth/oauth.js.map +1 -0
  131. package/dist/mcp/auth/probe.js +128 -0
  132. package/dist/mcp/auth/probe.js.map +1 -0
  133. package/dist/mcp/auth/secrets.js +446 -0
  134. package/dist/mcp/auth/secrets.js.map +1 -0
  135. package/dist/mcp/auth/store.js +260 -0
  136. package/dist/mcp/auth/store.js.map +1 -0
  137. package/dist/mcp/auth/tokenSource.js +55 -0
  138. package/dist/mcp/auth/tokenSource.js.map +1 -0
  139. package/dist/mcp/config/atomicWrite.js +98 -0
  140. package/dist/mcp/config/atomicWrite.js.map +1 -0
  141. package/dist/mcp/config/clients.js +171 -0
  142. package/dist/mcp/config/clients.js.map +1 -0
  143. package/dist/mcp/config/cybedefend-servers.js +48 -0
  144. package/dist/mcp/config/cybedefend-servers.js.map +1 -0
  145. package/dist/mcp/config/discover.js +114 -0
  146. package/dist/mcp/config/discover.js.map +1 -0
  147. package/dist/mcp/config/hook.js +123 -0
  148. package/dist/mcp/config/hook.js.map +1 -0
  149. package/dist/mcp/config/index.js +11 -0
  150. package/dist/mcp/config/index.js.map +1 -0
  151. package/dist/mcp/config/install.js +132 -0
  152. package/dist/mcp/config/install.js.map +1 -0
  153. package/dist/mcp/config/installed.js +37 -0
  154. package/dist/mcp/config/installed.js.map +1 -0
  155. package/dist/mcp/config/jsonEdit.js +119 -0
  156. package/dist/mcp/config/jsonEdit.js.map +1 -0
  157. package/dist/mcp/config/lock.js +102 -0
  158. package/dist/mcp/config/lock.js.map +1 -0
  159. package/dist/mcp/config/proxyBinary.js +211 -0
  160. package/dist/mcp/config/proxyBinary.js.map +1 -0
  161. package/dist/mcp/config/rewrite.js +388 -0
  162. package/dist/mcp/config/rewrite.js.map +1 -0
  163. package/dist/mcp/config/types.js +34 -0
  164. package/dist/mcp/config/types.js.map +1 -0
  165. package/dist/mcp/proxy/audit.js +83 -0
  166. package/dist/mcp/proxy/audit.js.map +1 -0
  167. package/dist/mcp/proxy/authInterceptor.js +119 -0
  168. package/dist/mcp/proxy/authInterceptor.js.map +1 -0
  169. package/dist/mcp/proxy/expand.js +93 -0
  170. package/dist/mcp/proxy/expand.js.map +1 -0
  171. package/dist/mcp/proxy/inject.js +397 -0
  172. package/dist/mcp/proxy/inject.js.map +1 -0
  173. package/dist/mcp/proxy/intercept.js +37 -0
  174. package/dist/mcp/proxy/intercept.js.map +1 -0
  175. package/dist/mcp/proxy/jsonrpc.js +153 -0
  176. package/dist/mcp/proxy/jsonrpc.js.map +1 -0
  177. package/dist/mcp/proxy/log.js +109 -0
  178. package/dist/mcp/proxy/log.js.map +1 -0
  179. package/dist/mcp/proxy/paths.js +53 -0
  180. package/dist/mcp/proxy/paths.js.map +1 -0
  181. package/dist/mcp/proxy/proxy.js +510 -0
  182. package/dist/mcp/proxy/proxy.js.map +1 -0
  183. package/dist/mcp/proxy/spec.js +203 -0
  184. package/dist/mcp/proxy/spec.js.map +1 -0
  185. package/dist/mcp/proxy/sse.js +77 -0
  186. package/dist/mcp/proxy/sse.js.map +1 -0
  187. package/dist/mcp/proxy/upstream.js +2 -0
  188. package/dist/mcp/proxy/upstream.js.map +1 -0
  189. package/dist/mcp/proxy/upstreamHttp.js +499 -0
  190. package/dist/mcp/proxy/upstreamHttp.js.map +1 -0
  191. package/dist/mcp/proxy/upstreamStdio.js +188 -0
  192. package/dist/mcp/proxy/upstreamStdio.js.map +1 -0
  193. package/dist/mcp/proxy/upstreams.js +143 -0
  194. package/dist/mcp/proxy/upstreams.js.map +1 -0
  195. package/dist/mcp/proxy/verifyInterceptor.js +559 -0
  196. package/dist/mcp/proxy/verifyInterceptor.js.map +1 -0
  197. package/dist/mcp/proxy/windowsCommand.js +99 -0
  198. package/dist/mcp/proxy/windowsCommand.js.map +1 -0
  199. package/dist/mcp/scan/args.js +228 -0
  200. package/dist/mcp/scan/args.js.map +1 -0
  201. package/dist/mcp/scan/results.js +253 -0
  202. package/dist/mcp/scan/results.js.map +1 -0
  203. package/dist/mcp/scan/types.js +40 -0
  204. package/dist/mcp/scan/types.js.map +1 -0
  205. package/dist/mcp/verify/api.js +475 -0
  206. package/dist/mcp/verify/api.js.map +1 -0
  207. package/dist/mcp/verify/backend.js +143 -0
  208. package/dist/mcp/verify/backend.js.map +1 -0
  209. package/dist/mcp/verify/cache.js +182 -0
  210. package/dist/mcp/verify/cache.js.map +1 -0
  211. package/dist/mcp/verify/contract-errors.js +198 -0
  212. package/dist/mcp/verify/contract-errors.js.map +1 -0
  213. package/dist/mcp/verify/digest.js +91 -0
  214. package/dist/mcp/verify/digest.js.map +1 -0
  215. package/dist/mcp/verify/factory.js +56 -0
  216. package/dist/mcp/verify/factory.js.map +1 -0
  217. package/dist/mcp/verify/identity.js +173 -0
  218. package/dist/mcp/verify/identity.js.map +1 -0
  219. package/dist/mcp/verify/policy-store.js +63 -0
  220. package/dist/mcp/verify/policy-store.js.map +1 -0
  221. package/dist/mcp/verify/policy.js +158 -0
  222. package/dist/mcp/verify/policy.js.map +1 -0
  223. package/dist/mcp/verify/pool.js +27 -0
  224. package/dist/mcp/verify/pool.js.map +1 -0
  225. package/dist/mcp/verify/token-refresh.js +44 -0
  226. package/dist/mcp/verify/token-refresh.js.map +1 -0
  227. package/dist/mcp/verify/token.js +67 -0
  228. package/dist/mcp/verify/token.js.map +1 -0
  229. package/dist/mcp/verify/verifier.js +1009 -0
  230. package/dist/mcp/verify/verifier.js.map +1 -0
  231. package/dist/org-gate.js +1 -1
  232. package/dist/org-gate.js.map +1 -1
  233. package/dist/org.js +146 -0
  234. package/dist/org.js.map +1 -0
  235. package/dist/project-create.js +129 -0
  236. package/dist/project-create.js.map +1 -0
  237. package/dist/prompts.js +21 -0
  238. package/dist/prompts.js.map +1 -1
  239. package/dist/re2.wasm +0 -0
  240. package/dist/status.js +61 -0
  241. package/dist/status.js.map +1 -1
  242. package/dist/uninstall.js +49 -4
  243. package/dist/uninstall.js.map +1 -1
  244. package/dist/utils.js +62 -6
  245. package/dist/utils.js.map +1 -1
  246. package/package.json +10 -3
@@ -0,0 +1,182 @@
1
+ /**
2
+ * What we already know about a server and its tools, on this machine.
3
+ *
4
+ * One JSON file per server under `~/.cybedefend/mcp/cache/`. One file, not
5
+ * one database, for a reason the plan states: the agent spawns one proxy per
6
+ * MCP server and they all start at once, so a shared file would be five
7
+ * processes writing the same document within the same second. Per-server
8
+ * files never collide, and the interface below is narrow enough that moving
9
+ * to SQLite later is a swap rather than a rewrite.
10
+ *
11
+ * ─── The two rules that matter ──────────────────────────────────────────────
12
+ *
13
+ * - `ok` expires after seven days. A tool that was clean last week is
14
+ * probably still clean, and re-asking a backend about it on every session
15
+ * would make the common case the expensive one. Seven days bounds how
16
+ * stale that assumption can get.
17
+ * - `malicious` NEVER expires. Nothing about the passage of time makes a
18
+ * known-bad tool good again, and an expiry would silently reopen a server
19
+ * the user was protected from. Only an explicit `vibedefend allow` clears
20
+ * it.
21
+ *
22
+ * Overrides live in the same file as the verdicts they override, so a server
23
+ * the user allowed cannot end up half-allowed because two files disagreed.
24
+ */
25
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync } from 'node:fs';
26
+ import { join } from 'node:path';
27
+ import { writeFileAtomic } from '../config/atomicWrite.js';
28
+ import { mcpCacheDir } from '../proxy/paths.js';
29
+ import { serverIdFileName } from './identity.js';
30
+ import { isServerAction } from './policy.js';
31
+ /** Seven days on `ok`; `malicious` never expires. */
32
+ export const OK_TTL_MS = 7 * 24 * 60 * 60 * 1000;
33
+ export function sameStamp(a, b) {
34
+ if (a === null || b === null)
35
+ return a === b;
36
+ return a.mtimeMs === b.mtimeMs && a.size === b.size;
37
+ }
38
+ export function emptyCacheFile(serverId, now) {
39
+ const stamp = now.toISOString();
40
+ return {
41
+ version: 1,
42
+ serverId,
43
+ server: { firstSeen: stamp, lastSeen: stamp, verdict: 'unknown' },
44
+ tools: {},
45
+ };
46
+ }
47
+ export function createJsonVerifyCache(options = {}) {
48
+ const dir = options.dir ?? mcpCacheDir(options.env ?? process.env);
49
+ const pathFor = (serverId) => join(dir, serverIdFileName(serverId));
50
+ return {
51
+ read(serverId) {
52
+ let text;
53
+ try {
54
+ text = readFileSync(pathFor(serverId), 'utf8');
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ return parseCacheFile(text, serverId);
60
+ },
61
+ write(file) {
62
+ // 0600: a `reason` may quote a tool description, which is the content
63
+ // of a server the user has not decided to trust yet.
64
+ writeFileAtomic(pathFor(file.serverId), `${JSON.stringify(file, null, 2)}\n`, { mode: 0o600 });
65
+ },
66
+ stat(serverId) {
67
+ const stats = statSync(pathFor(serverId), { throwIfNoEntry: false });
68
+ return stats ? { mtimeMs: stats.mtimeMs, size: stats.size } : null;
69
+ },
70
+ list() {
71
+ if (!existsSync(dir))
72
+ return [];
73
+ const files = [];
74
+ for (const name of readdirSync(dir)) {
75
+ if (!name.endsWith('.json'))
76
+ continue;
77
+ try {
78
+ const parsed = parseCacheFile(readFileSync(join(dir, name), 'utf8'), '');
79
+ if (parsed)
80
+ files.push(parsed);
81
+ }
82
+ catch {
83
+ /* a corrupt file is one server we cannot report, not a failure */
84
+ }
85
+ }
86
+ return files;
87
+ },
88
+ };
89
+ }
90
+ /**
91
+ * Reads a cache file, or `null`.
92
+ *
93
+ * A cache is a performance aid, never a source of truth for something we
94
+ * could not otherwise obtain, so a corrupt one reads as "nothing known" and
95
+ * the next verification rewrites it. The one thing NOT dropped on the way is
96
+ * a `malicious` verdict: see `mergeVerdict`.
97
+ */
98
+ export function parseCacheFile(text, serverId) {
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(text);
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ if (typeof parsed !== 'object' || parsed === null)
107
+ return null;
108
+ const typed = parsed;
109
+ if (typed.version !== 1 || typeof typed.server !== 'object' || !typed.server)
110
+ return null;
111
+ const tools = {};
112
+ for (const [digest, record] of Object.entries(typed.tools ?? {})) {
113
+ if (typeof record !== 'object' || record === null)
114
+ continue;
115
+ const entry = record;
116
+ if (!isVerdict(entry.verdict))
117
+ continue;
118
+ // An action this binary cannot read is no action: the verdict decides.
119
+ if (entry.action !== undefined && !isServerAction(entry.action))
120
+ delete entry.action;
121
+ tools[digest] = entry;
122
+ }
123
+ return {
124
+ version: 1,
125
+ serverId: typed.serverId || serverId,
126
+ server: {
127
+ firstSeen: typed.server.firstSeen ?? new Date(0).toISOString(),
128
+ lastSeen: typed.server.lastSeen ?? new Date(0).toISOString(),
129
+ verdict: isVerdict(typed.server.verdict) ? typed.server.verdict : 'unknown',
130
+ reason: typed.server.reason,
131
+ verdictAt: typed.server.verdictAt,
132
+ action: isServerAction(typed.server.action) ? typed.server.action : undefined,
133
+ override: typed.server.override === 'allow' || typed.server.override === 'deny' ? typed.server.override : undefined,
134
+ overrideAt: typed.server.overrideAt,
135
+ toolDigests: Array.isArray(typed.server.toolDigests)
136
+ ? typed.server.toolDigests.filter((digest) => typeof digest === 'string')
137
+ : undefined,
138
+ },
139
+ tools,
140
+ };
141
+ }
142
+ function isVerdict(value) {
143
+ return value === 'ok' || value === 'suspicious' || value === 'malicious' || value === 'pending' || value === 'unknown';
144
+ }
145
+ /** Has this record aged out? `malicious` never does. */
146
+ export function isExpired(record, now, ttlMs = OK_TTL_MS) {
147
+ if (record.verdict === 'malicious')
148
+ return false;
149
+ if (record.verdict !== 'ok')
150
+ return true;
151
+ if (!record.verdictAt)
152
+ return true;
153
+ const at = Date.parse(record.verdictAt);
154
+ return Number.isNaN(at) || now.getTime() - at > ttlMs;
155
+ }
156
+ /**
157
+ * Combines what we knew with what we just learned.
158
+ *
159
+ * The rule the previous generation arrived at and this keeps: a FRESH answer
160
+ * that says "not known" never overwrites a fact we already have, and a fresh
161
+ * answer that DOES know always wins. Without the first half, one backend
162
+ * outage would quietly clear every `malicious` on the machine; without the
163
+ * second, a tool that has just been reclassified would stay wrong for a week.
164
+ */
165
+ export function mergeVerdict(previous, next) {
166
+ if (!previous)
167
+ return next;
168
+ // The submission mark is a fact about what WE did, not a verdict: it
169
+ // survives whatever the fresh answer says, or the same digest would be
170
+ // bought again after every lookup that still came back "unknown".
171
+ const submittedAt = next.submittedAt ?? previous.submittedAt;
172
+ const marked = (record) => (submittedAt ? { ...record, submittedAt } : record);
173
+ if (next.verdict === 'unknown' || next.verdict === 'pending') {
174
+ return previous.verdict === 'unknown' || previous.verdict === 'pending' ? marked(next) : marked(previous);
175
+ }
176
+ return marked(next);
177
+ }
178
+ export function ensureCacheDir(options = {}) {
179
+ const dir = options.dir ?? mcpCacheDir(options.env ?? process.env);
180
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
181
+ }
182
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../../src/mcp/verify/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ /**
2
+ * What the backend refused, and since when.
3
+ *
4
+ * ── TWO FAMILIES OF FAILURE, AND ONLY ONE OF THEM IS FAIL-OPEN ──────────────
5
+ *
6
+ * A network error, a timeout or a 5xx means the backend could not answer. The
7
+ * proxy forwards the tool list, records `pending`, and says nothing to the
8
+ * agent: an outage must not turn every MCP server off, and the user cannot do
9
+ * anything about it anyway.
10
+ *
11
+ * A 400, 409 or 422 means the backend answered, and refused what we sent. That
12
+ * is not an outage, it is a DISAGREEMENT ABOUT THE CONTRACT — our request is
13
+ * malformed, or our digest formula is not one this server can compute. Filing
14
+ * it as `pending` would be a lie in the one direction that matters: `pending`
15
+ * says "we will retry and it will work", and retrying cannot help. Worse, it
16
+ * is indistinguishable from an ordinary outage, so a client that has stopped
17
+ * being able to verify ANYTHING looks exactly like a client waiting on a slow
18
+ * backend — for as long as nobody looks.
19
+ *
20
+ * ── WHY ITS OWN FILE, AND NOT THE PER-SERVER CACHE ─────────────────────────
21
+ *
22
+ * A contract error is not a property of the MCP server we happened to be
23
+ * asking about when it happened: it is a property of the client ↔ backend
24
+ * pair. A `409 DIGEST_VERSION_UNSUPPORTED` hits EVERY server, and writing it
25
+ * into the cache of whichever one we were looking at would draw a false map —
26
+ * one server "at fault", the others "clean", when in fact nothing is being
27
+ * verified anywhere.
28
+ *
29
+ * ── WHY SESSIONS AND NOT OCCURRENCES ───────────────────────────────────────
30
+ *
31
+ * The alert threshold is "it survived a restart". Counting occurrences would
32
+ * fire on a single session that listed three servers; counting distinct
33
+ * sessions asks the only question worth asking — did restarting fix it. Only
34
+ * the LAST session id is kept, not the set: the count is the answer, the list
35
+ * would only grow.
36
+ */
37
+ import { mkdirSync, readFileSync } from 'node:fs';
38
+ import { join } from 'node:path';
39
+ import { writeFileAtomic } from '../config/atomicWrite.js';
40
+ import { sanitise } from '../proxy/inject.js';
41
+ import { mcpHomeDir } from '../proxy/paths.js';
42
+ /**
43
+ * The statuses that mean "your request is the problem".
44
+ *
45
+ * `400` malformed, `422` unprocessable, `409` a state disagreement — which is
46
+ * what an unsupported digest version is. `403` and `404` joined them with the
47
+ * org-scoped routes: the organization is in the PATH, and "not a member" or
48
+ * "no such organization" is an answer about what this machine selected, not
49
+ * about the backend's health — retrying with the same organization cannot
50
+ * help. Everything else, 429 and 5xx included, describes something that
51
+ * happened to the BACKEND and stays fail-open.
52
+ */
53
+ export const CONTRACT_ERROR_STATUSES = [400, 403, 404, 409, 422];
54
+ export function isContractErrorStatus(status) {
55
+ return CONTRACT_ERROR_STATUSES.includes(status);
56
+ }
57
+ /**
58
+ * ── `code` AND `supported` COME OFF THE WIRE ────────────────────────────────
59
+ *
60
+ * They are fields of an HTTP response. They are written to disk here, and
61
+ * rendered into the user's terminal by `vibedefend status`. This repository
62
+ * already has a doctrine for that — every string that came from a server goes
63
+ * through `sanitise` before it reaches a reader — and this path had missed it
64
+ * because the fields look like metadata rather than prose.
65
+ *
66
+ * What it costs if forgotten: an ESC[2J clears the screen, a carriage return
67
+ * rewrites the line above, a newline forges a second status line. `apiBase`
68
+ * is gated by the origin allowlist, true — but "the source is trusted" is
69
+ * exactly the reasoning that cost the two defects found in the Phase 4
70
+ * end-of-session review.
71
+ *
72
+ * The bounds are the other half: the SENDER, not us, would otherwise choose
73
+ * how much we write to the user's disk on every single occurrence.
74
+ */
75
+ export const MAX_CONTRACT_CODE_CHARS = 64;
76
+ export const MAX_SUPPORTED_VERSIONS = 8;
77
+ export const MAX_SUPPORTED_VERSION_CHARS = 16;
78
+ /** One code, safe to store and to print. */
79
+ function safeCode(code) {
80
+ if (code === undefined)
81
+ return undefined;
82
+ const clean = sanitise(code, MAX_CONTRACT_CODE_CHARS);
83
+ return clean.length > 0 ? clean : undefined;
84
+ }
85
+ /** The supported-version list, bounded in both directions and sanitised. */
86
+ function safeSupported(supported) {
87
+ if (supported === undefined)
88
+ return undefined;
89
+ const clean = supported
90
+ .slice(0, MAX_SUPPORTED_VERSIONS)
91
+ .map((value) => sanitise(value, MAX_SUPPORTED_VERSION_CHARS))
92
+ .filter((value) => value.length > 0);
93
+ return clean.length > 0 ? clean : undefined;
94
+ }
95
+ /** Distinct sessions before the agent is told. See the header. */
96
+ export const PERSISTENT_AFTER_SESSIONS = 2;
97
+ export function contractErrorsFile(env = process.env) {
98
+ return join(mcpHomeDir(env), 'contract-errors.json');
99
+ }
100
+ /**
101
+ * The key of a record. Route + status + code, so an unsupported digest version
102
+ * and a malformed body on the same route stay two separate lines — they have
103
+ * two separate fixes.
104
+ */
105
+ function keyOf(input) {
106
+ return `${input.route}|${input.status}|${input.code ?? ''}`;
107
+ }
108
+ /**
109
+ * Reads the file, or an empty one.
110
+ *
111
+ * A malformed file reads as empty rather than throwing. Several proxies write
112
+ * this concurrently (one process per MCP server), and this is a JOURNAL: a
113
+ * corrupt journal must never be the reason a verification fails.
114
+ */
115
+ function read(env) {
116
+ try {
117
+ const parsed = JSON.parse(readFileSync(contractErrorsFile(env), 'utf8'));
118
+ if (!parsed || typeof parsed !== 'object')
119
+ return { version: 1, errors: {} };
120
+ const file = parsed;
121
+ if (!file.errors || typeof file.errors !== 'object')
122
+ return { version: 1, errors: {} };
123
+ return { version: 1, errors: file.errors };
124
+ }
125
+ catch {
126
+ return { version: 1, errors: {} };
127
+ }
128
+ }
129
+ /** Every contract error on file, worst-persisting first. */
130
+ export function listContractErrors(env = process.env) {
131
+ return Object.values(read(env).errors)
132
+ .map(({ lastSessionId: _ignored, ...record }) => record)
133
+ .sort((left, right) => right.sessions - left.sessions || left.route.localeCompare(right.route));
134
+ }
135
+ /** The ones that survived a restart — the threshold for telling the agent. */
136
+ export function persistentContractErrors(env = process.env) {
137
+ return listContractErrors(env).filter((record) => record.sessions >= PERSISTENT_AFTER_SESSIONS);
138
+ }
139
+ /**
140
+ * Files one occurrence.
141
+ *
142
+ * Best-effort by design: a failure to write must not take down the
143
+ * verification that was already failing. The caller is in a `catch`.
144
+ */
145
+ export function recordContractError(input, deps = {}) {
146
+ const env = deps.env ?? process.env;
147
+ const at = (deps.now ?? new Date()).toISOString();
148
+ // Sanitised HERE, at the one place wire data enters the record, rather than
149
+ // at each of the two places it leaves it. A guard on the way out is a guard
150
+ // the third reader forgets.
151
+ const code = safeCode(input.code);
152
+ const supported = safeSupported(input.supported);
153
+ try {
154
+ const file = read(env);
155
+ const key = keyOf({ ...input, code });
156
+ const existing = file.errors[key];
157
+ file.errors[key] = existing
158
+ ? {
159
+ ...existing,
160
+ // The code and the supported list can only get MORE precise: a
161
+ // backend that starts naming its refusal should not be ignored
162
+ // because the first occurrence was anonymous.
163
+ code: code ?? existing.code,
164
+ supported: supported ?? existing.supported,
165
+ lastSeenAt: at,
166
+ // Only a NEW session id advances the count. Three refusals inside
167
+ // one run are one session, which is the whole point.
168
+ sessions: existing.lastSessionId === input.sessionId ? existing.sessions : existing.sessions + 1,
169
+ lastSessionId: input.sessionId,
170
+ }
171
+ : {
172
+ route: input.route,
173
+ status: input.status,
174
+ code,
175
+ supported,
176
+ firstSeenAt: at,
177
+ lastSeenAt: at,
178
+ sessions: 1,
179
+ lastSessionId: input.sessionId,
180
+ };
181
+ mkdirSync(mcpHomeDir(env), { recursive: true });
182
+ writeFileAtomic(contractErrorsFile(env), `${JSON.stringify(file, null, 2)}\n`);
183
+ }
184
+ catch {
185
+ /* a journal that cannot be written is still not a reason to fail a check */
186
+ }
187
+ }
188
+ /** Clears the file. `vibedefend status` uses it once the disagreement is fixed. */
189
+ export function clearContractErrors(env = process.env) {
190
+ try {
191
+ mkdirSync(mcpHomeDir(env), { recursive: true });
192
+ writeFileAtomic(contractErrorsFile(env), `${JSON.stringify({ version: 1, errors: {} }, null, 2)}\n`);
193
+ }
194
+ catch {
195
+ /* nothing to do: the next successful call will not re-file anything */
196
+ }
197
+ }
198
+ //# sourceMappingURL=contract-errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-errors.js","sourceRoot":"","sources":["../../../src/mcp/verify/contract-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAK/C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAsB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAEpF,MAAM,UAAU,qBAAqB,CAAC,MAAc;IAClD,OAAO,uBAAuB,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAC1C,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC;AACxC,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAE9C,4CAA4C;AAC5C,SAAS,QAAQ,CAAC,IAAwB;IACxC,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,EAAE,uBAAuB,CAAC,CAAC;IACtD,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,4EAA4E;AAC5E,SAAS,aAAa,CAAC,SAA+B;IACpD,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC9C,MAAM,KAAK,GAAG,SAAS;SACpB,KAAK,CAAC,CAAC,EAAE,sBAAsB,CAAC;SAChC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,2BAA2B,CAAC,CAAC;SAC5D,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACvC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAmC3C,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,sBAAsB,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,SAAS,KAAK,CAAC,KAA4D;IACzE,OAAO,GAAG,KAAK,CAAC,KAAK,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AAC9D,CAAC;AAED;;;;;;GAMG;AACH,SAAS,IAAI,CAAC,GAAsB;IAClC,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;QAClF,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAC7E,MAAM,IAAI,GAAG,MAAqC,CAAC;QACnD,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ;YAAE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QACvF,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IACpC,CAAC;AACH,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,kBAAkB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACrE,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;SACnC,GAAG,CAAC,CAAC,EAAE,aAAa,EAAE,QAAQ,EAAE,GAAG,MAAM,EAAE,EAAE,EAAE,CAAC,MAAM,CAAC;SACvD,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;AACpG,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,wBAAwB,CACtC,MAAyB,OAAO,CAAC,GAAG;IAEpC,OAAO,kBAAkB,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,IAAI,yBAAyB,CAAC,CAAC;AAClG,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAyB,EACzB,OAA0B,EAAE;IAE5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;IACpC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAClD,4EAA4E;IAC5E,4EAA4E;IAC5E,4BAA4B;IAC5B,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,aAAa,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;QACvB,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAElC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,QAAQ;YACzB,CAAC,CAAC;gBACE,GAAG,QAAQ;gBACX,+DAA+D;gBAC/D,+DAA+D;gBAC/D,8CAA8C;gBAC9C,IAAI,EAAE,IAAI,IAAI,QAAQ,CAAC,IAAI;gBAC3B,SAAS,EAAE,SAAS,IAAI,QAAQ,CAAC,SAAS;gBAC1C,UAAU,EAAE,EAAE;gBACd,kEAAkE;gBAClE,qDAAqD;gBACrD,QAAQ,EACN,QAAQ,CAAC,aAAa,KAAK,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,GAAG,CAAC;gBACxF,aAAa,EAAE,KAAK,CAAC,SAAS;aAC/B;YACH,CAAC,CAAC;gBACE,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,IAAI;gBACJ,SAAS;gBACT,WAAW,EAAE,EAAE;gBACf,UAAU,EAAE,EAAE;gBACd,QAAQ,EAAE,CAAC;gBACX,aAAa,EAAE,KAAK,CAAC,SAAS;aAC/B,CAAC;QAEN,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,eAAe,CAAC,kBAAkB,CAAC,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;IACjF,CAAC;IAAC,MAAM,CAAC;QACP,4EAA4E;IAC9E,CAAC;AACH,CAAC;AAED,mFAAmF;AACnF,MAAM,UAAU,mBAAmB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACtE,IAAI,CAAC;QACH,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,eAAe,CACb,kBAAkB,CAAC,GAAG,CAAC,EACvB,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAC3D,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;AACH,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Maximum nesting depth accepted by the canonicalisation.
4
+ *
5
+ * The depth is attacker-controlled and the canonicalisation recurses. On this
6
+ * engine the stack overflows around 2,500 levels, while a 5,000-level schema
7
+ * weighs only ~30 KB, far below any size cap. Without a bound the resulting
8
+ * `RangeError` was neither a business error nor a shape error: it became an
9
+ * internal failure that took the whole batch down with it.
10
+ *
11
+ * 100 is two orders of magnitude above any real JSON Schema (the deepest
12
+ * schemas MCP servers produce stay around twenty levels) and twenty-five times
13
+ * below the measured overflow: the bound refuses an attack, never a
14
+ * legitimate schema.
15
+ */
16
+ export const MAX_CANONICAL_DEPTH = 100;
17
+ /**
18
+ * A caller-side shape error, not a server failure. Kept as a dedicated class
19
+ * rather than a bare `RangeError` so a controller can map it to an
20
+ * invalid-argument response, exactly as it already does for a `SyntaxError`
21
+ * on a syntactically invalid schema. A genuine stack overflow stays a failure.
22
+ */
23
+ export class ToolSchemaTooDeepError extends Error {
24
+ maxDepth;
25
+ constructor(maxDepth) {
26
+ super(`tool schema nesting exceeds ${maxDepth} levels`);
27
+ this.maxDepth = maxDepth;
28
+ this.name = 'ToolSchemaTooDeepError';
29
+ }
30
+ }
31
+ /**
32
+ * JSON with keys sorted at every depth: two payloads that differ only by key
33
+ * order produce a single string. Arrays keep their order, because in JSON
34
+ * Schema `required` and `enum` are ordered data.
35
+ *
36
+ * Depth-bounded: see `MAX_CANONICAL_DEPTH`.
37
+ */
38
+ export function stableStringify(value) {
39
+ return canonicalize(value, 0);
40
+ }
41
+ /**
42
+ * The depth is passed explicitly, never through a default parameter exposed to
43
+ * `Array.prototype.map`: `value.map(canonicalize)` would pass the array INDEX
44
+ * as the second argument and reset the bound on every first element, a guard
45
+ * that compiles, passes nominal tests, and guards nothing.
46
+ */
47
+ function canonicalize(value, depth) {
48
+ if (depth > MAX_CANONICAL_DEPTH) {
49
+ throw new ToolSchemaTooDeepError(MAX_CANONICAL_DEPTH);
50
+ }
51
+ if (value === null || typeof value !== 'object')
52
+ return JSON.stringify(value) ?? 'null';
53
+ if (Array.isArray(value))
54
+ return `[${value.map((item) => canonicalize(item, depth + 1)).join(',')}]`;
55
+ const entries = Object.keys(value)
56
+ .sort()
57
+ .map((k) => `${JSON.stringify(k)}:${canonicalize(value[k], depth + 1)}`);
58
+ return `{${entries.join(',')}}`;
59
+ }
60
+ /**
61
+ * Version of the digest formula, carried as a prefix of every digest
62
+ * (`d1:<sha256 hex>`). The digest is shared across organisations and
63
+ * repositories: bump this when the formula changes so old verdicts are
64
+ * ignored rather than silently mismatched.
65
+ */
66
+ export const DIGEST_VERSION = 'd1';
67
+ /**
68
+ * Digest of a tool definition: exactly the surface the agent consumes and the
69
+ * attacker controls. `title` and `annotations` are included on purpose: the
70
+ * agent reads them too (a lying `destructiveHint: false`, for instance).
71
+ *
72
+ * No whitespace normalisation. This digest is the cache key of the backend
73
+ * judge, and the judge reads the RAW text: two descriptions it would read
74
+ * differently must never share a verdict.
75
+ *
76
+ * Shared contract with the backend: `tests/mcp/verify/__fixtures__/digest-vectors.json`
77
+ * pins the expected output on both sides.
78
+ */
79
+ export function toolDigest(tool) {
80
+ const canonical = stableStringify({
81
+ name: tool.name ?? '',
82
+ title: tool.title ?? '',
83
+ description: tool.description ?? '',
84
+ inputSchema: tool.inputSchema ?? null,
85
+ outputSchema: tool.outputSchema ?? null,
86
+ annotations: tool.annotations ?? null,
87
+ });
88
+ const hex = createHash('sha256').update(canonical, 'utf8').digest('hex');
89
+ return `${DIGEST_VERSION}:${hex}`;
90
+ }
91
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"digest.js","sourceRoot":"","sources":["../../../src/mcp/verify/digest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAczC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEvC;;;;;GAKG;AACH,MAAM,OAAO,sBAAuB,SAAQ,KAAK;IAC1B;IAArB,YAAqB,QAAgB;QACnC,KAAK,CAAC,+BAA+B,QAAQ,SAAS,CAAC,CAAC;QADrC,aAAQ,GAAR,QAAQ,CAAQ;QAEnC,IAAI,CAAC,IAAI,GAAG,wBAAwB,CAAC;IACvC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAAc;IAC5C,OAAO,YAAY,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AAChC,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY,CAAC,KAAc,EAAE,KAAa;IACjD,IAAI,KAAK,GAAG,mBAAmB,EAAE,CAAC;QAChC,MAAM,IAAI,sBAAsB,CAAC,mBAAmB,CAAC,CAAC;IACxD,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAC7C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QACtB,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IAC7E,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC;SAC1D,IAAI,EAAE;SACN,GAAG,CACF,CAAC,CAAC,EAAE,EAAE,CACJ,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,YAAY,CAAE,KAAiC,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,EAAE,CAC3F,CAAC;IACJ,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC;AAEnC;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB;IACzC,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;QACvB,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,EAAE;QACnC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;QACrC,YAAY,EAAE,IAAI,CAAC,YAAY,IAAI,IAAI;QACvC,WAAW,EAAE,IAAI,CAAC,WAAW,IAAI,IAAI;KACtC,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACzE,OAAO,GAAG,cAAc,IAAI,GAAG,EAAE,CAAC;AACpC,CAAC"}
@@ -0,0 +1,56 @@
1
+ import { PROXY_ENV_PREFIX } from '../proxy/paths.js';
2
+ import { persistedOrganization, resolveBackendAccess } from './backend.js';
3
+ import { createJsonVerifyCache } from './cache.js';
4
+ import { upstreamServerId } from './identity.js';
5
+ import { verifyTokenResolver } from './token.js';
6
+ import { DEFAULT_VERIFY_TIMEOUT_MS, Verifier } from './verifier.js';
7
+ /**
8
+ * A verifier for `spec`, named `serverName` on every finding.
9
+ *
10
+ * `spec` is the STORED spec, never the expanded one: `upstreamServerId`
11
+ * hashes it, and that hash keys the cache and the user's overrides.
12
+ */
13
+ export function buildVerifier(spec, serverName, logger, env, overrides = {}) {
14
+ const serverId = upstreamServerId(spec);
15
+ const timeoutMs = overrides.timeoutMs ?? positiveMs(env[`${PROXY_ENV_PREFIX}VERIFY_TIMEOUT_MS`]) ?? DEFAULT_VERIFY_TIMEOUT_MS;
16
+ const resolved = overrides.backend === undefined
17
+ ? (overrides.resolveBackendAccessFn ?? resolveBackendAccess)({
18
+ env,
19
+ warn: (message, fields) => logger.warn(message, fields),
20
+ })
21
+ : overrides.backend;
22
+ const backend = resolved
23
+ ? {
24
+ ...resolved,
25
+ options: {
26
+ resolveTokenFn: verifyTokenResolver({ env }),
27
+ ...('options' in resolved ? (resolved.options ?? {}) : {}),
28
+ },
29
+ }
30
+ : null;
31
+ const verifier = new Verifier({
32
+ serverId,
33
+ serverName,
34
+ spec,
35
+ cache: overrides.cache ?? createJsonVerifyCache({ env }),
36
+ logger,
37
+ timeoutMs,
38
+ callTimeoutMs: overrides.callTimeoutMs,
39
+ now: overrides.now,
40
+ sessionId: overrides.sessionId,
41
+ env,
42
+ backend: backend ?? undefined,
43
+ // With no backend at all, the last known policy of the persisted
44
+ // organization still applies to what the cache decides.
45
+ organizationId: backend?.organizationId ?? persistedOrganization()?.id,
46
+ });
47
+ return { verifier, serverId, serverName, timeoutMs, backend };
48
+ }
49
+ /** A strictly positive number of milliseconds from an environment value, or nothing. */
50
+ export function positiveMs(value) {
51
+ if (!value)
52
+ return undefined;
53
+ const parsed = Number(value);
54
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
55
+ }
56
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../../src/mcp/verify/factory.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAsB,MAAM,cAAc,CAAC;AAC/F,OAAO,EAAE,qBAAqB,EAAoB,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,yBAAyB,EAAE,QAAQ,EAAsB,MAAM,eAAe,CAAC;AA8BxF;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAkB,EAClB,UAAkB,EAClB,MAAc,EACd,GAAsB,EACtB,YAAoC,EAAE;IAEtC,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GACb,SAAS,CAAC,SAAS,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,gBAAgB,mBAAmB,CAAC,CAAC,IAAI,yBAAyB,CAAC;IAC9G,MAAM,QAAQ,GACZ,SAAS,CAAC,OAAO,KAAK,SAAS;QAC7B,CAAC,CAAC,CAAC,SAAS,CAAC,sBAAsB,IAAI,oBAAoB,CAAC,CAAC;YACzD,GAAG;YACH,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC;SACxD,CAAC;QACJ,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC;IACxB,MAAM,OAAO,GAAyB,QAAQ;QAC5C,CAAC,CAAC;YACE,GAAG,QAAQ;YACX,OAAO,EAAE;gBACP,cAAc,EAAE,mBAAmB,CAAC,EAAE,GAAG,EAAE,CAAC;gBAC5C,GAAG,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;aAC3D;SACF;QACH,CAAC,CAAC,IAAI,CAAC;IAET,MAAM,QAAQ,GAAG,IAAI,QAAQ,CAAC;QAC5B,QAAQ;QACR,UAAU;QACV,IAAI;QACJ,KAAK,EAAE,SAAS,CAAC,KAAK,IAAI,qBAAqB,CAAC,EAAE,GAAG,EAAE,CAAC;QACxD,MAAM;QACN,SAAS;QACT,aAAa,EAAE,SAAS,CAAC,aAAa;QACtC,GAAG,EAAE,SAAS,CAAC,GAAG;QAClB,SAAS,EAAE,SAAS,CAAC,SAAS;QAC9B,GAAG;QACH,OAAO,EAAE,OAAO,IAAI,SAAS;QAC7B,iEAAiE;QACjE,wDAAwD;QACxD,cAAc,EAAE,OAAO,EAAE,cAAc,IAAI,qBAAqB,EAAE,EAAE,EAAE;KACvE,CAAC,CAAC;IAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,UAAU,CAAC,KAAyB;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACpE,CAAC"}