@edspencer/paddock 0.0.1 → 0.57.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 (296) hide show
  1. package/README.md +69 -0
  2. package/install-notice.mjs +29 -0
  3. package/package.json +58 -5
  4. package/packages/server/dist/adoptable.d.ts +127 -0
  5. package/packages/server/dist/adoptable.js +315 -0
  6. package/packages/server/dist/app.d.ts +67 -0
  7. package/packages/server/dist/app.js +325 -0
  8. package/packages/server/dist/archive.d.ts +42 -0
  9. package/packages/server/dist/archive.js +125 -0
  10. package/packages/server/dist/attachments-config.d.ts +114 -0
  11. package/packages/server/dist/attachments-config.js +165 -0
  12. package/packages/server/dist/attachments-hint.d.ts +53 -0
  13. package/packages/server/dist/attachments-hint.js +140 -0
  14. package/packages/server/dist/attachments.d.ts +33 -0
  15. package/packages/server/dist/attachments.js +117 -0
  16. package/packages/server/dist/auth.d.ts +84 -0
  17. package/packages/server/dist/auth.js +247 -0
  18. package/packages/server/dist/background.d.ts +36 -0
  19. package/packages/server/dist/background.js +137 -0
  20. package/packages/server/dist/bind-safety.d.ts +56 -0
  21. package/packages/server/dist/bind-safety.js +51 -0
  22. package/packages/server/dist/brand.d.ts +44 -0
  23. package/packages/server/dist/brand.js +88 -0
  24. package/packages/server/dist/chat-dto.d.ts +210 -0
  25. package/packages/server/dist/chat-dto.js +308 -0
  26. package/packages/server/dist/cli/import-chats.d.ts +21 -0
  27. package/packages/server/dist/cli/import-chats.js +197 -0
  28. package/packages/server/dist/cli/paddock.d.ts +2 -0
  29. package/packages/server/dist/cli/paddock.js +247 -0
  30. package/packages/server/dist/concurrency.d.ts +57 -0
  31. package/packages/server/dist/concurrency.js +71 -0
  32. package/packages/server/dist/config.d.ts +538 -0
  33. package/packages/server/dist/config.js +625 -0
  34. package/packages/server/dist/curation-config.d.ts +55 -0
  35. package/packages/server/dist/curation-config.js +64 -0
  36. package/packages/server/dist/environment-prompt.d.ts +52 -0
  37. package/packages/server/dist/environment-prompt.js +61 -0
  38. package/packages/server/dist/event-bus.d.ts +87 -0
  39. package/packages/server/dist/event-bus.js +48 -0
  40. package/packages/server/dist/git.d.ts +128 -0
  41. package/packages/server/dist/git.js +446 -0
  42. package/packages/server/dist/github-auth.d.ts +44 -0
  43. package/packages/server/dist/github-auth.js +169 -0
  44. package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
  45. package/packages/server/dist/herdctl-agent-config.js +331 -0
  46. package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
  47. package/packages/server/dist/herdctl-agent-names.js +228 -0
  48. package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
  49. package/packages/server/dist/herdctl-jobs-index.js +225 -0
  50. package/packages/server/dist/herdctl-jobs.d.ts +98 -0
  51. package/packages/server/dist/herdctl-jobs.js +225 -0
  52. package/packages/server/dist/herdctl.d.ts +623 -0
  53. package/packages/server/dist/herdctl.js +1279 -0
  54. package/packages/server/dist/hook-config.d.ts +217 -0
  55. package/packages/server/dist/hook-config.js +224 -0
  56. package/packages/server/dist/http-bytes.d.ts +24 -0
  57. package/packages/server/dist/http-bytes.js +52 -0
  58. package/packages/server/dist/index.d.ts +1 -0
  59. package/packages/server/dist/index.js +14 -0
  60. package/packages/server/dist/instance-config.d.ts +143 -0
  61. package/packages/server/dist/instance-config.js +385 -0
  62. package/packages/server/dist/localcommand.d.ts +23 -0
  63. package/packages/server/dist/localcommand.js +140 -0
  64. package/packages/server/dist/management-auth.d.ts +99 -0
  65. package/packages/server/dist/management-auth.js +165 -0
  66. package/packages/server/dist/management-config.d.ts +164 -0
  67. package/packages/server/dist/management-config.js +286 -0
  68. package/packages/server/dist/management-mcp-server.d.ts +20 -0
  69. package/packages/server/dist/management-mcp-server.js +95 -0
  70. package/packages/server/dist/management-metadata.d.ts +67 -0
  71. package/packages/server/dist/management-metadata.js +56 -0
  72. package/packages/server/dist/management-ops.d.ts +110 -0
  73. package/packages/server/dist/management-ops.js +449 -0
  74. package/packages/server/dist/management-policy.d.ts +172 -0
  75. package/packages/server/dist/management-policy.js +259 -0
  76. package/packages/server/dist/message-provenance.d.ts +112 -0
  77. package/packages/server/dist/message-provenance.js +260 -0
  78. package/packages/server/dist/models.d.ts +164 -0
  79. package/packages/server/dist/models.js +208 -0
  80. package/packages/server/dist/openapi.d.ts +66 -0
  81. package/packages/server/dist/openapi.js +183 -0
  82. package/packages/server/dist/parent-detach.d.ts +30 -0
  83. package/packages/server/dist/parent-detach.js +109 -0
  84. package/packages/server/dist/preload.d.ts +43 -0
  85. package/packages/server/dist/preload.js +55 -0
  86. package/packages/server/dist/project-files.d.ts +76 -0
  87. package/packages/server/dist/project-files.js +191 -0
  88. package/packages/server/dist/project-mime.d.ts +35 -0
  89. package/packages/server/dist/project-mime.js +55 -0
  90. package/packages/server/dist/project-paths.d.ts +88 -0
  91. package/packages/server/dist/project-paths.js +126 -0
  92. package/packages/server/dist/project-types.d.ts +316 -0
  93. package/packages/server/dist/project-types.js +48 -0
  94. package/packages/server/dist/projects.d.ts +202 -0
  95. package/packages/server/dist/projects.js +894 -0
  96. package/packages/server/dist/queued-message.d.ts +35 -0
  97. package/packages/server/dist/queued-message.js +116 -0
  98. package/packages/server/dist/read-state.d.ts +46 -0
  99. package/packages/server/dist/read-state.js +139 -0
  100. package/packages/server/dist/recovery-config.d.ts +111 -0
  101. package/packages/server/dist/recovery-config.js +105 -0
  102. package/packages/server/dist/recovery.d.ts +262 -0
  103. package/packages/server/dist/recovery.js +490 -0
  104. package/packages/server/dist/resume-drain.d.ts +122 -0
  105. package/packages/server/dist/resume-drain.js +158 -0
  106. package/packages/server/dist/route-context.d.ts +168 -0
  107. package/packages/server/dist/route-context.js +39 -0
  108. package/packages/server/dist/route-errors.d.ts +8 -0
  109. package/packages/server/dist/route-errors.js +13 -0
  110. package/packages/server/dist/routes/chats.d.ts +12 -0
  111. package/packages/server/dist/routes/chats.js +1406 -0
  112. package/packages/server/dist/routes/git.d.ts +13 -0
  113. package/packages/server/dist/routes/git.js +236 -0
  114. package/packages/server/dist/routes/mcp.d.ts +83 -0
  115. package/packages/server/dist/routes/mcp.js +250 -0
  116. package/packages/server/dist/routes/meta.d.ts +15 -0
  117. package/packages/server/dist/routes/meta.js +447 -0
  118. package/packages/server/dist/routes/projects.d.ts +25 -0
  119. package/packages/server/dist/routes/projects.js +736 -0
  120. package/packages/server/dist/routes/triggers.d.ts +21 -0
  121. package/packages/server/dist/routes/triggers.js +324 -0
  122. package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
  123. package/packages/server/dist/routes/workspace-mount.js +32 -0
  124. package/packages/server/dist/routes.d.ts +26 -0
  125. package/packages/server/dist/routes.js +29 -0
  126. package/packages/server/dist/run-provenance.d.ts +109 -0
  127. package/packages/server/dist/run-provenance.js +206 -0
  128. package/packages/server/dist/runs.d.ts +92 -0
  129. package/packages/server/dist/runs.js +72 -0
  130. package/packages/server/dist/schedule-config.d.ts +68 -0
  131. package/packages/server/dist/schedule-config.js +75 -0
  132. package/packages/server/dist/schedule-session.d.ts +34 -0
  133. package/packages/server/dist/schedule-session.js +120 -0
  134. package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
  135. package/packages/server/dist/self-mcp-descriptions.js +145 -0
  136. package/packages/server/dist/self-mcp-read.d.ts +15 -0
  137. package/packages/server/dist/self-mcp-read.js +97 -0
  138. package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
  139. package/packages/server/dist/self-mcp-triggers.js +144 -0
  140. package/packages/server/dist/self-mcp-types.d.ts +248 -0
  141. package/packages/server/dist/self-mcp-types.js +10 -0
  142. package/packages/server/dist/self-mcp-util.d.ts +73 -0
  143. package/packages/server/dist/self-mcp-util.js +157 -0
  144. package/packages/server/dist/self-mcp-write.d.ts +35 -0
  145. package/packages/server/dist/self-mcp-write.js +215 -0
  146. package/packages/server/dist/self-mcp.d.ts +95 -0
  147. package/packages/server/dist/self-mcp.js +448 -0
  148. package/packages/server/dist/send-file-mcp.d.ts +46 -0
  149. package/packages/server/dist/send-file-mcp.js +281 -0
  150. package/packages/server/dist/session-hub.d.ts +186 -0
  151. package/packages/server/dist/session-hub.js +291 -0
  152. package/packages/server/dist/spawn-capability.d.ts +99 -0
  153. package/packages/server/dist/spawn-capability.js +106 -0
  154. package/packages/server/dist/star.d.ts +26 -0
  155. package/packages/server/dist/star.js +94 -0
  156. package/packages/server/dist/start.d.ts +1 -0
  157. package/packages/server/dist/start.js +21 -0
  158. package/packages/server/dist/subagents.d.ts +337 -0
  159. package/packages/server/dist/subagents.js +626 -0
  160. package/packages/server/dist/sweep.d.ts +122 -0
  161. package/packages/server/dist/sweep.js +696 -0
  162. package/packages/server/dist/tooldetails.d.ts +33 -0
  163. package/packages/server/dist/tooldetails.js +466 -0
  164. package/packages/server/dist/transcribe.d.ts +69 -0
  165. package/packages/server/dist/transcribe.js +119 -0
  166. package/packages/server/dist/transcripts.d.ts +42 -0
  167. package/packages/server/dist/transcripts.js +164 -0
  168. package/packages/server/dist/trigger-config.d.ts +278 -0
  169. package/packages/server/dist/trigger-config.js +417 -0
  170. package/packages/server/dist/trigger-runtime.d.ts +94 -0
  171. package/packages/server/dist/trigger-runtime.js +82 -0
  172. package/packages/server/dist/trigger-session.d.ts +34 -0
  173. package/packages/server/dist/trigger-session.js +122 -0
  174. package/packages/server/dist/triggers.d.ts +83 -0
  175. package/packages/server/dist/triggers.js +124 -0
  176. package/packages/server/dist/trusted-proxy.d.ts +57 -0
  177. package/packages/server/dist/trusted-proxy.js +157 -0
  178. package/packages/server/dist/turn-notice.d.ts +154 -0
  179. package/packages/server/dist/turn-notice.js +271 -0
  180. package/packages/server/dist/unread.d.ts +36 -0
  181. package/packages/server/dist/unread.js +123 -0
  182. package/packages/server/dist/usage.d.ts +66 -0
  183. package/packages/server/dist/usage.js +243 -0
  184. package/packages/server/dist/wake-injection.d.ts +156 -0
  185. package/packages/server/dist/wake-injection.js +136 -0
  186. package/packages/server/dist/whisper-local.d.ts +9 -0
  187. package/packages/server/dist/whisper-local.js +122 -0
  188. package/packages/server/dist/ws-context.d.ts +158 -0
  189. package/packages/server/dist/ws-context.js +1 -0
  190. package/packages/server/dist/ws-protocol.d.ts +423 -0
  191. package/packages/server/dist/ws-protocol.js +251 -0
  192. package/packages/server/dist/ws-self-mcp.d.ts +52 -0
  193. package/packages/server/dist/ws-self-mcp.js +28 -0
  194. package/packages/server/dist/ws-triggers.d.ts +29 -0
  195. package/packages/server/dist/ws-triggers.js +261 -0
  196. package/packages/server/dist/ws-turn.d.ts +86 -0
  197. package/packages/server/dist/ws-turn.js +751 -0
  198. package/packages/server/dist/ws.d.ts +62 -0
  199. package/packages/server/dist/ws.js +1024 -0
  200. package/packages/server/package.json +6 -0
  201. package/packages/web/dist/assets/InstanceConfigPage-Bypd_quL.js +2 -0
  202. package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
  203. package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
  204. package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
  205. package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
  206. package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
  207. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
  208. package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
  209. package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
  210. package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
  211. package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
  212. package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
  213. package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
  214. package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
  215. package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
  216. package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
  217. package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
  218. package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
  219. package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
  220. package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
  221. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
  222. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
  223. package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
  224. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.js +2 -0
  225. package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
  226. package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
  227. package/packages/web/dist/assets/css-DazXZka4.js +2 -0
  228. package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
  229. package/packages/web/dist/assets/dagre-BM42HDAG-C6Uuq-dq.js +5 -0
  230. package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
  231. package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
  232. package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
  233. package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
  234. package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
  235. package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
  236. package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
  237. package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
  238. package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
  239. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
  240. package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.js +107 -0
  241. package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
  242. package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
  243. package/packages/web/dist/assets/index-B-YuzkF6.js +71 -0
  244. package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
  245. package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.js +3 -0
  246. package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
  247. package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
  248. package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-Cs0D32Ry.js +71 -0
  249. package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
  250. package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
  251. package/packages/web/dist/assets/journeyDiagram-JHISSGLW-Bc9fuI2i.js +140 -0
  252. package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
  253. package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.js +90 -0
  254. package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
  255. package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
  256. package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
  257. package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
  258. package/packages/web/dist/assets/linear-BzGHkF9K.js +2 -0
  259. package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
  260. package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.js +97 -0
  261. package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
  262. package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
  263. package/packages/web/dist/assets/pieDiagram-4H26LBE5-CDmilo8S.js +31 -0
  264. package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
  265. package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
  266. package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
  267. package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.js +85 -0
  268. package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
  269. package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
  270. package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-BkUK73W0.js +41 -0
  271. package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
  272. package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
  273. package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
  274. package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
  275. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
  276. package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
  277. package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
  278. package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
  279. package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
  280. package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
  281. package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
  282. package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
  283. package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.js +8 -0
  284. package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
  285. package/packages/web/dist/favicon.ico +0 -0
  286. package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
  287. package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
  288. package/packages/web/dist/icons/apple-touch-icon.png +0 -0
  289. package/packages/web/dist/icons/favicon-16.png +0 -0
  290. package/packages/web/dist/icons/favicon-32.png +0 -0
  291. package/packages/web/dist/icons/icon-192.png +0 -0
  292. package/packages/web/dist/icons/icon-512.png +0 -0
  293. package/packages/web/dist/icons/maskable-512.png +0 -0
  294. package/packages/web/dist/index.html +56 -0
  295. package/packages/web/dist/manifest.webmanifest +32 -0
  296. package/packages/web/dist/sw.js +139 -0
