@smthrs/control 0.0.0-stage → 1.0.0-rc.3

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 (418) hide show
  1. package/CHANGELOG.md +194 -0
  2. package/LICENSE +21 -0
  3. package/README.md +168 -2
  4. package/dist/cjs/ApprovalAuthority.d.ts +73 -0
  5. package/dist/cjs/ApprovalAuthority.d.ts.map +1 -0
  6. package/dist/cjs/ApprovalAuthority.js +62 -0
  7. package/dist/cjs/ApprovalAuthority.js.map +7 -0
  8. package/dist/cjs/Cancellation.d.ts +107 -0
  9. package/dist/cjs/Cancellation.d.ts.map +1 -0
  10. package/dist/cjs/Cancellation.js +72 -0
  11. package/dist/cjs/Cancellation.js.map +7 -0
  12. package/dist/cjs/Channels.d.ts +170 -0
  13. package/dist/cjs/Channels.d.ts.map +1 -0
  14. package/dist/cjs/Channels.js +278 -0
  15. package/dist/cjs/Channels.js.map +7 -0
  16. package/dist/cjs/Control.d.ts +202 -0
  17. package/dist/cjs/Control.d.ts.map +1 -0
  18. package/dist/cjs/Control.js +47 -0
  19. package/dist/cjs/Control.js.map +7 -0
  20. package/dist/cjs/ControlClient.d.ts +52 -0
  21. package/dist/cjs/ControlClient.d.ts.map +1 -0
  22. package/dist/cjs/ControlClient.js +191 -0
  23. package/dist/cjs/ControlClient.js.map +7 -0
  24. package/dist/cjs/ControlError.d.ts +318 -0
  25. package/dist/cjs/ControlError.d.ts.map +1 -0
  26. package/dist/cjs/ControlError.js +249 -0
  27. package/dist/cjs/ControlError.js.map +7 -0
  28. package/dist/cjs/ControlExecutor.d.ts +372 -0
  29. package/dist/cjs/ControlExecutor.d.ts.map +1 -0
  30. package/dist/cjs/ControlExecutor.js +123 -0
  31. package/dist/cjs/ControlExecutor.js.map +7 -0
  32. package/dist/cjs/ControlFacts.d.ts +454 -0
  33. package/dist/cjs/ControlFacts.d.ts.map +1 -0
  34. package/dist/cjs/ControlFacts.js +261 -0
  35. package/dist/cjs/ControlFacts.js.map +7 -0
  36. package/dist/cjs/ControlLive.d.ts +23 -0
  37. package/dist/cjs/ControlLive.d.ts.map +1 -0
  38. package/dist/cjs/ControlLive.js +1280 -0
  39. package/dist/cjs/ControlLive.js.map +7 -0
  40. package/dist/cjs/ControlRpcs.d.ts +1204 -0
  41. package/dist/cjs/ControlRpcs.d.ts.map +1 -0
  42. package/dist/cjs/ControlRpcs.js +247 -0
  43. package/dist/cjs/ControlRpcs.js.map +7 -0
  44. package/dist/cjs/ControlRuntime.d.ts +635 -0
  45. package/dist/cjs/ControlRuntime.d.ts.map +1 -0
  46. package/dist/cjs/ControlRuntime.js +740 -0
  47. package/dist/cjs/ControlRuntime.js.map +7 -0
  48. package/dist/cjs/ControlSchema.d.ts +2642 -0
  49. package/dist/cjs/ControlSchema.d.ts.map +1 -0
  50. package/dist/cjs/ControlSchema.js +634 -0
  51. package/dist/cjs/ControlSchema.js.map +7 -0
  52. package/dist/cjs/ControlServer.d.ts +51 -0
  53. package/dist/cjs/ControlServer.d.ts.map +1 -0
  54. package/dist/cjs/ControlServer.js +121 -0
  55. package/dist/cjs/ControlServer.js.map +7 -0
  56. package/dist/cjs/Credential.d.ts +136 -0
  57. package/dist/cjs/Credential.d.ts.map +1 -0
  58. package/dist/cjs/Credential.js +168 -0
  59. package/dist/cjs/Credential.js.map +7 -0
  60. package/dist/cjs/CredentialCipher.d.ts +90 -0
  61. package/dist/cjs/CredentialCipher.d.ts.map +1 -0
  62. package/dist/cjs/CredentialCipher.js +45 -0
  63. package/dist/cjs/CredentialCipher.js.map +7 -0
  64. package/dist/cjs/CredentialStore.d.ts +97 -0
  65. package/dist/cjs/CredentialStore.d.ts.map +1 -0
  66. package/dist/cjs/CredentialStore.js +81 -0
  67. package/dist/cjs/CredentialStore.js.map +7 -0
  68. package/dist/cjs/DispatchReader.d.ts +112 -0
  69. package/dist/cjs/DispatchReader.d.ts.map +1 -0
  70. package/dist/cjs/DispatchReader.js +45 -0
  71. package/dist/cjs/DispatchReader.js.map +7 -0
  72. package/dist/cjs/Health.d.ts +333 -0
  73. package/dist/cjs/Health.d.ts.map +1 -0
  74. package/dist/cjs/Health.js +311 -0
  75. package/dist/cjs/Health.js.map +7 -0
  76. package/dist/cjs/JevSessionChecker.d.ts +57 -0
  77. package/dist/cjs/JevSessionChecker.d.ts.map +1 -0
  78. package/dist/cjs/JevSessionChecker.js +113 -0
  79. package/dist/cjs/JevSessionChecker.js.map +7 -0
  80. package/dist/cjs/Lineage.d.ts +131 -0
  81. package/dist/cjs/Lineage.d.ts.map +1 -0
  82. package/dist/cjs/Lineage.js +81 -0
  83. package/dist/cjs/Lineage.js.map +7 -0
  84. package/dist/cjs/Migrations.d.ts +34 -0
  85. package/dist/cjs/Migrations.d.ts.map +1 -0
  86. package/dist/cjs/Migrations.js +60 -0
  87. package/dist/cjs/Migrations.js.map +7 -0
  88. package/dist/cjs/Monitor.d.ts +282 -0
  89. package/dist/cjs/Monitor.d.ts.map +1 -0
  90. package/dist/cjs/Monitor.js +283 -0
  91. package/dist/cjs/Monitor.js.map +7 -0
  92. package/dist/cjs/ScopedToken.d.ts +193 -0
  93. package/dist/cjs/ScopedToken.d.ts.map +1 -0
  94. package/dist/cjs/ScopedToken.js +135 -0
  95. package/dist/cjs/ScopedToken.js.map +7 -0
  96. package/dist/cjs/SqlControlRuntime.d.ts +161 -0
  97. package/dist/cjs/SqlControlRuntime.d.ts.map +1 -0
  98. package/dist/cjs/SqlControlRuntime.js +1521 -0
  99. package/dist/cjs/SqlControlRuntime.js.map +7 -0
  100. package/dist/cjs/SqlCredentialStore.d.ts +43 -0
  101. package/dist/cjs/SqlCredentialStore.d.ts.map +1 -0
  102. package/dist/cjs/SqlCredentialStore.js +113 -0
  103. package/dist/cjs/SqlCredentialStore.js.map +7 -0
  104. package/dist/cjs/Steering.d.ts +69 -0
  105. package/dist/cjs/Steering.d.ts.map +1 -0
  106. package/dist/cjs/Steering.js +49 -0
  107. package/dist/cjs/Steering.js.map +7 -0
  108. package/dist/cjs/SystemFlows.d.ts +223 -0
  109. package/dist/cjs/SystemFlows.d.ts.map +1 -0
  110. package/dist/cjs/SystemFlows.js +195 -0
  111. package/dist/cjs/SystemFlows.js.map +7 -0
  112. package/dist/cjs/WebCryptoCipher.d.ts +49 -0
  113. package/dist/cjs/WebCryptoCipher.d.ts.map +1 -0
  114. package/dist/cjs/WebCryptoCipher.js +129 -0
  115. package/dist/cjs/WebCryptoCipher.js.map +7 -0
  116. package/dist/cjs/WebhookChannel.d.ts +113 -0
  117. package/dist/cjs/WebhookChannel.d.ts.map +1 -0
  118. package/dist/cjs/WebhookChannel.js +98 -0
  119. package/dist/cjs/WebhookChannel.js.map +7 -0
  120. package/dist/cjs/index.d.ts +160 -0
  121. package/dist/cjs/index.d.ts.map +1 -0
  122. package/dist/cjs/index.js +91 -0
  123. package/dist/cjs/index.js.map +7 -0
  124. package/dist/cjs/internal/MutationBoundary.d.ts +27 -0
  125. package/dist/cjs/internal/MutationBoundary.d.ts.map +1 -0
  126. package/dist/cjs/internal/MutationBoundary.js +50 -0
  127. package/dist/cjs/internal/MutationBoundary.js.map +7 -0
  128. package/dist/cjs/internal/activeFibers.d.ts +12 -0
  129. package/dist/cjs/internal/activeFibers.d.ts.map +1 -0
  130. package/dist/cjs/internal/activeFibers.js +30 -0
  131. package/dist/cjs/internal/activeFibers.js.map +7 -0
  132. package/dist/cjs/internal/issues.d.ts +28 -0
  133. package/dist/cjs/internal/issues.d.ts.map +1 -0
  134. package/dist/cjs/internal/issues.js +34 -0
  135. package/dist/cjs/internal/issues.js.map +7 -0
  136. package/dist/cjs/internal/planning.d.ts +347 -0
  137. package/dist/cjs/internal/planning.d.ts.map +1 -0
  138. package/dist/cjs/internal/planning.js +137 -0
  139. package/dist/cjs/internal/planning.js.map +7 -0
  140. package/dist/cjs/internal/sqlSchemaErrors.d.ts +18 -0
  141. package/dist/cjs/internal/sqlSchemaErrors.d.ts.map +1 -0
  142. package/dist/cjs/internal/sqlSchemaErrors.js +40 -0
  143. package/dist/cjs/internal/sqlSchemaErrors.js.map +7 -0
  144. package/dist/cjs/migrations/0001_control_tables.d.ts +18 -0
  145. package/dist/cjs/migrations/0001_control_tables.d.ts.map +1 -0
  146. package/dist/cjs/migrations/0001_control_tables.js +115 -0
  147. package/dist/cjs/migrations/0001_control_tables.js.map +7 -0
  148. package/dist/cjs/migrations/0002_run_keys.d.ts +15 -0
  149. package/dist/cjs/migrations/0002_run_keys.d.ts.map +1 -0
  150. package/dist/cjs/migrations/0002_run_keys.js +44 -0
  151. package/dist/cjs/migrations/0002_run_keys.js.map +7 -0
  152. package/dist/cjs/migrations/0003_signal_commands.d.ts +13 -0
  153. package/dist/cjs/migrations/0003_signal_commands.d.ts.map +1 -0
  154. package/dist/cjs/migrations/0003_signal_commands.js +50 -0
  155. package/dist/cjs/migrations/0003_signal_commands.js.map +7 -0
  156. package/dist/cjs/migrations/0004_approval_decisions.d.ts +13 -0
  157. package/dist/cjs/migrations/0004_approval_decisions.d.ts.map +1 -0
  158. package/dist/cjs/migrations/0004_approval_decisions.js +45 -0
  159. package/dist/cjs/migrations/0004_approval_decisions.js.map +7 -0
  160. package/dist/cjs/migrations/0005_signal_principals.d.ts +18 -0
  161. package/dist/cjs/migrations/0005_signal_principals.d.ts.map +1 -0
  162. package/dist/cjs/migrations/0005_signal_principals.js +45 -0
  163. package/dist/cjs/migrations/0005_signal_principals.js.map +7 -0
  164. package/dist/cjs/migrations/0006_run_principals.d.ts +18 -0
  165. package/dist/cjs/migrations/0006_run_principals.d.ts.map +1 -0
  166. package/dist/cjs/migrations/0006_run_principals.js +49 -0
  167. package/dist/cjs/migrations/0006_run_principals.js.map +7 -0
  168. package/dist/cjs/migrations/0007_resume_consent.d.ts +18 -0
  169. package/dist/cjs/migrations/0007_resume_consent.d.ts.map +1 -0
  170. package/dist/cjs/migrations/0007_resume_consent.js +46 -0
  171. package/dist/cjs/migrations/0007_resume_consent.js.map +7 -0
  172. package/dist/cjs/package.json +1 -0
  173. package/dist/cjs/test/TestControl.d.ts +19 -0
  174. package/dist/cjs/test/TestControl.d.ts.map +1 -0
  175. package/dist/cjs/test/TestControl.js +62 -0
  176. package/dist/cjs/test/TestControl.js.map +7 -0
  177. package/dist/esm/ApprovalAuthority.d.ts +73 -0
  178. package/dist/esm/ApprovalAuthority.d.ts.map +1 -0
  179. package/dist/esm/ApprovalAuthority.js +72 -0
  180. package/dist/esm/ApprovalAuthority.js.map +1 -0
  181. package/dist/esm/Cancellation.d.ts +107 -0
  182. package/dist/esm/Cancellation.d.ts.map +1 -0
  183. package/dist/esm/Cancellation.js +116 -0
  184. package/dist/esm/Cancellation.js.map +1 -0
  185. package/dist/esm/Channels.d.ts +170 -0
  186. package/dist/esm/Channels.d.ts.map +1 -0
  187. package/dist/esm/Channels.js +312 -0
  188. package/dist/esm/Channels.js.map +1 -0
  189. package/dist/esm/Control.d.ts +202 -0
  190. package/dist/esm/Control.d.ts.map +1 -0
  191. package/dist/esm/Control.js +42 -0
  192. package/dist/esm/Control.js.map +1 -0
  193. package/dist/esm/ControlClient.d.ts +52 -0
  194. package/dist/esm/ControlClient.d.ts.map +1 -0
  195. package/dist/esm/ControlClient.js +217 -0
  196. package/dist/esm/ControlClient.js.map +1 -0
  197. package/dist/esm/ControlError.d.ts +318 -0
  198. package/dist/esm/ControlError.d.ts.map +1 -0
  199. package/dist/esm/ControlError.js +359 -0
  200. package/dist/esm/ControlError.js.map +1 -0
  201. package/dist/esm/ControlExecutor.d.ts +372 -0
  202. package/dist/esm/ControlExecutor.d.ts.map +1 -0
  203. package/dist/esm/ControlExecutor.js +212 -0
  204. package/dist/esm/ControlExecutor.js.map +1 -0
  205. package/dist/esm/ControlFacts.d.ts +454 -0
  206. package/dist/esm/ControlFacts.d.ts.map +1 -0
  207. package/dist/esm/ControlFacts.js +324 -0
  208. package/dist/esm/ControlFacts.js.map +1 -0
  209. package/dist/esm/ControlLive.d.ts +23 -0
  210. package/dist/esm/ControlLive.d.ts.map +1 -0
  211. package/dist/esm/ControlLive.js +1585 -0
  212. package/dist/esm/ControlLive.js.map +1 -0
  213. package/dist/esm/ControlRpcs.d.ts +1204 -0
  214. package/dist/esm/ControlRpcs.d.ts.map +1 -0
  215. package/dist/esm/ControlRpcs.js +299 -0
  216. package/dist/esm/ControlRpcs.js.map +1 -0
  217. package/dist/esm/ControlRuntime.d.ts +635 -0
  218. package/dist/esm/ControlRuntime.d.ts.map +1 -0
  219. package/dist/esm/ControlRuntime.js +807 -0
  220. package/dist/esm/ControlRuntime.js.map +1 -0
  221. package/dist/esm/ControlSchema.d.ts +2642 -0
  222. package/dist/esm/ControlSchema.d.ts.map +1 -0
  223. package/dist/esm/ControlSchema.js +1030 -0
  224. package/dist/esm/ControlSchema.js.map +1 -0
  225. package/dist/esm/ControlServer.d.ts +51 -0
  226. package/dist/esm/ControlServer.d.ts.map +1 -0
  227. package/dist/esm/ControlServer.js +145 -0
  228. package/dist/esm/ControlServer.js.map +1 -0
  229. package/dist/esm/Credential.d.ts +136 -0
  230. package/dist/esm/Credential.d.ts.map +1 -0
  231. package/dist/esm/Credential.js +190 -0
  232. package/dist/esm/Credential.js.map +1 -0
  233. package/dist/esm/CredentialCipher.d.ts +90 -0
  234. package/dist/esm/CredentialCipher.d.ts.map +1 -0
  235. package/dist/esm/CredentialCipher.js +56 -0
  236. package/dist/esm/CredentialCipher.js.map +1 -0
  237. package/dist/esm/CredentialStore.d.ts +97 -0
  238. package/dist/esm/CredentialStore.d.ts.map +1 -0
  239. package/dist/esm/CredentialStore.js +101 -0
  240. package/dist/esm/CredentialStore.js.map +1 -0
  241. package/dist/esm/DispatchReader.d.ts +112 -0
  242. package/dist/esm/DispatchReader.d.ts.map +1 -0
  243. package/dist/esm/DispatchReader.js +76 -0
  244. package/dist/esm/DispatchReader.js.map +1 -0
  245. package/dist/esm/Health.d.ts +333 -0
  246. package/dist/esm/Health.d.ts.map +1 -0
  247. package/dist/esm/Health.js +400 -0
  248. package/dist/esm/Health.js.map +1 -0
  249. package/dist/esm/JevSessionChecker.d.ts +57 -0
  250. package/dist/esm/JevSessionChecker.d.ts.map +1 -0
  251. package/dist/esm/JevSessionChecker.js +108 -0
  252. package/dist/esm/JevSessionChecker.js.map +1 -0
  253. package/dist/esm/Lineage.d.ts +131 -0
  254. package/dist/esm/Lineage.d.ts.map +1 -0
  255. package/dist/esm/Lineage.js +174 -0
  256. package/dist/esm/Lineage.js.map +1 -0
  257. package/dist/esm/Migrations.d.ts +34 -0
  258. package/dist/esm/Migrations.d.ts.map +1 -0
  259. package/dist/esm/Migrations.js +53 -0
  260. package/dist/esm/Migrations.js.map +1 -0
  261. package/dist/esm/Monitor.d.ts +282 -0
  262. package/dist/esm/Monitor.d.ts.map +1 -0
  263. package/dist/esm/Monitor.js +415 -0
  264. package/dist/esm/Monitor.js.map +1 -0
  265. package/dist/esm/ScopedToken.d.ts +193 -0
  266. package/dist/esm/ScopedToken.d.ts.map +1 -0
  267. package/dist/esm/ScopedToken.js +224 -0
  268. package/dist/esm/ScopedToken.js.map +1 -0
  269. package/dist/esm/SqlControlRuntime.d.ts +161 -0
  270. package/dist/esm/SqlControlRuntime.d.ts.map +1 -0
  271. package/dist/esm/SqlControlRuntime.js +1756 -0
  272. package/dist/esm/SqlControlRuntime.js.map +1 -0
  273. package/dist/esm/SqlCredentialStore.d.ts +43 -0
  274. package/dist/esm/SqlCredentialStore.d.ts.map +1 -0
  275. package/dist/esm/SqlCredentialStore.js +97 -0
  276. package/dist/esm/SqlCredentialStore.js.map +1 -0
  277. package/dist/esm/Steering.d.ts +69 -0
  278. package/dist/esm/Steering.d.ts.map +1 -0
  279. package/dist/esm/Steering.js +89 -0
  280. package/dist/esm/Steering.js.map +1 -0
  281. package/dist/esm/SystemFlows.d.ts +223 -0
  282. package/dist/esm/SystemFlows.d.ts.map +1 -0
  283. package/dist/esm/SystemFlows.js +198 -0
  284. package/dist/esm/SystemFlows.js.map +1 -0
  285. package/dist/esm/WebCryptoCipher.d.ts +49 -0
  286. package/dist/esm/WebCryptoCipher.d.ts.map +1 -0
  287. package/dist/esm/WebCryptoCipher.js +123 -0
  288. package/dist/esm/WebCryptoCipher.js.map +1 -0
  289. package/dist/esm/WebhookChannel.d.ts +113 -0
  290. package/dist/esm/WebhookChannel.d.ts.map +1 -0
  291. package/dist/esm/WebhookChannel.js +109 -0
  292. package/dist/esm/WebhookChannel.js.map +1 -0
  293. package/dist/esm/index.d.ts +160 -0
  294. package/dist/esm/index.d.ts.map +1 -0
  295. package/dist/esm/index.js +160 -0
  296. package/dist/esm/index.js.map +1 -0
  297. package/dist/esm/internal/MutationBoundary.d.ts +27 -0
  298. package/dist/esm/internal/MutationBoundary.d.ts.map +1 -0
  299. package/dist/esm/internal/MutationBoundary.js +40 -0
  300. package/dist/esm/internal/MutationBoundary.js.map +1 -0
  301. package/dist/esm/internal/activeFibers.d.ts +12 -0
  302. package/dist/esm/internal/activeFibers.d.ts.map +1 -0
  303. package/dist/esm/internal/activeFibers.js +17 -0
  304. package/dist/esm/internal/activeFibers.js.map +1 -0
  305. package/dist/esm/internal/issues.d.ts +28 -0
  306. package/dist/esm/internal/issues.d.ts.map +1 -0
  307. package/dist/esm/internal/issues.js +35 -0
  308. package/dist/esm/internal/issues.js.map +1 -0
  309. package/dist/esm/internal/planning.d.ts +347 -0
  310. package/dist/esm/internal/planning.d.ts.map +1 -0
  311. package/dist/esm/internal/planning.js +199 -0
  312. package/dist/esm/internal/planning.js.map +1 -0
  313. package/dist/esm/internal/sqlSchemaErrors.d.ts +18 -0
  314. package/dist/esm/internal/sqlSchemaErrors.d.ts.map +1 -0
  315. package/dist/esm/internal/sqlSchemaErrors.js +38 -0
  316. package/dist/esm/internal/sqlSchemaErrors.js.map +1 -0
  317. package/dist/esm/migrations/0001_control_tables.d.ts +18 -0
  318. package/dist/esm/migrations/0001_control_tables.d.ts.map +1 -0
  319. package/dist/esm/migrations/0001_control_tables.js +96 -0
  320. package/dist/esm/migrations/0001_control_tables.js.map +1 -0
  321. package/dist/esm/migrations/0002_run_keys.d.ts +15 -0
  322. package/dist/esm/migrations/0002_run_keys.d.ts.map +1 -0
  323. package/dist/esm/migrations/0002_run_keys.js +22 -0
  324. package/dist/esm/migrations/0002_run_keys.js.map +1 -0
  325. package/dist/esm/migrations/0003_signal_commands.d.ts +13 -0
  326. package/dist/esm/migrations/0003_signal_commands.d.ts.map +1 -0
  327. package/dist/esm/migrations/0003_signal_commands.js +26 -0
  328. package/dist/esm/migrations/0003_signal_commands.js.map +1 -0
  329. package/dist/esm/migrations/0004_approval_decisions.d.ts +13 -0
  330. package/dist/esm/migrations/0004_approval_decisions.d.ts.map +1 -0
  331. package/dist/esm/migrations/0004_approval_decisions.js +25 -0
  332. package/dist/esm/migrations/0004_approval_decisions.js.map +1 -0
  333. package/dist/esm/migrations/0005_signal_principals.d.ts +18 -0
  334. package/dist/esm/migrations/0005_signal_principals.d.ts.map +1 -0
  335. package/dist/esm/migrations/0005_signal_principals.js +27 -0
  336. package/dist/esm/migrations/0005_signal_principals.js.map +1 -0
  337. package/dist/esm/migrations/0006_run_principals.d.ts +18 -0
  338. package/dist/esm/migrations/0006_run_principals.d.ts.map +1 -0
  339. package/dist/esm/migrations/0006_run_principals.js +30 -0
  340. package/dist/esm/migrations/0006_run_principals.js.map +1 -0
  341. package/dist/esm/migrations/0007_resume_consent.d.ts +18 -0
  342. package/dist/esm/migrations/0007_resume_consent.d.ts.map +1 -0
  343. package/dist/esm/migrations/0007_resume_consent.js +27 -0
  344. package/dist/esm/migrations/0007_resume_consent.js.map +1 -0
  345. package/dist/esm/test/TestControl.d.ts +19 -0
  346. package/dist/esm/test/TestControl.d.ts.map +1 -0
  347. package/dist/esm/test/TestControl.js +30 -0
  348. package/dist/esm/test/TestControl.js.map +1 -0
  349. package/docs/README.md +189 -0
  350. package/docs/api.md +982 -0
  351. package/docs/concepts/authority.md +109 -0
  352. package/docs/concepts/cancellation.md +129 -0
  353. package/docs/concepts/lineage.md +132 -0
  354. package/docs/concepts/ownership.md +139 -0
  355. package/docs/concepts/projections.md +203 -0
  356. package/docs/concepts/receipts.md +128 -0
  357. package/docs/guides/approvals.md +284 -0
  358. package/docs/guides/cancel-and-resume.md +162 -0
  359. package/docs/guides/durable-storage.md +147 -0
  360. package/docs/guides/implement-an-executor.md +173 -0
  361. package/docs/guides/ingest-a-webhook.md +177 -0
  362. package/docs/guides/list-runs.md +160 -0
  363. package/docs/guides/monitor-runs.md +176 -0
  364. package/docs/guides/observe-health.md +147 -0
  365. package/docs/guides/postgres-tests.md +5 -0
  366. package/docs/guides/serve-over-rpc.md +220 -0
  367. package/docs/guides/signal-a-run.md +53 -0
  368. package/docs/guides/steer-a-run.md +138 -0
  369. package/docs/guides/store-credentials.md +164 -0
  370. package/docs/guides/testing.md +139 -0
  371. package/docs/guides/watch-a-run.md +154 -0
  372. package/docs/installation.md +106 -0
  373. package/docs/quickstart.md +163 -0
  374. package/docs/troubleshooting.md +208 -0
  375. package/package.json +405 -3
  376. package/src/ApprovalAuthority.ts +114 -0
  377. package/src/Cancellation.ts +172 -0
  378. package/src/Channels.ts +493 -0
  379. package/src/Control.ts +337 -0
  380. package/src/ControlClient.ts +319 -0
  381. package/src/ControlError.ts +378 -0
  382. package/src/ControlExecutor.ts +490 -0
  383. package/src/ControlFacts.ts +383 -0
  384. package/src/ControlLive.ts +2113 -0
  385. package/src/ControlRpcs.ts +443 -0
  386. package/src/ControlRuntime.ts +1597 -0
  387. package/src/ControlSchema.ts +1380 -0
  388. package/src/ControlServer.ts +182 -0
  389. package/src/Credential.ts +310 -0
  390. package/src/CredentialCipher.ts +110 -0
  391. package/src/CredentialStore.ts +152 -0
  392. package/src/DispatchReader.ts +122 -0
  393. package/src/Health.ts +591 -0
  394. package/src/JevSessionChecker.ts +127 -0
  395. package/src/Lineage.ts +203 -0
  396. package/src/Migrations.ts +56 -0
  397. package/src/Monitor.ts +600 -0
  398. package/src/ScopedToken.ts +306 -0
  399. package/src/SqlControlRuntime.ts +2476 -0
  400. package/src/SqlCredentialStore.ts +148 -0
  401. package/src/Steering.ts +96 -0
  402. package/src/SystemFlows.ts +225 -0
  403. package/src/WebCryptoCipher.ts +169 -0
  404. package/src/WebhookChannel.ts +166 -0
  405. package/src/index.ts +188 -0
  406. package/src/internal/MutationBoundary.ts +46 -0
  407. package/src/internal/activeFibers.ts +22 -0
  408. package/src/internal/issues.ts +40 -0
  409. package/src/internal/planning.ts +262 -0
  410. package/src/internal/sqlSchemaErrors.ts +37 -0
  411. package/src/migrations/0001_control_tables.ts +99 -0
  412. package/src/migrations/0002_run_keys.ts +23 -0
  413. package/src/migrations/0003_signal_commands.ts +27 -0
  414. package/src/migrations/0004_approval_decisions.ts +25 -0
  415. package/src/migrations/0005_signal_principals.ts +27 -0
  416. package/src/migrations/0006_run_principals.ts +31 -0
  417. package/src/migrations/0007_resume_consent.ts +28 -0
  418. package/src/test/TestControl.ts +47 -0
