mcpmake 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +691 -0
  2. package/bin/mcpmake.mjs +2 -0
  3. package/dist/analyzer/auth-detector.d.ts +12 -0
  4. package/dist/analyzer/auth-detector.js +142 -0
  5. package/dist/analyzer/dom-parser.d.ts +10 -0
  6. package/dist/analyzer/dom-parser.js +259 -0
  7. package/dist/analyzer/goal-crawler.d.ts +25 -0
  8. package/dist/analyzer/goal-crawler.js +177 -0
  9. package/dist/analyzer/hybrid-detector.d.ts +28 -0
  10. package/dist/analyzer/hybrid-detector.js +96 -0
  11. package/dist/analyzer/index.d.ts +12 -0
  12. package/dist/analyzer/index.js +8 -0
  13. package/dist/analyzer/screenshot-capture.d.ts +29 -0
  14. package/dist/analyzer/screenshot-capture.js +42 -0
  15. package/dist/analyzer/selector-builder.d.ts +19 -0
  16. package/dist/analyzer/selector-builder.js +199 -0
  17. package/dist/analyzer/semantic-analyzer.d.ts +13 -0
  18. package/dist/analyzer/semantic-analyzer.js +145 -0
  19. package/dist/analyzer/site-crawler.d.ts +38 -0
  20. package/dist/analyzer/site-crawler.js +235 -0
  21. package/dist/cloud/billing/billing-engine.d.ts +44 -0
  22. package/dist/cloud/billing/billing-engine.js +81 -0
  23. package/dist/cloud/billing/credit-store.d.ts +64 -0
  24. package/dist/cloud/billing/credit-store.js +168 -0
  25. package/dist/cloud/billing/index.d.ts +4 -0
  26. package/dist/cloud/billing/index.js +2 -0
  27. package/dist/cloud/billing/usage-store.d.ts +42 -0
  28. package/dist/cloud/billing/usage-store.js +85 -0
  29. package/dist/cloud/billing/usage-tracker.d.ts +38 -0
  30. package/dist/cloud/billing/usage-tracker.js +95 -0
  31. package/dist/cloud/build-pipeline.d.ts +39 -0
  32. package/dist/cloud/build-pipeline.js +310 -0
  33. package/dist/cloud/build-queue.d.ts +30 -0
  34. package/dist/cloud/build-queue.js +70 -0
  35. package/dist/cloud/caddy-manager.d.ts +18 -0
  36. package/dist/cloud/caddy-manager.js +97 -0
  37. package/dist/cloud/container-backend.d.ts +62 -0
  38. package/dist/cloud/container-backend.js +59 -0
  39. package/dist/cloud/container-manager.d.ts +64 -0
  40. package/dist/cloud/container-manager.js +301 -0
  41. package/dist/cloud/crypto.d.ts +27 -0
  42. package/dist/cloud/crypto.js +63 -0
  43. package/dist/cloud/db/index.d.ts +27 -0
  44. package/dist/cloud/db/index.js +53 -0
  45. package/dist/cloud/db/migrations.d.ts +12 -0
  46. package/dist/cloud/db/migrations.js +329 -0
  47. package/dist/cloud/db/pg-store.d.ts +45 -0
  48. package/dist/cloud/db/pg-store.js +336 -0
  49. package/dist/cloud/failure-tracker.d.ts +51 -0
  50. package/dist/cloud/failure-tracker.js +102 -0
  51. package/dist/cloud/idle-monitor.d.ts +30 -0
  52. package/dist/cloud/idle-monitor.js +70 -0
  53. package/dist/cloud/mailer.d.ts +21 -0
  54. package/dist/cloud/mailer.js +193 -0
  55. package/dist/cloud/mcp-proxy.d.ts +58 -0
  56. package/dist/cloud/mcp-proxy.js +203 -0
  57. package/dist/cloud/metric-samples.d.ts +43 -0
  58. package/dist/cloud/metric-samples.js +85 -0
  59. package/dist/cloud/metrics.d.ts +26 -0
  60. package/dist/cloud/metrics.js +59 -0
  61. package/dist/cloud/multipart.d.ts +26 -0
  62. package/dist/cloud/multipart.js +132 -0
  63. package/dist/cloud/observability.d.ts +27 -0
  64. package/dist/cloud/observability.js +98 -0
  65. package/dist/cloud/rate-limiter.d.ts +31 -0
  66. package/dist/cloud/rate-limiter.js +58 -0
  67. package/dist/cloud/request-security.d.ts +5 -0
  68. package/dist/cloud/request-security.js +74 -0
  69. package/dist/cloud/resource-monitor.d.ts +69 -0
  70. package/dist/cloud/resource-monitor.js +130 -0
  71. package/dist/cloud/secret-store.d.ts +38 -0
  72. package/dist/cloud/secret-store.js +103 -0
  73. package/dist/cloud/security.d.ts +26 -0
  74. package/dist/cloud/security.js +142 -0
  75. package/dist/cloud/server.d.ts +21 -0
  76. package/dist/cloud/server.js +1079 -0
  77. package/dist/cloud/shared-state.d.ts +72 -0
  78. package/dist/cloud/shared-state.js +159 -0
  79. package/dist/cloud/ssrf.d.ts +43 -0
  80. package/dist/cloud/ssrf.js +150 -0
  81. package/dist/cloud/store.d.ts +41 -0
  82. package/dist/cloud/store.js +75 -0
  83. package/dist/cloud/stripe.d.ts +78 -0
  84. package/dist/cloud/stripe.js +317 -0
  85. package/dist/cloud/telemetry-store.d.ts +53 -0
  86. package/dist/cloud/telemetry-store.js +108 -0
  87. package/dist/cloud/web/auth.d.ts +225 -0
  88. package/dist/cloud/web/auth.js +555 -0
  89. package/dist/cloud/web/charts.d.ts +70 -0
  90. package/dist/cloud/web/charts.js +178 -0
  91. package/dist/cloud/web/csrf.d.ts +14 -0
  92. package/dist/cloud/web/csrf.js +22 -0
  93. package/dist/cloud/web/docs.d.ts +40 -0
  94. package/dist/cloud/web/docs.js +174 -0
  95. package/dist/cloud/web/router.d.ts +25 -0
  96. package/dist/cloud/web/router.js +1921 -0
  97. package/dist/cloud/web/static/alpine.min.js +5 -0
  98. package/dist/cloud/web/static/favicon.svg +4 -0
  99. package/dist/cloud/web/static/htmx-sse.js +290 -0
  100. package/dist/cloud/web/static/htmx.min.js +1 -0
  101. package/dist/cloud/web/static/style.css +2683 -0
  102. package/dist/cloud/web/static-server.d.ts +13 -0
  103. package/dist/cloud/web/static-server.js +73 -0
  104. package/dist/cloud/web/template-engine.d.ts +27 -0
  105. package/dist/cloud/web/template-engine.js +146 -0
  106. package/dist/cloud/web/templates/layouts/admin.hbs +57 -0
  107. package/dist/cloud/web/templates/layouts/auth.hbs +138 -0
  108. package/dist/cloud/web/templates/layouts/base.hbs +16 -0
  109. package/dist/cloud/web/templates/layouts/dashboard.hbs +39 -0
  110. package/dist/cloud/web/templates/layouts/landing.hbs +82 -0
  111. package/dist/cloud/web/templates/pages/admin/overview.hbs +123 -0
  112. package/dist/cloud/web/templates/pages/admin/servers.hbs +129 -0
  113. package/dist/cloud/web/templates/pages/admin/telemetry.hbs +39 -0
  114. package/dist/cloud/web/templates/pages/admin/user-edit.hbs +91 -0
  115. package/dist/cloud/web/templates/pages/admin/users.hbs +179 -0
  116. package/dist/cloud/web/templates/pages/auth/forgot-password.hbs +25 -0
  117. package/dist/cloud/web/templates/pages/auth/login.hbs +33 -0
  118. package/dist/cloud/web/templates/pages/auth/register.hbs +32 -0
  119. package/dist/cloud/web/templates/pages/auth/reset-password.hbs +34 -0
  120. package/dist/cloud/web/templates/pages/dashboard/billing.hbs +140 -0
  121. package/dist/cloud/web/templates/pages/dashboard/create.hbs +173 -0
  122. package/dist/cloud/web/templates/pages/dashboard/index.hbs +8 -0
  123. package/dist/cloud/web/templates/pages/dashboard/server-detail.hbs +280 -0
  124. package/dist/cloud/web/templates/pages/dashboard/server-logs.hbs +35 -0
  125. package/dist/cloud/web/templates/pages/dashboard/server-metrics.hbs +63 -0
  126. package/dist/cloud/web/templates/pages/dashboard/servers-partial.hbs +21 -0
  127. package/dist/cloud/web/templates/pages/dashboard/servers.hbs +44 -0
  128. package/dist/cloud/web/templates/pages/docs/show.hbs +16 -0
  129. package/dist/cloud/web/templates/pages/errors/404.hbs +9 -0
  130. package/dist/cloud/web/templates/pages/errors/500.hbs +8 -0
  131. package/dist/cloud/web/templates/pages/landing/index.hbs +223 -0
  132. package/dist/cloud/web/templates/pages/legal/privacy.hbs +71 -0
  133. package/dist/cloud/web/templates/pages/legal/terms.hbs +73 -0
  134. package/dist/cloud/web/templates/partials/admin-stats.hbs +52 -0
  135. package/dist/cloud/web/templates/partials/flash-message.hbs +6 -0
  136. package/dist/cloud/web/templates/partials/pricing-table.hbs +103 -0
  137. package/dist/cloud/web/templates/partials/server-card.hbs +19 -0
  138. package/dist/cloud/web/templates/partials/status-badge.hbs +1 -0
  139. package/dist/commands/bundle.d.ts +18 -0
  140. package/dist/commands/bundle.js +82 -0
  141. package/dist/commands/ci.d.ts +25 -0
  142. package/dist/commands/ci.js +149 -0
  143. package/dist/commands/deploy.d.ts +24 -0
  144. package/dist/commands/deploy.js +145 -0
  145. package/dist/commands/diff.d.ts +18 -0
  146. package/dist/commands/diff.js +185 -0
  147. package/dist/commands/from/describe.d.ts +65 -0
  148. package/dist/commands/from/describe.js +173 -0
  149. package/dist/commands/from/har.d.ts +81 -0
  150. package/dist/commands/from/har.js +255 -0
  151. package/dist/commands/from/openapi.d.ts +105 -0
  152. package/dist/commands/from/openapi.js +302 -0
  153. package/dist/commands/from/postman.d.ts +51 -0
  154. package/dist/commands/from/postman.js +146 -0
  155. package/dist/commands/from/target-support.d.ts +11 -0
  156. package/dist/commands/from/target-support.js +33 -0
  157. package/dist/commands/from/url.d.ts +75 -0
  158. package/dist/commands/from/url.js +244 -0
  159. package/dist/commands/from/website.d.ts +75 -0
  160. package/dist/commands/from/website.js +284 -0
  161. package/dist/commands/lint.d.ts +24 -0
  162. package/dist/commands/lint.js +184 -0
  163. package/dist/commands/merge.d.ts +18 -0
  164. package/dist/commands/merge.js +161 -0
  165. package/dist/commands/publish.d.ts +27 -0
  166. package/dist/commands/publish.js +334 -0
  167. package/dist/commands/rescan.d.ts +40 -0
  168. package/dist/commands/rescan.js +255 -0
  169. package/dist/commands/update.d.ts +14 -0
  170. package/dist/commands/update.js +87 -0
  171. package/dist/commands/verify.d.ts +14 -0
  172. package/dist/commands/verify.js +71 -0
  173. package/dist/config/configurable-command.d.ts +13 -0
  174. package/dist/config/configurable-command.js +70 -0
  175. package/dist/config/mcpmake-config.d.ts +68 -0
  176. package/dist/config/mcpmake-config.js +207 -0
  177. package/dist/docs/cli.md +400 -0
  178. package/dist/docs/mcp-2026-07-28-migration.md +78 -0
  179. package/dist/docs/migrate-from-stainless.md +94 -0
  180. package/dist/docs/quickstart.md +166 -0
  181. package/dist/docs/show-hn.md +26 -0
  182. package/dist/docs/website-servers.md +169 -0
  183. package/dist/emitter/code-writer.d.ts +8 -0
  184. package/dist/emitter/code-writer.js +25 -0
  185. package/dist/emitter/index.d.ts +32 -0
  186. package/dist/emitter/index.js +280 -0
  187. package/dist/emitter/mcpb-bundler.d.ts +31 -0
  188. package/dist/emitter/mcpb-bundler.js +172 -0
  189. package/dist/emitter/project-scaffolder.d.ts +4 -0
  190. package/dist/emitter/project-scaffolder.js +89 -0
  191. package/dist/emitter/python-template-loader.d.ts +4 -0
  192. package/dist/emitter/python-template-loader.js +30 -0
  193. package/dist/emitter/python-templates/dockerfile.hbs +14 -0
  194. package/dist/emitter/python-templates/env.example.hbs +6 -0
  195. package/dist/emitter/python-templates/requirements.txt.hbs +4 -0
  196. package/dist/emitter/python-templates/server.py.hbs +77 -0
  197. package/dist/emitter/site-scaffolder.d.ts +13 -0
  198. package/dist/emitter/site-scaffolder.js +70 -0
  199. package/dist/emitter/site-template-loader.d.ts +5 -0
  200. package/dist/emitter/site-template-loader.js +47 -0
  201. package/dist/emitter/site-templates/browser-manager.ts.hbs +233 -0
  202. package/dist/emitter/site-templates/config.ts.hbs +28 -0
  203. package/dist/emitter/site-templates/dockerfile.hbs +31 -0
  204. package/dist/emitter/site-templates/env.example.hbs +19 -0
  205. package/dist/emitter/site-templates/package.json.hbs +26 -0
  206. package/dist/emitter/site-templates/server-main-http.ts.hbs +108 -0
  207. package/dist/emitter/site-templates/server-main.ts.hbs +23 -0
  208. package/dist/emitter/site-templates/tool-handler-action.ts.hbs +86 -0
  209. package/dist/emitter/site-templates/tool-handler-form.ts.hbs +116 -0
  210. package/dist/emitter/site-templates/tool-handler-lifecycle.ts.hbs +146 -0
  211. package/dist/emitter/site-templates/tool-index.ts.hbs +11 -0
  212. package/dist/emitter/template-loader.d.ts +1 -0
  213. package/dist/emitter/template-loader.js +27 -0
  214. package/dist/emitter/templates/auth-provider.ts.hbs +57 -0
  215. package/dist/emitter/templates/config.ts.hbs +63 -0
  216. package/dist/emitter/templates/discovery.ts.hbs +301 -0
  217. package/dist/emitter/templates/dockerfile.hbs +34 -0
  218. package/dist/emitter/templates/env.example.hbs +28 -0
  219. package/dist/emitter/templates/gitignore.hbs +5 -0
  220. package/dist/emitter/templates/http-executor.ts.hbs +117 -0
  221. package/dist/emitter/templates/oauth.ts.hbs +188 -0
  222. package/dist/emitter/templates/package.json.hbs +25 -0
  223. package/dist/emitter/templates/prompts.ts.hbs +22 -0
  224. package/dist/emitter/templates/readme.md.hbs +123 -0
  225. package/dist/emitter/templates/resources.ts.hbs +63 -0
  226. package/dist/emitter/templates/server-main-http.ts.hbs +407 -0
  227. package/dist/emitter/templates/server-main.ts.hbs +40 -0
  228. package/dist/emitter/templates/task-handlers.ts.hbs +189 -0
  229. package/dist/emitter/templates/task-manager.ts.hbs +139 -0
  230. package/dist/emitter/templates/task-sse.ts.hbs +105 -0
  231. package/dist/emitter/templates/tool-handler.ts.hbs +124 -0
  232. package/dist/emitter/templates/tool-index.ts.hbs +11 -0
  233. package/dist/emitter/templates/tool-test.ts.hbs +57 -0
  234. package/dist/emitter/templates/trace.ts.hbs +79 -0
  235. package/dist/emitter/templates/tsconfig.json.hbs +16 -0
  236. package/dist/emitter/templates/types.ts.hbs +5 -0
  237. package/dist/emitter/worker-template-loader.d.ts +5 -0
  238. package/dist/emitter/worker-template-loader.js +33 -0
  239. package/dist/emitter/worker-templates/config.ts.hbs +54 -0
  240. package/dist/emitter/worker-templates/dev-vars.example.hbs +10 -0
  241. package/dist/emitter/worker-templates/gitignore.hbs +6 -0
  242. package/dist/emitter/worker-templates/package.json.hbs +24 -0
  243. package/dist/emitter/worker-templates/readme.md.hbs +53 -0
  244. package/dist/emitter/worker-templates/server.test.ts.hbs +20 -0
  245. package/dist/emitter/worker-templates/tool-handler.ts.hbs +85 -0
  246. package/dist/emitter/worker-templates/tool-index.ts.hbs +28 -0
  247. package/dist/emitter/worker-templates/tsconfig.json.hbs +17 -0
  248. package/dist/emitter/worker-templates/worker.ts.hbs +242 -0
  249. package/dist/emitter/worker-templates/wrangler.toml.hbs +19 -0
  250. package/dist/generator/spec-generator.d.ts +6 -0
  251. package/dist/generator/spec-generator.js +50 -0
  252. package/dist/index.d.ts +1 -0
  253. package/dist/index.js +64 -0
  254. package/dist/parser/har-filter.d.ts +8 -0
  255. package/dist/parser/har-filter.js +71 -0
  256. package/dist/parser/har-loader.d.ts +2 -0
  257. package/dist/parser/har-loader.js +14 -0
  258. package/dist/parser/har-normalizer.d.ts +20 -0
  259. package/dist/parser/har-normalizer.js +78 -0
  260. package/dist/parser/index.d.ts +10 -0
  261. package/dist/parser/index.js +6 -0
  262. package/dist/parser/openapi-loader.d.ts +6 -0
  263. package/dist/parser/openapi-loader.js +308 -0
  264. package/dist/parser/operation-extractor.d.ts +13 -0
  265. package/dist/parser/operation-extractor.js +155 -0
  266. package/dist/parser/overlay-loader.d.ts +10 -0
  267. package/dist/parser/overlay-loader.js +184 -0
  268. package/dist/parser/postman-loader.d.ts +9 -0
  269. package/dist/parser/postman-loader.js +106 -0
  270. package/dist/parser/schema-converter.d.ts +12 -0
  271. package/dist/parser/schema-converter.js +117 -0
  272. package/dist/plugins/adapter.d.ts +40 -0
  273. package/dist/plugins/adapter.js +15 -0
  274. package/dist/plugins/loader.d.ts +25 -0
  275. package/dist/plugins/loader.js +58 -0
  276. package/dist/pricing.d.ts +55 -0
  277. package/dist/pricing.js +133 -0
  278. package/dist/providers/index.d.ts +15 -0
  279. package/dist/providers/index.js +56 -0
  280. package/dist/recorder/browser-recorder.d.ts +22 -0
  281. package/dist/recorder/browser-recorder.js +205 -0
  282. package/dist/registry/official-registry.d.ts +90 -0
  283. package/dist/registry/official-registry.js +129 -0
  284. package/dist/rescan/diff-engine.d.ts +5 -0
  285. package/dist/rescan/diff-engine.js +312 -0
  286. package/dist/rescan/index.d.ts +3 -0
  287. package/dist/rescan/index.js +2 -0
  288. package/dist/rescan/rescan-runner.d.ts +42 -0
  289. package/dist/rescan/rescan-runner.js +69 -0
  290. package/dist/rescan/rescan-scheduler.d.ts +41 -0
  291. package/dist/rescan/rescan-scheduler.js +179 -0
  292. package/dist/site-transformer/browser-tools.d.ts +10 -0
  293. package/dist/site-transformer/browser-tools.js +59 -0
  294. package/dist/site-transformer/index.d.ts +2 -0
  295. package/dist/site-transformer/index.js +2 -0
  296. package/dist/site-transformer/selector-healer.d.ts +8 -0
  297. package/dist/site-transformer/selector-healer.js +106 -0
  298. package/dist/site-transformer/tool-generator.d.ts +13 -0
  299. package/dist/site-transformer/tool-generator.js +245 -0
  300. package/dist/transformer/auth-detector.d.ts +13 -0
  301. package/dist/transformer/auth-detector.js +90 -0
  302. package/dist/transformer/catalog-builder.d.ts +18 -0
  303. package/dist/transformer/catalog-builder.js +56 -0
  304. package/dist/transformer/client-compat.d.ts +6 -0
  305. package/dist/transformer/client-compat.js +44 -0
  306. package/dist/transformer/har-clusterer.d.ts +9 -0
  307. package/dist/transformer/har-clusterer.js +27 -0
  308. package/dist/transformer/har-dedup.d.ts +10 -0
  309. package/dist/transformer/har-dedup.js +81 -0
  310. package/dist/transformer/har-schema-inferrer.d.ts +15 -0
  311. package/dist/transformer/har-schema-inferrer.js +90 -0
  312. package/dist/transformer/har-to-operations.d.ts +13 -0
  313. package/dist/transformer/har-to-operations.js +192 -0
  314. package/dist/transformer/index.d.ts +8 -0
  315. package/dist/transformer/index.js +6 -0
  316. package/dist/transformer/llm-namer.d.ts +6 -0
  317. package/dist/transformer/llm-namer.js +59 -0
  318. package/dist/transformer/naming.d.ts +4 -0
  319. package/dist/transformer/naming.js +30 -0
  320. package/dist/transformer/operation-filter.d.ts +13 -0
  321. package/dist/transformer/operation-filter.js +52 -0
  322. package/dist/transformer/resource-builder.d.ts +12 -0
  323. package/dist/transformer/resource-builder.js +80 -0
  324. package/dist/transformer/schema-merger.d.ts +14 -0
  325. package/dist/transformer/schema-merger.js +65 -0
  326. package/dist/transformer/tool-builder.d.ts +3 -0
  327. package/dist/transformer/tool-builder.js +114 -0
  328. package/dist/types/index.d.ts +131 -0
  329. package/dist/types/index.js +1 -0
  330. package/dist/types/site.d.ts +284 -0
  331. package/dist/types/site.js +8 -0
  332. package/dist/utils/fail.d.ts +48 -0
  333. package/dist/utils/fail.js +204 -0
  334. package/dist/utils/fs.d.ts +5 -0
  335. package/dist/utils/fs.js +28 -0
  336. package/dist/utils/interactive.d.ts +6 -0
  337. package/dist/utils/interactive.js +30 -0
  338. package/dist/utils/logger.d.ts +1 -0
  339. package/dist/utils/logger.js +2 -0
  340. package/dist/utils/sanitize.d.ts +28 -0
  341. package/dist/utils/sanitize.js +44 -0
  342. package/dist/utils/watcher.d.ts +11 -0
  343. package/dist/utils/watcher.js +36 -0
  344. package/package.json +65 -0
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Dynamic reverse proxy routing via Caddy Admin API.
3
+ *
4
+ * Manages routes that map {slug}.{domain} -> localhost:{port}.
5
+ * Caddy Admin API is assumed to run at http://localhost:2019.
6
+ */
7
+ import http from 'node:http';
8
+ import { logger } from '../utils/logger.js';
9
+ const CADDY_ADMIN = 'http://localhost:2019';
10
+ /**
11
+ * Add a route: {slug}.{domain} -> localhost:{port}
12
+ */
13
+ export async function addRoute(slug, port, domain) {
14
+ const hostname = `${slug}.${domain}`;
15
+ const route = {
16
+ '@id': `mcpmake-${slug}`,
17
+ match: [
18
+ {
19
+ host: [hostname],
20
+ },
21
+ ],
22
+ handle: [
23
+ {
24
+ handler: 'reverse_proxy',
25
+ upstreams: [
26
+ {
27
+ dial: `localhost:${port}`,
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ };
33
+ await caddyRequest('POST', '/config/apps/http/servers/srv0/routes', route);
34
+ logger.info(`Caddy route added: ${hostname} -> localhost:${port}`);
35
+ }
36
+ /**
37
+ * Remove a route by slug.
38
+ */
39
+ export async function removeRoute(slug, _domain) {
40
+ await caddyRequest('DELETE', `/id/mcpmake-${slug}`, undefined);
41
+ logger.info(`Caddy route removed: ${slug}`);
42
+ }
43
+ /**
44
+ * Check if a route exists for a given slug.
45
+ */
46
+ export async function routeExists(slug, _domain) {
47
+ try {
48
+ const body = await caddyRequest('GET', `/id/mcpmake-${slug}`, undefined);
49
+ return body !== null;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Low-level helper to call the Caddy Admin API.
57
+ */
58
+ function caddyRequest(method, path, body) {
59
+ return new Promise((resolve, reject) => {
60
+ const url = new URL(path, CADDY_ADMIN);
61
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
62
+ const req = http.request({
63
+ hostname: url.hostname,
64
+ port: Number(url.port),
65
+ path: url.pathname,
66
+ method,
67
+ headers: {
68
+ ...(payload
69
+ ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) }
70
+ : {}),
71
+ },
72
+ }, (res) => {
73
+ const chunks = [];
74
+ res.on('data', (chunk) => chunks.push(chunk));
75
+ res.on('end', () => {
76
+ const responseBody = Buffer.concat(chunks).toString('utf-8');
77
+ if (res.statusCode && res.statusCode >= 400) {
78
+ reject(new Error(`Caddy API ${method} ${path} returned ${res.statusCode}: ${responseBody}`));
79
+ return;
80
+ }
81
+ try {
82
+ resolve(responseBody ? JSON.parse(responseBody) : null);
83
+ }
84
+ catch {
85
+ resolve(responseBody || null);
86
+ }
87
+ });
88
+ });
89
+ req.on('error', (err) => {
90
+ reject(new Error(`Caddy API request failed: ${err.message}`));
91
+ });
92
+ if (payload) {
93
+ req.write(payload);
94
+ }
95
+ req.end();
96
+ });
97
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Container backend driver abstraction.
3
+ *
4
+ * All container lifecycle operations go through a `ContainerBackend` rather than
5
+ * calling the Docker socket helpers directly. Today the only implementation is
6
+ * `LocalDockerDriver` (the local `/var/run/docker.sock`, no behavior change).
7
+ *
8
+ * The seam exists so a future cloud / multi-node move is a driver swap, not a
9
+ * rewrite: a `RemoteDockerDriver` (TCP + mTLS) or a managed-cloud driver
10
+ * (Fly Machines / Cloud Run) implements the same interface, and a placement
11
+ * layer picks which driver to use per server. See TODO.md Sprint 8 — "Prep
12
+ * seams" and "Tiered placement".
13
+ */
14
+ import type { StartContainerOpts, ContainerStatus } from './container-manager.js';
15
+ export type { StartContainerOpts, ContainerStatus };
16
+ /**
17
+ * A backend capable of building images and running tenant containers.
18
+ *
19
+ * Method contracts mirror the existing `container-manager` functions exactly,
20
+ * so swapping the default driver is invisible to callers.
21
+ */
22
+ export interface ContainerBackend {
23
+ /** Stable identifier for logs / the server record (which backend ran a container). */
24
+ readonly name: string;
25
+ /** Build an image from a generated project directory containing a Dockerfile. */
26
+ buildImage(buildDir: string, imageName: string, imageTag: string): Promise<void>;
27
+ /** Create + start a container; resolves to the container ID. */
28
+ startContainer(opts: StartContainerOpts): Promise<string>;
29
+ /** Stop and remove a container (best-effort; never throws on already-stopped). */
30
+ stopContainer(containerId: string): Promise<void>;
31
+ /** Stream container logs (multiplexed Docker frames decoded to text lines). */
32
+ getContainerLogs(containerId: string): AsyncIterable<string>;
33
+ /** Current runtime status; resolves to 'error' if the backend is unreachable. */
34
+ getContainerStatus(containerId: string): Promise<ContainerStatus>;
35
+ /** Reclaim disk by removing dangling images (best-effort). */
36
+ pruneDanglingImages(): Promise<void>;
37
+ }
38
+ /**
39
+ * Default driver: the local Docker daemon over its Unix socket.
40
+ *
41
+ * Delegates to the `container-manager` helpers at call time (not construction)
42
+ * so the well-tested socket code stays the single implementation and module
43
+ * mocks still intercept.
44
+ */
45
+ export declare class LocalDockerDriver implements ContainerBackend {
46
+ readonly name = "local-docker";
47
+ buildImage(buildDir: string, imageName: string, imageTag: string): Promise<void>;
48
+ startContainer(opts: StartContainerOpts): Promise<string>;
49
+ stopContainer(containerId: string): Promise<void>;
50
+ getContainerLogs(containerId: string): AsyncIterable<string>;
51
+ getContainerStatus(containerId: string): Promise<ContainerStatus>;
52
+ pruneDanglingImages(): Promise<void>;
53
+ }
54
+ /**
55
+ * Resolve the container backend.
56
+ *
57
+ * Single local driver for now; the future placement layer will pass the target
58
+ * server/plan here to pick a driver per tenant.
59
+ */
60
+ export declare function getContainerBackend(): ContainerBackend;
61
+ /** Override the backend (tests / future placement wiring). Pass null to reset. */
62
+ export declare function setContainerBackend(backend: ContainerBackend | null): void;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Container backend driver abstraction.
3
+ *
4
+ * All container lifecycle operations go through a `ContainerBackend` rather than
5
+ * calling the Docker socket helpers directly. Today the only implementation is
6
+ * `LocalDockerDriver` (the local `/var/run/docker.sock`, no behavior change).
7
+ *
8
+ * The seam exists so a future cloud / multi-node move is a driver swap, not a
9
+ * rewrite: a `RemoteDockerDriver` (TCP + mTLS) or a managed-cloud driver
10
+ * (Fly Machines / Cloud Run) implements the same interface, and a placement
11
+ * layer picks which driver to use per server. See TODO.md Sprint 8 — "Prep
12
+ * seams" and "Tiered placement".
13
+ */
14
+ import * as docker from './container-manager.js';
15
+ /**
16
+ * Default driver: the local Docker daemon over its Unix socket.
17
+ *
18
+ * Delegates to the `container-manager` helpers at call time (not construction)
19
+ * so the well-tested socket code stays the single implementation and module
20
+ * mocks still intercept.
21
+ */
22
+ export class LocalDockerDriver {
23
+ name = 'local-docker';
24
+ buildImage(buildDir, imageName, imageTag) {
25
+ return docker.buildImage(buildDir, imageName, imageTag);
26
+ }
27
+ startContainer(opts) {
28
+ return docker.startContainer(opts);
29
+ }
30
+ stopContainer(containerId) {
31
+ return docker.stopContainer(containerId);
32
+ }
33
+ getContainerLogs(containerId) {
34
+ return docker.getContainerLogs(containerId);
35
+ }
36
+ getContainerStatus(containerId) {
37
+ return docker.getContainerStatus(containerId);
38
+ }
39
+ pruneDanglingImages() {
40
+ return docker.pruneDanglingImages();
41
+ }
42
+ }
43
+ let cached = null;
44
+ /**
45
+ * Resolve the container backend.
46
+ *
47
+ * Single local driver for now; the future placement layer will pass the target
48
+ * server/plan here to pick a driver per tenant.
49
+ */
50
+ export function getContainerBackend() {
51
+ if (!cached) {
52
+ cached = new LocalDockerDriver();
53
+ }
54
+ return cached;
55
+ }
56
+ /** Override the backend (tests / future placement wiring). Pass null to reset. */
57
+ export function setContainerBackend(backend) {
58
+ cached = backend;
59
+ }
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Docker container lifecycle management via the Docker Engine API.
3
+ *
4
+ * Communicates over the Unix socket at /var/run/docker.sock using
5
+ * native http.request — no dockerode dependency.
6
+ */
7
+ export interface ContainerInfo {
8
+ slug: string;
9
+ containerId: string;
10
+ port: number;
11
+ status: 'building' | 'running' | 'stopped' | 'error';
12
+ endpoint: string;
13
+ bearerToken: string;
14
+ createdAt: string;
15
+ lastActiveAt: string;
16
+ }
17
+ /** Runtime status of a container as reported by the backend. */
18
+ export type ContainerStatus = 'running' | 'stopped' | 'error';
19
+ /** Parameters for launching a tenant container. */
20
+ export interface StartContainerOpts {
21
+ slug: string;
22
+ imageName: string;
23
+ imageTag: string;
24
+ envVars: Record<string, string>;
25
+ port: number;
26
+ serverType?: 'http' | 'playwright';
27
+ /** Decrypted secrets to merge into the container environment. */
28
+ secrets?: Record<string, string>;
29
+ }
30
+ /**
31
+ * Build a Docker image from a generated project directory.
32
+ *
33
+ * Uses `tar` to create the build context and streams it to the Docker build API.
34
+ * The directory must contain a Dockerfile.
35
+ */
36
+ export declare function buildImage(buildDir: string, imageName: string, imageTag: string): Promise<void>;
37
+ /**
38
+ * Start a container with strict resource limits and security constraints.
39
+ *
40
+ * Returns the container ID.
41
+ */
42
+ export declare function startContainer(opts: StartContainerOpts): Promise<string>;
43
+ /**
44
+ * Stop and remove a container.
45
+ */
46
+ export declare function stopContainer(containerId: string): Promise<void>;
47
+ /**
48
+ * Get container logs as an async generator (streaming).
49
+ */
50
+ export declare function getContainerLogs(containerId: string): AsyncGenerator<string>;
51
+ /**
52
+ * Check if a container is running.
53
+ */
54
+ export declare function getContainerStatus(containerId: string): Promise<ContainerStatus>;
55
+ /**
56
+ * Reclaim disk by removing dangling (untagged) images. A re-deploy reuses the
57
+ * same image tag, so the previous build becomes dangling — left unchecked these
58
+ * silently fill the disk. Best-effort: errors are logged, never thrown.
59
+ *
60
+ * NOTE: images orphaned by *deleting* a server are still tagged (not dangling),
61
+ * so they are not reclaimed here — that is left to the daily `docker image
62
+ * prune` cron / a future targeted removal.
63
+ */
64
+ export declare function pruneDanglingImages(): Promise<void>;
@@ -0,0 +1,301 @@
1
+ /**
2
+ * Docker container lifecycle management via the Docker Engine API.
3
+ *
4
+ * Communicates over the Unix socket at /var/run/docker.sock using
5
+ * native http.request — no dockerode dependency.
6
+ */
7
+ import http from 'node:http';
8
+ import { logger } from '../utils/logger.js';
9
+ import { execFile } from 'node:child_process';
10
+ const DOCKER_SOCKET = '/var/run/docker.sock';
11
+ const TENANT_NETWORK = 'mcp-tenant';
12
+ /** Docker container IDs are 64-char hex strings (or short 12-char prefixes). */
13
+ const CONTAINER_ID_RE = /^[a-f0-9]{12,64}$/;
14
+ function validateContainerId(id) {
15
+ if (!CONTAINER_ID_RE.test(id)) {
16
+ throw new Error(`Invalid container ID: ${id}`);
17
+ }
18
+ }
19
+ /**
20
+ * Build a Docker image from a generated project directory.
21
+ *
22
+ * Uses `tar` to create the build context and streams it to the Docker build API.
23
+ * The directory must contain a Dockerfile.
24
+ */
25
+ export async function buildImage(buildDir, imageName, imageTag) {
26
+ const fullTag = `${imageName}:${imageTag}`;
27
+ logger.info(`Building Docker image: ${fullTag} from ${buildDir}`);
28
+ // Create a tar archive of the build directory and pipe it to Docker build API.
29
+ // We use a child process for tar since Node's native APIs don't include tar.
30
+ const tarStream = await createTarStream(buildDir);
31
+ await new Promise((resolve, reject) => {
32
+ const req = http.request({
33
+ socketPath: DOCKER_SOCKET,
34
+ path: `/build?t=${encodeURIComponent(fullTag)}&rm=true`,
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/x-tar',
38
+ },
39
+ }, (res) => {
40
+ const chunks = [];
41
+ res.on('data', (chunk) => chunks.push(chunk));
42
+ res.on('end', () => {
43
+ if (res.statusCode && res.statusCode >= 400) {
44
+ const body = Buffer.concat(chunks).toString('utf-8');
45
+ reject(new Error(`Docker build failed (${res.statusCode}): ${body}`));
46
+ return;
47
+ }
48
+ // Check for build errors in the stream output
49
+ const output = Buffer.concat(chunks).toString('utf-8');
50
+ if (output.includes('"error"')) {
51
+ // Docker build streams JSON objects; check for error fields
52
+ for (const line of output.split('\n')) {
53
+ if (!line.trim())
54
+ continue;
55
+ try {
56
+ const msg = JSON.parse(line);
57
+ if (msg.error) {
58
+ reject(new Error(`Docker build error: ${msg.error}`));
59
+ return;
60
+ }
61
+ }
62
+ catch {
63
+ // Not JSON, skip
64
+ }
65
+ }
66
+ }
67
+ logger.info(`Docker image built: ${fullTag}`);
68
+ resolve();
69
+ });
70
+ });
71
+ req.on('error', (err) => {
72
+ reject(new Error(`Docker build request failed: ${err.message}`));
73
+ });
74
+ tarStream.pipe(req);
75
+ });
76
+ }
77
+ /**
78
+ * Start a container with strict resource limits and security constraints.
79
+ *
80
+ * Returns the container ID.
81
+ */
82
+ export async function startContainer(opts) {
83
+ const fullTag = `${opts.imageName}:${opts.imageTag}`;
84
+ const containerName = `mcpmake-${opts.slug}`;
85
+ const isPlaywright = opts.serverType === 'playwright';
86
+ // Merge explicit env vars with decrypted secrets (envVars take precedence)
87
+ const mergedEnv = { ...(opts.secrets ?? {}), ...opts.envVars };
88
+ const envList = Object.entries(mergedEnv).map(([k, v]) => `${k}=${v}`);
89
+ // Resource limits differ for Playwright containers (Chromium needs more resources)
90
+ const hostConfig = {
91
+ // Port mapping: host port -> container port 3000
92
+ PortBindings: {
93
+ '3000/tcp': [{ HostIp: '127.0.0.1', HostPort: String(opts.port) }],
94
+ },
95
+ // Resource limits
96
+ Memory: isPlaywright ? 768 * 1024 * 1024 : 256 * 1024 * 1024,
97
+ NanoCPUs: isPlaywright ? 1_000_000_000 : 500_000_000,
98
+ // Security: Chromium needs write access to the root filesystem
99
+ ReadonlyRootfs: !isPlaywright,
100
+ // tmpfs: Playwright needs a larger temp directory
101
+ Tmpfs: isPlaywright
102
+ ? { '/tmp': 'rw,noexec,nosuid,size=256m' }
103
+ : { '/tmp': 'rw,noexec,nosuid,size=64m' },
104
+ // Network
105
+ NetworkMode: TENANT_NETWORK,
106
+ // Drop all capabilities
107
+ CapDrop: ['ALL'],
108
+ // Prevent privilege escalation
109
+ SecurityOpt: ['no-new-privileges'],
110
+ };
111
+ // Chromium rendering requires a larger shared memory segment
112
+ if (isPlaywright) {
113
+ hostConfig.ShmSize = 256 * 1024 * 1024;
114
+ }
115
+ // Create the container
116
+ const createBody = {
117
+ Image: fullTag,
118
+ name: containerName,
119
+ Env: [...envList, 'PORT=3000', 'TRANSPORT=http', 'NODE_ENV=production'],
120
+ ExposedPorts: { '3000/tcp': {} },
121
+ HostConfig: hostConfig,
122
+ Labels: {
123
+ 'mcpmake.slug': opts.slug,
124
+ 'mcpmake.managed': 'true',
125
+ },
126
+ };
127
+ const createResult = await dockerRequest('POST', `/containers/create?name=${encodeURIComponent(containerName)}`, createBody);
128
+ const containerId = createResult.Id;
129
+ validateContainerId(containerId);
130
+ logger.info(`Container created: ${containerId.slice(0, 12)} (${containerName})`);
131
+ // Start the container
132
+ await dockerRequest('POST', `/containers/${containerId}/start`, undefined);
133
+ logger.info(`Container started: ${containerId.slice(0, 12)}`);
134
+ return containerId;
135
+ }
136
+ /**
137
+ * Stop and remove a container.
138
+ */
139
+ export async function stopContainer(containerId) {
140
+ validateContainerId(containerId);
141
+ const shortId = containerId.slice(0, 12);
142
+ try {
143
+ await dockerRequest('POST', `/containers/${containerId}/stop?t=10`, undefined);
144
+ logger.info(`Container stopped: ${shortId}`);
145
+ }
146
+ catch (err) {
147
+ // Container might already be stopped
148
+ logger.warn(`Stop failed for ${shortId} (may already be stopped): ${err}`);
149
+ }
150
+ try {
151
+ await dockerRequest('DELETE', `/containers/${containerId}?force=true`, undefined);
152
+ logger.info(`Container removed: ${shortId}`);
153
+ }
154
+ catch (err) {
155
+ logger.warn(`Remove failed for ${shortId}: ${err}`);
156
+ }
157
+ }
158
+ /**
159
+ * Get container logs as an async generator (streaming).
160
+ */
161
+ export async function* getContainerLogs(containerId) {
162
+ validateContainerId(containerId);
163
+ const res = await new Promise((resolve, reject) => {
164
+ const req = http.request({
165
+ socketPath: DOCKER_SOCKET,
166
+ path: `/containers/${containerId}/logs?follow=true&stdout=true&stderr=true&timestamps=true`,
167
+ method: 'GET',
168
+ }, resolve);
169
+ req.on('error', reject);
170
+ req.end();
171
+ });
172
+ if (res.statusCode && res.statusCode >= 400) {
173
+ const chunks = [];
174
+ for await (const chunk of res) {
175
+ chunks.push(chunk);
176
+ }
177
+ throw new Error(`Failed to get logs (${res.statusCode}): ${Buffer.concat(chunks).toString('utf-8')}`);
178
+ }
179
+ // Docker log stream uses a multiplexed format:
180
+ // 8-byte header: [stream_type(1), 0, 0, 0, size(4 big-endian)]
181
+ // followed by `size` bytes of payload.
182
+ let buffer = Buffer.alloc(0);
183
+ for await (const chunk of res) {
184
+ buffer = Buffer.concat([buffer, chunk]);
185
+ while (buffer.length >= 8) {
186
+ const payloadSize = buffer.readUInt32BE(4);
187
+ const totalFrameSize = 8 + payloadSize;
188
+ if (buffer.length < totalFrameSize)
189
+ break;
190
+ const payload = buffer.subarray(8, totalFrameSize).toString('utf-8');
191
+ buffer = buffer.subarray(totalFrameSize);
192
+ yield payload;
193
+ }
194
+ }
195
+ }
196
+ /**
197
+ * Check if a container is running.
198
+ */
199
+ export async function getContainerStatus(containerId) {
200
+ validateContainerId(containerId);
201
+ try {
202
+ const info = await dockerRequest('GET', `/containers/${containerId}/json`, undefined);
203
+ if (info.State.Running)
204
+ return 'running';
205
+ if (info.State.Status === 'exited' || info.State.Status === 'dead')
206
+ return 'stopped';
207
+ return 'error';
208
+ }
209
+ catch {
210
+ return 'error';
211
+ }
212
+ }
213
+ /**
214
+ * Reclaim disk by removing dangling (untagged) images. A re-deploy reuses the
215
+ * same image tag, so the previous build becomes dangling — left unchecked these
216
+ * silently fill the disk. Best-effort: errors are logged, never thrown.
217
+ *
218
+ * NOTE: images orphaned by *deleting* a server are still tagged (not dangling),
219
+ * so they are not reclaimed here — that is left to the daily `docker image
220
+ * prune` cron / a future targeted removal.
221
+ */
222
+ export async function pruneDanglingImages() {
223
+ try {
224
+ const filters = encodeURIComponent(JSON.stringify({ dangling: ['true'] }));
225
+ await dockerRequest('POST', `/images/prune?filters=${filters}`, undefined);
226
+ logger.info('Pruned dangling Docker images');
227
+ }
228
+ catch (err) {
229
+ logger.warn(`Dangling image prune failed: ${err}`);
230
+ }
231
+ }
232
+ // ---------------------------------------------------------------------------
233
+ // Internal helpers
234
+ // ---------------------------------------------------------------------------
235
+ /**
236
+ * Create a tar stream from a directory using the system `tar` command.
237
+ * Uses execFile (no shell) to avoid command injection.
238
+ * Returns a readable stream.
239
+ */
240
+ function createTarStream(dir) {
241
+ // Validate path contains only safe characters
242
+ if (!/^[a-zA-Z0-9_.\/\-]+$/.test(dir)) {
243
+ throw new Error(`Unsafe path for tar: ${dir}`);
244
+ }
245
+ return new Promise((resolve, reject) => {
246
+ const child = execFile('tar', ['-cf', '-', '-C', dir, '.'], {
247
+ maxBuffer: 100 * 1024 * 1024, // 100 MB max
248
+ encoding: 'buffer',
249
+ });
250
+ if (!child.stdout) {
251
+ reject(new Error('Failed to create tar stream'));
252
+ return;
253
+ }
254
+ child.on('error', reject);
255
+ resolve(child.stdout);
256
+ });
257
+ }
258
+ /**
259
+ * Make a request to the Docker Engine API via Unix socket.
260
+ */
261
+ function dockerRequest(method, path, body) {
262
+ return new Promise((resolve, reject) => {
263
+ const payload = body !== undefined ? JSON.stringify(body) : undefined;
264
+ const req = http.request({
265
+ socketPath: DOCKER_SOCKET,
266
+ path,
267
+ method,
268
+ headers: {
269
+ ...(payload
270
+ ? {
271
+ 'Content-Type': 'application/json',
272
+ 'Content-Length': Buffer.byteLength(payload),
273
+ }
274
+ : {}),
275
+ },
276
+ }, (res) => {
277
+ const chunks = [];
278
+ res.on('data', (chunk) => chunks.push(chunk));
279
+ res.on('end', () => {
280
+ const responseBody = Buffer.concat(chunks).toString('utf-8');
281
+ if (res.statusCode && res.statusCode >= 400) {
282
+ reject(new Error(`Docker API ${method} ${path} returned ${res.statusCode}: ${responseBody}`));
283
+ return;
284
+ }
285
+ try {
286
+ resolve(responseBody ? JSON.parse(responseBody) : null);
287
+ }
288
+ catch {
289
+ resolve(responseBody);
290
+ }
291
+ });
292
+ });
293
+ req.on('error', (err) => {
294
+ reject(new Error(`Docker API request failed: ${err.message}`));
295
+ });
296
+ if (payload) {
297
+ req.write(payload);
298
+ }
299
+ req.end();
300
+ });
301
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * AES-256-GCM encryption module for secret storage.
3
+ *
4
+ * Uses Node.js built-in `crypto` — no external dependencies.
5
+ * Each record gets its own random salt so that the same plaintext
6
+ * encrypted twice produces different ciphertexts (key isolation).
7
+ *
8
+ * Wire format: "enc:v1:<base64 payload>"
9
+ * payload = salt(16) || iv(12) || authTag(16) || ciphertext
10
+ */
11
+ /**
12
+ * Encrypt a plaintext string with AES-256-GCM.
13
+ *
14
+ * Derives a per-record key via scrypt(masterKey, randomSalt, 32).
15
+ * Returns a prefixed, base64-encoded string.
16
+ */
17
+ export declare function encrypt(plaintext: string, masterKey: string): string;
18
+ /**
19
+ * Decrypt a value previously produced by `encrypt()`.
20
+ *
21
+ * Throws on tampered data, wrong key, or malformed input.
22
+ */
23
+ export declare function decrypt(encrypted: string, masterKey: string): string;
24
+ /**
25
+ * Check whether a value looks like an encrypted string from this module.
26
+ */
27
+ export declare function isEncrypted(value: string): boolean;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * AES-256-GCM encryption module for secret storage.
3
+ *
4
+ * Uses Node.js built-in `crypto` — no external dependencies.
5
+ * Each record gets its own random salt so that the same plaintext
6
+ * encrypted twice produces different ciphertexts (key isolation).
7
+ *
8
+ * Wire format: "enc:v1:<base64 payload>"
9
+ * payload = salt(16) || iv(12) || authTag(16) || ciphertext
10
+ */
11
+ import { randomBytes, scryptSync, createCipheriv, createDecipheriv } from 'node:crypto';
12
+ const PREFIX = 'enc:v1:';
13
+ const SALT_LEN = 16;
14
+ const IV_LEN = 12;
15
+ const TAG_LEN = 16;
16
+ const KEY_LEN = 32; // 256 bits
17
+ /**
18
+ * Encrypt a plaintext string with AES-256-GCM.
19
+ *
20
+ * Derives a per-record key via scrypt(masterKey, randomSalt, 32).
21
+ * Returns a prefixed, base64-encoded string.
22
+ */
23
+ export function encrypt(plaintext, masterKey) {
24
+ const salt = randomBytes(SALT_LEN);
25
+ const key = scryptSync(masterKey, salt, KEY_LEN);
26
+ const iv = randomBytes(IV_LEN);
27
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
28
+ const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
29
+ const tag = cipher.getAuthTag();
30
+ // Pack: salt || iv || tag || ciphertext
31
+ const payload = Buffer.concat([salt, iv, tag, encrypted]);
32
+ return PREFIX + payload.toString('base64');
33
+ }
34
+ /**
35
+ * Decrypt a value previously produced by `encrypt()`.
36
+ *
37
+ * Throws on tampered data, wrong key, or malformed input.
38
+ */
39
+ export function decrypt(encrypted, masterKey) {
40
+ if (!isEncrypted(encrypted)) {
41
+ throw new Error('Value is not in encrypted format (missing enc:v1: prefix)');
42
+ }
43
+ const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');
44
+ const minLen = SALT_LEN + IV_LEN + TAG_LEN;
45
+ if (payload.length < minLen) {
46
+ throw new Error('Encrypted payload too short');
47
+ }
48
+ const salt = payload.subarray(0, SALT_LEN);
49
+ const iv = payload.subarray(SALT_LEN, SALT_LEN + IV_LEN);
50
+ const tag = payload.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN);
51
+ const ciphertext = payload.subarray(SALT_LEN + IV_LEN + TAG_LEN);
52
+ const key = scryptSync(masterKey, salt, KEY_LEN);
53
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
54
+ decipher.setAuthTag(tag);
55
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
56
+ return decrypted.toString('utf8');
57
+ }
58
+ /**
59
+ * Check whether a value looks like an encrypted string from this module.
60
+ */
61
+ export function isEncrypted(value) {
62
+ return value.startsWith(PREFIX);
63
+ }