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,555 @@
1
+ /**
2
+ * User accounts and session management.
3
+ *
4
+ * Uses Node.js built-in crypto for password hashing (scrypt) and
5
+ * session token generation. Zero external dependencies.
6
+ */
7
+ import { randomBytes, scrypt, timingSafeEqual, createHash } from 'node:crypto';
8
+ import { isSecureRequest } from '../request-security.js';
9
+ import { getPgUserStore, getPgSessionStore } from '../shared-state.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Constants
12
+ // ---------------------------------------------------------------------------
13
+ const SESSION_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
14
+ const SESSION_MAX_AGE_SECONDS = 604800;
15
+ const SCRYPT_KEYLEN = 64;
16
+ const SCRYPT_SALT_BYTES = 16;
17
+ const SCRYPT_COST = 16384; // N
18
+ const SCRYPT_BLOCK_SIZE = 8; // r
19
+ const SCRYPT_PARALLELIZATION = 1; // p
20
+ // ---------------------------------------------------------------------------
21
+ // Password hashing
22
+ // ---------------------------------------------------------------------------
23
+ /**
24
+ * Hash a password using scrypt.
25
+ * Returns "salt:hash" where salt is 16 random bytes hex, hash is 64-byte scrypt output hex.
26
+ */
27
+ export async function hashPassword(password) {
28
+ const salt = randomBytes(SCRYPT_SALT_BYTES).toString('hex');
29
+ const hash = await new Promise((resolve, reject) => {
30
+ scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_COST, r: SCRYPT_BLOCK_SIZE, p: SCRYPT_PARALLELIZATION }, (err, derived) => {
31
+ if (err)
32
+ reject(err);
33
+ else
34
+ resolve(derived);
35
+ });
36
+ });
37
+ return `${salt}:${hash.toString('hex')}`;
38
+ }
39
+ /**
40
+ * Verify a password against a stored "salt:hash" string.
41
+ * Uses constant-time comparison to prevent timing attacks.
42
+ */
43
+ export async function verifyPassword(password, stored) {
44
+ const colonIndex = stored.indexOf(':');
45
+ if (colonIndex === -1)
46
+ return false;
47
+ const salt = stored.slice(0, colonIndex);
48
+ const storedHash = stored.slice(colonIndex + 1);
49
+ const computed = await new Promise((resolve, reject) => {
50
+ scrypt(password, salt, SCRYPT_KEYLEN, { N: SCRYPT_COST, r: SCRYPT_BLOCK_SIZE, p: SCRYPT_PARALLELIZATION }, (err, derived) => {
51
+ if (err)
52
+ reject(err);
53
+ else
54
+ resolve(derived);
55
+ });
56
+ });
57
+ const storedBuffer = Buffer.from(storedHash, 'hex');
58
+ const computedBuffer = computed;
59
+ if (storedBuffer.length !== computedBuffer.length)
60
+ return false;
61
+ return timingSafeEqual(storedBuffer, computedBuffer);
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // UserStore
65
+ // ---------------------------------------------------------------------------
66
+ export class UserStore {
67
+ users = new Map();
68
+ create(record) {
69
+ // Check email uniqueness (case-insensitive)
70
+ const normalizedEmail = record.email.toLowerCase().trim();
71
+ for (const existing of this.users.values()) {
72
+ if (existing.email === normalizedEmail) {
73
+ throw new Error(`User with email "${normalizedEmail}" already exists`);
74
+ }
75
+ }
76
+ this.users.set(record.id, { ...record, email: normalizedEmail });
77
+ }
78
+ getById(id) {
79
+ const record = this.users.get(id);
80
+ return record ? { ...record } : undefined;
81
+ }
82
+ getByEmail(email) {
83
+ const normalized = email.toLowerCase().trim();
84
+ for (const record of this.users.values()) {
85
+ if (record.email === normalized) {
86
+ return { ...record };
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+ list() {
92
+ return [...this.users.values()].map((r) => ({ ...r }));
93
+ }
94
+ update(id, updates) {
95
+ const existing = this.users.get(id);
96
+ if (!existing) {
97
+ throw new Error(`User with id "${id}" not found`);
98
+ }
99
+ // Prevent changing the id via update
100
+ const { id: _ignored, ...safeUpdates } = updates;
101
+ this.users.set(id, { ...existing, ...safeUpdates });
102
+ }
103
+ delete(id) {
104
+ this.users.delete(id);
105
+ }
106
+ getByResetToken(tokenHash) {
107
+ for (const record of this.users.values()) {
108
+ if (record.passwordResetTokenHash === tokenHash &&
109
+ record.passwordResetExpiresAt &&
110
+ Date.now() < record.passwordResetExpiresAt) {
111
+ return { ...record };
112
+ }
113
+ }
114
+ return undefined;
115
+ }
116
+ getByVerificationToken(tokenHash) {
117
+ for (const record of this.users.values()) {
118
+ if (record.emailVerificationTokenHash === tokenHash) {
119
+ return { ...record };
120
+ }
121
+ }
122
+ return undefined;
123
+ }
124
+ getByStripeCustomerId(customerId) {
125
+ for (const record of this.users.values()) {
126
+ if (record.stripeCustomerId === customerId) {
127
+ return { ...record };
128
+ }
129
+ }
130
+ return undefined;
131
+ }
132
+ count() {
133
+ return this.users.size;
134
+ }
135
+ }
136
+ // ---------------------------------------------------------------------------
137
+ // SessionStore
138
+ // ---------------------------------------------------------------------------
139
+ export class SessionStore {
140
+ sessions = new Map();
141
+ create(userId) {
142
+ const token = randomBytes(32).toString('hex');
143
+ const csrfToken = randomBytes(16).toString('hex');
144
+ const now = Date.now();
145
+ const session = {
146
+ token,
147
+ userId,
148
+ csrfToken,
149
+ createdAt: now,
150
+ expiresAt: now + SESSION_MAX_AGE_MS,
151
+ };
152
+ this.sessions.set(token, session);
153
+ return session;
154
+ }
155
+ get(token) {
156
+ const session = this.sessions.get(token);
157
+ if (!session)
158
+ return undefined;
159
+ // Check expiry
160
+ if (Date.now() > session.expiresAt) {
161
+ this.sessions.delete(token);
162
+ return undefined;
163
+ }
164
+ return session;
165
+ }
166
+ destroy(token) {
167
+ this.sessions.delete(token);
168
+ }
169
+ setPendingServerToken(sessionToken, slug, token) {
170
+ const session = this.sessions.get(sessionToken);
171
+ if (!session)
172
+ return;
173
+ session.pendingServerTokens ??= {};
174
+ session.pendingServerTokens[slug] = token;
175
+ }
176
+ consumePendingServerToken(sessionToken, slug) {
177
+ const session = this.sessions.get(sessionToken);
178
+ if (!session?.pendingServerTokens)
179
+ return undefined;
180
+ const token = session.pendingServerTokens[slug];
181
+ delete session.pendingServerTokens[slug];
182
+ if (Object.keys(session.pendingServerTokens).length === 0) {
183
+ delete session.pendingServerTokens;
184
+ }
185
+ return token;
186
+ }
187
+ cleanup() {
188
+ const now = Date.now();
189
+ for (const [token, session] of this.sessions) {
190
+ if (now > session.expiresAt) {
191
+ this.sessions.delete(token);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ // ---------------------------------------------------------------------------
197
+ // Singleton instances
198
+ // ---------------------------------------------------------------------------
199
+ export const userStore = new UserStore();
200
+ export const sessionStore = new SessionStore();
201
+ // ---------------------------------------------------------------------------
202
+ // Pg-aware store helpers (try Pg first, fall back to in-memory)
203
+ // ---------------------------------------------------------------------------
204
+ /**
205
+ * Look up a user by ID, preferring the Pg store when available.
206
+ */
207
+ export async function getUserById(id) {
208
+ const pg = getPgUserStore();
209
+ if (pg) {
210
+ return await pg.getById(id);
211
+ }
212
+ return userStore.getById(id);
213
+ }
214
+ /**
215
+ * Look up a user by email, preferring the Pg store when available.
216
+ */
217
+ export async function getUserByEmail(email) {
218
+ const pg = getPgUserStore();
219
+ if (pg) {
220
+ return await pg.getByEmail(email);
221
+ }
222
+ return userStore.getByEmail(email);
223
+ }
224
+ /**
225
+ * Look up a user by Stripe customer ID, preferring the Pg store when available.
226
+ */
227
+ export async function getUserByStripeCustomerId(customerId) {
228
+ const pg = getPgUserStore();
229
+ if (pg) {
230
+ return await pg.getByStripeCustomerId(customerId);
231
+ }
232
+ return userStore.getByStripeCustomerId(customerId);
233
+ }
234
+ /**
235
+ * Look up a user by email verification token hash.
236
+ */
237
+ export async function getUserByVerificationToken(tokenHash) {
238
+ const pg = getPgUserStore();
239
+ if (pg) {
240
+ return await pg.getByVerificationToken(tokenHash);
241
+ }
242
+ return userStore.getByVerificationToken(tokenHash);
243
+ }
244
+ /**
245
+ * Create a user, persisting to Pg when available, otherwise in-memory.
246
+ */
247
+ export async function createUser(record) {
248
+ const pg = getPgUserStore();
249
+ if (pg) {
250
+ await pg.create(record);
251
+ return;
252
+ }
253
+ userStore.create(record);
254
+ }
255
+ /**
256
+ * Update a user, persisting to Pg when available, otherwise in-memory.
257
+ */
258
+ export async function updateUser(id, updates) {
259
+ const pg = getPgUserStore();
260
+ if (pg) {
261
+ await pg.update(id, updates);
262
+ return;
263
+ }
264
+ userStore.update(id, updates);
265
+ }
266
+ /**
267
+ * Delete a user, removing from Pg when available, otherwise in-memory.
268
+ */
269
+ export async function deleteUser(id) {
270
+ const pg = getPgUserStore();
271
+ if (pg) {
272
+ await pg.delete(id);
273
+ return;
274
+ }
275
+ userStore.delete(id);
276
+ }
277
+ /**
278
+ * Count users, preferring Pg when available.
279
+ */
280
+ export async function countUsers() {
281
+ const pg = getPgUserStore();
282
+ if (pg) {
283
+ return await pg.count();
284
+ }
285
+ return userStore.count();
286
+ }
287
+ /**
288
+ * List all users, preferring Pg when available.
289
+ */
290
+ export async function listUsers() {
291
+ const pg = getPgUserStore();
292
+ if (pg) {
293
+ return await pg.list();
294
+ }
295
+ return userStore.list();
296
+ }
297
+ /**
298
+ * Get a session by token, preferring Pg when available.
299
+ */
300
+ export async function getSession(token) {
301
+ // Check in-memory first (anon sessions always live here, even when Pg is active)
302
+ const inMemory = sessionStore.get(token);
303
+ if (inMemory)
304
+ return inMemory;
305
+ const pg = getPgSessionStore();
306
+ if (pg) {
307
+ return await pg.get(token);
308
+ }
309
+ return undefined;
310
+ }
311
+ /**
312
+ * Create a session, persisting to Pg when available, otherwise in-memory.
313
+ * Note: anonymous sessions (userId === '__anon__') always use in-memory
314
+ * because the Pg sessions table has a FK constraint on users(id).
315
+ */
316
+ export async function createSession(userId) {
317
+ if (userId === '__anon__') {
318
+ return sessionStore.create(userId);
319
+ }
320
+ const pg = getPgSessionStore();
321
+ if (pg) {
322
+ return await pg.create(userId);
323
+ }
324
+ return sessionStore.create(userId);
325
+ }
326
+ /**
327
+ * Destroy a session, removing from Pg when available, otherwise in-memory.
328
+ * Destroys from both stores to handle sessions that may have been created
329
+ * in-memory (anon sessions) even when Pg is available.
330
+ */
331
+ export async function destroySession(token) {
332
+ const pg = getPgSessionStore();
333
+ if (pg) {
334
+ await pg.destroy(token);
335
+ }
336
+ // Always also clean in-memory (anon sessions live there even when Pg is active)
337
+ sessionStore.destroy(token);
338
+ }
339
+ /**
340
+ * Set a pending server token on a session.
341
+ */
342
+ export async function setPendingToken(sessionToken, slug, serverBearerToken) {
343
+ const pg = getPgSessionStore();
344
+ if (pg) {
345
+ await pg.setPendingServerToken(sessionToken, slug, serverBearerToken);
346
+ return;
347
+ }
348
+ sessionStore.setPendingServerToken(sessionToken, slug, serverBearerToken);
349
+ }
350
+ /**
351
+ * Consume a pending server token from a session.
352
+ */
353
+ export async function consumePendingToken(sessionToken, slug) {
354
+ const pg = getPgSessionStore();
355
+ if (pg) {
356
+ return await pg.consumePendingServerToken(sessionToken, slug);
357
+ }
358
+ return sessionStore.consumePendingServerToken(sessionToken, slug);
359
+ }
360
+ /**
361
+ * Clean up expired sessions from both stores.
362
+ */
363
+ export async function cleanupSessions() {
364
+ const pg = getPgSessionStore();
365
+ if (pg) {
366
+ await pg.cleanup();
367
+ }
368
+ sessionStore.cleanup();
369
+ }
370
+ // ---------------------------------------------------------------------------
371
+ // Password reset helpers
372
+ // ---------------------------------------------------------------------------
373
+ const RESET_TOKEN_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
374
+ /**
375
+ * Generate a password reset token.
376
+ * Returns the raw token (for the email link) and its SHA-256 hash (for storage).
377
+ */
378
+ export function generateResetToken() {
379
+ const token = randomBytes(32).toString('hex');
380
+ const hash = createHash('sha256').update(token).digest('hex');
381
+ return { token, hash };
382
+ }
383
+ /**
384
+ * Hash a reset token for lookup.
385
+ */
386
+ export function hashResetToken(token) {
387
+ return createHash('sha256').update(token).digest('hex');
388
+ }
389
+ /**
390
+ * Store a password reset token for a user.
391
+ */
392
+ export async function setPasswordResetToken(userId, tokenHash) {
393
+ await updateUser(userId, {
394
+ passwordResetTokenHash: tokenHash,
395
+ passwordResetExpiresAt: Date.now() + RESET_TOKEN_EXPIRY_MS,
396
+ });
397
+ }
398
+ /**
399
+ * Look up a user by their password reset token hash.
400
+ * Returns undefined if token is invalid or expired.
401
+ */
402
+ export async function getUserByResetToken(tokenHash) {
403
+ const pg = getPgUserStore();
404
+ if (pg) {
405
+ return await pg.getByResetToken(tokenHash);
406
+ }
407
+ return userStore.getByResetToken(tokenHash);
408
+ }
409
+ // ---------------------------------------------------------------------------
410
+ // Email verification helpers
411
+ // ---------------------------------------------------------------------------
412
+ /**
413
+ * Whether email verification is enforced for this deployment. Enforced when a
414
+ * mailer is configured (production) or explicitly requested. In pure dev (no
415
+ * SMTP) the flow still works — the link is logged to the console — but server
416
+ * creation is not blocked, so local testing isn't impeded.
417
+ */
418
+ export function isEmailVerificationRequired() {
419
+ return process.env.REQUIRE_EMAIL_VERIFICATION === 'true' || !!process.env.SMTP_HOST;
420
+ }
421
+ /**
422
+ * Generate an email verification token. Returns the raw token (for the link)
423
+ * and its SHA-256 hash (for storage).
424
+ */
425
+ export function generateVerificationToken() {
426
+ const token = randomBytes(32).toString('hex');
427
+ const hash = createHash('sha256').update(token).digest('hex');
428
+ return { token, hash };
429
+ }
430
+ /** Hash a verification token for lookup. */
431
+ export function hashVerificationToken(token) {
432
+ return createHash('sha256').update(token).digest('hex');
433
+ }
434
+ /** Store a pending email verification token hash for a user. */
435
+ export async function setEmailVerificationToken(userId, tokenHash) {
436
+ await updateUser(userId, { emailVerified: false, emailVerificationTokenHash: tokenHash });
437
+ }
438
+ /** Mark a user's email as verified and clear the pending token. */
439
+ export async function markEmailVerified(userId) {
440
+ const pg = getPgUserStore();
441
+ if (pg) {
442
+ await pg.update(userId, { emailVerified: true });
443
+ // Clear the token hash explicitly (NULL) so it can't be reused.
444
+ await pg.clearVerificationToken(userId);
445
+ return;
446
+ }
447
+ userStore.update(userId, { emailVerified: true, emailVerificationTokenHash: undefined });
448
+ }
449
+ /** Clear a user's email verification token. */
450
+ export async function clearEmailVerificationToken(userId) {
451
+ const pg = getPgUserStore();
452
+ if (pg) {
453
+ await pg.clearVerificationToken(userId);
454
+ return;
455
+ }
456
+ userStore.update(userId, { emailVerificationTokenHash: undefined });
457
+ }
458
+ /**
459
+ * Clear a user's password reset token after successful reset.
460
+ */
461
+ export async function clearPasswordResetToken(userId) {
462
+ const pg = getPgUserStore();
463
+ if (pg) {
464
+ await pg.clearResetToken(userId);
465
+ return;
466
+ }
467
+ const user = userStore.getById(userId);
468
+ if (user) {
469
+ userStore.update(userId, {
470
+ passwordResetTokenHash: undefined,
471
+ passwordResetExpiresAt: undefined,
472
+ });
473
+ }
474
+ }
475
+ // ---------------------------------------------------------------------------
476
+ // Session helpers
477
+ // ---------------------------------------------------------------------------
478
+ /**
479
+ * Parse the mf_session cookie from request headers.
480
+ */
481
+ export function getSessionToken(req) {
482
+ const cookie = req.headers.cookie;
483
+ if (!cookie)
484
+ return undefined;
485
+ const parts = cookie.split(';');
486
+ for (const part of parts) {
487
+ const trimmed = part.trim();
488
+ if (trimmed.startsWith('mf_session=')) {
489
+ return trimmed.slice('mf_session='.length);
490
+ }
491
+ }
492
+ return undefined;
493
+ }
494
+ /**
495
+ * Set session cookie on response.
496
+ * Cookie: mf_session={token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=604800
497
+ * Adds Secure when the request is HTTPS or forwarded as HTTPS.
498
+ */
499
+ export function setSessionCookie(res, req, token) {
500
+ const secure = isSecureRequest(req) ? '; Secure' : '';
501
+ const cookie = `mf_session=${token}; HttpOnly; SameSite=Strict; Path=/; Max-Age=${SESSION_MAX_AGE_SECONDS}${secure}`;
502
+ res.setHeader('Set-Cookie', cookie);
503
+ }
504
+ /**
505
+ * Clear session cookie on response.
506
+ */
507
+ export function clearSessionCookie(res, req) {
508
+ const secure = isSecureRequest(req) ? '; Secure' : '';
509
+ res.setHeader('Set-Cookie', `mf_session=; HttpOnly; SameSite=Strict; Path=/; Max-Age=0${secure}`);
510
+ }
511
+ /**
512
+ * Get authenticated user from request, or null.
513
+ * Reads the mf_session cookie, looks up the session, then the user.
514
+ * Tries Pg stores first when available, falls back to in-memory.
515
+ */
516
+ export async function getAuthenticatedUser(req) {
517
+ const token = getSessionToken(req);
518
+ if (!token)
519
+ return null;
520
+ const session = await getSession(token);
521
+ if (!session)
522
+ return null;
523
+ const user = await getUserById(session.userId);
524
+ if (!user)
525
+ return null;
526
+ return { user, session };
527
+ }
528
+ /**
529
+ * Require authentication. Returns user+session or sends 302 redirect to /login.
530
+ * Returns null if the redirect was sent (caller should stop processing).
531
+ */
532
+ export async function requireAuth(req, res) {
533
+ const auth = await getAuthenticatedUser(req);
534
+ if (!auth) {
535
+ res.writeHead(302, { Location: '/login' });
536
+ res.end();
537
+ return null;
538
+ }
539
+ return auth;
540
+ }
541
+ /**
542
+ * Require admin. Returns user+session or sends 403.
543
+ * Returns null if the error response was sent (caller should stop processing).
544
+ */
545
+ export async function requireAdmin(req, res) {
546
+ const auth = await requireAuth(req, res);
547
+ if (!auth)
548
+ return null;
549
+ if (!auth.user.isAdmin) {
550
+ res.writeHead(403, { 'Content-Type': 'text/html; charset=utf-8' });
551
+ res.end('<h1>403 — Forbidden</h1>');
552
+ return null;
553
+ }
554
+ return auth;
555
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Server-rendered inline-SVG charts for the admin dashboard.
3
+ *
4
+ * The admin CSP forbids inline scripts and `unsafe-inline` (see `sendHtml` in
5
+ * router.ts), so charts MUST be pure static SVG: no `<script>`, no `onclick`,
6
+ * no inline JS. Hover values are exposed only via `<title>` elements.
7
+ *
8
+ * Every function is a pure `(numbers) -> string`, so they are trivially unit
9
+ * testable and have no runtime dependencies. SVGs use a fixed `viewBox` and NO
10
+ * width/height attributes — they scale to their container via CSS.
11
+ *
12
+ * SECURITY / CORRECTNESS: every numeric input is clamped to a finite number
13
+ * (NaN / Infinity / non-number -> 0) BEFORE it is interpolated into any
14
+ * `points=""`, `d=""`, or coordinate attribute. This is both a correctness
15
+ * guard (a single bad sample can't blow up the whole path) and an injection
16
+ * guard (no caller-controlled string ever reaches the SVG markup; only numbers
17
+ * we produced ourselves are emitted).
18
+ */
19
+ export interface SparklineOptions {
20
+ /** viewBox width (logical units). */
21
+ width?: number;
22
+ /** viewBox height (logical units). */
23
+ height?: number;
24
+ /** Stroke colour (must be a CSS colour literal). */
25
+ color?: string;
26
+ /** Optional label shown in the `<title>` (e.g. "Requests / 5min"). */
27
+ label?: string;
28
+ /** Optional unit suffix for hover values (e.g. "%"). */
29
+ unit?: string;
30
+ }
31
+ /**
32
+ * A single-series trend line (`<polyline>`).
33
+ *
34
+ * Y is scaled to the data's own min/max so small variations stay visible. With
35
+ * 0 or 1 points an empty-but-valid SVG is returned (no NaN coordinates).
36
+ */
37
+ export declare function renderSparkline(values: number[], opts?: SparklineOptions): string;
38
+ export interface BarsOptions {
39
+ width?: number;
40
+ height?: number;
41
+ color?: string;
42
+ /** Per-bar labels for `<title>` hover (aligned by index). */
43
+ labels?: string[];
44
+ /** Unit suffix for hover values. */
45
+ unit?: string;
46
+ /** Optional aria/title prefix for the whole chart. */
47
+ label?: string;
48
+ }
49
+ /**
50
+ * A simple vertical bar chart. Bars are scaled to the data max (0-baselined).
51
+ */
52
+ export declare function renderBars(values: number[], opts?: BarsOptions): string;
53
+ export interface DualLineOptions {
54
+ width?: number;
55
+ height?: number;
56
+ /** Colour of series A. */
57
+ colorA?: string;
58
+ /** Colour of series B. */
59
+ colorB?: string;
60
+ /** Names of each series (used in hover titles + aria). */
61
+ labelA?: string;
62
+ labelB?: string;
63
+ unit?: string;
64
+ }
65
+ /**
66
+ * Two overlaid trend lines on a shared Y scale — built for the
67
+ * requests-vs-errors time series. Both series share one min/max so they are
68
+ * visually comparable. Missing / short series degrade gracefully.
69
+ */
70
+ export declare function renderDualLine(seriesA: number[], seriesB: number[], opts?: DualLineOptions): string;