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

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 +1293 -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 +744 -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 +1522 -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 +1589 -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 +808 -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 +7 -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 +2127 -0
  385. package/src/ControlRpcs.ts +443 -0
  386. package/src/ControlRuntime.ts +1601 -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 +2478 -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,220 @@
1
+ ---
2
+ title: "Serve the control plane over RPC"
3
+ description: "Mount the same Control service as HTTP and WebSocket RPC, project it back into the Control interface on a client, authenticate with a bearer token, and read the transport failures a client can retry."
4
+ sidebar:
5
+ order: 9
6
+ ---
7
+
8
+ `ControlServer.layerHttp` mounts the `Control` service as RPC;
9
+ `ControlClient.layer` projects the RPC client back into the same interface.
10
+ Remote operations also require transport configuration and authentication.
11
+
12
+ ## Mount the server
13
+
14
+ ```ts
15
+ import { NodeHttpServer } from "@effect/platform-node"
16
+ import * as ControlRpcs from "@smthrs/control/ControlRpcs"
17
+ import * as ControlServer from "@smthrs/control/ControlServer"
18
+ import * as Layer from "effect/Layer"
19
+ import { HttpRouter } from "effect/unstable/http"
20
+ import { RpcSerialization } from "effect/unstable/rpc"
21
+ import { createServer } from "node:http"
22
+
23
+ const served = HttpRouter.serve(
24
+ ControlServer.layerHttp.pipe(
25
+ Layer.provide(ControlRpcs.layerBearerAuth({
26
+ token: process.env["SMITHERS_CONTROL_TOKEN"] ?? "",
27
+ principal: { id: "operator", kind: "bearer" }
28
+ })),
29
+ Layer.provide(RpcSerialization.layerNdjson)
30
+ )
31
+ ).pipe(
32
+ Layer.provideMerge(NodeHttpServer.layer(createServer, { host: "127.0.0.1", port: 0 }))
33
+ )
34
+ ```
35
+
36
+ `layerHttp` mounts both protocols together: unary procedures over
37
+ `POST /rpc`, and the `watch` stream over `WebSocket /rpc/ws`. The operations
38
+ divide cleanly. Plan, approve, run, and list are requests with answers; `watch`
39
+ is a projection that keeps arriving, so it rides a socket.
40
+
41
+ `ControlServer.layer` is the handler layer alone, for a host that mounts its
42
+ own protocols.
43
+
44
+ ## Connect a client
45
+
46
+ `credential` authenticates HTTP requests only. Authenticated `watch` also
47
+ requires an `Authorization` header on the WebSocket upgrade. An ordinary
48
+ `NodeSocket.layerWebSocket` does not send it and watch fails with
49
+ `Unauthorized`. This Node composition uses the `ws` constructor exported by
50
+ `NodeSocket` to supply the upgrade header:
51
+
52
+ ```ts
53
+ import { NodeHttpClient, NodeSocket } from "@effect/platform-node"
54
+ import * as ControlClient from "@smthrs/control/ControlClient"
55
+ import * as Layer from "effect/Layer"
56
+ import { RpcSerialization } from "effect/unstable/rpc"
57
+ import { Socket } from "effect/unstable/socket"
58
+
59
+ const credential = process.env["SMITHERS_CONTROL_TOKEN"] ?? ""
60
+ const authenticatedSocket = Socket.layerWebSocket("ws://127.0.0.1:4000/rpc/ws").pipe(
61
+ Layer.provide(
62
+ Layer.succeed(Socket.WebSocketConstructor)((url, options) => {
63
+ const configured = options !== undefined && typeof options !== "string" && !Array.isArray(options)
64
+ ? options
65
+ : undefined
66
+ const protocols = configured === undefined ? options as string | Array<string> | undefined : undefined
67
+ const socket = new NodeSocket.NodeWS.WebSocket(url, protocols, {
68
+ ...configured,
69
+ headers: { ...configured?.headers, Authorization: `Bearer ${credential}` }
70
+ })
71
+ // Effect removes reader listeners before closing. `ws` can report a
72
+ // late handshake error during that close; keep Node from treating it as
73
+ // an unhandled event. Active Effect listeners still receive all errors.
74
+ socket.on("error", () => {})
75
+ return socket
76
+ })
77
+ )
78
+ )
79
+
80
+ const client = ControlClient.layer({
81
+ url: "http://127.0.0.1:4000/rpc",
82
+ credential
83
+ }).pipe(
84
+ Layer.provide([
85
+ NodeHttpClient.layerUndici,
86
+ authenticatedSocket,
87
+ RpcSerialization.layerNdjson
88
+ ])
89
+ )
90
+ ```
91
+
92
+ With both HTTP and the socket upgrade authenticated, programs written against
93
+ `Control` can use this client layer. Handle `TransportError` and `Unauthorized`
94
+ in addition to each operation's domain failures; both are declared on every
95
+ `Control.Service` method.
96
+
97
+ Browser WebSockets cannot set upgrade headers. Browser deployments need a
98
+ trusted proxy that authenticates the caller and supplies the header. Tokens
99
+ in URL query strings are not supported.
100
+
101
+ `layerHttp` refuses, with 403 and before authentication, any request whose
102
+ `Origin` header does not match its `Host` header, on POST `/rpc` and on the
103
+ `/rpc/ws` upgrade. A browser always sends `Origin`, so a page on another site
104
+ cannot use a credential the proxy attaches for its victim. Non-browser
105
+ clients send no `Origin` and are unaffected. Serve the browser app from the
106
+ same origin as the control mount, and keep the proxy's `Host` header intact.
107
+
108
+ ## Authenticate
109
+
110
+ `ControlRpcs.ControlAuth` is the middleware boundary, and it provides
111
+ `ControlPrincipal` to every handler.
112
+
113
+ | Layer | Use |
114
+ | --------------------------------------- | ------------------------------------------------------------------------ |
115
+ | `layerBearerAuth({ token, principal })` | One shared token. Every request carrying it receives the same principal. |
116
+ | `layerAuth(authenticator)` | Your own header authenticator, returning a principal or `Unauthorized`. |
117
+ | `layerNoopAuth(principal?)` | Trusted in-process use and tests. Authenticates nothing. |
118
+
119
+ The bearer comparison is constant time, and a missing, malformed, empty, or
120
+ incorrect credential all fail closed with the same `Unauthorized` response.
121
+
122
+ ## Who reads which runs
123
+
124
+ Every run the control plane launches records the principal that launched it as
125
+ `RunSummary.launchedBy`. `List` and `Watch` answer an operator with every run
126
+ and every other principal with only the runs it launched: its own run
127
+ summaries, the fires that started them, and their events. `Steer`, `Signal`,
128
+ `Cancel` and `Resume` accept the same runs. Triggers, a plan's events and a run
129
+ the engine created (a child, a fork, a later round) reach operators only.
130
+ Another principal's run answers `RunNotFound`, exactly as a missing one does.
131
+
132
+ The authentication layer names the operators, because it knows which
133
+ identities it stamps. `layerBearerAuth` and `layerNoopAuth` stamp one principal
134
+ and make it the operator. `layerAuth` takes the rule as `seesAllRuns`, and
135
+ without it no principal is an operator:
136
+
137
+ ```ts
138
+ const auth = ControlRpcs.layerAuth(authenticator, {
139
+ seesAllRuns: (principal) => principal.kind === "operator"
140
+ })
141
+ ```
142
+
143
+ An authenticator also receives the call it is guarding, `{ rpc, payload }`,
144
+ on every in-band frame and nothing at a transport edge; `anyAuthenticator`
145
+ asks several in turn. `ScopedToken.authenticator` uses the call to confine a
146
+ minted token to the procedures, run, or flow it names, and composes with the
147
+ bearer:
148
+
149
+ ```ts
150
+ import * as ControlRpcs from "@smthrs/control/ControlRpcs"
151
+ import * as ScopedToken from "@smthrs/control/ScopedToken"
152
+
153
+ const auth = ControlRpcs.layerAuth(ControlRpcs.anyAuthenticator([
154
+ ControlRpcs.bearerAuthenticator({ token, principal: { id: "gateway", kind: "bearer" } }),
155
+ ScopedToken.authenticator({ key: token, principal: { id: "gateway", kind: "scoped" } })
156
+ ]))
157
+ ```
158
+
159
+ A custom authenticator is an object with one method:
160
+
161
+ ```ts
162
+ import { Unauthorized } from "@smthrs/control/ControlError"
163
+ import type * as ControlRpcs from "@smthrs/control/ControlRpcs"
164
+ import * as Effect from "effect/Effect"
165
+
166
+ const authenticator: ControlRpcs.Authenticator = {
167
+ authenticate: (headers) =>
168
+ lookupSession(headers["x-session"]).pipe(
169
+ Effect.map((session) => ({ id: session.userId, kind: "user", stampedAt: Date.now() })),
170
+ Effect.mapError(() => new Unauthorized({ message: "A valid session is required" }))
171
+ )
172
+ }
173
+ ```
174
+
175
+ ## The server stamps identity, always
176
+
177
+ Every mutation that records who asked reads `ControlPrincipal` and stamps it,
178
+ rather than forwarding whatever the client sent. The identity the middleware
179
+ authenticated is the only one the server can stand behind, and it is what
180
+ reaches the journal, `RunSummary.cancellation`, and a steer's notification
181
+ provenance.
182
+
183
+ `Steer` is the one payload that carries a principal on the wire, because an
184
+ in-process caller names one that is not an operator. Over RPC the
185
+ authenticated identity replaces whatever arrived. See
186
+ [attribution over a wire](./steer-a-run.md).
187
+
188
+ ## Failures a client sees
189
+
190
+ `ControlClient` normalizes everything into `ControlError`. A declared control
191
+ failure crosses the wire as itself; anything else becomes `TransportError`,
192
+ whose `retryable` flag classifies only the transport phase:
193
+
194
+ | Cause | `retryable` |
195
+ | ------------------------------------------------------ | ----------- |
196
+ | Connection, socket open, read, write, or close failure | `true` |
197
+ | HTTP 5xx | `true` |
198
+ | HTTP 4xx | `false` |
199
+ | Request encode failure | `false` |
200
+ | Response decode failure | `false` |
201
+ | Invalid client URL | `false` |
202
+
203
+ Resend a retryable mutation only when its idempotency key makes replay safe. A
204
+ keyless request can have reached the server even when its response was lost.
205
+
206
+ `ControlClient.isControlError` is `Schema.is` of the same union the errors are
207
+ declared in, so an error class added to the package reaches the refinement
208
+ without anyone updating a second list.
209
+
210
+ An operator's own interruption is re-raised exactly as it arrived rather than
211
+ described as a transport failure. Cancelling a request is not the server
212
+ failing.
213
+
214
+ ## Where to go next
215
+
216
+ - [The complete loopback example](https://github.com/smithersai/smithers/blob/main/examples/src/24-control-plane-and-gateway.ts):
217
+ a discovered flow planned, approved, launched, and watched over the wire.
218
+ - [Watch a run's events](./watch-a-run.md): the operation the WebSocket exists
219
+ for.
220
+ - [`smthrs serve`](/cli/serve): the shipped server over this layer.
@@ -0,0 +1,53 @@
1
+ # Signal a run
2
+
3
+ `Control.signal` admits a named JSON payload under an actor-scoped idempotency
4
+ key. `Accepted` confirms that admission is durable. It does not claim that the
5
+ run has consumed the payload. Reusing the key with different input returns
6
+ `Conflict` before the conflicting payload reaches an executor.
7
+
8
+ The command, its receipt, and `control.signal.admitted` journal event commit
9
+ in the control database first. Execution occurs after that transaction ends;
10
+ there is no transaction spanning control.db and engine.db.
11
+
12
+ The production executor binds each command to one concrete durable wait token
13
+ before applying it. One token has at most one admitted command. Application
14
+ atomically checks the engine's current wait token, and recovery verifies the
15
+ stored deferred result. A crash after application but before acknowledgment
16
+ retries the original token. It cannot move the command to a later wait.
17
+
18
+ Commands without a visible wait remain pending. The running executor
19
+ reconciles a bounded page every 250 milliseconds, starting at host startup,
20
+ so a signal admitted before its wait opens does not require a restart or
21
+ manual resend. Pages rotate so unavailable waits cannot starve later commands.
22
+ Malformed stored payloads are rejected and logged rather than blocking a page.
23
+
24
+ `ControlRuntime.signalCommand(commandId)` exposes `pending`, `delivered`,
25
+ `rejected`, and `terminal` dispositions for integrations holding the admitted
26
+ command identity. The CLI does not yet expose a dedicated delivery-status
27
+ lookup. A definite incompatible wait raises `NoMatchingWait`; retries preserve
28
+ that refusal. A signal initially submitted to a settled run returns `Terminal`.
29
+
30
+ A human wait, one parked with reason `approval` such as a `HumanTask`
31
+ question, is an approval gate. `Control.signal` stamps the caller's principal
32
+ on the admitted command. Before the executor completes a human wait, it asks
33
+ `ApprovalAuthority` whether that principal may approve the wait's `Node`
34
+ target, with the wait name as `requestId`, the wait token as `digest`, and
35
+ scope `once`. A refused principal fails `Unauthorized`, the command is
36
+ rejected, and the wait stays open. A replay after restart is judged by the
37
+ principal recorded at admission; a command with no recorded principal cannot
38
+ answer a human wait. Plain `WaitFor` events need no approval.
39
+
40
+ A signal from a webhook channel carries the principal
41
+ `{ id: <channel name>, kind: "channel" }`. It can answer a human wait only if
42
+ the host delegates approval to that principal.
43
+
44
+ `WaitFor` names one durable fact per `(flowName, executionId, name)`. Calling
45
+ it twice with the same name in the same execution intentionally observes the
46
+ same fact. Use distinct names for distinct rendezvous points. This is not a
47
+ stream of repeated same-name events.
48
+
49
+ Legacy `control_run_messages` signals have no application identity or token
50
+ binding. They remain readable through `deliveredSignals`, but are not
51
+ replayed automatically by the new inbox. Operators must inspect legacy wait
52
+ state before explicitly resubmitting under a new key; silently replaying them
53
+ could apply historical intent to a different wait.
@@ -0,0 +1,138 @@
1
+ ---
2
+ title: "Steer a running agent"
3
+ description: "Send a message, a seat change, a thinking level, or a tool set to a run's next turn boundary: the four variants, which parks a steer wakes, and the two durable moments a steer has."
4
+ sidebar:
5
+ order: 4
6
+ ---
7
+
8
+ `steer` writes one durable item into the notification queue and journals the
9
+ enqueue beside it. The run picks it up at its next turn boundary.
10
+
11
+ ```ts
12
+ import { Control } from "@smthrs/control/Control"
13
+ import * as Effect from "effect/Effect"
14
+
15
+ const steer = Effect.gen(function*() {
16
+ const control = yield* Control
17
+ return yield* control.steer({
18
+ runId: "run-17",
19
+ message: {
20
+ messageId: "steer-1",
21
+ runId: "run-17",
22
+ principal: { id: "ada", kind: "user", stampedAt: Date.now() },
23
+ createdAt: Date.now(),
24
+ body: "prefer the smaller diff"
25
+ },
26
+ idempotencyKey: "steer:run-17:1"
27
+ })
28
+ })
29
+ ```
30
+
31
+ ## The four variants
32
+
33
+ An operator steers a run for four different reasons, and only one of them is
34
+ something to tell the model. Saying "your seat changed" would spend a turn on
35
+ bookkeeping; changing the seat is what was asked for.
36
+
37
+ | Variant | Field | What the next turn does |
38
+ | ----------------------------------------------------------- | ----------- | ------------------------------------- |
39
+ | `Message` (the default, and what a `body` alone decodes as) | `body` | Inserts the body into the transcript. |
40
+ | `Seat` | `seat` | Runs the turn on that model seat. |
41
+ | `Thinking` | `thinking` | Runs the turn at that thinking level. |
42
+ | `Tools` | `toolNames` | Adds those tools to the active set. |
43
+
44
+ `kind` is optional on `Message` and required on the other three, which is what
45
+ keeps a steer written before the vocabulary widened readable: a body and no
46
+ kind is a message, and always was.
47
+
48
+ ```ts
49
+ import { steerItem } from "@smthrs/control/ControlSchema"
50
+
51
+ steerItem({ ...envelope, body: "prefer the smaller diff" })
52
+ // { kind: "Message", body: "prefer the smaller diff" }
53
+ steerItem({ ...envelope, kind: "Seat", seat: "anthropic:claude-sonnet-4-5" })
54
+ // { kind: "Seat", seat: "anthropic:claude-sonnet-4-5" }
55
+ ```
56
+
57
+ `ControlSchema.steerItem` strips the control envelope, which is who asked,
58
+ when, and for which run, and returns the
59
+ [`@smthrs/notifications`](/api/notifications) payload the harness reads back.
60
+ The harness maps each payload onto its matching steering item, so a seat steer
61
+ changes the seat instead of spending a turn announcing it.
62
+
63
+ ## The two durable moments
64
+
65
+ | Event | Writer | Payload |
66
+ | ------------------------- | ---------------------------------------------------------------------------------- | --------------------------------------- |
67
+ | `control.steer.enqueued` | `Control.steer` | `{ runId, messageId, kind, createdAt }` |
68
+ | `control.steer.delivered` | derived by `Steering.derive` from the queue's `flows/notifications/Promoted` entry | `{ runId, messageId, boundary }` |
69
+
70
+ Delivery is derived rather than recorded, because the boundary that delivered
71
+ the steer runs in the agent process and not in the control plane. A control
72
+ plane that wrote its own delivery record would be asserting a fact it did not
73
+ observe.
74
+
75
+ One promotion entry names a batch, so it derives one delta per message id, each
76
+ carrying the sequence of the entry it came from. Checkpoint `event.cursor`
77
+ and resume with `afterCursor` to continue even between deliveries in a batch.
78
+
79
+ `RunSummary.steering.pending` counts what has been admitted and not yet
80
+ promoted. It comes from the queue rather than a column, because the queue owns
81
+ both halves.
82
+
83
+ ## Waking a parked run
84
+
85
+ A steer resumes a parked run when the park is one a message can end:
86
+
87
+ | `waitingReason` | Steered |
88
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
89
+ | `event` | Resumed. The run is waiting for something to arrive, and a steer is something arriving. |
90
+ | `released` | Resumed. A sweep took the run away from a dead owner, and nothing is coming to claim it. |
91
+ | `approval`, `timer`, `quota` | Left parked. The run is waiting for a decision, a clock, or a budget that a message does not supply. |
92
+ | absent | Left parked. A park with no reason is an operator's own park, and a message queued behind it is queued for when they resume it. |
93
+
94
+ A reason this table does not name is left parked too: a control plane that
95
+ cannot explain a park should not end it. The wake claims with
96
+ `scope: "launched"`, so a run another driver created keeps its park and that
97
+ driver delivers the steer at the run's next boundary. The steer is already
98
+ durable either way.
99
+
100
+ A successful wake journals `control.steer.woke` with the run's new status.
101
+
102
+ ## Refusals
103
+
104
+ A steer whose `message.runId` names a different run than the call does is
105
+ refused before anything is admitted:
106
+
107
+ ```text
108
+ InvalidInput: message.runId: must be "run-17", received "run-18"
109
+ ```
110
+
111
+ The notification would be admitted to the call's run while the stored message
112
+ claimed another, so an operator reading it later would be told it belongs
113
+ somewhere it was never delivered.
114
+
115
+ A steer to a run that already reached `cancelled`, `completed`, or `failed`
116
+ answers `Terminal` and stores nothing. Storing it anyway would leave an
117
+ operator watching a message with no boundary left to deliver it.
118
+
119
+ ## Attribution over a wire
120
+
121
+ `SteerMessage` carries a `principal` even though `cancel` refuses one on the
122
+ wire, and the difference is who the callers are. A cancel is only ever an
123
+ operator command, so the server can be its sole source of identity. A steer is
124
+ not: `agent/send` steers a child run and attributes the message to the parent
125
+ flow, which is an identity no authenticator knows and no operator issued.
126
+
127
+ So the field stays, and `ControlServer` overwrites it with the authenticated
128
+ principal on every steer that arrives over RPC. An in-process caller keeps
129
+ naming its own. The value reaches the notification's `sourceActor` and the run
130
+ transcript, which is exactly where a spoofed name would be read as truth.
131
+
132
+ ## Where to go next
133
+
134
+ - [Watch a run's events](./watch-a-run.md): where both moments show up.
135
+ - [Deliver a signal to a waiting run](./signal-a-run.md): the other way to
136
+ reach a parked run, and why it is not the same thing.
137
+ - [`smthrs runs steer`](/cli/runs) and
138
+ [steering on smithers.sh](/docs/guides/steering/): the operator surface.
@@ -0,0 +1,164 @@
1
+ ---
2
+ title: "Store and resolve a credential"
3
+ description: "Keep a connection secret out of flow input, plan digests, journals, and model context: the reference that crosses the boundary, the store and cipher ports behind it, and the compare-and-set that serializes a rotation."
4
+ sidebar:
5
+ order: 12
6
+ ---
7
+
8
+ Credentials are capabilities. They must never enter flow input, plan digests,
9
+ journal payloads, or model context, so only a `CredentialRef` crosses the
10
+ browser-safe contract:
11
+
12
+ ```ts
13
+ interface CredentialRef {
14
+ readonly id: string
15
+ readonly name: string
16
+ }
17
+ ```
18
+
19
+ Plaintext exists in exactly two places: inside a `Redacted` handed to `create`
20
+ or `rotate`, and inside the `Redacted` returned by `resolve`.
21
+
22
+ ## Compose the boundary
23
+
24
+ `Credential` composes two ports, and a host chooses an adapter for each:
25
+
26
+ ```ts
27
+ import * as Credential from "@smthrs/control/Credential"
28
+ import * as CredentialStore from "@smthrs/control/CredentialStore"
29
+ import * as WebCryptoCipher from "@smthrs/control/WebCryptoCipher"
30
+ import * as Layer from "effect/Layer"
31
+ import * as Redacted from "effect/Redacted"
32
+
33
+ const credentials = Credential.layer().pipe(
34
+ Layer.provide(Layer.merge(
35
+ CredentialStore.layerMemory,
36
+ WebCryptoCipher.layer({ key: Redacted.make(process.env["SMITHERS_CREDENTIAL_KEY"]!) })
37
+ ))
38
+ )
39
+ ```
40
+
41
+ | Port | What it does | Adapters |
42
+ | ------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ |
43
+ | `CredentialStore` | Persists an opaque sealed record, with compare-and-set on writes. | `layerMemory`, `SqlCredentialStore.layer`, `layerNoop` |
44
+ | `CredentialCipher` | Seals and opens the secret under host-managed keys. | `WebCryptoCipher.layer`, `layerNoop` |
45
+
46
+ `Credential.layerNoop` is the whole boundary reporting `Unavailable`, which is
47
+ the honest composition for a host with no credential storage.
48
+
49
+ ## Use it
50
+
51
+ ```ts
52
+ const program = Effect.gen(function*() {
53
+ const credentials = yield* Credential.Credential
54
+
55
+ const reference = yield* credentials.create({
56
+ id: "github-webhook",
57
+ name: "GitHub webhook",
58
+ secret: Redacted.make(incomingSecret)
59
+ })
60
+
61
+ // Only this call sees plaintext again.
62
+ const secret = yield* credentials.resolve(reference)
63
+
64
+ const rotated = yield* credentials.rotate(reference, Redacted.make(nextSecret))
65
+ yield* credentials.revoke(rotated)
66
+ })
67
+ ```
68
+
69
+ `list` and `get` answer references. `resolve` is the one operation that crosses
70
+ into a secret-bearing adapter boundary.
71
+
72
+ ## Authorization is the host's
73
+
74
+ `Credential.layer({ authorize })` injects a policy hook, called with the
75
+ operation and the reference before anything else runs:
76
+
77
+ ```ts
78
+ Credential.layer({
79
+ authorize: (operation, reference) =>
80
+ operation === "resolve" && !Option.exists(reference, allowed)
81
+ ? Effect.fail(new Unauthorized({ message: "Not available to this caller" }))
82
+ : Effect.void
83
+ })
84
+ ```
85
+
86
+ The default allows every operation, which is correct for a single-principal
87
+ local process. The six operations are `list`, `get`, `create`, `resolve`,
88
+ `rotate`, and `revoke`.
89
+
90
+ A reference is also _authenticated_ on every operation: caller-owned fields are
91
+ snapshotted before policy effects run, and a forged or stale `CredentialRef` is
92
+ refused because the snapshotted name must still match the stored record. The
93
+ refusal for a missing credential and the refusal for a denied one are
94
+ deliberately indistinguishable, because telling an unauthorized caller which
95
+ ids exist is itself a leak.
96
+
97
+ ## What is stored, and what is not
98
+
99
+ `SealedRecord` is everything at rest:
100
+
101
+ | Field | Meaning |
102
+ | ------------- | ------------------------------------------------------------ |
103
+ | `id`, `name` | Opaque metadata, and the cipher's authenticated data. |
104
+ | `ciphertext` | Base64, produced by the cipher. |
105
+ | `nonce` | Base64, per record, never reused across versions. |
106
+ | `version` | Monotonic write counter, 1 for a freshly created credential. |
107
+ | `updatedAtMs` | When the record was last written. |
108
+
109
+ The key never reaches the store, so a stolen store is ciphertext and nothing
110
+ else. `WebCryptoCipher` holds it as a non-extractable `CryptoKey`, so it cannot
111
+ be read back out of the cipher either.
112
+
113
+ The id, name, and version are written beside the blob _and_ authenticated with
114
+ it, so moving a blob to another id, name, or version makes it unreadable.
115
+
116
+ ## Rotation is serialized
117
+
118
+ Writes are compare-and-set on `version`. A writer that read version _n_ commits
119
+ version _n + 1_; a concurrent writer that read the same _n_ is refused with
120
+ `CredentialConflict` carrying both versions, rather than silently overwriting
121
+ the winner.
122
+
123
+ `SqlCredentialStore` does the read and the write in one transaction, so the
124
+ version a writer read and the row it guards cannot interleave.
125
+
126
+ ## The key
127
+
128
+ `WebCryptoCipher` uses AES-256-GCM over the Web Crypto API, which is the
129
+ browser's own and has been Node's since v19, so this adapter imports nothing
130
+ from `node:*` and runs unmodified on a server. `Options.key` is 32 raw bytes,
131
+ base64-encoded, held redacted so it cannot be printed or serialized by
132
+ accident.
133
+
134
+ A host without Web Crypto, an old runtime or a locked-down worker, fails with
135
+ the typed `Unavailable` rather than a defect. A key that is not 32
136
+ base64-encoded bytes fails with `InvalidInput`.
137
+
138
+ A record that does not open fails with `PersistenceError` on operation
139
+ `credential.open`, and the host log records why. The message names a malformed
140
+ stored nonce, or a failed authentication: the key is not the one that sealed
141
+ the record, the record's id, name or version changed, or the ciphertext was
142
+ tampered with.
143
+
144
+ ## Durable storage
145
+
146
+ ```ts
147
+ import * as SqlCredentialStore from "@smthrs/control/SqlCredentialStore"
148
+
149
+ const store = SqlCredentialStore.layer
150
+ ```
151
+
152
+ It requires `DurableWriter` and `SqlClient`, and creates `control_credentials`
153
+ on construction. The table is part of the package's
154
+ [migration set](./durable-storage.md), so a host that composes that set has it
155
+ already.
156
+
157
+ ## Where to go next
158
+
159
+ - [Accept a webhook](./ingest-a-webhook.md): the caller that carries a
160
+ `CredentialRef` into a signature verifier.
161
+ - [Store control state in a database](./durable-storage.md): the migration set
162
+ the credential table belongs to.
163
+ - [Troubleshooting](../troubleshooting.md): what `Unavailable` and
164
+ `CredentialConflict` mean in practice.