enpilink 1.0.2

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 (477) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +289 -0
  3. package/bin/run.js +5 -0
  4. package/dist/cli/build-helpers.d.ts +8 -0
  5. package/dist/cli/build-helpers.js +105 -0
  6. package/dist/cli/build-helpers.js.map +1 -0
  7. package/dist/cli/build-helpers.test.d.ts +1 -0
  8. package/dist/cli/build-helpers.test.js +100 -0
  9. package/dist/cli/build-helpers.test.js.map +1 -0
  10. package/dist/cli/detect-port.d.ts +18 -0
  11. package/dist/cli/detect-port.js +50 -0
  12. package/dist/cli/detect-port.js.map +1 -0
  13. package/dist/cli/ensure-ssh-key.d.ts +17 -0
  14. package/dist/cli/ensure-ssh-key.js +45 -0
  15. package/dist/cli/ensure-ssh-key.js.map +1 -0
  16. package/dist/cli/ensure-ssh-key.test.d.ts +1 -0
  17. package/dist/cli/ensure-ssh-key.test.js +68 -0
  18. package/dist/cli/ensure-ssh-key.test.js.map +1 -0
  19. package/dist/cli/header.d.ts +4 -0
  20. package/dist/cli/header.js +6 -0
  21. package/dist/cli/header.js.map +1 -0
  22. package/dist/cli/resolve-views-dir.d.ts +1 -0
  23. package/dist/cli/resolve-views-dir.js +17 -0
  24. package/dist/cli/resolve-views-dir.js.map +1 -0
  25. package/dist/cli/run-command.d.ts +2 -0
  26. package/dist/cli/run-command.js +43 -0
  27. package/dist/cli/run-command.js.map +1 -0
  28. package/dist/cli/telemetry.d.ts +14 -0
  29. package/dist/cli/telemetry.js +24 -0
  30. package/dist/cli/telemetry.js.map +1 -0
  31. package/dist/cli/tunnel-control-server.d.ts +11 -0
  32. package/dist/cli/tunnel-control-server.js +35 -0
  33. package/dist/cli/tunnel-control-server.js.map +1 -0
  34. package/dist/cli/tunnel-control-server.test.d.ts +1 -0
  35. package/dist/cli/tunnel-control-server.test.js +39 -0
  36. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  37. package/dist/cli/tunnel-handler.d.ts +3 -0
  38. package/dist/cli/tunnel-handler.js +48 -0
  39. package/dist/cli/tunnel-handler.js.map +1 -0
  40. package/dist/cli/tunnel-handler.test.d.ts +1 -0
  41. package/dist/cli/tunnel-handler.test.js +107 -0
  42. package/dist/cli/tunnel-handler.test.js.map +1 -0
  43. package/dist/cli/tunnel-providers/index.d.ts +5 -0
  44. package/dist/cli/tunnel-providers/index.js +5 -0
  45. package/dist/cli/tunnel-providers/index.js.map +1 -0
  46. package/dist/cli/tunnel-providers/srv-us.d.ts +18 -0
  47. package/dist/cli/tunnel-providers/srv-us.js +66 -0
  48. package/dist/cli/tunnel-providers/srv-us.js.map +1 -0
  49. package/dist/cli/tunnel-providers/srv-us.test.d.ts +1 -0
  50. package/dist/cli/tunnel-providers/srv-us.test.js +74 -0
  51. package/dist/cli/tunnel-providers/srv-us.test.js.map +1 -0
  52. package/dist/cli/tunnel-providers/types.d.ts +49 -0
  53. package/dist/cli/tunnel-providers/types.js +2 -0
  54. package/dist/cli/tunnel-providers/types.js.map +1 -0
  55. package/dist/cli/tunnel.d.ts +75 -0
  56. package/dist/cli/tunnel.js +254 -0
  57. package/dist/cli/tunnel.js.map +1 -0
  58. package/dist/cli/tunnel.test.d.ts +1 -0
  59. package/dist/cli/tunnel.test.js +255 -0
  60. package/dist/cli/tunnel.test.js.map +1 -0
  61. package/dist/cli/types.d.ts +5 -0
  62. package/dist/cli/types.js +2 -0
  63. package/dist/cli/types.js.map +1 -0
  64. package/dist/cli/use-execute-steps.d.ts +11 -0
  65. package/dist/cli/use-execute-steps.js +36 -0
  66. package/dist/cli/use-execute-steps.js.map +1 -0
  67. package/dist/cli/use-messages.d.ts +3 -0
  68. package/dist/cli/use-messages.js +11 -0
  69. package/dist/cli/use-messages.js.map +1 -0
  70. package/dist/cli/use-nodemon.d.ts +2 -0
  71. package/dist/cli/use-nodemon.js +73 -0
  72. package/dist/cli/use-nodemon.js.map +1 -0
  73. package/dist/cli/use-open-browser.d.ts +1 -0
  74. package/dist/cli/use-open-browser.js +44 -0
  75. package/dist/cli/use-open-browser.js.map +1 -0
  76. package/dist/cli/use-open-tunnel-browser.d.ts +6 -0
  77. package/dist/cli/use-open-tunnel-browser.js +19 -0
  78. package/dist/cli/use-open-tunnel-browser.js.map +1 -0
  79. package/dist/cli/use-tunnel.d.ts +17 -0
  80. package/dist/cli/use-tunnel.js +131 -0
  81. package/dist/cli/use-tunnel.js.map +1 -0
  82. package/dist/cli/use-typescript-check.d.ts +9 -0
  83. package/dist/cli/use-typescript-check.js +94 -0
  84. package/dist/cli/use-typescript-check.js.map +1 -0
  85. package/dist/commands/build.d.ts +8 -0
  86. package/dist/commands/build.js +97 -0
  87. package/dist/commands/build.js.map +1 -0
  88. package/dist/commands/create.d.ts +9 -0
  89. package/dist/commands/create.js +30 -0
  90. package/dist/commands/create.js.map +1 -0
  91. package/dist/commands/dev.d.ts +13 -0
  92. package/dist/commands/dev.js +112 -0
  93. package/dist/commands/dev.js.map +1 -0
  94. package/dist/commands/start.d.ts +10 -0
  95. package/dist/commands/start.js +76 -0
  96. package/dist/commands/start.js.map +1 -0
  97. package/dist/commands/telemetry/disable.d.ts +5 -0
  98. package/dist/commands/telemetry/disable.js +12 -0
  99. package/dist/commands/telemetry/disable.js.map +1 -0
  100. package/dist/commands/telemetry/enable.d.ts +5 -0
  101. package/dist/commands/telemetry/enable.js +12 -0
  102. package/dist/commands/telemetry/enable.js.map +1 -0
  103. package/dist/commands/telemetry/status.d.ts +5 -0
  104. package/dist/commands/telemetry/status.js +12 -0
  105. package/dist/commands/telemetry/status.js.map +1 -0
  106. package/dist/server/admin.d.ts +79 -0
  107. package/dist/server/admin.js +239 -0
  108. package/dist/server/admin.js.map +1 -0
  109. package/dist/server/admin.test.d.ts +1 -0
  110. package/dist/server/admin.test.js +226 -0
  111. package/dist/server/admin.test.js.map +1 -0
  112. package/dist/server/analytics.d.ts +60 -0
  113. package/dist/server/analytics.js +168 -0
  114. package/dist/server/analytics.js.map +1 -0
  115. package/dist/server/analytics.test.d.ts +1 -0
  116. package/dist/server/analytics.test.js +179 -0
  117. package/dist/server/analytics.test.js.map +1 -0
  118. package/dist/server/asset-base-url-transform-plugin.d.ts +11 -0
  119. package/dist/server/asset-base-url-transform-plugin.js +48 -0
  120. package/dist/server/asset-base-url-transform-plugin.js.map +1 -0
  121. package/dist/server/asset-base-url-transform-plugin.test.d.ts +1 -0
  122. package/dist/server/asset-base-url-transform-plugin.test.js +134 -0
  123. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -0
  124. package/dist/server/auth.d.ts +20 -0
  125. package/dist/server/auth.js +28 -0
  126. package/dist/server/auth.js.map +1 -0
  127. package/dist/server/build-manifest.test.d.ts +1 -0
  128. package/dist/server/build-manifest.test.js +27 -0
  129. package/dist/server/build-manifest.test.js.map +1 -0
  130. package/dist/server/config/config.test.d.ts +1 -0
  131. package/dist/server/config/config.test.js +214 -0
  132. package/dist/server/config/config.test.js.map +1 -0
  133. package/dist/server/config/index.d.ts +3 -0
  134. package/dist/server/config/index.js +4 -0
  135. package/dist/server/config/index.js.map +1 -0
  136. package/dist/server/config/resolve.d.ts +73 -0
  137. package/dist/server/config/resolve.js +167 -0
  138. package/dist/server/config/resolve.js.map +1 -0
  139. package/dist/server/config/router.d.ts +23 -0
  140. package/dist/server/config/router.js +119 -0
  141. package/dist/server/config/router.js.map +1 -0
  142. package/dist/server/config/schema.d.ts +78 -0
  143. package/dist/server/config/schema.js +158 -0
  144. package/dist/server/config/schema.js.map +1 -0
  145. package/dist/server/content-helpers.d.ts +67 -0
  146. package/dist/server/content-helpers.js +79 -0
  147. package/dist/server/content-helpers.js.map +1 -0
  148. package/dist/server/content-helpers.test.d.ts +1 -0
  149. package/dist/server/content-helpers.test.js +70 -0
  150. package/dist/server/content-helpers.test.js.map +1 -0
  151. package/dist/server/express.d.ts +11 -0
  152. package/dist/server/express.js +129 -0
  153. package/dist/server/express.js.map +1 -0
  154. package/dist/server/express.test.d.ts +1 -0
  155. package/dist/server/express.test.js +464 -0
  156. package/dist/server/express.test.js.map +1 -0
  157. package/dist/server/file-ref.d.ts +28 -0
  158. package/dist/server/file-ref.js +27 -0
  159. package/dist/server/file-ref.js.map +1 -0
  160. package/dist/server/index.d.ts +17 -0
  161. package/dist/server/index.js +14 -0
  162. package/dist/server/index.js.map +1 -0
  163. package/dist/server/inferUtilityTypes.d.ts +64 -0
  164. package/dist/server/inferUtilityTypes.js +2 -0
  165. package/dist/server/inferUtilityTypes.js.map +1 -0
  166. package/dist/server/log-sink.d.ts +16 -0
  167. package/dist/server/log-sink.js +66 -0
  168. package/dist/server/log-sink.js.map +1 -0
  169. package/dist/server/metric.d.ts +12 -0
  170. package/dist/server/metric.js +13 -0
  171. package/dist/server/metric.js.map +1 -0
  172. package/dist/server/middleware.d.ts +137 -0
  173. package/dist/server/middleware.js +93 -0
  174. package/dist/server/middleware.js.map +1 -0
  175. package/dist/server/middleware.test-d.d.ts +1 -0
  176. package/dist/server/middleware.test-d.js +75 -0
  177. package/dist/server/middleware.test-d.js.map +1 -0
  178. package/dist/server/middleware.test.d.ts +1 -0
  179. package/dist/server/middleware.test.js +493 -0
  180. package/dist/server/middleware.test.js.map +1 -0
  181. package/dist/server/mock-seed.d.ts +62 -0
  182. package/dist/server/mock-seed.js +251 -0
  183. package/dist/server/mock-seed.js.map +1 -0
  184. package/dist/server/mock-seed.test.d.ts +1 -0
  185. package/dist/server/mock-seed.test.js +122 -0
  186. package/dist/server/mock-seed.test.js.map +1 -0
  187. package/dist/server/observability.d.ts +149 -0
  188. package/dist/server/observability.js +340 -0
  189. package/dist/server/observability.js.map +1 -0
  190. package/dist/server/observability.test.d.ts +1 -0
  191. package/dist/server/observability.test.js +251 -0
  192. package/dist/server/observability.test.js.map +1 -0
  193. package/dist/server/otel.d.ts +45 -0
  194. package/dist/server/otel.js +117 -0
  195. package/dist/server/otel.js.map +1 -0
  196. package/dist/server/otel.test.d.ts +1 -0
  197. package/dist/server/otel.test.js +122 -0
  198. package/dist/server/otel.test.js.map +1 -0
  199. package/dist/server/server.d.ts +422 -0
  200. package/dist/server/server.js +684 -0
  201. package/dist/server/server.js.map +1 -0
  202. package/dist/server/storage/index.d.ts +23 -0
  203. package/dist/server/storage/index.js +46 -0
  204. package/dist/server/storage/index.js.map +1 -0
  205. package/dist/server/storage/memory.d.ts +30 -0
  206. package/dist/server/storage/memory.js +98 -0
  207. package/dist/server/storage/memory.js.map +1 -0
  208. package/dist/server/storage/memory.test.d.ts +1 -0
  209. package/dist/server/storage/memory.test.js +81 -0
  210. package/dist/server/storage/memory.test.js.map +1 -0
  211. package/dist/server/storage/postgres.d.ts +65 -0
  212. package/dist/server/storage/postgres.js +242 -0
  213. package/dist/server/storage/postgres.js.map +1 -0
  214. package/dist/server/storage/postgres.test.d.ts +1 -0
  215. package/dist/server/storage/postgres.test.js +182 -0
  216. package/dist/server/storage/postgres.test.js.map +1 -0
  217. package/dist/server/storage/sqlite.d.ts +33 -0
  218. package/dist/server/storage/sqlite.js +250 -0
  219. package/dist/server/storage/sqlite.js.map +1 -0
  220. package/dist/server/storage/sqlite.test.d.ts +1 -0
  221. package/dist/server/storage/sqlite.test.js +133 -0
  222. package/dist/server/storage/sqlite.test.js.map +1 -0
  223. package/dist/server/storage/types.d.ts +119 -0
  224. package/dist/server/storage/types.js +11 -0
  225. package/dist/server/storage/types.js.map +1 -0
  226. package/dist/server/templateHelper.d.ts +16 -0
  227. package/dist/server/templateHelper.js +11 -0
  228. package/dist/server/templateHelper.js.map +1 -0
  229. package/dist/server/templates.generated.d.ts +4 -0
  230. package/dist/server/templates.generated.js +47 -0
  231. package/dist/server/templates.generated.js.map +1 -0
  232. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  233. package/dist/server/tunnel-proxy-router.js +110 -0
  234. package/dist/server/tunnel-proxy-router.js.map +1 -0
  235. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  236. package/dist/server/tunnel-proxy-router.test.js +229 -0
  237. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  238. package/dist/server/viewsDevServer.d.ts +14 -0
  239. package/dist/server/viewsDevServer.js +45 -0
  240. package/dist/server/viewsDevServer.js.map +1 -0
  241. package/dist/test/utils.d.ts +127 -0
  242. package/dist/test/utils.js +247 -0
  243. package/dist/test/utils.js.map +1 -0
  244. package/dist/test/view.test.d.ts +1 -0
  245. package/dist/test/view.test.js +568 -0
  246. package/dist/test/view.test.js.map +1 -0
  247. package/dist/version.d.ts +1 -0
  248. package/dist/version.js +3 -0
  249. package/dist/version.js.map +1 -0
  250. package/dist/web/bridges/apps-sdk/adaptor.d.ts +54 -0
  251. package/dist/web/bridges/apps-sdk/adaptor.js +164 -0
  252. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -0
  253. package/dist/web/bridges/apps-sdk/bridge.d.ts +11 -0
  254. package/dist/web/bridges/apps-sdk/bridge.js +47 -0
  255. package/dist/web/bridges/apps-sdk/bridge.js.map +1 -0
  256. package/dist/web/bridges/apps-sdk/index.d.ts +5 -0
  257. package/dist/web/bridges/apps-sdk/index.js +5 -0
  258. package/dist/web/bridges/apps-sdk/index.js.map +1 -0
  259. package/dist/web/bridges/apps-sdk/types.d.ts +147 -0
  260. package/dist/web/bridges/apps-sdk/types.js +10 -0
  261. package/dist/web/bridges/apps-sdk/types.js.map +1 -0
  262. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.d.ts +13 -0
  263. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js +18 -0
  264. package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -0
  265. package/dist/web/bridges/get-adaptor.d.ts +9 -0
  266. package/dist/web/bridges/get-adaptor.js +15 -0
  267. package/dist/web/bridges/get-adaptor.js.map +1 -0
  268. package/dist/web/bridges/index.d.ts +5 -0
  269. package/dist/web/bridges/index.js +6 -0
  270. package/dist/web/bridges/index.js.map +1 -0
  271. package/dist/web/bridges/mcp-app/adaptor.d.ts +81 -0
  272. package/dist/web/bridges/mcp-app/adaptor.js +346 -0
  273. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -0
  274. package/dist/web/bridges/mcp-app/bridge.d.ts +28 -0
  275. package/dist/web/bridges/mcp-app/bridge.js +124 -0
  276. package/dist/web/bridges/mcp-app/bridge.js.map +1 -0
  277. package/dist/web/bridges/mcp-app/index.d.ts +4 -0
  278. package/dist/web/bridges/mcp-app/index.js +4 -0
  279. package/dist/web/bridges/mcp-app/index.js.map +1 -0
  280. package/dist/web/bridges/mcp-app/types.d.ts +8 -0
  281. package/dist/web/bridges/mcp-app/types.js +2 -0
  282. package/dist/web/bridges/mcp-app/types.js.map +1 -0
  283. package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +19 -0
  284. package/dist/web/bridges/mcp-app/use-mcp-app-context.js +19 -0
  285. package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -0
  286. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.d.ts +1 -0
  287. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js +26 -0
  288. package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -0
  289. package/dist/web/bridges/mcp-app/view-tools.test.d.ts +1 -0
  290. package/dist/web/bridges/mcp-app/view-tools.test.js +144 -0
  291. package/dist/web/bridges/mcp-app/view-tools.test.js.map +1 -0
  292. package/dist/web/bridges/types.d.ts +243 -0
  293. package/dist/web/bridges/types.js +2 -0
  294. package/dist/web/bridges/types.js.map +1 -0
  295. package/dist/web/bridges/use-host-context.d.ts +7 -0
  296. package/dist/web/bridges/use-host-context.js +13 -0
  297. package/dist/web/bridges/use-host-context.js.map +1 -0
  298. package/dist/web/components/modal-provider.d.ts +4 -0
  299. package/dist/web/components/modal-provider.js +45 -0
  300. package/dist/web/components/modal-provider.js.map +1 -0
  301. package/dist/web/create-store.d.ts +29 -0
  302. package/dist/web/create-store.js +64 -0
  303. package/dist/web/create-store.js.map +1 -0
  304. package/dist/web/create-store.test.d.ts +1 -0
  305. package/dist/web/create-store.test.js +129 -0
  306. package/dist/web/create-store.test.js.map +1 -0
  307. package/dist/web/data-llm.d.ts +47 -0
  308. package/dist/web/data-llm.js +100 -0
  309. package/dist/web/data-llm.js.map +1 -0
  310. package/dist/web/data-llm.test.d.ts +1 -0
  311. package/dist/web/data-llm.test.js +142 -0
  312. package/dist/web/data-llm.test.js.map +1 -0
  313. package/dist/web/generate-helpers.d.ts +120 -0
  314. package/dist/web/generate-helpers.js +115 -0
  315. package/dist/web/generate-helpers.js.map +1 -0
  316. package/dist/web/generate-helpers.test-d.d.ts +1 -0
  317. package/dist/web/generate-helpers.test-d.js +211 -0
  318. package/dist/web/generate-helpers.test-d.js.map +1 -0
  319. package/dist/web/generate-helpers.test.d.ts +1 -0
  320. package/dist/web/generate-helpers.test.js +17 -0
  321. package/dist/web/generate-helpers.test.js.map +1 -0
  322. package/dist/web/helpers/state.d.ts +7 -0
  323. package/dist/web/helpers/state.js +45 -0
  324. package/dist/web/helpers/state.js.map +1 -0
  325. package/dist/web/helpers/state.test.d.ts +1 -0
  326. package/dist/web/helpers/state.test.js +53 -0
  327. package/dist/web/helpers/state.test.js.map +1 -0
  328. package/dist/web/hooks/index.d.ts +17 -0
  329. package/dist/web/hooks/index.js +18 -0
  330. package/dist/web/hooks/index.js.map +1 -0
  331. package/dist/web/hooks/test/utils.d.ts +20 -0
  332. package/dist/web/hooks/test/utils.js +75 -0
  333. package/dist/web/hooks/test/utils.js.map +1 -0
  334. package/dist/web/hooks/use-call-tool.d.ts +146 -0
  335. package/dist/web/hooks/use-call-tool.js +96 -0
  336. package/dist/web/hooks/use-call-tool.js.map +1 -0
  337. package/dist/web/hooks/use-call-tool.test-d.d.ts +1 -0
  338. package/dist/web/hooks/use-call-tool.test-d.js +104 -0
  339. package/dist/web/hooks/use-call-tool.test-d.js.map +1 -0
  340. package/dist/web/hooks/use-call-tool.test.d.ts +1 -0
  341. package/dist/web/hooks/use-call-tool.test.js +211 -0
  342. package/dist/web/hooks/use-call-tool.test.js.map +1 -0
  343. package/dist/web/hooks/use-display-mode.d.ts +24 -0
  344. package/dist/web/hooks/use-display-mode.js +29 -0
  345. package/dist/web/hooks/use-display-mode.js.map +1 -0
  346. package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
  347. package/dist/web/hooks/use-display-mode.test-d.js +8 -0
  348. package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
  349. package/dist/web/hooks/use-display-mode.test.d.ts +1 -0
  350. package/dist/web/hooks/use-display-mode.test.js +41 -0
  351. package/dist/web/hooks/use-display-mode.test.js.map +1 -0
  352. package/dist/web/hooks/use-download.d.ts +5 -0
  353. package/dist/web/hooks/use-download.js +8 -0
  354. package/dist/web/hooks/use-download.js.map +1 -0
  355. package/dist/web/hooks/use-download.test.d.ts +1 -0
  356. package/dist/web/hooks/use-download.test.js +95 -0
  357. package/dist/web/hooks/use-download.test.js.map +1 -0
  358. package/dist/web/hooks/use-files.d.ts +39 -0
  359. package/dist/web/hooks/use-files.js +42 -0
  360. package/dist/web/hooks/use-files.js.map +1 -0
  361. package/dist/web/hooks/use-files.test.d.ts +1 -0
  362. package/dist/web/hooks/use-files.test.js +54 -0
  363. package/dist/web/hooks/use-files.test.js.map +1 -0
  364. package/dist/web/hooks/use-intent.d.ts +30 -0
  365. package/dist/web/hooks/use-intent.js +34 -0
  366. package/dist/web/hooks/use-intent.js.map +1 -0
  367. package/dist/web/hooks/use-intent.test.d.ts +1 -0
  368. package/dist/web/hooks/use-intent.test.js +85 -0
  369. package/dist/web/hooks/use-intent.test.js.map +1 -0
  370. package/dist/web/hooks/use-layout.d.ts +24 -0
  371. package/dist/web/hooks/use-layout.js +25 -0
  372. package/dist/web/hooks/use-layout.js.map +1 -0
  373. package/dist/web/hooks/use-layout.test.d.ts +1 -0
  374. package/dist/web/hooks/use-layout.test.js +96 -0
  375. package/dist/web/hooks/use-layout.test.js.map +1 -0
  376. package/dist/web/hooks/use-notify.d.ts +29 -0
  377. package/dist/web/hooks/use-notify.js +33 -0
  378. package/dist/web/hooks/use-notify.js.map +1 -0
  379. package/dist/web/hooks/use-notify.test.d.ts +1 -0
  380. package/dist/web/hooks/use-notify.test.js +105 -0
  381. package/dist/web/hooks/use-notify.test.js.map +1 -0
  382. package/dist/web/hooks/use-open-external.d.ts +20 -0
  383. package/dist/web/hooks/use-open-external.js +24 -0
  384. package/dist/web/hooks/use-open-external.js.map +1 -0
  385. package/dist/web/hooks/use-open-external.test.d.ts +1 -0
  386. package/dist/web/hooks/use-open-external.test.js +65 -0
  387. package/dist/web/hooks/use-open-external.test.js.map +1 -0
  388. package/dist/web/hooks/use-register-view-tool.d.ts +38 -0
  389. package/dist/web/hooks/use-register-view-tool.js +50 -0
  390. package/dist/web/hooks/use-register-view-tool.js.map +1 -0
  391. package/dist/web/hooks/use-request-close.d.ts +16 -0
  392. package/dist/web/hooks/use-request-close.js +21 -0
  393. package/dist/web/hooks/use-request-close.js.map +1 -0
  394. package/dist/web/hooks/use-request-close.test.d.ts +1 -0
  395. package/dist/web/hooks/use-request-close.test.js +52 -0
  396. package/dist/web/hooks/use-request-close.test.js.map +1 -0
  397. package/dist/web/hooks/use-request-modal.d.ts +24 -0
  398. package/dist/web/hooks/use-request-modal.js +31 -0
  399. package/dist/web/hooks/use-request-modal.js.map +1 -0
  400. package/dist/web/hooks/use-request-modal.test.d.ts +1 -0
  401. package/dist/web/hooks/use-request-modal.test.js +61 -0
  402. package/dist/web/hooks/use-request-modal.test.js.map +1 -0
  403. package/dist/web/hooks/use-request-size.d.ts +20 -0
  404. package/dist/web/hooks/use-request-size.js +24 -0
  405. package/dist/web/hooks/use-request-size.js.map +1 -0
  406. package/dist/web/hooks/use-request-size.test.d.ts +1 -0
  407. package/dist/web/hooks/use-request-size.test.js +65 -0
  408. package/dist/web/hooks/use-request-size.test.js.map +1 -0
  409. package/dist/web/hooks/use-send-follow-up-message.d.ts +19 -0
  410. package/dist/web/hooks/use-send-follow-up-message.js +25 -0
  411. package/dist/web/hooks/use-send-follow-up-message.js.map +1 -0
  412. package/dist/web/hooks/use-set-open-in-app-url.d.ts +18 -0
  413. package/dist/web/hooks/use-set-open-in-app-url.js +25 -0
  414. package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -0
  415. package/dist/web/hooks/use-set-open-in-app-url.test.d.ts +1 -0
  416. package/dist/web/hooks/use-set-open-in-app-url.test.js +43 -0
  417. package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -0
  418. package/dist/web/hooks/use-tool-info.d.ts +87 -0
  419. package/dist/web/hooks/use-tool-info.js +49 -0
  420. package/dist/web/hooks/use-tool-info.js.map +1 -0
  421. package/dist/web/hooks/use-tool-info.test-d.d.ts +1 -0
  422. package/dist/web/hooks/use-tool-info.test-d.js +91 -0
  423. package/dist/web/hooks/use-tool-info.test-d.js.map +1 -0
  424. package/dist/web/hooks/use-tool-info.test.d.ts +1 -0
  425. package/dist/web/hooks/use-tool-info.test.js +130 -0
  426. package/dist/web/hooks/use-tool-info.test.js.map +1 -0
  427. package/dist/web/hooks/use-user.d.ts +20 -0
  428. package/dist/web/hooks/use-user.js +37 -0
  429. package/dist/web/hooks/use-user.js.map +1 -0
  430. package/dist/web/hooks/use-user.test.d.ts +1 -0
  431. package/dist/web/hooks/use-user.test.js +122 -0
  432. package/dist/web/hooks/use-user.test.js.map +1 -0
  433. package/dist/web/hooks/use-view-state.d.ts +25 -0
  434. package/dist/web/hooks/use-view-state.js +32 -0
  435. package/dist/web/hooks/use-view-state.js.map +1 -0
  436. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  437. package/dist/web/hooks/use-view-state.test.js +177 -0
  438. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  439. package/dist/web/index.d.ts +7 -0
  440. package/dist/web/index.js +8 -0
  441. package/dist/web/index.js.map +1 -0
  442. package/dist/web/mount-view.d.ts +20 -0
  443. package/dist/web/mount-view.js +46 -0
  444. package/dist/web/mount-view.js.map +1 -0
  445. package/dist/web/plugin/data-llm.test.d.ts +1 -0
  446. package/dist/web/plugin/data-llm.test.js +81 -0
  447. package/dist/web/plugin/data-llm.test.js.map +1 -0
  448. package/dist/web/plugin/plugin.d.ts +33 -0
  449. package/dist/web/plugin/plugin.js +189 -0
  450. package/dist/web/plugin/plugin.js.map +1 -0
  451. package/dist/web/plugin/scan-views.d.ts +16 -0
  452. package/dist/web/plugin/scan-views.js +88 -0
  453. package/dist/web/plugin/scan-views.js.map +1 -0
  454. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  455. package/dist/web/plugin/scan-views.test.js +99 -0
  456. package/dist/web/plugin/scan-views.test.js.map +1 -0
  457. package/dist/web/plugin/transform-data-llm.d.ts +12 -0
  458. package/dist/web/plugin/transform-data-llm.js +96 -0
  459. package/dist/web/plugin/transform-data-llm.js.map +1 -0
  460. package/dist/web/plugin/transform-data-llm.test.d.ts +1 -0
  461. package/dist/web/plugin/transform-data-llm.test.js +81 -0
  462. package/dist/web/plugin/transform-data-llm.test.js.map +1 -0
  463. package/dist/web/plugin/validate-view.d.ts +1 -0
  464. package/dist/web/plugin/validate-view.js +9 -0
  465. package/dist/web/plugin/validate-view.js.map +1 -0
  466. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  467. package/dist/web/plugin/validate-view.test.js +24 -0
  468. package/dist/web/plugin/validate-view.test.js.map +1 -0
  469. package/dist/web/proxy.d.ts +1 -0
  470. package/dist/web/proxy.js +52 -0
  471. package/dist/web/proxy.js.map +1 -0
  472. package/dist/web/types.d.ts +20 -0
  473. package/dist/web/types.js +2 -0
  474. package/dist/web/types.js.map +1 -0
  475. package/package.json +125 -0
  476. package/scripts/postinstall.mjs +45 -0
  477. package/tsconfig.base.json +36 -0