@@ -0,0 +1,122 @@
1
+ /**
2
+ * TriggerSessionStore — a Paddock-side sidecar mapping an *accreting* trigger
3
+ * (`run.session: "resume"`) to the ONE chat session it owns (Epic T / T1). A trigger
4
+ * whose run declares `session: "resume"` should build up a single long-lived
5
+ * transcript across every fire (a "manager" that accretes context) rather than
6
+ * spawning a fresh chat each time. To make that resume unambiguous, Paddock records
7
+ * the session id the trigger created on its FIRST fire and reuses it thereafter — so
8
+ * the binding survives a server restart (the resume rebinds off the reloaded sidecar).
9
+ *
10
+ * This is the unified successor to `ScheduleSessionStore` (issue #265): the same
11
+ * pattern, keyed by (project slug, TRIGGER name) instead of schedule name, so both
12
+ * schedule-type and event-type triggers can own an accreting session. A
13
+ * `session: "new"` trigger owns nothing — it gets a new chat every fire and never
14
+ * touches this store.
15
+ *
16
+ * Shape + concurrency: a `"<slug>\0<triggerName>" -> sessionId` map persisted as a
17
+ * plain JSON object — the exact ArchiveStore / RunProvenanceStore / ScheduleSessionStore
18
+ * sidecar pattern (lazy-loaded, write-through, serialised, corruption-tolerant,
19
+ * `0o600`), INCLUDING the in-flight-load-promise fix: {@link ensureLoaded} caches the
20
+ * load PROMISE (not just the resolved map) so concurrent first-callers share ONE read
21
+ * and can't each build a separate map and race on the assignment (a lost update).
22
+ * Reachable if several triggers fire in the same tick right after boot.
23
+ */
24
+ import { promises as fs } from "node:fs";
25
+ import path from "node:path";
26
+ const STATE_FILE = "trigger-sessions.json";
27
+ /** The composite key separator — NUL can't appear in a slug or trigger name. */
28
+ const SEP = "\0";
29
+ /** A session id we're willing to key on — mirrors RunProvenanceStore's guard. */
30
+ function isSafeId(sessionId) {
31
+ return typeof sessionId === "string" && /^[A-Za-z0-9._-]+$/.test(sessionId);
32
+ }
33
+ export class TriggerSessionStore {
34
+ stateFile;
35
+ /** In-memory map of `<slug>\0<triggerName>` -> owned sessionId. */
36
+ state = null;
37
+ /**
38
+ * The in-flight load, cached so concurrent first-callers share ONE read (the
39
+ * RunProvenanceStore fix): caching only the resolved `state` would let several
40
+ * fires that begin before the first `fs.readFile` resolves each build their own
41
+ * map and race on the assignment — a lost update.
42
+ */
43
+ loadPromise = null;
44
+ /** Serialises concurrent writes so the file never interleaves. */
45
+ writing = Promise.resolve();
46
+ constructor(dataDir) {
47
+ this.stateFile = path.join(dataDir, STATE_FILE);
48
+ }
49
+ static key(slug, triggerName) {
50
+ return `${slug}${SEP}${triggerName}`;
51
+ }
52
+ /** Load the persisted map (lazily, deduped; tolerant of a missing/corrupt file). */
53
+ ensureLoaded() {
54
+ if (this.state)
55
+ return Promise.resolve(this.state);
56
+ if (!this.loadPromise) {
57
+ this.loadPromise = (async () => {
58
+ const map = new Map();
59
+ try {
60
+ const raw = await fs.readFile(this.stateFile, "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
63
+ for (const [k, v] of Object.entries(parsed)) {
64
+ if (typeof v === "string" && isSafeId(v))
65
+ map.set(k, v);
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ /* missing or unreadable — start empty */
71
+ }
72
+ this.state = map;
73
+ return map;
74
+ })();
75
+ }
76
+ return this.loadPromise;
77
+ }
78
+ /** The session id this trigger owns, or undefined if it hasn't fired yet. Non-throwing. */
79
+ async get(slug, triggerName) {
80
+ const map = await this.ensureLoaded().catch(() => new Map());
81
+ return map.get(TriggerSessionStore.key(slug, triggerName));
82
+ }
83
+ /**
84
+ * Record the session a trigger owns (its first-fire chat). Idempotent for the same
85
+ * id; an unsafe/blank id is ignored. Overwrites if called again (used only on a
86
+ * fresh first fire, or to re-point after a stale id was cleared).
87
+ */
88
+ async set(slug, triggerName, sessionId) {
89
+ if (!isSafeId(sessionId))
90
+ return;
91
+ const key = TriggerSessionStore.key(slug, triggerName);
92
+ const map = await this.ensureLoaded();
93
+ if (map.get(key) === sessionId)
94
+ return; // no-op
95
+ map.set(key, sessionId);
96
+ await this.persist(map);
97
+ }
98
+ /**
99
+ * Forget a trigger's owned session — used when the recorded transcript has vanished
100
+ * (so the next fire re-creates one) or the trigger was removed. No-op if there was
101
+ * nothing recorded.
102
+ */
103
+ async clear(slug, triggerName) {
104
+ const key = TriggerSessionStore.key(slug, triggerName);
105
+ const map = await this.ensureLoaded();
106
+ if (!map.has(key))
107
+ return;
108
+ map.delete(key);
109
+ await this.persist(map);
110
+ }
111
+ /** Write-through, serialised so overlapping writes can't corrupt the file. */
112
+ persist(map) {
113
+ this.writing = this.writing.then(async () => {
114
+ const obj = {};
115
+ for (const [k, v] of map)
116
+ obj[k] = v;
117
+ const json = JSON.stringify(obj, null, 2);
118
+ await fs.writeFile(this.stateFile, json, { encoding: "utf8", mode: 0o600 }).catch(() => undefined);
119
+ });
120
+ return this.writing;
121
+ }
122
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Unified trigger CRUD/registry service (Epic T "Unify Triggers", ticket T1).
3
+ *
4
+ * ONE registry over the single `startAgentTurn` execution core that subsumes what
5
+ * were two services — the Epic G `HookService` (event hooks) and the Epic D schedule
6
+ * registration (spread across `ProjectStore` + `HerdctlService`). A trigger is
7
+ * persisted per project (`project.yaml` `triggers` map) and armed against herdctl by
8
+ * type:
9
+ *
10
+ * - **event** trigger → its OWN herdctl agent `trigger-<slug>-<name>` (tool config
11
+ * = capability), fired by the in-process event bus (`onArchive` today).
12
+ * - **schedule** trigger → forwarded into the keeper agent's `schedules` block (the
13
+ * herdctl cron engine arms it — the cron TIMING), fired via
14
+ * `setScheduleTriggerHandler`. It executes on its OWN scoped `trigger-<slug>-<name>`
15
+ * agent (tool config = capability) when it declares a `run.tools` allow-list (T2 —
16
+ * #307), else on the keeper with the project-agent default toolset.
17
+ * - **webhook** trigger → shape reserved; persisted + validated but NOT armed and
18
+ * never fired (the ingress is the deferred T6/#311).
19
+ *
20
+ * The service is the FROZEN surface T2–T5 build on (the analogue of the shipped
21
+ * `HookService`): reads come straight off the live project record (project.yaml is the
22
+ * source of truth, re-armed on restart); writes persist THEN arm (best-effort — a
23
+ * transient fleet hiccup never loses the persisted definition, it's re-armed from the
24
+ * record on the next boot / `ensureProjectAgent`).
25
+ */
26
+ import type { ProjectStore } from "./projects.js";
27
+ import type { HerdctlService } from "./herdctl.js";
28
+ import type { PaddockTrigger, TriggerDto, TriggerEvent } from "./trigger-config.js";
29
+ /** Project a persisted {@link PaddockTrigger} + its name/slug onto the {@link TriggerDto}. */
30
+ export declare function toTriggerDto(slug: string, name: string, trigger: PaddockTrigger): TriggerDto;
31
+ /**
32
+ * List/get/set/remove a project's triggers — the frozen surface T2 (schedule tool
33
+ * allow-list), T3 (REST + self-MCP verb collapse), T4 (Triggers tab) and T5 (sweeper
34
+ * fold-in) build on. Reads come off the live project record; writes persist THEN arm
35
+ * the trigger's herdctl agent / schedule.
36
+ */
37
+ export declare class TriggerService {
38
+ private readonly projects;
39
+ private readonly herdctl;
40
+ constructor(projects: ProjectStore, herdctl: HerdctlService);
41
+ /** Every trigger declared by `slug`, as DTOs (empty when the project has none). */
42
+ list(slug: string): Promise<TriggerDto[]>;
43
+ /** One trigger by name, or `null` if the project declares no such trigger. */
44
+ get(slug: string, name: string): Promise<TriggerDto | null>;
45
+ /**
46
+ * The trigger whose registered agent is `agentName` (`trigger-<slug>-<name>`), or
47
+ * `null` — the reverse-map (mirrors `HookService.getByAgentName`). Resolved by
48
+ * matching the project's DECLARED triggers rather than by PARSING the agent string
49
+ * (a slug may contain hyphens, so `trigger-<slug>-<name>` isn't unambiguously
50
+ * splittable). Never throws (a missing project → `null`).
51
+ */
52
+ getByAgentName(slug: string, agentName: string): Promise<TriggerDto | null>;
53
+ /**
54
+ * Create or update a trigger: persist it to `project.yaml` (validates the name +
55
+ * sanitises the record via the Zod schema, throwing `ProjectError` on a malformed
56
+ * trigger), then arm it so it's immediately fireable. Returns the persisted
57
+ * trigger's DTO. The arm is best-effort — re-armed from the record on the next
58
+ * restart / `ensureProjectAgent` if it fails — so a transient fleet hiccup never
59
+ * loses the persisted definition.
60
+ */
61
+ set(slug: string, name: string, trigger: unknown): Promise<TriggerDto>;
62
+ /**
63
+ * Remove a trigger: delete it from `project.yaml` and unregister/disarm it. Returns
64
+ * whether a trigger actually existed (so a caller can echo `removed: false`).
65
+ */
66
+ remove(slug: string, name: string): Promise<boolean>;
67
+ /**
68
+ * The enabled EVENT triggers a project declares for `event` (the dispatcher's
69
+ * resolver — the analogue of `HookService.enabledForEvent`). Schedule/webhook
70
+ * triggers are never returned here (they fire on cron / an ingress, not the bus).
71
+ */
72
+ enabledForEvent(slug: string, event: TriggerEvent): Promise<TriggerDto[]>;
73
+ /**
74
+ * Arm a single trigger against herdctl (best-effort): an event trigger becomes its
75
+ * own `trigger-<slug>-<name>` agent; a schedule trigger is (re-)forwarded into the
76
+ * keeper's `schedules` block via a keeper re-register — which ALSO registers its OWN
77
+ * scoped `trigger-<slug>-<name>` agent when it declares a `run.tools` allow-list (T2,
78
+ * via `ensureProjectAgent` → `registerTriggerAgents`); a webhook trigger is reserved
79
+ * (nothing to arm — no ingress in T1).
80
+ */
81
+ private arm;
82
+ private dtosOf;
83
+ }
@@ -0,0 +1,124 @@
1
+ import { triggerAgentName } from "./herdctl.js";
2
+ import { triggerRunsOnOwnAgent } from "./trigger-config.js";
3
+ /** Project a persisted {@link PaddockTrigger} + its name/slug onto the {@link TriggerDto}. */
4
+ export function toTriggerDto(slug, name, trigger) {
5
+ return { name, agentName: triggerAgentName(slug, name), ...trigger };
6
+ }
7
+ /**
8
+ * List/get/set/remove a project's triggers — the frozen surface T2 (schedule tool
9
+ * allow-list), T3 (REST + self-MCP verb collapse), T4 (Triggers tab) and T5 (sweeper
10
+ * fold-in) build on. Reads come off the live project record; writes persist THEN arm
11
+ * the trigger's herdctl agent / schedule.
12
+ */
13
+ export class TriggerService {
14
+ projects;
15
+ herdctl;
16
+ constructor(projects, herdctl) {
17
+ this.projects = projects;
18
+ this.herdctl = herdctl;
19
+ }
20
+ /** Every trigger declared by `slug`, as DTOs (empty when the project has none). */
21
+ async list(slug) {
22
+ const project = await this.projects.get(slug); // throws not_found
23
+ return this.dtosOf(project);
24
+ }
25
+ /** One trigger by name, or `null` if the project declares no such trigger. */
26
+ async get(slug, name) {
27
+ const project = await this.projects.get(slug); // throws not_found
28
+ const trigger = project.triggers?.[name];
29
+ return trigger ? toTriggerDto(slug, name, trigger) : null;
30
+ }
31
+ /**
32
+ * The trigger whose registered agent is `agentName` (`trigger-<slug>-<name>`), or
33
+ * `null` — the reverse-map (mirrors `HookService.getByAgentName`). Resolved by
34
+ * matching the project's DECLARED triggers rather than by PARSING the agent string
35
+ * (a slug may contain hyphens, so `trigger-<slug>-<name>` isn't unambiguously
36
+ * splittable). Never throws (a missing project → `null`).
37
+ */
38
+ async getByAgentName(slug, agentName) {
39
+ const project = await this.projects.get(slug).catch(() => null);
40
+ if (!project)
41
+ return null;
42
+ for (const [name, trigger] of Object.entries(project.triggers ?? {})) {
43
+ if (triggerAgentName(slug, name) === agentName)
44
+ return toTriggerDto(slug, name, trigger);
45
+ }
46
+ return null;
47
+ }
48
+ /**
49
+ * Create or update a trigger: persist it to `project.yaml` (validates the name +
50
+ * sanitises the record via the Zod schema, throwing `ProjectError` on a malformed
51
+ * trigger), then arm it so it's immediately fireable. Returns the persisted
52
+ * trigger's DTO. The arm is best-effort — re-armed from the record on the next
53
+ * restart / `ensureProjectAgent` if it fails — so a transient fleet hiccup never
54
+ * loses the persisted definition.
55
+ */
56
+ async set(slug, name, trigger) {
57
+ const project = await this.projects.setTrigger(slug, name, trigger);
58
+ const rec = project.triggers?.[name];
59
+ if (!rec)
60
+ throw new Error(`trigger not persisted: ${name}`);
61
+ await this.arm(project, name, rec).catch(() => undefined);
62
+ return toTriggerDto(slug, name, rec);
63
+ }
64
+ /**
65
+ * Remove a trigger: delete it from `project.yaml` and unregister/disarm it. Returns
66
+ * whether a trigger actually existed (so a caller can echo `removed: false`).
67
+ */
68
+ async remove(slug, name) {
69
+ const before = await this.projects.get(slug); // throws not_found
70
+ const existed = Boolean(before.triggers?.[name]);
71
+ const rec = before.triggers?.[name];
72
+ const project = await this.projects.removeTrigger(slug, name);
73
+ if (rec) {
74
+ // Tear down the trigger's OWN scoped agent only if it HAD one — every event
75
+ // trigger, or a scoped schedule trigger with a `run.tools` allow-list (T2). An
76
+ // unscoped, keeper-run schedule never registered an agent, so skip the fleet call.
77
+ if (triggerRunsOnOwnAgent(rec)) {
78
+ await this.herdctl.removeTriggerAgent(slug, name).catch(() => undefined);
79
+ }
80
+ // A schedule trigger ALSO rode the keeper's forwarded `schedules` block (the cron
81
+ // timing) — re-register the keeper from the updated record so that arming is
82
+ // dropped (and any remaining scoped trigger agents are re-affirmed).
83
+ if (rec.trigger.type === "schedule") {
84
+ await this.herdctl.ensureProjectAgent(project).catch(() => undefined);
85
+ }
86
+ }
87
+ return existed;
88
+ }
89
+ /**
90
+ * The enabled EVENT triggers a project declares for `event` (the dispatcher's
91
+ * resolver — the analogue of `HookService.enabledForEvent`). Schedule/webhook
92
+ * triggers are never returned here (they fire on cron / an ingress, not the bus).
93
+ */
94
+ async enabledForEvent(slug, event) {
95
+ const project = await this.projects.get(slug).catch(() => null);
96
+ if (!project)
97
+ return [];
98
+ return this.dtosOf(project).filter((t) => t.trigger.type === "event" && t.trigger.on === event && t.enabled === true);
99
+ }
100
+ /**
101
+ * Arm a single trigger against herdctl (best-effort): an event trigger becomes its
102
+ * own `trigger-<slug>-<name>` agent; a schedule trigger is (re-)forwarded into the
103
+ * keeper's `schedules` block via a keeper re-register — which ALSO registers its OWN
104
+ * scoped `trigger-<slug>-<name>` agent when it declares a `run.tools` allow-list (T2,
105
+ * via `ensureProjectAgent` → `registerTriggerAgents`); a webhook trigger is reserved
106
+ * (nothing to arm — no ingress in T1).
107
+ */
108
+ async arm(project, name, trigger) {
109
+ if (trigger.trigger.type === "event") {
110
+ await this.herdctl.ensureTriggerAgent(project, name, trigger);
111
+ }
112
+ else if (trigger.trigger.type === "schedule") {
113
+ // Re-register the keeper so its forwarded `schedules` block picks up the new /
114
+ // edited schedule trigger (un-gated). ensureProjectAgent
115
+ // also re-runs registerTriggerAgents, so a scoped schedule (T2) gets its own agent
116
+ // armed here too. Idempotent.
117
+ await this.herdctl.ensureProjectAgent(project);
118
+ }
119
+ // webhook: shape reserved — nothing armed (deferred T6).
120
+ }
121
+ dtosOf(project) {
122
+ return Object.entries(project.triggers ?? {}).map(([name, trigger]) => toTriggerDto(project.slug, name, trigger));
123
+ }
124
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The compatibility default: loopback plus the private address space. See the
3
+ * module header for why this is the default and what it does not prove.
4
+ */
5
+ export declare const DEFAULT_TRUSTED_PROXIES: readonly string[];
6
+ /** Believe no peer at all — the strict posture. */
7
+ export declare const TRUST_NONE = "none";
8
+ /** Believe every peer — restores the pre-#474 behaviour. Reported loudly. */
9
+ export declare const TRUST_ALL = "all";
10
+ /** A resolved trust list plus how we came by it. */
11
+ export interface TrustedProxies {
12
+ /**
13
+ * The entries as configured (already validated). Empty means "believe no
14
+ * peer". `["all"]` means "believe every peer".
15
+ */
16
+ entries: string[];
17
+ /**
18
+ * True when the operator named these peers (env or YAML), false when this is
19
+ * {@link DEFAULT_TRUSTED_PROXIES}. Only an explicit list is a control, so the
20
+ * guard warns when a forwarded scheme is believed under the default.
21
+ */
22
+ explicit: boolean;
23
+ }
24
+ /** The default list, wrapped. */
25
+ export declare function defaultTrustedProxies(): TrustedProxies;
26
+ export interface ParsedTrustedProxies {
27
+ trusted: TrustedProxies;
28
+ /** Entries that were dropped because they are not an IP, CIDR, or preset. */
29
+ errors: string[];
30
+ /** Advice worth logging at boot (currently: `all` disables the guard). */
31
+ warnings: string[];
32
+ }
33
+ /**
34
+ * Parse an operator-supplied list (a YAML array, or a comma/newline-delimited
35
+ * string) into a validated trust list.
36
+ *
37
+ * An unparseable entry is DROPPED with an error rather than taking the instance
38
+ * down — one typo in a CIDR must not stop Paddock booting. Note the direction of
39
+ * that failure: dropping an entry can only make the guard stricter (a peer stops
40
+ * being trusted), never more permissive, so the fail-safe way is also the safe
41
+ * way here.
42
+ *
43
+ * `undefined` / empty input means "the operator said nothing" and yields the
44
+ * default list; an explicit `none` yields an empty, explicit list.
45
+ */
46
+ export declare function parseTrustedProxies(raw: string[] | string | undefined): ParsedTrustedProxies;
47
+ /**
48
+ * Whether `addr` — the IMMEDIATE peer from the socket, never a header — is a
49
+ * trusted proxy. An unknown/absent address is never trusted.
50
+ */
51
+ export declare function isTrustedProxy(addr: string | undefined, trusted: TrustedProxies): boolean;
52
+ /**
53
+ * Whether `addr` is a loopback socket address — the client is on this host, so
54
+ * nothing was put on a wire. Covers IPv4 `127.0.0.0/8`, IPv6 `::1`, and the
55
+ * IPv4-mapped form Node reports on a dual-stack listener.
56
+ */
57
+ export declare function isLoopbackAddress(addr: string | undefined): boolean;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Which peers' `X-Forwarded-Proto` the `/mcp` transport guard believes (#474).
3
+ *
4
+ * #471 gave the Management API a plaintext guard: a bearer token must not cross
5
+ * a real network readable-in-transit, so `/mcp` refuses plaintext from anything
6
+ * but a loopback client. It also accepted `X-Forwarded-Proto: https` — from ANY
7
+ * peer, with no trust check — so the guard could be switched off by the very
8
+ * operator it protects, with a header copy-pasted out of a smoke-test recipe.
9
+ *
10
+ * ── What this is and is not ────────────────────────────────────────────────
11
+ * NOT an authentication control. A valid bearer token is still required, and an
12
+ * attacker who spoofs the header gains nothing they could not already do — they
13
+ * still need the credential. What the header could do was mislead an OPERATOR
14
+ * into believing their token was protected while it travelled in clear. So the
15
+ * job here is to make the "is this connection secure?" answer rest on something
16
+ * the caller cannot set: the immediate peer's address, taken from the socket.
17
+ *
18
+ * ── The trust list ─────────────────────────────────────────────────────────
19
+ * A forwarded scheme is believed only from a peer in the trusted list. Entries
20
+ * are IPs, CIDRs, or the {@link proxyAddr} preset names (`loopback`,
21
+ * `linklocal`, `uniquelocal`), plus two words of our own: `none` (believe no
22
+ * peer) and `all` (believe everyone — the pre-#474 behaviour, reportable and
23
+ * loud).
24
+ *
25
+ * The DEFAULT is {@link DEFAULT_TRUSTED_PROXIES}: loopback + link-local +
26
+ * unique-local (RFC1918 / fc00::/7 / fe80::/10). That is a COMPATIBILITY
27
+ * posture, chosen because in every deployment recipe the TLS terminator sits on
28
+ * the host or on a private container/pod network — an nginx or Caddy sidecar on
29
+ * a Compose bridge, an ingress in a pod network, a proxy on loopback. A peer
30
+ * with a PUBLIC address is never one of those, so believing it is always wrong;
31
+ * that case is now refused where it previously succeeded.
32
+ *
33
+ * The default's honest limit: it cannot tell your reverse proxy at 172.18.0.5
34
+ * from a laptop at 192.168.1.50, because nothing in an IP packet says which one
35
+ * is a proxy. Naming the proxy explicitly is what upgrades this from a footgun
36
+ * preventer to a control, and the guard says so at runtime — believing a
37
+ * forwarded scheme under the default list logs a one-per-peer warning.
38
+ *
39
+ * Deliberately NOT loopback-equivalent: the Docker bridge gateway (`172.17.0.1`).
40
+ * A request from another host to a `0.0.0.0`-published port is SNAT'd to that
41
+ * same address, so "peer is the gateway" does not prove the traffic stayed on
42
+ * the box. Smoke-test from INSIDE the container instead (`docker compose exec
43
+ * paddock curl http://127.0.0.1:4000/mcp …`), where the peer really is loopback.
44
+ */
45
+ import proxyAddr from "proxy-addr";
46
+ /**
47
+ * The compatibility default: loopback plus the private address space. See the
48
+ * module header for why this is the default and what it does not prove.
49
+ */
50
+ export const DEFAULT_TRUSTED_PROXIES = ["loopback", "linklocal", "uniquelocal"];
51
+ /** Believe no peer at all — the strict posture. */
52
+ export const TRUST_NONE = "none";
53
+ /** Believe every peer — restores the pre-#474 behaviour. Reported loudly. */
54
+ export const TRUST_ALL = "all";
55
+ /** The default list, wrapped. */
56
+ export function defaultTrustedProxies() {
57
+ return { entries: [...DEFAULT_TRUSTED_PROXIES], explicit: false };
58
+ }
59
+ /**
60
+ * Parse an operator-supplied list (a YAML array, or a comma/newline-delimited
61
+ * string) into a validated trust list.
62
+ *
63
+ * An unparseable entry is DROPPED with an error rather than taking the instance
64
+ * down — one typo in a CIDR must not stop Paddock booting. Note the direction of
65
+ * that failure: dropping an entry can only make the guard stricter (a peer stops
66
+ * being trusted), never more permissive, so the fail-safe way is also the safe
67
+ * way here.
68
+ *
69
+ * `undefined` / empty input means "the operator said nothing" and yields the
70
+ * default list; an explicit `none` yields an empty, explicit list.
71
+ */
72
+ export function parseTrustedProxies(raw) {
73
+ const errors = [];
74
+ const warnings = [];
75
+ const tokens = (Array.isArray(raw)
76
+ ? raw.map((v) => String(v))
77
+ : String(raw ?? "")
78
+ .split(/[\n,]/)
79
+ .map((v) => v))
80
+ .map((v) => v.trim().toLowerCase())
81
+ .filter((v) => v.length > 0);
82
+ if (tokens.length === 0)
83
+ return { trusted: defaultTrustedProxies(), errors, warnings };
84
+ if (tokens.includes(TRUST_ALL) || tokens.includes("*")) {
85
+ warnings.push(`managementApi.trustedProxies: "all" — every peer's X-Forwarded-Proto is believed, so the ` +
86
+ `/mcp plaintext guard can be switched off by any client that can reach this instance. ` +
87
+ `This is the pre-#474 behaviour; name your proxy's address instead.`);
88
+ return {
89
+ trusted: { entries: [TRUST_ALL], explicit: true },
90
+ errors,
91
+ warnings,
92
+ };
93
+ }
94
+ const entries = [];
95
+ for (const token of tokens) {
96
+ if (token === TRUST_NONE)
97
+ continue; // an explicit empty list
98
+ try {
99
+ proxyAddr.compile([token]);
100
+ entries.push(token);
101
+ }
102
+ catch {
103
+ errors.push(`managementApi.trustedProxies: "${token}" is not an IP address, CIDR, or one of ` +
104
+ `loopback/linklocal/uniquelocal/none/all — entry ignored`);
105
+ }
106
+ }
107
+ return { trusted: { entries, explicit: true }, errors, warnings };
108
+ }
109
+ /**
110
+ * Compiled matchers, keyed by the joined list. `proxyAddr.compile` parses every
111
+ * entry, so we do it once per distinct list rather than once per request.
112
+ */
113
+ const compiled = new Map();
114
+ function matcherFor(entries) {
115
+ const key = entries.join(",");
116
+ let fn = compiled.get(key);
117
+ if (!fn) {
118
+ if (entries.length === 0) {
119
+ fn = () => false;
120
+ }
121
+ else if (entries.length === 1 && entries[0] === TRUST_ALL) {
122
+ fn = () => true;
123
+ }
124
+ else {
125
+ const trust = proxyAddr.compile(entries);
126
+ fn = (addr) => trust(addr, 0);
127
+ }
128
+ compiled.set(key, fn);
129
+ }
130
+ return fn;
131
+ }
132
+ /**
133
+ * Whether `addr` — the IMMEDIATE peer from the socket, never a header — is a
134
+ * trusted proxy. An unknown/absent address is never trusted.
135
+ */
136
+ export function isTrustedProxy(addr, trusted) {
137
+ if (!addr)
138
+ return false;
139
+ try {
140
+ return matcherFor(trusted.entries)(addr);
141
+ }
142
+ catch {
143
+ // proxyAddr throws on an address it cannot parse (it should not happen for a
144
+ // real socket, but a malformed value must not 500 the request).
145
+ return false;
146
+ }
147
+ }
148
+ /**
149
+ * Whether `addr` is a loopback socket address — the client is on this host, so
150
+ * nothing was put on a wire. Covers IPv4 `127.0.0.0/8`, IPv6 `::1`, and the
151
+ * IPv4-mapped form Node reports on a dual-stack listener.
152
+ */
153
+ export function isLoopbackAddress(addr) {
154
+ if (!addr)
155
+ return false;
156
+ return isTrustedProxy(addr, { entries: ["loopback"], explicit: false });
157
+ }