@@ -0,0 +1,162 @@
1
+ ---
2
+ title: "Cancel a run, and restart one"
3
+ description: "Stop a run you may not own and restart one nobody is driving: what each receipt means, why both verbs read terminality first, and how a cancel reaches a run in another process."
4
+ sidebar:
5
+ order: 6
6
+ ---
7
+
8
+ `cancel` and `resume` are the two lifecycle verbs, and they share a shape:
9
+ a run id, a caller-stated `reason`, and an idempotency key. Both record who
10
+ asked, and both read the run's terminality before anything else.
11
+
12
+ ## Cancel a run
13
+
14
+ ```ts
15
+ import { Control } from "@smthrs/control/Control"
16
+ import * as Effect from "effect/Effect"
17
+
18
+ const cancel = Effect.gen(function*() {
19
+ const control = yield* Control
20
+ return yield* control.cancel({
21
+ runId: "run-17",
22
+ reason: "budget",
23
+ idempotencyKey: "cancel:run-17"
24
+ })
25
+ })
26
+ ```
27
+
28
+ The `reason` is free text and it is recorded on the
29
+ `control.run.cancel-requested` entry the mutation writes, then projected back
30
+ onto `RunSummary.cancellation`. An operator reading a cancelled run a week
31
+ later asks "why", and a control plane that never carried the answer cannot
32
+ produce one afterwards.
33
+
34
+ ### What comes back
35
+
36
+ | Receipt | Meaning |
37
+ | -------------------------------- | ---------------------------------------------------------- |
38
+ | `Terminal` with the run's status | The run settled, either before this call or because of it. |
39
+ | `Accepted` | The request is durable and a live peer will act on it. |
40
+
41
+ A cancel that this process could interrupt answers `Terminal` naming
42
+ `cancelled`, because the run really did settle in this call. A cancel against a
43
+ run a live peer is holding answers `Accepted`: the request is on the engine
44
+ row, and the owner stops the run at its next cancel poll.
45
+
46
+ `cancel` deliberately does not replay its recorded receipt. Its answer is a
47
+ statement about a run, and the run moves on. See
48
+ [Receipts and idempotency](../concepts/receipts.md).
49
+
50
+ ### How a cancel reaches another process
51
+
52
+ Fibers are process-local, so an interrupt only stops a run this process is
53
+ driving. The durable half travels through the executor:
54
+
55
+ 1. `ControlExecutor.requestCancel` writes `cancel_requested_at_ms` on the
56
+ engine row, inside the mutation's transaction. An engine that refuses rolls
57
+ the whole cancel back, because a control row that says `cancelled` over an
58
+ engine row that is still running is the one state an operator cannot
59
+ recover from.
60
+ 2. The attribution entry is written, unless the executor answered
61
+ `already-requested`, which means the column was already set and the record
62
+ already exists.
63
+ 3. The local fiber is interrupted. A parked run has no owner, so the cancelling
64
+ process claims the park itself in order to end it.
65
+ 4. `ControlExecutor.settleCancelledPark` runs _after_ the mutation commits, so
66
+ the parked execution is finished before the process that asked goes away.
67
+ Driving a run re-enters the engine, whose writes would wait on the writer
68
+ the transaction holds.
69
+
70
+ An executor that answers with a `CancelTerminal` reports that the engine row
71
+ had already settled. Nothing was cancelled, so no attribution is written, and
72
+ the control row is reconciled onto the engine's own status instead.
73
+
74
+ ## Restart a run
75
+
76
+ ```ts
77
+ const resumed = yield * control.resume({
78
+ runId: "run-17",
79
+ reason: "operator retry",
80
+ idempotencyKey: "resume:run-17"
81
+ })
82
+ ```
83
+
84
+ `run` with a `Resume` input is the same operation:
85
+
86
+ ```ts
87
+ yield * control.run({ _tag: "Resume", runId: "run-17", idempotencyKey: "resume:run-17" })
88
+ ```
89
+
90
+ One resume, one implementation. The two spellings exist because RPC clients and
91
+ the CLI reach for different ones.
92
+
93
+ ### What comes back
94
+
95
+ | Receipt | Meaning |
96
+ | ----------------------- | --------------------------------------------------------------------------- |
97
+ | `Terminal` | The run had already settled. Nothing to restart. |
98
+ | `Accepted` | The run was claimed, or the restart was recorded for the host that owns it. |
99
+ | `Accepted` + `handedTo` | A live host parked the run. The restart was handed to that host. |
100
+ | `AlreadyApplied` | An earlier call under this key already restarted it. |
101
+
102
+ `ClaimLost` is the failure, and it names a real peer: a run at `running` or at
103
+ the `accepted` a claim writes is being held by a live process, so there is
104
+ nothing to restart and pretending otherwise would hide the peer.
105
+
106
+ A run the _engine_ created, a child, a fork, or a later trampoline round, keeps
107
+ its own driver. Both public resume spellings use `scope: "launched"` and journal
108
+ `control.run.resume`; they leave engine-created rows unclaimed to preserve
109
+ the continuation state. The caller or a journal subscriber must drive the
110
+ execution, even when the plane claims a control-launched run. A run the caller
111
+ claims creates no `pendingResumes` entry. An `Accepted` receipt does not
112
+ establish that execution started.
113
+
114
+ ### A run a live host parked
115
+
116
+ A detached host that parks a run, for example over children released when its
117
+ lease lapsed during a stall, stays alive and keeps the run. Claiming that run
118
+ would take its execution from the host. `resume` hands the restart to the host
119
+ instead:
120
+
121
+ 1. The runtime refuses the claim with `ClaimLost` naming the host in
122
+ `parkedBy`.
123
+ 2. `resume` journals `control.run.resume` with `handedTo` set to that host,
124
+ then records a durable delegation whose `consent` is that entry's journal
125
+ sequence.
126
+ 3. The receipt is `Accepted` with `handedTo`. The caller holds nothing to
127
+ drive.
128
+ 4. The host polls `pendingResumes` every second and takes the delegation up.
129
+ Because it carries consent, the host records the per-release retry
130
+ permission under that sequence, journals `control.run.claimed`, and
131
+ re-drives the run as an operator resume.
132
+
133
+ An approval or a wake is background intent and never carries consent, and a
134
+ later approval delegation keeps consent that no host has taken up yet. The
135
+ permission covers the releases that exist when the host takes the delegation
136
+ up. A later lease lapse is a new release, so it needs a new resume. A run whose
137
+ parking host has exited is claimed by the caller as before.
138
+
139
+ ## What the CLI does
140
+
141
+ [`smthrs runs cancel`](/cli/runs) and `smthrs runs cancel-all` both reach
142
+ `cancel`; [`smthrs runs resume`](/cli/runs) reaches `resume`. Both record the
143
+ principal the CLI authenticated, so `RunSummary.cancellation.principal` names a
144
+ person rather than a process.
145
+
146
+ `smthrs runs resume` keys each request by the run's latest park, so resuming a
147
+ second park is a new request rather than a replay of the first. A run it claims
148
+ is driven in the foreground until it settles. A run handed to its live host is
149
+ not: the CLI waits up to 15 seconds for the host's `control.run.claimed` and for
150
+ the run to leave its `released` park, then prints the receipt with the run's
151
+ `status` and `waitingReason`. If the host does not take it up in that time, the
152
+ command fails with `resume_not_taken_up`, naming the host's process. The
153
+ request stays recorded for that host.
154
+
155
+ ## Where to go next
156
+
157
+ - [Cancellation attribution](../concepts/cancellation.md): what the answer to
158
+ "who cancelled this" is built from.
159
+ - [Ownership, fences, and claims](../concepts/ownership.md): why `ClaimLost` is
160
+ the right refusal.
161
+ - [Connect an execution engine](./implement-an-executor.md): the four methods
162
+ these verbs call.
@@ -0,0 +1,147 @@
1
+ ---
2
+ title: "Store control state in a database"
3
+ description: "Compose SqlControlRuntime over a SQL database and the fenced run store: the layer stack, the migration order, the owner identity every claim is stamped with, and what sharing a database with the engine buys."
4
+ sidebar:
5
+ order: 7
6
+ ---
7
+
8
+ `ControlRuntime.layerMemory` models the production seams in a `Map`, and
9
+ nothing it decides survives the process. `SqlControlRuntime` is the durable
10
+ adapter: the same contract, over a SQL database and the fenced run store from
11
+ [`@smthrs/run-store`](/api/run-store).
12
+
13
+ ## Compose the layer
14
+
15
+ ```ts
16
+ import * as ControlLive from "@smthrs/control/ControlLive"
17
+ import * as SqlControlRuntime from "@smthrs/control/SqlControlRuntime"
18
+ import * as DurableWriter from "@smthrs/database/DurableWriter"
19
+ import * as NodeDatabase from "@smthrs/database/node/NodeDatabase"
20
+ import { NotificationQueue } from "@smthrs/notifications"
21
+ import { Registry } from "@smthrs/registry"
22
+ import { Migrations as RunStoreMigrations, RunStore } from "@smthrs/run-store"
23
+ import * as Layer from "effect/Layer"
24
+
25
+ const storage = RunStore.layer.pipe(
26
+ Layer.provideMerge(RunStoreMigrations.layer),
27
+ Layer.provideMerge(DurableWriter.layer()),
28
+ Layer.provideMerge(NodeDatabase.layer({ filename: "control.sqlite" }))
29
+ )
30
+
31
+ const controlPlane = ControlLive.layer.pipe(
32
+ Layer.provideMerge(
33
+ Layer.mergeAll(
34
+ SqlControlRuntime.layer({ owner: { hostId: "gateway", pid: process.pid, nonce: "boot" } })
35
+ .pipe(Layer.orDie),
36
+ NotificationQueue.layer,
37
+ Registry.layerNoop()
38
+ )
39
+ )
40
+ )
41
+ ```
42
+
43
+ `SqlControlRuntime.layer` requires `Crypto`, `DurableWriter`, `SqlClient`, and
44
+ `RunStore`, and fails with `PersistenceError` if its migration cannot run.
45
+ `layerWithStore` is the same layer with `RunStore.layer` already provided, for
46
+ a composition that has no other use for the store.
47
+
48
+ Build the storage once. `Layer.provideMerge` builds what it provides privately,
49
+ so composing it twice hands the control plane its own empty copy of the rows it
50
+ is supposed to be reading.
51
+
52
+ `SqlControlRuntime` stores the decoded plan input and the plan card summary as
53
+ raw plaintext JSON. The durable adapter does not redact or encrypt those
54
+ columns because the input must be replayed exactly. Never put a credential in
55
+ plan input: store it through `Credential`, pass only a `CredentialRef`, and
56
+ resolve that reference at the adapter boundary that needs the secret.
57
+
58
+ The Node CLI creates its `.flows/` directory with mode `0700` and, on POSIX,
59
+ repairs existing directory permissions to `0700` and SQLite database, WAL,
60
+ and shared-memory file permissions to `0600` when opening the control store.
61
+ Windows does not use this POSIX chmod policy. If you embed the durable adapter
62
+ with your own database layer, configure equivalent storage access controls.
63
+
64
+ ### Options
65
+
66
+ | Option | Meaning |
67
+ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
68
+ | `flows` | The catalog this plane may plan, as `DurableFlow` entries. Defaults to the plannable reserved system flows. |
69
+ | `owner` | The process identity every claim is stamped with. Omitted, one synthetic identity is minted for this runtime only, so separately constructed runtimes cannot cross each other's fences. |
70
+ | `principal` | The fallback identity stamped on a mutation whose caller named none. |
71
+
72
+ Supply a real `owner` whenever the host can report its process identity, so
73
+ liveness probes can reason about the operating-system process.
74
+
75
+ ## Run the migrations
76
+
77
+ The package reserves a namespaced migration block, and a host composes it
78
+ beside the journal and run-store sets before opening a shared control database:
79
+
80
+ ```ts
81
+ import * as ControlMigrations from "@smthrs/control/Migrations"
82
+ import * as DatabaseMigrations from "@smthrs/database/Migrations"
83
+ import * as JournalMigrations from "@smthrs/journal/Migrations"
84
+ import * as RunStoreMigrations from "@smthrs/run-store/Migrations"
85
+
86
+ const migrations = DatabaseMigrations.run([
87
+ JournalMigrations.set,
88
+ RunStoreMigrations.set,
89
+ ControlMigrations.set
90
+ ])
91
+ ```
92
+
93
+ `ControlMigrations.layer` runs the control set alone before exposing the
94
+ database to control services, and `ControlMigrations.run` is the effect behind
95
+ it.
96
+
97
+ Each adapter also bootstraps its own tables idempotently, through
98
+ `SqlControlRuntime.migrate` and `SqlCredentialStore.migrate`, so standalone
99
+ construction works. Prefer the composed set: a standalone runtime that recorded
100
+ control's high-offset migration first would make the later journal and
101
+ run-store sets look skipped.
102
+
103
+ The tables the set creates are `control_plans`, `control_plan_keys`,
104
+ `control_tokens`, `control_grants`, `control_mutations`, `control_runs`,
105
+ `control_run_resumes`, `control_run_messages`, `control_sequences`, and
106
+ `control_credentials`.
107
+
108
+ ## What the durable runtime adds
109
+
110
+ Several `RunSummary` fields exist only here, because they are read from the
111
+ engine's own columns and journal entries:
112
+
113
+ | Field | Read from |
114
+ | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
115
+ | `waitingReason` | `flows_runs.waiting_reason` |
116
+ | `parentRunId`, `lineageId`, `roundOrdinal`, `origin` | the run row's columns and the `flows_run_parents` spawn edges |
117
+ | `cancellation` | `cancel_requested_at_ms`, `flows.engine.interrupted`, and the plane's own attributed requests |
118
+
119
+ They are read through the `SqlClient` the runtime was built over. A composition
120
+ that wants them must give the control runtime and the engine **one** database.
121
+ The [`smthrs` CLI](/api/cli) does not: it keeps `.flows/control.db` and
122
+ `.flows/engine.db` as two files, so one run has two rows and these projections
123
+ are empty there. Cancellation still converges, because the request travels
124
+ through the [executor port](./implement-an-executor.md) and the owning driver
125
+ settles from it.
126
+
127
+ The listing covers every row in `flows_runs`, not only the runs this plane
128
+ launched. A run whose `state_json` is not a control summary, an engine-created
129
+ run, is projected from the row's own columns with the engine's `flowName` as
130
+ its `flowId`.
131
+
132
+ ## Reading one run stays cheap
133
+
134
+ A listing folds the whole database, because every row is going to be answered
135
+ for anyway. Reading one run, which is what every mutation does before it
136
+ writes, reads that run and its ancestor chain and nothing else. The cost of
137
+ steering or cancelling a run therefore does not grow with the size of the
138
+ database.
139
+
140
+ ## Where to go next
141
+
142
+ - [Ownership, fences, and claims](../concepts/ownership.md): what a fence is
143
+ and what the status mapping means.
144
+ - [Connect an execution engine](./implement-an-executor.md): the other half of
145
+ a real deployment.
146
+ - [Store and resolve a credential](./store-credentials.md): the durable
147
+ credential table lives in this same set.
@@ -0,0 +1,173 @@
1
+ ---
2
+ title: "Connect an execution engine"
3
+ description: "Implement ControlExecutor so plan launches start real runs and cancels, signals, and resumes reach the engine: the five methods, the answers each one may give, and why a launch forks."
4
+ sidebar:
5
+ order: 8
6
+ ---
7
+
8
+ `ControlExecutor` is the seam between authority and execution. The plane hands
9
+ work over and learns only what the executor did with it.
10
+
11
+ Without it, the plane records facts nobody reads: a cancel that answers
12
+ `ClaimLost` to every process but the owner, a signal a parked run never sees,
13
+ and a resume nothing subscribes to.
14
+
15
+ ## The five methods
16
+
17
+ ```ts
18
+ interface Service {
19
+ readonly launch: (input: Launch) => Effect.Effect<Acceptance, LaunchFailed>
20
+ readonly requestCancel: (input: CancelRequest) => Effect.Effect<CancelRecord, PersistenceError>
21
+ readonly deliverSignal: (input: Signal) => Effect.Effect<SignalDelivery, PersistenceError>
22
+ readonly resumeRun: (input: ResumeRequest) => Effect.Effect<ResumeUptake, PersistenceError>
23
+ readonly settleCancelledPark: (input: CancelRequest) => Effect.Effect<void, PersistenceError>
24
+ }
25
+ ```
26
+
27
+ Each answer is a small closed vocabulary, and every value in it is a real
28
+ deployment:
29
+
30
+ | Method | Answer | Means |
31
+ | --------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
32
+ | `launch` | `accepted` | This executor took the launch. The plane writes `running`. |
33
+ | | `pending` | It queued the launch. The plane releases the row as `control.run.pending`. |
34
+ | | fails `LaunchFailed` | Nothing will ever drive this run. The plane settles the row as `failed`. |
35
+ | `requestCancel` | `recorded` | This call set `cancel_requested_at_ms` on the engine row. |
36
+ | | `already-requested` | The column was already set, so the attribution record already exists. |
37
+ | | `unknown` | This executor's engine has no row for the run. |
38
+ | | `{ _tag: "Terminal", status }` | The engine row has already settled. |
39
+ | `deliverSignal` | `delivered`, `no-match`, `refused`, `unknown` | See [Deliver a signal](./signal-a-run.md). |
40
+ | `resumeRun` | `resuming` | This executor hosts the run, took the fence, and is re-driving it. |
41
+ | | `unknown` | It drives no execution for this run. |
42
+ | `settleCancelledPark` | | Finishes a parked execution whose cancellation is already durable. |
43
+
44
+ `already-requested` is not a detail. The write is first-writer-wins and every
45
+ repeat of `cancel` re-runs the whole mutation, so answering `recorded` to all
46
+ of them journals one `control.run.cancel-requested` per ask for a single
47
+ cancellation.
48
+
49
+ `settleCancelledPark` exists because a park has no owner, so nothing is driving
50
+ the run and nothing reads the request `requestCancel` wrote. The engine's
51
+ parked-run sweep does, once per heartbeat, but a short-lived `smthrs runs cancel`
52
+ process writes the request at the very end of its life and exits first. The
53
+ plane calls this _after_ the cancel mutation commits, never inside it: driving
54
+ a run re-enters the engine, whose writes would wait on the writer the
55
+ transaction holds.
56
+
57
+ ## Start from the noop
58
+
59
+ `ControlExecutor.makeNoop` answers the honest absence for every method, so an
60
+ implementation overrides only what it supports:
61
+
62
+ ```ts
63
+ import * as ControlExecutor from "@smthrs/control/ControlExecutor"
64
+ import * as Effect from "effect/Effect"
65
+
66
+ const executor = ControlExecutor.makeNoop({
67
+ launch: ({ plan, run }) =>
68
+ Effect.sync(() => {
69
+ queue.push({ flowId: plan.card.flowId, runId: run.runId })
70
+ return "pending" as const
71
+ })
72
+ })
73
+ ```
74
+
75
+ `ControlExecutor.layer(executor)` and `ControlExecutor.layerNoop(overrides)`
76
+ provide it.
77
+
78
+ ## Start the run the plane minted
79
+
80
+ `launch` receives the stored plan and the run row the plane has committed, and
81
+ it must start _that_ run: `run.runId` is the execution id, so the events the
82
+ engine journals and the row the plane projects name one run.
83
+
84
+ ```ts
85
+ import * as ControlExecutor from "@smthrs/control/ControlExecutor"
86
+ import * as ControlRuntime from "@smthrs/control/ControlRuntime"
87
+ import type * as ControlSchema from "@smthrs/control/ControlSchema"
88
+ import type { FlowRuntime } from "@smthrs/flow"
89
+ import { Executable, Registry } from "@smthrs/registry"
90
+ import { RunStore } from "@smthrs/run-store"
91
+ import type * as Crypto from "effect/Crypto"
92
+ import * as Effect from "effect/Effect"
93
+ import type * as FileSystem from "effect/FileSystem"
94
+ import * as Layer from "effect/Layer"
95
+ import type * as Path from "effect/Path"
96
+
97
+ /** How a discovered descriptor is loaded, and what it may delegate to. */
98
+ const bridge: Executable.Options = { delegates: [] }
99
+
100
+ const executorLayer = Layer.effect(ControlExecutor.ControlExecutor)(
101
+ Effect.gen(function*() {
102
+ const plane = yield* ControlRuntime.ControlRuntime
103
+ const services = yield* Effect.context<
104
+ | Crypto.Crypto
105
+ | FileSystem.FileSystem
106
+ | FlowRuntime.FlowRuntime
107
+ | Path.Path
108
+ | Registry.Registry
109
+ | RunStore.RunStore
110
+ >()
111
+
112
+ return ControlExecutor.makeNoop({
113
+ launch: ({ plan, run }) =>
114
+ Effect.gen(function*() {
115
+ const executable = yield* Executable.fromRegistry(plan.card.flowId, bridge)
116
+ Effect.runForkWith(services)(
117
+ executable.flow.execute(
118
+ { input: plan.decodedInput },
119
+ { executionId: run.runId, discard: true }
120
+ ).pipe(Effect.andThen(mirror(plane, run.runId)))
121
+ )
122
+ return "accepted" as const
123
+ }).pipe(Effect.provide(services), Effect.orDie)
124
+ })
125
+ })
126
+ )
127
+ ```
128
+
129
+ It forks so launch acceptance does not wait for execution to finish. Admission
130
+ is already committed before `launch` runs. After the executor answers
131
+ `accepted`, the plane marks a still-accepted run as running; an outcome the
132
+ executor has already recorded, including a parked or completed run, is preserved.
133
+
134
+ ## Mirror the engine's status back
135
+
136
+ The plane cannot see into the engine's database, so an executor that walked
137
+ away after starting a run would leave every run reading `running` forever.
138
+ Reading the engine's own row back and writing the plane's vocabulary onto the
139
+ plane's row is the whole of that duty, and it is the `mirror` the launch above
140
+ chains onto:
141
+
142
+ ```ts
143
+ /** The engine's run vocabulary, in the plane's. */
144
+ const planeStatus = (status: RunStore.RunStatus): ControlSchema.RunStatus =>
145
+ status === "suspended" ? "parked" : status === "pending" ? "accepted" : status
146
+
147
+ const mirror = (plane: ControlRuntime.Service, runId: string) =>
148
+ Effect.gen(function*() {
149
+ const runs = yield* RunStore.RunStore
150
+ const row = yield* runs.get(runId)
151
+ const id = runId as ControlSchema.RunId
152
+ const fence = yield* plane.claimFence(id)
153
+ yield* plane.writeStatus(id, fence, planeStatus(row.status))
154
+ }).pipe(Effect.orDie)
155
+ ```
156
+
157
+ The engine calls a parked run `suspended`; an operator calls it `parked`. The
158
+ two vocabularies are not the same set, so the mapping is explicit: every status
159
+ the engine reports has to land on a member of `ControlSchema.RunStatus`, which
160
+ is the only vocabulary the plane's row accepts.
161
+
162
+ The complete, runnable bridge, with discovery, two databases, and one shared
163
+ journal, is
164
+ [`examples/src/24-control-plane-and-gateway.ts`](https://github.com/smithersai/smithers/blob/main/examples/src/24-control-plane-and-gateway.ts).
165
+
166
+ ## Where to go next
167
+
168
+ - [Ownership, fences, and claims](../concepts/ownership.md): what
169
+ `claimFence` and `writeStatus` are doing.
170
+ - [Cancel a run, and restart one](./cancel-and-resume.md): the sequence
171
+ `requestCancel` and `settleCancelledPark` sit inside.
172
+ - [Store control state in a database](./durable-storage.md): the other half of
173
+ a real deployment.
@@ -0,0 +1,177 @@
1
+ ---
2
+ title: "Accept a webhook as a control request"
3
+ description: "Turn a verified external request into one control mutation: the verify-then-decode order, the durable idempotency a redelivery replays, the headers that may enter identity, and the body ceiling."
4
+ sidebar:
5
+ order: 11
6
+ ---
7
+
8
+ A channel turns an external request into a control mutation, once. It verifies
9
+ opaque bytes before it decodes them, and it acquires no execution path of its
10
+ own: everything it does, it does through `Control`.
11
+
12
+ ## Build a webhook channel
13
+
14
+ ```ts
15
+ import * as WebhookChannel from "@smthrs/control/WebhookChannel"
16
+ import * as Effect from "effect/Effect"
17
+ import * as Redacted from "effect/Redacted"
18
+ import * as Schema from "effect/Schema"
19
+
20
+ const Push = Schema.Struct({ ref: Schema.String })
21
+
22
+ const github = WebhookChannel.make({
23
+ name: "github",
24
+ schema: Push,
25
+ credential: Redacted.make({ id: "github-webhook", name: "GitHub webhook" }),
26
+ fingerprintHeaders: ["x-github-event"],
27
+ verify: (raw, credential) => verifySignature(raw, credential),
28
+ map: (payload) =>
29
+ Effect.succeed({
30
+ _tag: "Start" as const,
31
+ flowId: "ops/Deploy",
32
+ input: { build: payload.ref }
33
+ }),
34
+ project: (run) => ({ cursor: run.status, operation: "post", message: { text: run.runId } })
35
+ })
36
+ ```
37
+
38
+ `map` returns one of two results:
39
+
40
+ | Result | Becomes |
41
+ | ----------------------------------- | ---------------------------------------- |
42
+ | `{ _tag: "Start", flowId, input }` | `control.plan` followed by `control.run` |
43
+ | `{ _tag: "Signal", runId, signal }` | `control.signal` |
44
+
45
+ `decode` and `map` must be deterministic and free of side effects. A retry may
46
+ evaluate either of them again.
47
+
48
+ The credential is a redacted `CredentialRef`, never a secret. Resolving it
49
+ belongs at the host adapter boundary, so a webhook's persisted record never
50
+ holds key material. See [Store and resolve a credential](./store-credentials.md).
51
+
52
+ ## Register and mount it
53
+
54
+ ```ts
55
+ import * as Channels from "@smthrs/control/Channels"
56
+
57
+ const program = Effect.gen(function*() {
58
+ const channels = yield* Channels.Channels
59
+ yield* channels.register(github)
60
+ })
61
+ ```
62
+
63
+ `register` accepts `Channel<A>` directly, including typed webhook payloads.
64
+ `lookup` returns a `RegisteredChannel`: the declared schema and transport
65
+ metadata remain available, while `decodeAndMap` keeps the hidden payload type
66
+ inside the adapter. Use `ingest` for verified dispatch through `Control`.
67
+
68
+ `Channels.layer` builds the coordinator over `ControlRuntime`'s durable
69
+ mutation store, so inbound idempotency survives a coordinator restart. Only
70
+ registration and outbound projection cursors are process-local.
71
+ `Channels.layerMemory` is process-local throughout and exists for adapter unit
72
+ tests.
73
+
74
+ `WebhookChannel.handler` reads an abstract Effect HTTP request and dispatches
75
+ it, so any Effect HTTP host can mount it:
76
+
77
+ ```ts
78
+ HttpRouter.post("/webhooks/github", WebhookChannel.handler("github", deliveryId))
79
+ ```
80
+
81
+ Take `deliveryId` from the platform's own delivery header. That is what makes a
82
+ redelivery the same mutation instead of a second one.
83
+
84
+ ## The order verification happens in
85
+
86
+ `ingest` runs one request at a time, and in this order:
87
+
88
+ 1. Copy the request: the bytes, and only the enumerable own string headers,
89
+ lower-cased, with a duplicate case-insensitive name refused.
90
+ 2. Compute the body fingerprint.
91
+ 3. **Verify**, with the verifier's own copy of the body. Signature verification
92
+ is the amplification guard, so it happens before decode and before any
93
+ `Control` access. The verifier receiving its own copy means even a verifier
94
+ that edits bytes cannot change what the decoder sees after approval.
95
+ 4. Look the durable idempotency record up. A match replays the stored receipt.
96
+ 5. Decode, map, and dispatch through `Control`.
97
+ 6. Record the receipt, unless it was a `Conflict` or `Parked`. A parked start
98
+ leaves the ingress key unsettled: approve its stored plan, then redeliver
99
+ with the same delivery id to retry the launch. Once accepted, later
100
+ redeliveries return `AlreadyApplied` for the same run.
101
+
102
+ The receipt handed back carries the platform's own delivery id as its
103
+ `receiptId`, so a caller correlating against its own logs sees the id it sent.
104
+
105
+ ## What enters durable identity
106
+
107
+ The fingerprint is the SHA-256 of the body plus only the header names the
108
+ adapter declared in `fingerprintHeaders`, matched case-insensitively and sorted.
109
+
110
+ Declare a header there only when its value changes the decoded command, as
111
+ `x-github-event` does. Signature, authorization, cookie, token, and credential
112
+ headers must not be declared: rotating an excluded credential header leaves the
113
+ delivery identical, which is what you want.
114
+
115
+ Reusing one delivery id with different declared semantics answers `Conflict`.
116
+
117
+ ## The body ceiling
118
+
119
+ A webhook is the one control-plane ingress a caller reaches with an arbitrary
120
+ payload, so `handler` bounds the body twice:
121
+
122
+ - A `content-length` over the limit is refused before the body is read at all,
123
+ so a declared flood costs nothing.
124
+ - Each streamed chunk is measured before it is retained. Reading stops and the
125
+ stream is cancelled at the first chunk exceeding the limit, even if the
126
+ caller understates or omits the length. Verification has not run at this point.
127
+
128
+ Both refusals are `InvalidInput` naming the two byte counts and no body
129
+ content. The default is `WebhookChannel.maximumBodyBytes`, 1 MiB, and one mount
130
+ lowers it:
131
+
132
+ ```ts
133
+ WebhookChannel.handler("github", deliveryId, { maximumBodyBytes: 256 * 1024 })
134
+ ```
135
+
136
+ The default is deliberately smaller than the 4 MiB mutation identity budget: a
137
+ body that cannot become a durable mutation is refused at the door rather than
138
+ copied, decoded, and refused later.
139
+
140
+ Malformed JSON returns `InvalidInput` with the fixed issue `invalid webhook
141
+ JSON`. Parser messages and payload fragments are excluded from the error.
142
+
143
+ ## Project a run back out
144
+
145
+ `project` is side-effect free. It turns a `RunSummary` and the previous
146
+ delivery record into a `DeliveryProjection`, and the transport adapter performs
147
+ the network call after the projection is journaled:
148
+
149
+ | `operation` | Meaning |
150
+ | ----------- | ------------------------------------- |
151
+ | `post` | Send a new message. |
152
+ | `edit` | Update the message `messageId` names. |
153
+ | `noop` | Nothing changed worth sending. |
154
+
155
+ The coordinator keeps delivery identities for live runs, including parked runs
156
+ and runs waiting for approval, so later projections can edit the same message.
157
+ Completed, failed, and cancelled runs share a FIFO window of 1,024 delivery
158
+ records across channels. Repeated terminal projections do not extend that
159
+ window. A run projected as live again leaves the terminal window.
160
+
161
+ A `noop` does not create or replace a delivery record. Unchanged cursor and
162
+ message identities reuse the previous record. Terminal status still moves an
163
+ existing record into the retention window even when the projection is a noop.
164
+
165
+ Outbound records are process-local. After terminal eviction or coordinator
166
+ restart, the adapter receives no previous delivery and may post a new message.
167
+ Hosts needing edits beyond this window must keep remote message identities in
168
+ their own durable transport storage.
169
+
170
+ ## Where to go next
171
+
172
+ - [Store and resolve a credential](./store-credentials.md): where the verifier's
173
+ secret comes from.
174
+ - [Receipts and idempotency](../concepts/receipts.md): the store behind a
175
+ replayed redelivery.
176
+ - [Gate work behind an approval](./approvals.md): an ingested `Start` still
177
+ parks until somebody approves it.