@@ -0,0 +1,239 @@
1
+ import crypto from "node:crypto";
2
+ import { InvalidTokenError, requireBearerAuth } from "./auth.js";
3
+ import { resolveConfig } from "./config/index.js";
4
+ import { getActiveStorage, setActiveStorage } from "./log-sink.js";
5
+ import { resolveStorageAdapter } from "./storage/index.js";
6
+ /**
7
+ * Admin / control-plane mounting (M5).
8
+ *
9
+ * The admin plane is the devtools static UI + the observability read API + the
10
+ * config admin API. In DEV (`NODE_ENV !== "production"`) these are mounted
11
+ * unauthenticated on localhost (today's behavior). In PROD they are OFF by
12
+ * default and opt-in via `--admin` / `ENPILINK_ADMIN=1`, guarded by
13
+ * `requireBearerAuth` using `ENPILINK_ADMIN_TOKEN`.
14
+ *
15
+ * This module centralizes BOTH the dev and prod mounts so the route surface is
16
+ * identical and the auth wrapping lives in one place. The console static UI is
17
+ * imported through a non-literal specifier so core type-checks WITHOUT
18
+ * `@enpilink/console` being built first (the core↔console workspace cycle).
19
+ */
20
+ /** Truthy env values that enable the admin plane. */
21
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
22
+ /**
23
+ * Whether the prod admin plane is enabled. OFF by default; enable with
24
+ * `ENPILINK_ADMIN=1` (also accepts `true`/`yes`/`on`, case-insensitive) — or via
25
+ * the `enpilink start --admin` flag, which sets the same env var.
26
+ */
27
+ export function adminEnabled() {
28
+ const raw = process.env.ENPILINK_ADMIN;
29
+ return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());
30
+ }
31
+ /**
32
+ * The raw admin bearer token, read in-process from the env (NOT the masked
33
+ * config API). Empty / unset → `undefined`. Read via `resolveConfig(null)`'s
34
+ * raw `values.adminAuthToken` so the single source of truth is the config
35
+ * schema's env mapping. Never logged, never returned by any HTTP route.
36
+ */
37
+ export async function readAdminToken() {
38
+ const { values } = await resolveConfig(null);
39
+ const token = values.adminAuthToken;
40
+ if (typeof token !== "string") {
41
+ return undefined;
42
+ }
43
+ const trimmed = token.trim();
44
+ return trimmed.length > 0 ? trimmed : undefined;
45
+ }
46
+ /**
47
+ * Error thrown when admin is enabled in prod but no token is configured. The
48
+ * prod entry should let this propagate so the process exits non-zero with a
49
+ * clear message — never default-open.
50
+ */
51
+ export class AdminTokenMissingError extends Error {
52
+ constructor() {
53
+ super("Admin mode is enabled (ENPILINK_ADMIN) but no admin token is set. " +
54
+ "Set ENPILINK_ADMIN_TOKEN to a non-empty secret, or disable admin. " +
55
+ "Refusing to start an unauthenticated admin plane.");
56
+ this.name = "AdminTokenMissingError";
57
+ }
58
+ }
59
+ /**
60
+ * A minimal OAuth-token verifier that accepts exactly one static bearer token
61
+ * (the configured admin token) and rejects everything else. On match it returns
62
+ * a synthetic {@link AuthInfo} with a far-future expiry so `requireBearerAuth`
63
+ * is satisfied; on mismatch it throws {@link InvalidTokenError} → 401.
64
+ *
65
+ * The comparison is constant-time to avoid leaking the token via timing.
66
+ */
67
+ function adminTokenVerifier(token) {
68
+ const expected = Buffer.from(token);
69
+ return {
70
+ async verifyAccessToken(presented) {
71
+ const got = Buffer.from(presented);
72
+ const ok = got.length === expected.length && crypto.timingSafeEqual(got, expected);
73
+ if (!ok) {
74
+ throw new InvalidTokenError("Invalid admin token");
75
+ }
76
+ return {
77
+ token: "admin",
78
+ clientId: "enpilink-admin",
79
+ scopes: [],
80
+ // Far-future expiry (epoch seconds): the token is a static shared
81
+ // secret, not an OAuth access token, so it does not expire on its own.
82
+ expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365,
83
+ };
84
+ },
85
+ };
86
+ }
87
+ /** Path prefixes for the admin DATA APIs (guarded). The shell is everything else. */
88
+ const DATA_API_PREFIXES = [
89
+ "/__enpilink/observability",
90
+ "/__enpilink/config",
91
+ ];
92
+ /** The observability SSE stream route — the one endpoint that needs `?token=`. */
93
+ const STREAM_PATH = "/__enpilink/observability/stream";
94
+ /** Whether `path` belongs to a guarded data API. */
95
+ function isDataApiPath(path) {
96
+ return DATA_API_PREFIXES.some((prefix) => path === prefix || path.startsWith(`${prefix}/`));
97
+ }
98
+ /**
99
+ * Remove the `token` query param from a URL (path + querystring), leaving the
100
+ * rest intact. Used to scrub the SSE bearer from `req.url`/`req.originalUrl`
101
+ * after promoting it to the Authorization header, so it never reaches logs or
102
+ * the route handler.
103
+ */
104
+ function stripTokenParam(url) {
105
+ const qIdx = url.indexOf("?");
106
+ if (qIdx === -1) {
107
+ return url;
108
+ }
109
+ const pathPart = url.slice(0, qIdx);
110
+ const params = new URLSearchParams(url.slice(qIdx + 1));
111
+ params.delete("token");
112
+ const rest = params.toString();
113
+ return rest.length > 0 ? `${pathPart}?${rest}` : pathPart;
114
+ }
115
+ /**
116
+ * Build the bearer-auth middleware that guards the admin **data APIs** in prod
117
+ * (`/__enpilink/observability/*` + `/__enpilink/config*`).
118
+ *
119
+ * M6.5: this guard is mounted at the app root but enforces auth ONLY on the
120
+ * data-API paths — the devtools static SPA shell (and everything else,
121
+ * including `/mcp`) passes straight through unauthenticated, so a browser can
122
+ * always load the app and render its own token-login screen. The app then
123
+ * authenticates its own fetch/SSE calls; no data leaks without the token.
124
+ *
125
+ * **SSE auth (`?token=`):** browsers' `EventSource` cannot set an
126
+ * `Authorization` header, so for the observability `/stream` route ONLY we also
127
+ * accept the bearer via a `?token=` query param. We copy it into the
128
+ * `Authorization` header (so the SAME constant-time verifier enforces it — no
129
+ * separate compare path) and then delete the query param so it never reaches
130
+ * the route handler, logs, or any persisted request line. The header path stays
131
+ * the primary mechanism for every other route.
132
+ */
133
+ export function adminAuthMiddleware(token) {
134
+ const required = requireBearerAuth({ verifier: adminTokenVerifier(token) });
135
+ return (req, res, next) => {
136
+ // `req.path` is the full path (guard mounted at root). Only the data APIs
137
+ // are guarded; the SPA shell, static assets, and `/mcp` pass through.
138
+ if (!isDataApiPath(req.path)) {
139
+ next();
140
+ return;
141
+ }
142
+ // SSE-only: promote `?token=` to the Authorization header so EventSource
143
+ // can authenticate, then strip it from the URL so the secret never lands in
144
+ // the request log line, `req.query`, or the route handler. Only here, only
145
+ // where the header path is also enforced (prod admin).
146
+ if (req.path === STREAM_PATH && !req.headers.authorization) {
147
+ const queryToken = req.query.token;
148
+ if (typeof queryToken === "string" && queryToken.length > 0) {
149
+ req.headers.authorization = `Bearer ${queryToken}`;
150
+ }
151
+ // Rewrite req.url / req.originalUrl to drop the `token` param. Express's
152
+ // `req.query` is derived from `req.url`, so this removes it everywhere
153
+ // (query object + any downstream logging of the URL).
154
+ req.url = stripTokenParam(req.url);
155
+ if (typeof req.originalUrl === "string") {
156
+ req.originalUrl = stripTokenParam(req.originalUrl);
157
+ }
158
+ }
159
+ return required(req, res, next);
160
+ };
161
+ }
162
+ /**
163
+ * Ensure there is an active {@link StorageAdapter} backing the admin plane, even
164
+ * when analytics RECORDING is off (`ENPILINK_ANALYTICS` unset). Analytics gates
165
+ * whether events are *recorded*; the admin still needs a store to READ/write
166
+ * config + observability data.
167
+ *
168
+ * If analytics already installed a store ({@link getActiveStorage}), reuse it.
169
+ * Otherwise resolve a fresh adapter (prod default = sqlite via
170
+ * `resolveStorageAdapter()` / `ENPILINK_DB_PATH`), `init()` it, register it as
171
+ * the active store, and return it so the caller can close it on shutdown.
172
+ *
173
+ * @returns the storage adapter the caller now OWNS (must close on shutdown), or
174
+ * `null` when an existing analytics store was reused (the server already owns
175
+ * that one) or when initialization failed.
176
+ */
177
+ export async function ensureAdminStorage() {
178
+ const existing = getActiveStorage();
179
+ if (existing) {
180
+ // Analytics already set up a store; reuse it. The server owns its lifecycle.
181
+ return null;
182
+ }
183
+ let storage;
184
+ try {
185
+ storage = resolveStorageAdapter();
186
+ await storage.init();
187
+ }
188
+ catch (err) {
189
+ console.error("[enpilink] admin storage init failed:", err instanceof Error ? err.message : err);
190
+ return null;
191
+ }
192
+ setActiveStorage(storage);
193
+ return storage;
194
+ }
195
+ /** The non-literal specifier keeps the core↔devtools clean-build cycle intact. */
196
+ const DEVTOOLS_SPECIFIER = "@enpilink/console";
197
+ async function loadDevtoolsStaticServer() {
198
+ const { devtoolsStaticServer } = (await import(DEVTOOLS_SPECIFIER));
199
+ return devtoolsStaticServer();
200
+ }
201
+ /**
202
+ * Mount the admin plane (devtools static UI + observability API + config API)
203
+ * onto `app`.
204
+ *
205
+ * **Shell vs data auth (M6.5).** The plane has two halves:
206
+ *
207
+ * 1. The **devtools static SPA shell** (HTML/JS/CSS — non-sensitive app code).
208
+ * Served WITHOUT auth so an unauthenticated browser can always load the app
209
+ * and render its own token-login screen.
210
+ * 2. The **data APIs** (`/__enpilink/observability/*` + `/__enpilink/config*`),
211
+ * which expose real analytics/config data. In prod these are guarded by
212
+ * `opts.auth` (a single bearer token); the guard is applied ONLY in front of
213
+ * these two routers, never the shell.
214
+ *
215
+ * In DEV (`auth` omitted) nothing is guarded — today's localhost behavior. The
216
+ * net result in prod: a browser with no token gets the app shell + a login
217
+ * screen, but no data leaks until the token is presented.
218
+ */
219
+ export async function mountAdmin(app, opts = {}) {
220
+ const { createObservabilityRouter } = await import("./observability.js");
221
+ const { createConfigRouter } = await import("./config/index.js");
222
+ const staticUi = await loadDevtoolsStaticServer();
223
+ const observability = createObservabilityRouter();
224
+ const config = createConfigRouter();
225
+ // 1) Static SPA shell — ALWAYS unauthenticated so the browser can load the
226
+ // app and show the login screen. It serves only non-sensitive app assets.
227
+ app.use(staticUi);
228
+ // 2) Data-API guard (prod only). Mounted at root, but `adminAuthMiddleware`
229
+ // enforces auth ONLY on the `/__enpilink/observability|config` paths — the
230
+ // shell above and `/mcp` pass through. It also accepts the SSE `?token=`
231
+ // query param for the stream route. In dev (`opts.auth` omitted) there is
232
+ // no guard at all.
233
+ if (opts.auth) {
234
+ app.use(opts.auth);
235
+ }
236
+ app.use(observability);
237
+ app.use(config);
238
+ }
239
+ //# sourceMappingURL=admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.js","sourceRoot":"","sources":["../../src/server/admin.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"node:crypto\";\nimport type { AuthInfo } from \"@modelcontextprotocol/sdk/server/auth/types.js\";\nimport type { Express, RequestHandler } from \"express\";\nimport { InvalidTokenError, requireBearerAuth } from \"./auth.js\";\nimport { resolveConfig } from \"./config/index.js\";\nimport { getActiveStorage, setActiveStorage } from \"./log-sink.js\";\nimport { resolveStorageAdapter } from \"./storage/index.js\";\nimport type { StorageAdapter } from \"./storage/types.js\";\n\n/**\n * Admin / control-plane mounting (M5).\n *\n * The admin plane is the devtools static UI + the observability read API + the\n * config admin API. In DEV (`NODE_ENV !== \"production\"`) these are mounted\n * unauthenticated on localhost (today's behavior). In PROD they are OFF by\n * default and opt-in via `--admin` / `ENPILINK_ADMIN=1`, guarded by\n * `requireBearerAuth` using `ENPILINK_ADMIN_TOKEN`.\n *\n * This module centralizes BOTH the dev and prod mounts so the route surface is\n * identical and the auth wrapping lives in one place. The console static UI is\n * imported through a non-literal specifier so core type-checks WITHOUT\n * `@enpilink/console` being built first (the core↔console workspace cycle).\n */\n\n/** Truthy env values that enable the admin plane. */\nconst TRUTHY = new Set([\"1\", \"true\", \"yes\", \"on\"]);\n\n/**\n * Whether the prod admin plane is enabled. OFF by default; enable with\n * `ENPILINK_ADMIN=1` (also accepts `true`/`yes`/`on`, case-insensitive) — or via\n * the `enpilink start --admin` flag, which sets the same env var.\n */\nexport function adminEnabled(): boolean {\n const raw = process.env.ENPILINK_ADMIN;\n return raw !== undefined && TRUTHY.has(raw.trim().toLowerCase());\n}\n\n/**\n * The raw admin bearer token, read in-process from the env (NOT the masked\n * config API). Empty / unset → `undefined`. Read via `resolveConfig(null)`'s\n * raw `values.adminAuthToken` so the single source of truth is the config\n * schema's env mapping. Never logged, never returned by any HTTP route.\n */\nexport async function readAdminToken(): Promise<string | undefined> {\n const { values } = await resolveConfig(null);\n const token = values.adminAuthToken;\n if (typeof token !== \"string\") {\n return undefined;\n }\n const trimmed = token.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\n/**\n * Error thrown when admin is enabled in prod but no token is configured. The\n * prod entry should let this propagate so the process exits non-zero with a\n * clear message — never default-open.\n */\nexport class AdminTokenMissingError extends Error {\n constructor() {\n super(\n \"Admin mode is enabled (ENPILINK_ADMIN) but no admin token is set. \" +\n \"Set ENPILINK_ADMIN_TOKEN to a non-empty secret, or disable admin. \" +\n \"Refusing to start an unauthenticated admin plane.\",\n );\n this.name = \"AdminTokenMissingError\";\n }\n}\n\n/**\n * A minimal OAuth-token verifier that accepts exactly one static bearer token\n * (the configured admin token) and rejects everything else. On match it returns\n * a synthetic {@link AuthInfo} with a far-future expiry so `requireBearerAuth`\n * is satisfied; on mismatch it throws {@link InvalidTokenError} → 401.\n *\n * The comparison is constant-time to avoid leaking the token via timing.\n */\nfunction adminTokenVerifier(token: string): {\n verifyAccessToken: (presented: string) => Promise<AuthInfo>;\n} {\n const expected = Buffer.from(token);\n return {\n async verifyAccessToken(presented: string): Promise<AuthInfo> {\n const got = Buffer.from(presented);\n const ok =\n got.length === expected.length && crypto.timingSafeEqual(got, expected);\n if (!ok) {\n throw new InvalidTokenError(\"Invalid admin token\");\n }\n return {\n token: \"admin\",\n clientId: \"enpilink-admin\",\n scopes: [],\n // Far-future expiry (epoch seconds): the token is a static shared\n // secret, not an OAuth access token, so it does not expire on its own.\n expiresAt: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 365,\n };\n },\n };\n}\n\n/** Path prefixes for the admin DATA APIs (guarded). The shell is everything else. */\nconst DATA_API_PREFIXES = [\n \"/__enpilink/observability\",\n \"/__enpilink/config\",\n] as const;\n\n/** The observability SSE stream route — the one endpoint that needs `?token=`. */\nconst STREAM_PATH = \"/__enpilink/observability/stream\";\n\n/** Whether `path` belongs to a guarded data API. */\nfunction isDataApiPath(path: string): boolean {\n return DATA_API_PREFIXES.some(\n (prefix) => path === prefix || path.startsWith(`${prefix}/`),\n );\n}\n\n/**\n * Remove the `token` query param from a URL (path + querystring), leaving the\n * rest intact. Used to scrub the SSE bearer from `req.url`/`req.originalUrl`\n * after promoting it to the Authorization header, so it never reaches logs or\n * the route handler.\n */\nfunction stripTokenParam(url: string): string {\n const qIdx = url.indexOf(\"?\");\n if (qIdx === -1) {\n return url;\n }\n const pathPart = url.slice(0, qIdx);\n const params = new URLSearchParams(url.slice(qIdx + 1));\n params.delete(\"token\");\n const rest = params.toString();\n return rest.length > 0 ? `${pathPart}?${rest}` : pathPart;\n}\n\n/**\n * Build the bearer-auth middleware that guards the admin **data APIs** in prod\n * (`/__enpilink/observability/*` + `/__enpilink/config*`).\n *\n * M6.5: this guard is mounted at the app root but enforces auth ONLY on the\n * data-API paths — the devtools static SPA shell (and everything else,\n * including `/mcp`) passes straight through unauthenticated, so a browser can\n * always load the app and render its own token-login screen. The app then\n * authenticates its own fetch/SSE calls; no data leaks without the token.\n *\n * **SSE auth (`?token=`):** browsers' `EventSource` cannot set an\n * `Authorization` header, so for the observability `/stream` route ONLY we also\n * accept the bearer via a `?token=` query param. We copy it into the\n * `Authorization` header (so the SAME constant-time verifier enforces it — no\n * separate compare path) and then delete the query param so it never reaches\n * the route handler, logs, or any persisted request line. The header path stays\n * the primary mechanism for every other route.\n */\nexport function adminAuthMiddleware(token: string): RequestHandler {\n const required = requireBearerAuth({ verifier: adminTokenVerifier(token) });\n return (req, res, next) => {\n // `req.path` is the full path (guard mounted at root). Only the data APIs\n // are guarded; the SPA shell, static assets, and `/mcp` pass through.\n if (!isDataApiPath(req.path)) {\n next();\n return;\n }\n\n // SSE-only: promote `?token=` to the Authorization header so EventSource\n // can authenticate, then strip it from the URL so the secret never lands in\n // the request log line, `req.query`, or the route handler. Only here, only\n // where the header path is also enforced (prod admin).\n if (req.path === STREAM_PATH && !req.headers.authorization) {\n const queryToken = req.query.token;\n if (typeof queryToken === \"string\" && queryToken.length > 0) {\n req.headers.authorization = `Bearer ${queryToken}`;\n }\n // Rewrite req.url / req.originalUrl to drop the `token` param. Express's\n // `req.query` is derived from `req.url`, so this removes it everywhere\n // (query object + any downstream logging of the URL).\n req.url = stripTokenParam(req.url);\n if (typeof req.originalUrl === \"string\") {\n req.originalUrl = stripTokenParam(req.originalUrl);\n }\n }\n\n return required(req, res, next);\n };\n}\n\n/**\n * Ensure there is an active {@link StorageAdapter} backing the admin plane, even\n * when analytics RECORDING is off (`ENPILINK_ANALYTICS` unset). Analytics gates\n * whether events are *recorded*; the admin still needs a store to READ/write\n * config + observability data.\n *\n * If analytics already installed a store ({@link getActiveStorage}), reuse it.\n * Otherwise resolve a fresh adapter (prod default = sqlite via\n * `resolveStorageAdapter()` / `ENPILINK_DB_PATH`), `init()` it, register it as\n * the active store, and return it so the caller can close it on shutdown.\n *\n * @returns the storage adapter the caller now OWNS (must close on shutdown), or\n * `null` when an existing analytics store was reused (the server already owns\n * that one) or when initialization failed.\n */\nexport async function ensureAdminStorage(): Promise<StorageAdapter | null> {\n const existing = getActiveStorage();\n if (existing) {\n // Analytics already set up a store; reuse it. The server owns its lifecycle.\n return null;\n }\n let storage: StorageAdapter;\n try {\n storage = resolveStorageAdapter();\n await storage.init();\n } catch (err) {\n console.error(\n \"[enpilink] admin storage init failed:\",\n err instanceof Error ? err.message : err,\n );\n return null;\n }\n setActiveStorage(storage);\n return storage;\n}\n\n/** The non-literal specifier keeps the core↔devtools clean-build cycle intact. */\nconst DEVTOOLS_SPECIFIER = \"@enpilink/console\";\n\nasync function loadDevtoolsStaticServer(): Promise<RequestHandler> {\n const { devtoolsStaticServer } = (await import(DEVTOOLS_SPECIFIER)) as {\n devtoolsStaticServer: () => Promise<RequestHandler>;\n };\n return devtoolsStaticServer();\n}\n\n/**\n * Mount the admin plane (devtools static UI + observability API + config API)\n * onto `app`.\n *\n * **Shell vs data auth (M6.5).** The plane has two halves:\n *\n * 1. The **devtools static SPA shell** (HTML/JS/CSS — non-sensitive app code).\n * Served WITHOUT auth so an unauthenticated browser can always load the app\n * and render its own token-login screen.\n * 2. The **data APIs** (`/__enpilink/observability/*` + `/__enpilink/config*`),\n * which expose real analytics/config data. In prod these are guarded by\n * `opts.auth` (a single bearer token); the guard is applied ONLY in front of\n * these two routers, never the shell.\n *\n * In DEV (`auth` omitted) nothing is guarded — today's localhost behavior. The\n * net result in prod: a browser with no token gets the app shell + a login\n * screen, but no data leaks until the token is presented.\n */\nexport async function mountAdmin(\n app: Express,\n opts: { auth?: RequestHandler } = {},\n): Promise<void> {\n const { createObservabilityRouter } = await import(\"./observability.js\");\n const { createConfigRouter } = await import(\"./config/index.js\");\n\n const staticUi = await loadDevtoolsStaticServer();\n const observability = createObservabilityRouter();\n const config = createConfigRouter();\n\n // 1) Static SPA shell — ALWAYS unauthenticated so the browser can load the\n // app and show the login screen. It serves only non-sensitive app assets.\n app.use(staticUi);\n\n // 2) Data-API guard (prod only). Mounted at root, but `adminAuthMiddleware`\n // enforces auth ONLY on the `/__enpilink/observability|config` paths — the\n // shell above and `/mcp` pass through. It also accepts the SSE `?token=`\n // query param for the stream route. In dev (`opts.auth` omitted) there is\n // no guard at all.\n if (opts.auth) {\n app.use(opts.auth);\n }\n app.use(observability);\n app.use(config);\n}\n"]}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,226 @@
1
+ import http from "node:http";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { AdminTokenMissingError, adminAuthMiddleware, adminEnabled, readAdminToken, } from "./admin.js";
4
+ import { mockEnabled } from "./analytics.js";
5
+ import { setActiveStorage } from "./log-sink.js";
6
+ import { McpServer } from "./server.js";
7
+ // The admin static UI imports @enpilink/console via a non-literal specifier;
8
+ // stub it so these tests don't require the devtools dist (clean-build cycle).
9
+ vi.mock("@enpilink/console", () => ({
10
+ // Stand in for the static SPA shell: answer `/` with a shell marker, let
11
+ // everything else fall through to the data routers.
12
+ devtoolsStaticServer: () => ((req, res, next) => {
13
+ if (req.path === "/") {
14
+ res.json({ shell: true });
15
+ return;
16
+ }
17
+ next();
18
+ }),
19
+ }));
20
+ vi.mock("./viewsDevServer.js", () => ({
21
+ viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
22
+ }));
23
+ async function listen(app) {
24
+ const server = http.createServer(app);
25
+ await new Promise((resolve) => server.listen(0, resolve));
26
+ const port = server.address().port;
27
+ return { port, server };
28
+ }
29
+ let openServer;
30
+ const ORIGINAL_ENV = { ...process.env };
31
+ beforeEach(() => {
32
+ // Ensure no leftover active storage from another test influences these.
33
+ setActiveStorage(null);
34
+ });
35
+ afterEach(() => {
36
+ openServer?.close();
37
+ openServer = undefined;
38
+ setActiveStorage(null);
39
+ process.env = { ...ORIGINAL_ENV };
40
+ });
41
+ const OBS = "/__enpilink/observability/summary";
42
+ describe("adminEnabled", () => {
43
+ it("is off by default and on for truthy env values", () => {
44
+ process.env.ENPILINK_ADMIN = undefined;
45
+ delete process.env.ENPILINK_ADMIN;
46
+ expect(adminEnabled()).toBe(false);
47
+ for (const v of ["1", "true", "YES", "On"]) {
48
+ process.env.ENPILINK_ADMIN = v;
49
+ expect(adminEnabled()).toBe(true);
50
+ }
51
+ process.env.ENPILINK_ADMIN = "0";
52
+ expect(adminEnabled()).toBe(false);
53
+ });
54
+ });
55
+ describe("readAdminToken", () => {
56
+ it("reads the raw token from env, trims, and treats empty as unset", async () => {
57
+ delete process.env.ENPILINK_ADMIN_TOKEN;
58
+ expect(await readAdminToken()).toBeUndefined();
59
+ process.env.ENPILINK_ADMIN_TOKEN = " ";
60
+ expect(await readAdminToken()).toBeUndefined();
61
+ process.env.ENPILINK_ADMIN_TOKEN = "s3cret";
62
+ expect(await readAdminToken()).toBe("s3cret");
63
+ });
64
+ });
65
+ describe("mockEnabled is dev-only", () => {
66
+ it("ignores ENPILINK_MOCK in production", () => {
67
+ process.env.ENPILINK_MOCK = "1";
68
+ process.env.NODE_ENV = "production";
69
+ expect(mockEnabled()).toBe(false);
70
+ process.env.NODE_ENV = "development";
71
+ expect(mockEnabled()).toBe(true);
72
+ });
73
+ });
74
+ describe("adminAuthMiddleware", () => {
75
+ it("401s without a token and passes with the valid bearer token", async () => {
76
+ const app = (await import("express")).default();
77
+ // Guard a DATA-API path (the guard only enforces auth on those).
78
+ app.use(adminAuthMiddleware("topsecret"));
79
+ app.get("/__enpilink/observability/summary", (_req, res) => res.json({ ok: true }));
80
+ const { port, server } = await listen(app);
81
+ openServer = server;
82
+ const base = `http://localhost:${port}/__enpilink/observability/summary`;
83
+ const noAuth = await fetch(base);
84
+ expect(noAuth.status).toBe(401);
85
+ const badAuth = await fetch(base, {
86
+ headers: { Authorization: "Bearer wrong" },
87
+ });
88
+ expect(badAuth.status).toBe(401);
89
+ const ok = await fetch(base, {
90
+ headers: { Authorization: "Bearer topsecret" },
91
+ });
92
+ expect(ok.status).toBe(200);
93
+ expect(await ok.json()).toEqual({ ok: true });
94
+ });
95
+ it("does NOT guard non-data paths (SPA shell, /mcp)", async () => {
96
+ const app = (await import("express")).default();
97
+ app.use(adminAuthMiddleware("topsecret"));
98
+ app.get("/", (_req, res) => res.json({ shell: true }));
99
+ app.get("/assets/app.js", (_req, res) => res.send("// js"));
100
+ const { port, server } = await listen(app);
101
+ openServer = server;
102
+ // No Authorization header, yet the shell + assets are reachable.
103
+ const shell = await fetch(`http://localhost:${port}/`);
104
+ expect(shell.status).toBe(200);
105
+ const asset = await fetch(`http://localhost:${port}/assets/app.js`);
106
+ expect(asset.status).toBe(200);
107
+ });
108
+ it("accepts the SSE ?token= query param on the stream route only", async () => {
109
+ const app = (await import("express")).default();
110
+ app.use(adminAuthMiddleware("topsecret"));
111
+ // Echo whether the token leaked into the parsed query (it must NOT).
112
+ app.get("/__enpilink/observability/stream", (req, res) => res.json({ ok: true, tokenInQuery: "token" in req.query }));
113
+ app.get("/__enpilink/observability/summary", (req, res) => res.json({ ok: true, tokenInQuery: "token" in req.query }));
114
+ const { port, server } = await listen(app);
115
+ openServer = server;
116
+ // Stream with valid ?token= → 200, and token stripped from req.query.
117
+ const okStream = await fetch(`http://localhost:${port}/__enpilink/observability/stream?token=topsecret`);
118
+ expect(okStream.status).toBe(200);
119
+ expect(await okStream.json()).toEqual({ ok: true, tokenInQuery: false });
120
+ // Wrong ?token= on the stream → 401.
121
+ const badStream = await fetch(`http://localhost:${port}/__enpilink/observability/stream?token=wrong`);
122
+ expect(badStream.status).toBe(401);
123
+ // ?token= is NOT honored on other data routes (header-only there).
124
+ const summaryViaQuery = await fetch(`http://localhost:${port}/__enpilink/observability/summary?token=topsecret`);
125
+ expect(summaryViaQuery.status).toBe(401);
126
+ });
127
+ });
128
+ describe("createApp — prod admin mode", () => {
129
+ it("does NOT mount the admin plane when admin is disabled", async () => {
130
+ process.env.NODE_ENV = "production";
131
+ delete process.env.ENPILINK_ADMIN;
132
+ const { createApp } = await import("./express.js");
133
+ const server = new McpServer({ name: "t", version: "0.0.0" });
134
+ const httpServer = http.createServer();
135
+ await createApp({ mcpServer: server, httpServer });
136
+ const { port, server: listening } = await listen(server.express);
137
+ openServer = listening;
138
+ const obs = await fetch(`http://localhost:${port}${OBS}`);
139
+ expect(obs.status).toBe(404);
140
+ // /mcp still works regardless of admin.
141
+ const mcp = await fetch(`http://localhost:${port}/mcp`, {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
145
+ });
146
+ expect(mcp.status).not.toBe(404);
147
+ });
148
+ it("refuses to start when admin is enabled but no token is set", async () => {
149
+ process.env.NODE_ENV = "production";
150
+ process.env.ENPILINK_ADMIN = "1";
151
+ delete process.env.ENPILINK_ADMIN_TOKEN;
152
+ const { createApp } = await import("./express.js");
153
+ const server = new McpServer({ name: "t", version: "0.0.0" });
154
+ const httpServer = http.createServer();
155
+ await expect(createApp({ mcpServer: server, httpServer })).rejects.toBeInstanceOf(AdminTokenMissingError);
156
+ });
157
+ it("mounts the admin plane behind bearer auth when enabled with a token", async () => {
158
+ process.env.NODE_ENV = "production";
159
+ process.env.ENPILINK_ADMIN = "1";
160
+ process.env.ENPILINK_ADMIN_TOKEN = "letmein";
161
+ process.env.ENPILINK_STORAGE = "memory"; // never touch disk in tests
162
+ const { createApp } = await import("./express.js");
163
+ const server = new McpServer({ name: "t", version: "0.0.0" });
164
+ const httpServer = http.createServer();
165
+ await createApp({ mcpServer: server, httpServer });
166
+ const { port, server: listening } = await listen(server.express);
167
+ openServer = listening;
168
+ // SPA shell is reachable WITHOUT auth (so the browser can load the app).
169
+ const shell = await fetch(`http://localhost:${port}/`);
170
+ expect(shell.status).toBe(200);
171
+ expect(await shell.json()).toEqual({ shell: true });
172
+ // Unauthenticated data API → 401.
173
+ const unauth = await fetch(`http://localhost:${port}${OBS}`);
174
+ expect(unauth.status).toBe(401);
175
+ // Valid bearer → 200.
176
+ const authed = await fetch(`http://localhost:${port}${OBS}`, {
177
+ headers: { Authorization: "Bearer letmein" },
178
+ });
179
+ expect(authed.status).toBe(200);
180
+ // SSE stream authenticates via ?token= (EventSource can't set headers).
181
+ const STREAM = "/__enpilink/observability/stream";
182
+ const streamUnauth = await fetch(`http://localhost:${port}${STREAM}`);
183
+ expect(streamUnauth.status).toBe(401);
184
+ const streamAuthed = await fetch(`http://localhost:${port}${STREAM}?token=letmein`);
185
+ expect(streamAuthed.status).toBe(200);
186
+ // Close the open SSE connection so the server can shut down cleanly.
187
+ await streamAuthed.body?.cancel();
188
+ // Admin storage was initialized independent of analytics (analytics OFF).
189
+ expect(server.storage).not.toBeNull();
190
+ // /mcp still works and is NOT guarded by the admin auth (no 401/404).
191
+ const mcp = await fetch(`http://localhost:${port}/mcp`, {
192
+ method: "POST",
193
+ headers: {
194
+ "Content-Type": "application/json",
195
+ Accept: "application/json, text/event-stream",
196
+ },
197
+ body: JSON.stringify({
198
+ jsonrpc: "2.0",
199
+ method: "initialize",
200
+ id: 1,
201
+ params: {
202
+ protocolVersion: "2024-11-05",
203
+ capabilities: {},
204
+ clientInfo: { name: "c", version: "1" },
205
+ },
206
+ }),
207
+ });
208
+ expect(mcp.status).toBe(200);
209
+ await server.storage?.close();
210
+ });
211
+ });
212
+ describe("createApp — dev mode unchanged", () => {
213
+ it("mounts the admin plane on localhost with NO auth", async () => {
214
+ delete process.env.NODE_ENV; // dev
215
+ const { createApp } = await import("./express.js");
216
+ const server = new McpServer({ name: "t", version: "0.0.0" });
217
+ const httpServer = http.createServer();
218
+ await createApp({ mcpServer: server, httpServer });
219
+ const { port, server: listening } = await listen(server.express);
220
+ openServer = listening;
221
+ // No Authorization header, yet the observability route answers (200).
222
+ const obs = await fetch(`http://localhost:${port}${OBS}`);
223
+ expect(obs.status).toBe(200);
224
+ });
225
+ });
226
+ //# sourceMappingURL=admin.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"admin.test.js","sourceRoot":"","sources":["../../src/server/admin.test.ts"],"names":[],"mappings":"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","sourcesContent":["import http from \"node:http\";\nimport type { RequestHandler } from \"express\";\nimport { afterEach, beforeEach, describe, expect, it, vi } from \"vitest\";\nimport {\n AdminTokenMissingError,\n adminAuthMiddleware,\n adminEnabled,\n readAdminToken,\n} from \"./admin.js\";\nimport { mockEnabled } from \"./analytics.js\";\nimport { setActiveStorage } from \"./log-sink.js\";\nimport { McpServer } from \"./server.js\";\n\n// The admin static UI imports @enpilink/console via a non-literal specifier;\n// stub it so these tests don't require the devtools dist (clean-build cycle).\nvi.mock(\"@enpilink/console\", () => ({\n // Stand in for the static SPA shell: answer `/` with a shell marker, let\n // everything else fall through to the data routers.\n devtoolsStaticServer: () =>\n ((\n req: { path: string },\n res: { json: (b: unknown) => void },\n next: () => void,\n ) => {\n if (req.path === \"/\") {\n res.json({ shell: true });\n return;\n }\n next();\n }) as unknown as RequestHandler,\n}));\nvi.mock(\"./viewsDevServer.js\", () => ({\n viewsDevServer: (_httpServer: unknown) =>\n ((_req: unknown, _res: unknown, next: () => void) =>\n next()) as RequestHandler,\n}));\n\nasync function listen(app: Parameters<typeof http.createServer>[1]) {\n const server = http.createServer(app);\n await new Promise<void>((resolve) => server.listen(0, resolve));\n const port = (server.address() as { port: number }).port;\n return { port, server };\n}\n\nlet openServer: http.Server | undefined;\nconst ORIGINAL_ENV = { ...process.env };\n\nbeforeEach(() => {\n // Ensure no leftover active storage from another test influences these.\n setActiveStorage(null);\n});\n\nafterEach(() => {\n openServer?.close();\n openServer = undefined;\n setActiveStorage(null);\n process.env = { ...ORIGINAL_ENV };\n});\n\nconst OBS = \"/__enpilink/observability/summary\";\n\ndescribe(\"adminEnabled\", () => {\n it(\"is off by default and on for truthy env values\", () => {\n process.env.ENPILINK_ADMIN = undefined;\n delete process.env.ENPILINK_ADMIN;\n expect(adminEnabled()).toBe(false);\n for (const v of [\"1\", \"true\", \"YES\", \"On\"]) {\n process.env.ENPILINK_ADMIN = v;\n expect(adminEnabled()).toBe(true);\n }\n process.env.ENPILINK_ADMIN = \"0\";\n expect(adminEnabled()).toBe(false);\n });\n});\n\ndescribe(\"readAdminToken\", () => {\n it(\"reads the raw token from env, trims, and treats empty as unset\", async () => {\n delete process.env.ENPILINK_ADMIN_TOKEN;\n expect(await readAdminToken()).toBeUndefined();\n process.env.ENPILINK_ADMIN_TOKEN = \" \";\n expect(await readAdminToken()).toBeUndefined();\n process.env.ENPILINK_ADMIN_TOKEN = \"s3cret\";\n expect(await readAdminToken()).toBe(\"s3cret\");\n });\n});\n\ndescribe(\"mockEnabled is dev-only\", () => {\n it(\"ignores ENPILINK_MOCK in production\", () => {\n process.env.ENPILINK_MOCK = \"1\";\n process.env.NODE_ENV = \"production\";\n expect(mockEnabled()).toBe(false);\n process.env.NODE_ENV = \"development\";\n expect(mockEnabled()).toBe(true);\n });\n});\n\ndescribe(\"adminAuthMiddleware\", () => {\n it(\"401s without a token and passes with the valid bearer token\", async () => {\n const app = (await import(\"express\")).default();\n // Guard a DATA-API path (the guard only enforces auth on those).\n app.use(adminAuthMiddleware(\"topsecret\"));\n app.get(\"/__enpilink/observability/summary\", (_req, res) =>\n res.json({ ok: true }),\n );\n const { port, server } = await listen(app);\n openServer = server;\n\n const base = `http://localhost:${port}/__enpilink/observability/summary`;\n const noAuth = await fetch(base);\n expect(noAuth.status).toBe(401);\n\n const badAuth = await fetch(base, {\n headers: { Authorization: \"Bearer wrong\" },\n });\n expect(badAuth.status).toBe(401);\n\n const ok = await fetch(base, {\n headers: { Authorization: \"Bearer topsecret\" },\n });\n expect(ok.status).toBe(200);\n expect(await ok.json()).toEqual({ ok: true });\n });\n\n it(\"does NOT guard non-data paths (SPA shell, /mcp)\", async () => {\n const app = (await import(\"express\")).default();\n app.use(adminAuthMiddleware(\"topsecret\"));\n app.get(\"/\", (_req, res) => res.json({ shell: true }));\n app.get(\"/assets/app.js\", (_req, res) => res.send(\"// js\"));\n const { port, server } = await listen(app);\n openServer = server;\n\n // No Authorization header, yet the shell + assets are reachable.\n const shell = await fetch(`http://localhost:${port}/`);\n expect(shell.status).toBe(200);\n const asset = await fetch(`http://localhost:${port}/assets/app.js`);\n expect(asset.status).toBe(200);\n });\n\n it(\"accepts the SSE ?token= query param on the stream route only\", async () => {\n const app = (await import(\"express\")).default();\n app.use(adminAuthMiddleware(\"topsecret\"));\n // Echo whether the token leaked into the parsed query (it must NOT).\n app.get(\"/__enpilink/observability/stream\", (req, res) =>\n res.json({ ok: true, tokenInQuery: \"token\" in req.query }),\n );\n app.get(\"/__enpilink/observability/summary\", (req, res) =>\n res.json({ ok: true, tokenInQuery: \"token\" in req.query }),\n );\n const { port, server } = await listen(app);\n openServer = server;\n\n // Stream with valid ?token= → 200, and token stripped from req.query.\n const okStream = await fetch(\n `http://localhost:${port}/__enpilink/observability/stream?token=topsecret`,\n );\n expect(okStream.status).toBe(200);\n expect(await okStream.json()).toEqual({ ok: true, tokenInQuery: false });\n\n // Wrong ?token= on the stream → 401.\n const badStream = await fetch(\n `http://localhost:${port}/__enpilink/observability/stream?token=wrong`,\n );\n expect(badStream.status).toBe(401);\n\n // ?token= is NOT honored on other data routes (header-only there).\n const summaryViaQuery = await fetch(\n `http://localhost:${port}/__enpilink/observability/summary?token=topsecret`,\n );\n expect(summaryViaQuery.status).toBe(401);\n });\n});\n\ndescribe(\"createApp — prod admin mode\", () => {\n it(\"does NOT mount the admin plane when admin is disabled\", async () => {\n process.env.NODE_ENV = \"production\";\n delete process.env.ENPILINK_ADMIN;\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"t\", version: \"0.0.0\" });\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const { port, server: listening } = await listen(server.express);\n openServer = listening;\n\n const obs = await fetch(`http://localhost:${port}${OBS}`);\n expect(obs.status).toBe(404);\n\n // /mcp still works regardless of admin.\n const mcp = await fetch(`http://localhost:${port}/mcp`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ jsonrpc: \"2.0\", method: \"initialize\", id: 1 }),\n });\n expect(mcp.status).not.toBe(404);\n });\n\n it(\"refuses to start when admin is enabled but no token is set\", async () => {\n process.env.NODE_ENV = \"production\";\n process.env.ENPILINK_ADMIN = \"1\";\n delete process.env.ENPILINK_ADMIN_TOKEN;\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"t\", version: \"0.0.0\" });\n const httpServer = http.createServer();\n await expect(\n createApp({ mcpServer: server, httpServer }),\n ).rejects.toBeInstanceOf(AdminTokenMissingError);\n });\n\n it(\"mounts the admin plane behind bearer auth when enabled with a token\", async () => {\n process.env.NODE_ENV = \"production\";\n process.env.ENPILINK_ADMIN = \"1\";\n process.env.ENPILINK_ADMIN_TOKEN = \"letmein\";\n process.env.ENPILINK_STORAGE = \"memory\"; // never touch disk in tests\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"t\", version: \"0.0.0\" });\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const { port, server: listening } = await listen(server.express);\n openServer = listening;\n\n // SPA shell is reachable WITHOUT auth (so the browser can load the app).\n const shell = await fetch(`http://localhost:${port}/`);\n expect(shell.status).toBe(200);\n expect(await shell.json()).toEqual({ shell: true });\n\n // Unauthenticated data API → 401.\n const unauth = await fetch(`http://localhost:${port}${OBS}`);\n expect(unauth.status).toBe(401);\n\n // Valid bearer → 200.\n const authed = await fetch(`http://localhost:${port}${OBS}`, {\n headers: { Authorization: \"Bearer letmein\" },\n });\n expect(authed.status).toBe(200);\n\n // SSE stream authenticates via ?token= (EventSource can't set headers).\n const STREAM = \"/__enpilink/observability/stream\";\n const streamUnauth = await fetch(`http://localhost:${port}${STREAM}`);\n expect(streamUnauth.status).toBe(401);\n const streamAuthed = await fetch(\n `http://localhost:${port}${STREAM}?token=letmein`,\n );\n expect(streamAuthed.status).toBe(200);\n // Close the open SSE connection so the server can shut down cleanly.\n await streamAuthed.body?.cancel();\n\n // Admin storage was initialized independent of analytics (analytics OFF).\n expect(server.storage).not.toBeNull();\n\n // /mcp still works and is NOT guarded by the admin auth (no 401/404).\n const mcp = await fetch(`http://localhost:${port}/mcp`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json, text/event-stream\",\n },\n body: JSON.stringify({\n jsonrpc: \"2.0\",\n method: \"initialize\",\n id: 1,\n params: {\n protocolVersion: \"2024-11-05\",\n capabilities: {},\n clientInfo: { name: \"c\", version: \"1\" },\n },\n }),\n });\n expect(mcp.status).toBe(200);\n\n await server.storage?.close();\n });\n});\n\ndescribe(\"createApp — dev mode unchanged\", () => {\n it(\"mounts the admin plane on localhost with NO auth\", async () => {\n delete process.env.NODE_ENV; // dev\n const { createApp } = await import(\"./express.js\");\n const server = new McpServer({ name: \"t\", version: \"0.0.0\" });\n const httpServer = http.createServer();\n await createApp({ mcpServer: server, httpServer });\n const { port, server: listening } = await listen(server.express);\n openServer = listening;\n\n // No Authorization header, yet the observability route answers (200).\n const obs = await fetch(`http://localhost:${port}${OBS}`);\n expect(obs.status).toBe(200);\n });\n});\n"]}
@@ -0,0 +1,60 @@
1
+ import type { McpMiddlewareEntry, McpMiddlewareFn } from "./middleware.js";
2
+ import { type OtelSink } from "./otel.js";
3
+ import type { StorageAdapter } from "./storage/types.js";
4
+ /**
5
+ * Analytics + log capture (M2). Opt-in, env-gated, zero overhead when off.
6
+ *
7
+ * When enabled, a single {@link StorageAdapter} is resolved + `init()`ed at
8
+ * server startup and shared in-process: the analytics middleware writes
9
+ * `tool_call` events to it, the log sink mirrors server logs to it, and (M3)
10
+ * the observability API reads from the very same instance via the
11
+ * `server.storage` getter or {@link getActiveStorage}.
12
+ *
13
+ * Gating: OFF unless `ENPILINK_ANALYTICS` is `1` or `true` (case-insensitive).
14
+ * When OFF, no adapter is resolved or initialized (so no `enpilink.db` is
15
+ * created), no middleware is registered, and there is zero network activity.
16
+ */
17
+ /** Options for {@link installAnalytics}. */
18
+ export interface InstallAnalyticsOptions {
19
+ /**
20
+ * Inject a clock for deterministic tests. Defaults to `Date.now`.
21
+ */
22
+ now?: () => number;
23
+ }
24
+ /**
25
+ * Whether analytics is enabled. OFF by default; enable with
26
+ * `ENPILINK_ANALYTICS=1` (also accepts `true`/`yes`/`on`, case-insensitive).
27
+ */
28
+ export declare function analyticsEnabled(): boolean;
29
+ /**
30
+ * Whether the `--mock` demo seed is enabled (`ENPILINK_MOCK`=1/true/yes/on).
31
+ * Mock mode is opt-in only and IMPLIES analytics-on + in-memory storage for the
32
+ * session, so the Dashboard renders full demo data with NO real traffic. It
33
+ * NEVER touches disk and is never on by default.
34
+ */
35
+ export declare function mockEnabled(): boolean;
36
+ /**
37
+ * Build the analytics middleware entry. Times each request around `next()`,
38
+ * records a `tool_call`-typed event (capturing the tool name for `tools/call`),
39
+ * and ALWAYS swallows storage errors so a storage failure can never break or
40
+ * slow a tool call. Recording is fire-and-forget (non-blocking).
41
+ */
42
+ export declare function createAnalyticsMiddleware(storage: StorageAdapter, now?: () => number, otel?: OtelSink | null): McpMiddlewareFn;
43
+ /**
44
+ * Install analytics on a server, ONLY when enabled (`ENPILINK_ANALYTICS`).
45
+ *
46
+ * When enabled: resolves a {@link StorageAdapter} via `resolveStorageAdapter()`
47
+ * (`ENPILINK_STORAGE` / `ENPILINK_DB_PATH`), `init()`s it, registers it as the
48
+ * active storage for the log sink + `getActiveStorage()`, and returns the
49
+ * built analytics middleware entry to splice into the chain.
50
+ *
51
+ * When disabled: resolves/initializes NOTHING and returns `null` — zero
52
+ * overhead, zero network, no `enpilink.db` created.
53
+ *
54
+ * @returns the active storage + middleware entry, or `null` when disabled.
55
+ */
56
+ export declare function installAnalytics(opts?: InstallAnalyticsOptions): Promise<{
57
+ storage: StorageAdapter;
58
+ entry: McpMiddlewareEntry;
59
+ otel: OtelSink | null;
60
+ } | null>;