@smthrs/triggers 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 (226) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/LICENSE +21 -0
  3. package/README.md +109 -2
  4. package/dist/cjs/CatchUp.d.ts +25 -0
  5. package/dist/cjs/CatchUp.d.ts.map +1 -0
  6. package/dist/cjs/CatchUp.js +72 -0
  7. package/dist/cjs/CatchUp.js.map +7 -0
  8. package/dist/cjs/Channel.d.ts +108 -0
  9. package/dist/cjs/Channel.d.ts.map +1 -0
  10. package/dist/cjs/Channel.js +25 -0
  11. package/dist/cjs/Channel.js.map +7 -0
  12. package/dist/cjs/ClaimDecision.d.ts +183 -0
  13. package/dist/cjs/ClaimDecision.d.ts.map +1 -0
  14. package/dist/cjs/ClaimDecision.js +144 -0
  15. package/dist/cjs/ClaimDecision.js.map +7 -0
  16. package/dist/cjs/Cron.d.ts +91 -0
  17. package/dist/cjs/Cron.d.ts.map +1 -0
  18. package/dist/cjs/Cron.js +108 -0
  19. package/dist/cjs/Cron.js.map +7 -0
  20. package/dist/cjs/DispatchReader.d.ts +77 -0
  21. package/dist/cjs/DispatchReader.d.ts.map +1 -0
  22. package/dist/cjs/DispatchReader.js +121 -0
  23. package/dist/cjs/DispatchReader.js.map +7 -0
  24. package/dist/cjs/Overlap.d.ts +40 -0
  25. package/dist/cjs/Overlap.d.ts.map +1 -0
  26. package/dist/cjs/Overlap.js +37 -0
  27. package/dist/cjs/Overlap.js.map +7 -0
  28. package/dist/cjs/Schedule.d.ts +96 -0
  29. package/dist/cjs/Schedule.d.ts.map +1 -0
  30. package/dist/cjs/Schedule.js +61 -0
  31. package/dist/cjs/Schedule.js.map +7 -0
  32. package/dist/cjs/Scheduler.d.ts +248 -0
  33. package/dist/cjs/Scheduler.d.ts.map +1 -0
  34. package/dist/cjs/Scheduler.js +647 -0
  35. package/dist/cjs/Scheduler.js.map +7 -0
  36. package/dist/cjs/SqlTriggerStore.d.ts +29 -0
  37. package/dist/cjs/SqlTriggerStore.d.ts.map +1 -0
  38. package/dist/cjs/SqlTriggerStore.js +483 -0
  39. package/dist/cjs/SqlTriggerStore.js.map +7 -0
  40. package/dist/cjs/Trigger.d.ts +81 -0
  41. package/dist/cjs/Trigger.d.ts.map +1 -0
  42. package/dist/cjs/Trigger.js +54 -0
  43. package/dist/cjs/Trigger.js.map +7 -0
  44. package/dist/cjs/TriggerError.d.ts +58 -0
  45. package/dist/cjs/TriggerError.d.ts.map +1 -0
  46. package/dist/cjs/TriggerError.js +83 -0
  47. package/dist/cjs/TriggerError.js.map +7 -0
  48. package/dist/cjs/TriggerStore.d.ts +411 -0
  49. package/dist/cjs/TriggerStore.d.ts.map +1 -0
  50. package/dist/cjs/TriggerStore.js +127 -0
  51. package/dist/cjs/TriggerStore.js.map +7 -0
  52. package/dist/cjs/Webhook.d.ts +118 -0
  53. package/dist/cjs/Webhook.d.ts.map +1 -0
  54. package/dist/cjs/Webhook.js +153 -0
  55. package/dist/cjs/Webhook.js.map +7 -0
  56. package/dist/cjs/index.d.ts +30 -0
  57. package/dist/cjs/index.d.ts.map +1 -0
  58. package/dist/cjs/index.js +57 -0
  59. package/dist/cjs/index.js.map +7 -0
  60. package/dist/cjs/internal/ActiveRuns.d.ts +51 -0
  61. package/dist/cjs/internal/ActiveRuns.d.ts.map +1 -0
  62. package/dist/cjs/internal/ActiveRuns.js +56 -0
  63. package/dist/cjs/internal/ActiveRuns.js.map +7 -0
  64. package/dist/cjs/internal/DueOccurrences.d.ts +32 -0
  65. package/dist/cjs/internal/DueOccurrences.d.ts.map +1 -0
  66. package/dist/cjs/internal/DueOccurrences.js +80 -0
  67. package/dist/cjs/internal/DueOccurrences.js.map +7 -0
  68. package/dist/cjs/internal/WallClock.d.ts +59 -0
  69. package/dist/cjs/internal/WallClock.d.ts.map +1 -0
  70. package/dist/cjs/internal/WallClock.js +132 -0
  71. package/dist/cjs/internal/WallClock.js.map +7 -0
  72. package/dist/cjs/migrations/0001_triggers.d.ts +15 -0
  73. package/dist/cjs/migrations/0001_triggers.d.ts.map +1 -0
  74. package/dist/cjs/migrations/0001_triggers.js +64 -0
  75. package/dist/cjs/migrations/0001_triggers.js.map +7 -0
  76. package/dist/cjs/migrations/0002_reservation_lease.d.ts +15 -0
  77. package/dist/cjs/migrations/0002_reservation_lease.d.ts.map +1 -0
  78. package/dist/cjs/migrations/0002_reservation_lease.js +41 -0
  79. package/dist/cjs/migrations/0002_reservation_lease.js.map +7 -0
  80. package/dist/cjs/migrations/0003_heartbeat.d.ts +16 -0
  81. package/dist/cjs/migrations/0003_heartbeat.d.ts.map +1 -0
  82. package/dist/cjs/migrations/0003_heartbeat.js +44 -0
  83. package/dist/cjs/migrations/0003_heartbeat.js.map +7 -0
  84. package/dist/cjs/migrations/0004_fire_run_index.d.ts +17 -0
  85. package/dist/cjs/migrations/0004_fire_run_index.d.ts.map +1 -0
  86. package/dist/cjs/migrations/0004_fire_run_index.js +40 -0
  87. package/dist/cjs/migrations/0004_fire_run_index.js.map +7 -0
  88. package/dist/cjs/migrations/index.d.ts +33 -0
  89. package/dist/cjs/migrations/index.d.ts.map +1 -0
  90. package/dist/cjs/migrations/index.js +50 -0
  91. package/dist/cjs/migrations/index.js.map +7 -0
  92. package/dist/cjs/package.json +1 -0
  93. package/dist/cjs/test/TestTriggers.d.ts +12 -0
  94. package/dist/cjs/test/TestTriggers.d.ts.map +1 -0
  95. package/dist/cjs/test/TestTriggers.js +421 -0
  96. package/dist/cjs/test/TestTriggers.js.map +7 -0
  97. package/dist/esm/CatchUp.d.ts +25 -0
  98. package/dist/esm/CatchUp.d.ts.map +1 -0
  99. package/dist/esm/CatchUp.js +52 -0
  100. package/dist/esm/CatchUp.js.map +1 -0
  101. package/dist/esm/Channel.d.ts +108 -0
  102. package/dist/esm/Channel.d.ts.map +1 -0
  103. package/dist/esm/Channel.js +19 -0
  104. package/dist/esm/Channel.js.map +1 -0
  105. package/dist/esm/ClaimDecision.d.ts +183 -0
  106. package/dist/esm/ClaimDecision.d.ts.map +1 -0
  107. package/dist/esm/ClaimDecision.js +172 -0
  108. package/dist/esm/ClaimDecision.js.map +1 -0
  109. package/dist/esm/Cron.d.ts +91 -0
  110. package/dist/esm/Cron.d.ts.map +1 -0
  111. package/dist/esm/Cron.js +132 -0
  112. package/dist/esm/Cron.js.map +1 -0
  113. package/dist/esm/DispatchReader.d.ts +77 -0
  114. package/dist/esm/DispatchReader.d.ts.map +1 -0
  115. package/dist/esm/DispatchReader.js +152 -0
  116. package/dist/esm/DispatchReader.js.map +1 -0
  117. package/dist/esm/Overlap.d.ts +40 -0
  118. package/dist/esm/Overlap.d.ts.map +1 -0
  119. package/dist/esm/Overlap.js +31 -0
  120. package/dist/esm/Overlap.js.map +1 -0
  121. package/dist/esm/Schedule.d.ts +96 -0
  122. package/dist/esm/Schedule.d.ts.map +1 -0
  123. package/dist/esm/Schedule.js +73 -0
  124. package/dist/esm/Schedule.js.map +1 -0
  125. package/dist/esm/Scheduler.d.ts +248 -0
  126. package/dist/esm/Scheduler.d.ts.map +1 -0
  127. package/dist/esm/Scheduler.js +734 -0
  128. package/dist/esm/Scheduler.js.map +1 -0
  129. package/dist/esm/SqlTriggerStore.d.ts +29 -0
  130. package/dist/esm/SqlTriggerStore.d.ts.map +1 -0
  131. package/dist/esm/SqlTriggerStore.js +483 -0
  132. package/dist/esm/SqlTriggerStore.js.map +1 -0
  133. package/dist/esm/Trigger.d.ts +81 -0
  134. package/dist/esm/Trigger.d.ts.map +1 -0
  135. package/dist/esm/Trigger.js +56 -0
  136. package/dist/esm/Trigger.js.map +1 -0
  137. package/dist/esm/TriggerError.d.ts +58 -0
  138. package/dist/esm/TriggerError.d.ts.map +1 -0
  139. package/dist/esm/TriggerError.js +110 -0
  140. package/dist/esm/TriggerError.js.map +1 -0
  141. package/dist/esm/TriggerStore.d.ts +411 -0
  142. package/dist/esm/TriggerStore.d.ts.map +1 -0
  143. package/dist/esm/TriggerStore.js +209 -0
  144. package/dist/esm/TriggerStore.js.map +1 -0
  145. package/dist/esm/Webhook.d.ts +118 -0
  146. package/dist/esm/Webhook.d.ts.map +1 -0
  147. package/dist/esm/Webhook.js +186 -0
  148. package/dist/esm/Webhook.js.map +1 -0
  149. package/dist/esm/index.d.ts +30 -0
  150. package/dist/esm/index.d.ts.map +1 -0
  151. package/dist/esm/index.js +30 -0
  152. package/dist/esm/index.js.map +1 -0
  153. package/dist/esm/internal/ActiveRuns.d.ts +51 -0
  154. package/dist/esm/internal/ActiveRuns.d.ts.map +1 -0
  155. package/dist/esm/internal/ActiveRuns.js +34 -0
  156. package/dist/esm/internal/ActiveRuns.js.map +1 -0
  157. package/dist/esm/internal/DueOccurrences.d.ts +32 -0
  158. package/dist/esm/internal/DueOccurrences.d.ts.map +1 -0
  159. package/dist/esm/internal/DueOccurrences.js +55 -0
  160. package/dist/esm/internal/DueOccurrences.js.map +1 -0
  161. package/dist/esm/internal/WallClock.d.ts +59 -0
  162. package/dist/esm/internal/WallClock.d.ts.map +1 -0
  163. package/dist/esm/internal/WallClock.js +155 -0
  164. package/dist/esm/internal/WallClock.js.map +1 -0
  165. package/dist/esm/migrations/0001_triggers.d.ts +15 -0
  166. package/dist/esm/migrations/0001_triggers.d.ts.map +1 -0
  167. package/dist/esm/migrations/0001_triggers.js +42 -0
  168. package/dist/esm/migrations/0001_triggers.js.map +1 -0
  169. package/dist/esm/migrations/0002_reservation_lease.d.ts +15 -0
  170. package/dist/esm/migrations/0002_reservation_lease.d.ts.map +1 -0
  171. package/dist/esm/migrations/0002_reservation_lease.js +19 -0
  172. package/dist/esm/migrations/0002_reservation_lease.js.map +1 -0
  173. package/dist/esm/migrations/0003_heartbeat.d.ts +16 -0
  174. package/dist/esm/migrations/0003_heartbeat.d.ts.map +1 -0
  175. package/dist/esm/migrations/0003_heartbeat.js +23 -0
  176. package/dist/esm/migrations/0003_heartbeat.js.map +1 -0
  177. package/dist/esm/migrations/0004_fire_run_index.d.ts +17 -0
  178. package/dist/esm/migrations/0004_fire_run_index.d.ts.map +1 -0
  179. package/dist/esm/migrations/0004_fire_run_index.js +20 -0
  180. package/dist/esm/migrations/0004_fire_run_index.js.map +1 -0
  181. package/dist/esm/migrations/index.d.ts +33 -0
  182. package/dist/esm/migrations/index.d.ts.map +1 -0
  183. package/dist/esm/migrations/index.js +37 -0
  184. package/dist/esm/migrations/index.js.map +1 -0
  185. package/dist/esm/test/TestTriggers.d.ts +12 -0
  186. package/dist/esm/test/TestTriggers.d.ts.map +1 -0
  187. package/dist/esm/test/TestTriggers.js +439 -0
  188. package/dist/esm/test/TestTriggers.js.map +1 -0
  189. package/docs/README.md +140 -0
  190. package/docs/api.md +1186 -0
  191. package/docs/concepts/channels.md +103 -0
  192. package/docs/concepts/claim-protocol.md +229 -0
  193. package/docs/concepts/policies.md +124 -0
  194. package/docs/concepts/schedules.md +134 -0
  195. package/docs/guides/choose-a-policy.md +113 -0
  196. package/docs/guides/declare-a-trigger.md +136 -0
  197. package/docs/guides/ingest-a-webhook.md +164 -0
  198. package/docs/guides/run-the-scheduler.md +173 -0
  199. package/docs/guides/testing.md +157 -0
  200. package/docs/installation.md +81 -0
  201. package/docs/quickstart.md +163 -0
  202. package/docs/troubleshooting.md +281 -0
  203. package/package.json +241 -3
  204. package/src/CatchUp.ts +73 -0
  205. package/src/Channel.ts +121 -0
  206. package/src/ClaimDecision.ts +303 -0
  207. package/src/Cron.ts +173 -0
  208. package/src/DispatchReader.ts +179 -0
  209. package/src/Overlap.ts +53 -0
  210. package/src/Schedule.ts +112 -0
  211. package/src/Scheduler.ts +1154 -0
  212. package/src/SqlTriggerStore.ts +638 -0
  213. package/src/Trigger.ts +84 -0
  214. package/src/TriggerError.ts +129 -0
  215. package/src/TriggerStore.ts +514 -0
  216. package/src/Webhook.ts +270 -0
  217. package/src/index.ts +30 -0
  218. package/src/internal/ActiveRuns.ts +83 -0
  219. package/src/internal/DueOccurrences.ts +101 -0
  220. package/src/internal/WallClock.ts +172 -0
  221. package/src/migrations/0001_triggers.ts +43 -0
  222. package/src/migrations/0002_reservation_lease.ts +20 -0
  223. package/src/migrations/0003_heartbeat.ts +24 -0
  224. package/src/migrations/0004_fire_run_index.ts +21 -0
  225. package/src/migrations/index.ts +40 -0
  226. package/src/test/TestTriggers.ts +563 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TriggerStore.js","sourceRoot":"","sources":["../../src/TriggerStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Durable trigger declaration and fire store.\n *\n * @see packages/smithers/agent/triggers/docs/api.md\n *\n * @since 0.1.0\n */\n\nimport * as Context from \"effect/Context\"\nimport * as Effect from \"effect/Effect\"\nimport * as Layer from \"effect/Layer\"\nimport type * as Option from \"effect/Option\"\n// `Result` already names the reported end of an occurrence in this module, so\n// the `effect/Result` a listed row decodes to is imported as `Decode`.\nimport * as Decode from \"effect/Result\"\nimport type { Action } from \"./Overlap.ts\"\nimport type { Trigger } from \"./Trigger.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * A stored trigger, with the revision that fences concurrent edits and\n * when it last fired.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Registered extends Trigger {\n readonly revision: number\n readonly lastFiredAt?: number | undefined\n}\n/**\n * One scheduled occurrence of a trigger, addressed by its occurrence\n * number so a retry cannot fire it twice.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Fire {\n readonly triggerId: string\n readonly occurrence: number\n}\n/**\n * A {@link Fire} together with the revision it was computed from and whether\n * it is resuming a buffered occurrence.\n *\n * The overlap policy is deliberately absent. A claim applies the policy stored\n * on the trigger row, read inside the same transaction, so a caller holding a\n * stale snapshot cannot fire a trigger that has since been disabled, cannot\n * point it at a different flow, and cannot supersede a run that the stored\n * declaration says to leave alone. `expectedRevision` is the fence: a claim\n * whose revision no longer matches the row is refused with `revision_mismatch`\n * so the caller re-reads before deciding again.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface ClaimFire extends Fire {\n readonly expectedRevision: number\n readonly resumeBuffered?: boolean | undefined\n}\n/**\n * The outcome of claiming an occurrence: either another worker holds it, or\n * this caller does and must take `action`.\n *\n * A claim that hands the caller work to launch always names the reservation it\n * wrote against the trigger row, so the caller has an id to release. A claim\n * that only records a decision has no reservation and names none: the two\n * shapes are separate so a caller cannot read a reservation id that was never\n * written.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Claim =\n | { readonly claimed: false }\n | { readonly claimed: true; readonly action: Extract<Action, \"skip\" | \"buffer\"> }\n | {\n readonly claimed: true\n readonly action: Extract<Action, \"fire\" | \"supersede\">\n readonly reservationId: string\n readonly activeRunId?: string | undefined\n }\n\n/**\n * Time after which an uncommitted launch reservation may be reclaimed.\n *\n * Both store implementations use this value so swapping the test store for\n * the SQL store cannot change recovery timing.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const reservationLeaseMs = 5 * 60 * 1000\n\n/**\n * The prefix marking an `active_run_id` that is a launch reservation rather\n * than a run the runtime knows about.\n *\n * A reservation is written by the claim and replaced by the real run id once\n * the launch reports one, so both stores and the scheduler read this prefix.\n * It is a contract between them and lives here rather than being re-spelled at\n * each site.\n *\n * @category constants\n * @since 0.1.0\n */\nexport const reservationPrefix = \"trigger-reservation:\"\n\n/**\n * The reservation id one occurrence of one trigger claims.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const reservationId = (triggerId: string, occurrence: number, attempt?: string): string =>\n `${reservationPrefix}${triggerId}:${attempt === undefined ? \"\" : `${attempt}:`}${occurrence}`\n\n/**\n * Whether a stored `active_run_id` is a launch reservation.\n *\n * @category predicates\n * @since 0.1.0\n */\nexport const isReservation = (runId: string | undefined): boolean =>\n runId !== undefined && runId.startsWith(reservationPrefix)\n\n/**\n * Reads the occurrence encoded in a launch reservation.\n *\n * @category getters\n * @since 0.1.0\n */\nexport const reservationOccurrence = (runId: string): number | undefined => {\n if (!isReservation(runId)) return undefined\n const occurrence = Number(runId.slice(runId.lastIndexOf(\":\") + 1))\n return Number.isFinite(occurrence) ? occurrence : undefined\n}\n\n/**\n * How a claimed occurrence ended.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Outcome = \"launched\" | \"completed\" | \"skipped\" | \"buffered\" | \"superseded\" | \"failed\"\n/**\n * The reported end of one occurrence, with the run it started when it\n * launched one.\n *\n * @category models\n * @since 0.1.0\n */\nexport type Result =\n & Fire\n & { readonly error?: string | undefined }\n & (\n | { readonly outcome: \"launched\"; readonly runId: string; readonly reservationId: string }\n | {\n readonly outcome: Exclude<Outcome, \"launched\">\n readonly runId?: string | undefined\n readonly reservationId?: string | undefined\n }\n )\n\n/**\n * Refuses results that no longer own a permissible fire transition.\n * Launched run ids are validated at runtime for adapters outside TypeScript.\n *\n * @category predicates\n * @since 1.0.0-rc.0\n */\nexport const resultRefusal = (\n result: Result,\n fire: FireRecord | undefined,\n activeRunId: string | undefined\n): TriggerError | undefined => {\n if (result.outcome === \"launched\" && (typeof result.runId !== \"string\" || result.runId.trim().length === 0)) {\n return new TriggerError({ code: \"invalid_options\", message: \"launched requires a non-empty runId\", path: \"runId\" })\n }\n const unfinished = fire?.outcome === null || fire?.outcome === \"buffered\"\n const reservation = activeRunId !== undefined && isReservation(activeRunId) &&\n reservationOccurrence(activeRunId) === result.occurrence\n const owner = result.reservationId ?? result.runId ?? (reservation ? activeRunId : undefined)\n const permitted = result.outcome === \"launched\"\n ? unfinished && reservation && result.reservationId === activeRunId\n : fire !== undefined && (\n // A repeated decision or settlement is harmless; callers do not rewrite it.\n (fire.outcome === result.outcome && (result.runId === undefined || result.runId === fire.runId)) ||\n (fire.outcome === \"launched\" && result.outcome !== \"skipped\" && result.outcome !== \"buffered\" &&\n result.reservationId === undefined && (result.runId === undefined || result.runId === fire.runId)) ||\n (unfinished && reservation && owner === activeRunId)\n )\n return permitted ? undefined : new TriggerError({\n code: \"stale_owner\",\n message: `result no longer owns trigger ${result.triggerId} occurrence ${result.occurrence}`\n })\n}\n\n/**\n * One row of the fire ledger: a claimed occurrence and what became of it.\n *\n * `outcome` is `null` between a claim and its `recordResult`, the window in\n * which a launch is reserved but not yet reported.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface FireRecord extends Fire {\n readonly outcome: Outcome | null\n readonly runId?: string | undefined\n readonly error?: string | undefined\n}\n\n/**\n * A ledger query. Every filter narrows; `cursor` is the last record of the\n * previous page, so the page after it holds only older records; `limit` caps\n * a page and must be a positive safe integer when stated. With no limit the\n * whole ledger answers in one page.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface HistoryQuery {\n readonly triggerId?: string | undefined\n readonly runId?: string | undefined\n readonly outcome?: Outcome | undefined\n readonly cursor?: Fire | undefined\n readonly limit?: number | undefined\n}\n\n/**\n * One page of the ledger, newest occurrence first, with the cursor that\n * addresses the next page when the limit cut the page short.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface HistoryPage {\n readonly items: ReadonlyArray<FireRecord>\n readonly nextCursor?: Fire | undefined\n}\n\n/**\n * What one trigger holds right now: the run or launch reservation on its row\n * and the occurrence buffered behind it. A plain read; `activeRun` is the read\n * that expires a stale reservation.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Held {\n readonly activeRunId?: string | undefined\n readonly pendingAt?: number | undefined\n}\n\n/**\n * One row of a listing: what the trigger holds right now, and either the\n * declaration the row decoded to or the failure it could not be read through.\n *\n * `trigger` is an `effect/Result`: narrow it with `Result.isFailure` before\n * reading the declaration. An undecodable row is isolated here rather than\n * failing the whole listing,\n * so one corrupt input cannot stop every other trigger from being scheduled.\n * The held state travels with the row because a scheduler tick would otherwise\n * ask for it again per trigger, once to expire a reservation that is not there\n * and once for a buffered occurrence that is not there either.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Listed extends Held {\n readonly triggerId: string\n readonly trigger: Decode.Result<Registered, TriggerError>\n}\n\n/**\n * A listed row for a declaration that decoded, holding what the trigger holds\n * right now. Both stores build their rows through this, so a caller composing\n * a store or a scheduler fixture of its own does not restate the shape.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const listed = (trigger: Registered, held: Held = {}): Listed => ({\n triggerId: trigger.id,\n trigger: Decode.succeed(trigger),\n ...held\n})\n\n/**\n * The last poll one scheduler host recorded.\n *\n * @category models\n * @since 1.0.0-rc.0\n */\nexport interface Heartbeat {\n readonly host: string\n readonly tickedAt: number\n}\n\n/**\n * Validates a {@link HistoryQuery} limit: absent, or a positive safe integer.\n * Both stores apply this before reading so swapping one for the other cannot\n * change which queries are refused.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const historyLimit = (limit: number | undefined): Effect.Effect<number | undefined, TriggerError> =>\n limit === undefined || (Number.isSafeInteger(limit) && limit > 0)\n ? Effect.succeed(limit)\n : Effect.fail(\n new TriggerError({\n code: \"invalid_options\",\n message: `history limit must be a positive safe integer, received ${limit}`,\n path: \"limit\"\n })\n )\n\n/**\n * Validates a prune cutoff: any safe integer. Both stores apply this before\n * deleting so swapping one for the other cannot change which cutoffs are\n * refused.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const pruneCutoff = (olderThan: number): Effect.Effect<number, TriggerError> =>\n Number.isSafeInteger(olderThan)\n ? Effect.succeed(olderThan)\n : Effect.fail(\n new TriggerError({\n code: \"invalid_options\",\n message: `prune cutoff must be a safe integer, received ${olderThan}`,\n path: \"olderThan\"\n })\n )\n\n/**\n * Newest occurrence first; equal occurrences of different triggers order by\n * descending trigger id so a cursor names one position.\n *\n * @category ordering\n * @since 1.0.0-rc.0\n */\nexport const compareNewestFirst = (left: Fire, right: Fire): number =>\n right.occurrence - left.occurrence ||\n (right.triggerId < left.triggerId ? -1 : right.triggerId > left.triggerId ? 1 : 0)\n\n/**\n * Whether a record lies after `cursor` in {@link compareNewestFirst} order.\n *\n * @category predicates\n * @since 1.0.0-rc.0\n */\nexport const isAfterCursor = (record: Fire, cursor: Fire): boolean => compareNewestFirst(cursor, record) < 0\n\n/**\n * Cuts an ordered, filtered record list to one page. The caller fetches one\n * record past the limit; that record's presence is what says a next page\n * exists, and the page's last record is the cursor addressing it.\n *\n * @category constructors\n * @since 1.0.0-rc.0\n */\nexport const historyPage = (records: ReadonlyArray<FireRecord>, limit: number | undefined): HistoryPage => {\n if (limit === undefined || records.length <= limit) return { items: records }\n const items = records.slice(0, limit)\n const last = items[items.length - 1] as FireRecord\n return { items, nextCursor: { triggerId: last.triggerId, occurrence: last.occurrence } }\n}\n\n/**\n * Durable trigger state: registration, listing, and the claim protocol that\n * keeps two schedulers from firing the same occurrence.\n *\n * Neither listing is a due-time query. Due-ness is a cron computation the\n * scheduler performs against its own watermark, so the store is asked only for\n * the rows eligible to be considered. A scheduler tick reads `list`, not\n * `listEnabled`: a disabled trigger can still hold an active occurrence that\n * has to recover, and the tick skips only its new claims. `listEnabled`\n * answers the narrower question for a caller that wants the declarations.\n *\n * A claim, result, pending-state, or active-run method fails with\n * `unknown_trigger` when the row it addresses does not exist. `get` answers\n * `None` for an absent row and `register` creates one. `clearActive` is a\n * compare-and-swap that cannot tell a missing trigger from a run id that no\n * longer matches, so it stays a no-op for both. `history`, `pruneFires`,\n * `heartbeat`, and `lastHeartbeat` address the whole store.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Service {\n readonly register: (trigger: Trigger) => Effect.Effect<Registered, TriggerError>\n readonly get: (triggerId: string) => Effect.Effect<Option.Option<Registered>, TriggerError>\n /**\n * Every trigger, enabled or not, ordered by id. This is what a scheduler\n * tick polls: see {@link Listed} for why a row carries its decode result and\n * its held state rather than a bare declaration.\n */\n readonly list: () => Effect.Effect<ReadonlyArray<Listed>, TriggerError>\n readonly listEnabled: () => Effect.Effect<ReadonlyArray<Registered>, TriggerError>\n readonly claimFire: (fire: ClaimFire) => Effect.Effect<Claim, TriggerError>\n /**\n * Claims the buffered occurrence, when one exists.\n *\n * One transaction reads the occurrence, applies the same claim protocol as\n * {@link Service.claimFire}, and clears the buffer only when the decision\n * consumes it. A concurrent buffer decision keeps it pending. No failure can\n * land between reading the buffer and claiming it.\n */\n readonly claimPending: (fire: {\n readonly triggerId: string\n readonly expectedRevision: number\n }) => Effect.Effect<\n Option.Option<{ readonly occurrence: number; readonly claim: Claim }>,\n TriggerError\n >\n readonly recordResult: (result: Result) => Effect.Effect<void, TriggerError>\n /** Atomically re-arms unfinished work and releases only its matching reservation.\n * A retained predecessor is restored with the pending pointer in the same write.\n * A stale token fails with `stale_owner`; a failed write leaves the lease intact.\n */\n readonly restorePending: (fire: Fire & { readonly reservationId: string }) => Effect.Effect<void, TriggerError>\n readonly setPending: (fire: Fire) => Effect.Effect<void, TriggerError>\n readonly activeRun: (triggerId: string) => Effect.Effect<Option.Option<string>, TriggerError>\n /**\n * Returns the occurrence owned by one active run or launch reservation.\n *\n * `lastFiredAt` cannot answer this: later skipped and buffered occurrences\n * advance that cursor while an older run remains active.\n */\n readonly activeOccurrence: (\n triggerId: string,\n runId: string\n ) => Effect.Effect<Option.Option<number>, TriggerError>\n readonly clearActive: (triggerId: string, runId: string) => Effect.Effect<void, TriggerError>\n /**\n * Reads the fire ledger newest first. See {@link HistoryQuery} for the\n * filters and paging contract; a limit that is not a positive safe integer\n * is refused with `invalid_options`.\n */\n readonly history: (query?: HistoryQuery) => Effect.Effect<HistoryPage, TriggerError>\n /**\n * Deletes settled fire ledger rows older than `olderThan`, answering how\n * many it removed. Nothing else in the store deletes from the ledger, so a\n * host that never calls this keeps one row per occurrence forever.\n *\n * A row is settled when its outcome is `completed`, `failed`, `skipped`, or\n * `superseded`. The occurrence a trigger currently buffers and the row\n * naming its active run are kept whatever their age, so pruning cannot take\n * state the scheduler still reads. `olderThan` that is not a safe integer is\n * refused with `invalid_options`.\n */\n readonly pruneFires: (options: { readonly olderThan: number }) => Effect.Effect<number, TriggerError>\n /**\n * Reads what one trigger holds without expiring anything, so a listing can\n * report a reservation or a buffered occurrence exactly as the row has it.\n */\n readonly inspect: (triggerId: string) => Effect.Effect<Held, TriggerError>\n /** Records that `host` polled the store at the store clock's current time. */\n readonly heartbeat: (host: string) => Effect.Effect<void, TriggerError>\n /** The most recent heartbeat across every host, or `None` when no scheduler has ever polled. */\n readonly lastHeartbeat: () => Effect.Effect<Option.Option<Heartbeat>, TriggerError>\n}\n\n/**\n * The {@link Service} tag.\n *\n * @category services\n * @since 0.1.0\n */\nexport class TriggerStore extends Context.Service<TriggerStore, Service>()(\"flows/triggers/TriggerStore\") {}\n\nconst unavailable = (method: string): Effect.Effect<never, TriggerError> =>\n Effect.fail(new TriggerError({ code: \"store\", message: `${method} is unavailable` }))\n\n/**\n * A {@link Service} that fails every method as unavailable, for an\n * environment with no trigger store. Overrides replace individual methods.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeNoop = (overrides: Partial<Service> = {}): Service => ({\n register: () => unavailable(\"register\"),\n get: () => unavailable(\"get\"),\n list: () => unavailable(\"list\"),\n listEnabled: () => unavailable(\"listEnabled\"),\n claimFire: () => unavailable(\"claimFire\"),\n claimPending: () => unavailable(\"claimPending\"),\n recordResult: () => unavailable(\"recordResult\"),\n restorePending: () => unavailable(\"restorePending\"),\n setPending: () => unavailable(\"setPending\"),\n activeRun: () => unavailable(\"activeRun\"),\n activeOccurrence: () => unavailable(\"activeOccurrence\"),\n clearActive: () => unavailable(\"clearActive\"),\n history: () => unavailable(\"history\"),\n pruneFires: () => unavailable(\"pruneFires\"),\n inspect: () => unavailable(\"inspect\"),\n heartbeat: () => unavailable(\"heartbeat\"),\n lastHeartbeat: () => unavailable(\"lastHeartbeat\"),\n ...overrides\n})\n\n/**\n * Provides {@link makeNoop}.\n *\n * @category layers\n * @since 0.1.0\n */\nexport const layerNoop = (overrides: Partial<Service> = {}): Layer.Layer<TriggerStore> =>\n Layer.succeed(TriggerStore)(makeNoop(overrides))\n"]}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Verified webhook ingestion through the authoritative Control boundary.
3
+ *
4
+ * @see packages/smithers/agent/triggers/docs/api.md
5
+ * @since 0.1.0
6
+ */
7
+ import * as ControlChannels from "@smthrs/control/Channels";
8
+ import type { ControlError } from "@smthrs/control/ControlError";
9
+ import type { Receipt, RunSummary } from "@smthrs/control/ControlSchema";
10
+ import type { CredentialRef } from "@smthrs/control/Credential";
11
+ import * as Effect from "effect/Effect";
12
+ import type * as Redacted from "effect/Redacted";
13
+ import type * as Channel from "./Channel.ts";
14
+ import { TriggerError } from "./TriggerError.ts";
15
+ /**
16
+ * Compares a supplied byte string against the expected one without returning
17
+ * early on a mismatch.
18
+ *
19
+ * The loop runs exactly `expected.length` times, so its iteration count is
20
+ * fixed by the secret side of the comparison and never by the caller's. Reading
21
+ * the longer of the two instead let a caller lengthen its input until the work
22
+ * stopped growing, which reports the expected signature's length. The length
23
+ * difference is folded into the result, so inputs of unequal length always
24
+ * disagree.
25
+ *
26
+ * @category verification
27
+ * @since 0.1.0
28
+ */
29
+ export declare const constantTimeEqual: (expected: Uint8Array, supplied: Uint8Array) => boolean;
30
+ /**
31
+ * Configuration for a raw-byte signature verifier.
32
+ *
33
+ * `expected` receives a private copy of the request bytes and the redacted
34
+ * credential reference the channel was declared with, and answers with the
35
+ * signature bytes the request must carry in `header`. It returns an Effect so
36
+ * the secret is resolved through the host's resolver per request rather than
37
+ * captured in a closure at declaration time, and so a resolution or HMAC
38
+ * failure arrives as a typed `verification_failed` instead of a defect that
39
+ * kills the fiber.
40
+ *
41
+ * @category models
42
+ * @since 0.1.0
43
+ */
44
+ export interface SignatureConfig {
45
+ readonly header: string;
46
+ readonly expected: (body: Uint8Array, credential: Redacted.Redacted<CredentialRef>) => Effect.Effect<Uint8Array, TriggerError>;
47
+ }
48
+ /**
49
+ * Builds a verifier that compares a signature header in constant time.
50
+ *
51
+ * An absent or empty header is refused before `expected` runs, and an
52
+ * `expected` that answers with zero bytes is refused after it. Neither is a
53
+ * comparison the constant-time equality can decide: it agrees on two empty
54
+ * byte strings, so a request carrying no signature at all would have
55
+ * authenticated against a secret that resolved to the empty string. A
56
+ * zero-length expected signature is a misconfigured credential, never a valid
57
+ * one, so the door stays closed instead of opening for everyone.
58
+ *
59
+ * Every refusal carries the same message, `webhook signature in <header> did
60
+ * not verify`, whatever the reason. A failure raised by `expected` is kept in
61
+ * `cause` only: its message names the credential reference or the resolver
62
+ * that could not produce a secret, which is a host detail, and the refusal
63
+ * that answers a bad signature answers a broken resolver the same way.
64
+ *
65
+ * @category constructors
66
+ * @since 0.1.0
67
+ */
68
+ export declare const makeSignatureVerifier: (config: SignatureConfig) => Channel.Verify;
69
+ /**
70
+ * Webhook declaration configuration.
71
+ *
72
+ * `credential` is required. It used to default to a reference named after the
73
+ * channel, so a webhook declared without one verified against whatever
74
+ * credential happened to share its name instead of being refused, and two
75
+ * declarations differing only in credential verified identically.
76
+ *
77
+ * @category models
78
+ * @since 0.1.0
79
+ */
80
+ export interface Config<Payload, Outbound = never> extends Channel.Config<Payload, RunSummary, Outbound> {
81
+ readonly credential: Redacted.Redacted<CredentialRef>;
82
+ }
83
+ /**
84
+ * A webhook door that can only ingest through `Channels` and `Control`.
85
+ *
86
+ * @category services
87
+ * @since 0.1.0
88
+ */
89
+ export interface Webhook {
90
+ readonly name: string;
91
+ readonly register: Effect.Effect<void, never, ControlChannels.Channels>;
92
+ readonly ingest: (raw: Channel.RawInbound) => Effect.Effect<Receipt, ControlError | TriggerError, ControlChannels.Channels>;
93
+ }
94
+ /**
95
+ * Builds a webhook door whose only dispatch path is the Control channel
96
+ * coordinator.
97
+ *
98
+ * Verification occurs inside `Channels.ingest` before the adapter's JSON or
99
+ * schema decoder and before any Control operation.
100
+ *
101
+ * `ingest` does not register: a channel is registered once, deliberately,
102
+ * through {@link Webhook.register}, so traffic to an unregistered channel is
103
+ * reported as unavailable rather than silently self-registering the door it
104
+ * arrived at.
105
+ *
106
+ * `ingest` also copies `body`, `headers`, and `idempotencyKey` before anything
107
+ * reads them. Verification, delivery fingerprinting, and decoding then all see
108
+ * one private snapshot, so a verifier that edits the bytes it was handed, or a
109
+ * caller that mutates its own object between building this Effect and running
110
+ * it, cannot authenticate one payload and have another decoded. A
111
+ * `SharedArrayBuffer`-backed view is copied out of shared memory by the same
112
+ * step.
113
+ *
114
+ * @category constructors
115
+ * @since 0.1.0
116
+ */
117
+ export declare const make: <Payload, Outbound = never>(config: Config<Payload, Outbound>) => Webhook;
118
+ //# sourceMappingURL=Webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Webhook.d.ts","sourceRoot":"","sources":["../../src/Webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAA;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAA;AAE/D,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,QAAQ,MAAM,iBAAiB,CAAA;AAEhD,OAAO,KAAK,KAAK,OAAO,MAAM,cAAc,CAAA;AAC5C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,GAAI,UAAU,UAAU,EAAE,UAAU,UAAU,KAAG,OAM9E,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,CACjB,IAAI,EAAE,UAAU,EAChB,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,KACzC,MAAM,CAAC,MAAM,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;CAC7C;AAcD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,qBAAqB,GAAI,QAAQ,eAAe,KAAG,OAAO,CAAC,MA2BvE,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,MAAM,CAAC,OAAO,EAAE,QAAQ,GAAG,KAAK,CAAE,SAAQ,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,QAAQ,CAAC;IACtG,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAA;CACtD;AAED;;;;;GAKG;AACH,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;IACvE,QAAQ,CAAC,MAAM,EAAE,CACf,GAAG,EAAE,OAAO,CAAC,UAAU,KACpB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAA;CACnF;AA6DD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,QAAQ,GAAG,KAAK,EAC5C,QAAQ,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,KAChC,OAgCF,CAAA"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Verified webhook ingestion through the authoritative Control boundary.
3
+ *
4
+ * @see packages/smithers/agent/triggers/docs/api.md
5
+ * @since 0.1.0
6
+ */
7
+ import * as ControlChannels from "@smthrs/control/Channels";
8
+ import { InvalidInput, Unauthorized } from "@smthrs/control/ControlError";
9
+ import * as ControlWebhook from "@smthrs/control/WebhookChannel";
10
+ import * as Effect from "effect/Effect";
11
+ import * as Schema from "effect/Schema";
12
+ import { TriggerError } from "./TriggerError.js";
13
+ /**
14
+ * Compares a supplied byte string against the expected one without returning
15
+ * early on a mismatch.
16
+ *
17
+ * The loop runs exactly `expected.length` times, so its iteration count is
18
+ * fixed by the secret side of the comparison and never by the caller's. Reading
19
+ * the longer of the two instead let a caller lengthen its input until the work
20
+ * stopped growing, which reports the expected signature's length. The length
21
+ * difference is folded into the result, so inputs of unequal length always
22
+ * disagree.
23
+ *
24
+ * @category verification
25
+ * @since 0.1.0
26
+ */
27
+ export const constantTimeEqual = (expected, supplied) => {
28
+ let difference = expected.length ^ supplied.length;
29
+ for (const [index, byte] of expected.entries()) {
30
+ difference |= byte ^ (supplied[index] ?? 0);
31
+ }
32
+ return difference === 0;
33
+ };
34
+ /**
35
+ * Copies request bytes into memory nobody else holds.
36
+ *
37
+ * `body.slice()` is not enough: a Node `Buffer` is a `Uint8Array` whose
38
+ * `slice` is an alias of `subarray` and answers with a view over the same
39
+ * memory, so a caller that reused its `Buffer` after handing it over changed
40
+ * what got verified, and a verifier that edited the bytes it was handed edited
41
+ * the caller's. `Uint8Array.from` always allocates, and it copies only the
42
+ * viewed range of an offset view and only a snapshot of a `SharedArrayBuffer`.
43
+ */
44
+ const ownedCopy = (body) => Uint8Array.from(body);
45
+ /**
46
+ * Builds a verifier that compares a signature header in constant time.
47
+ *
48
+ * An absent or empty header is refused before `expected` runs, and an
49
+ * `expected` that answers with zero bytes is refused after it. Neither is a
50
+ * comparison the constant-time equality can decide: it agrees on two empty
51
+ * byte strings, so a request carrying no signature at all would have
52
+ * authenticated against a secret that resolved to the empty string. A
53
+ * zero-length expected signature is a misconfigured credential, never a valid
54
+ * one, so the door stays closed instead of opening for everyone.
55
+ *
56
+ * Every refusal carries the same message, `webhook signature in <header> did
57
+ * not verify`, whatever the reason. A failure raised by `expected` is kept in
58
+ * `cause` only: its message names the credential reference or the resolver
59
+ * that could not produce a secret, which is a host detail, and the refusal
60
+ * that answers a bad signature answers a broken resolver the same way.
61
+ *
62
+ * @category constructors
63
+ * @since 0.1.0
64
+ */
65
+ export const makeSignatureVerifier = (config) => (raw, credential) => {
66
+ const refuse = (cause) => Effect.fail(new TriggerError({
67
+ code: "verification_failed",
68
+ message: `webhook signature in ${config.header} did not verify`,
69
+ ...(cause === undefined ? {} : { cause })
70
+ }));
71
+ return Effect.suspend(() => {
72
+ const supplied = raw.headers[config.header.toLowerCase()] ?? raw.headers[config.header];
73
+ if (supplied === undefined || supplied.length === 0) {
74
+ return refuse();
75
+ }
76
+ const actual = new TextEncoder().encode(supplied);
77
+ // The verifier gets its own copy: nothing it does to these bytes can reach
78
+ // the buffer that is about to be fingerprinted and decoded.
79
+ return config.expected(ownedCopy(raw.body), credential).pipe(Effect.matchEffect({
80
+ onFailure: (error) => refuse(error),
81
+ onSuccess: (expected) => expected.length > 0 && constantTimeEqual(expected, actual)
82
+ ? Effect.void
83
+ : refuse()
84
+ }));
85
+ });
86
+ };
87
+ const invalidInput = (issue) => new InvalidInput({ issue });
88
+ const toControlChannel = (config) => {
89
+ const channel = ControlWebhook.make({
90
+ name: config.name,
91
+ schema: config.schema,
92
+ credential: config.credential,
93
+ // The message is fixed here rather than forwarded from the verifier: a
94
+ // custom `verify` may say why it refused, and that reason travels toward
95
+ // the unauthenticated sender on the same error that answers a bad
96
+ // signature. `Unauthorized` carries no cause, so nothing else survives
97
+ // the crossing.
98
+ verify: (raw, credential) => config.verify(raw, credential).pipe(Effect.mapError(() => new Unauthorized({ message: `webhook ${config.name} did not verify the request` }))),
99
+ map: (payload) => {
100
+ const inbound = config.inbound(payload);
101
+ if ("start" in inbound) {
102
+ return Effect.succeed({
103
+ _tag: "Start",
104
+ flowId: inbound.start.flowId,
105
+ input: inbound.start.input
106
+ });
107
+ }
108
+ return Schema.decodeUnknownEffect(Schema.Json)(inbound.signal.value).pipe(Effect.map((value) => ({
109
+ _tag: "Signal",
110
+ runId: inbound.signal.runId,
111
+ signal: {
112
+ name: inbound.signal.stepId,
113
+ payload: value
114
+ }
115
+ })), Effect.mapError((error) => invalidInput(String(error))));
116
+ },
117
+ project: (run) => {
118
+ if (config.outbound === undefined) {
119
+ return {
120
+ cursor: String(run.updatedAt),
121
+ operation: "noop",
122
+ message: null
123
+ };
124
+ }
125
+ return {
126
+ cursor: String(run.updatedAt),
127
+ operation: "post",
128
+ message: config.outbound(run)
129
+ };
130
+ }
131
+ });
132
+ return channel;
133
+ };
134
+ /**
135
+ * Builds a webhook door whose only dispatch path is the Control channel
136
+ * coordinator.
137
+ *
138
+ * Verification occurs inside `Channels.ingest` before the adapter's JSON or
139
+ * schema decoder and before any Control operation.
140
+ *
141
+ * `ingest` does not register: a channel is registered once, deliberately,
142
+ * through {@link Webhook.register}, so traffic to an unregistered channel is
143
+ * reported as unavailable rather than silently self-registering the door it
144
+ * arrived at.
145
+ *
146
+ * `ingest` also copies `body`, `headers`, and `idempotencyKey` before anything
147
+ * reads them. Verification, delivery fingerprinting, and decoding then all see
148
+ * one private snapshot, so a verifier that edits the bytes it was handed, or a
149
+ * caller that mutates its own object between building this Effect and running
150
+ * it, cannot authenticate one payload and have another decoded. A
151
+ * `SharedArrayBuffer`-backed view is copied out of shared memory by the same
152
+ * step.
153
+ *
154
+ * @category constructors
155
+ * @since 0.1.0
156
+ */
157
+ export const make = (config) => {
158
+ const channel = toControlChannel(config);
159
+ const register = Effect.flatMap(ControlChannels.Channels, (channels) => channels.register(channel));
160
+ return {
161
+ name: config.name,
162
+ register,
163
+ ingest: (raw) => {
164
+ // Copied here rather than inside the Effect: the copy has to happen when
165
+ // the caller hands the request over, not when the returned Effect
166
+ // eventually runs, or a caller that reuses its own buffer in between
167
+ // changes what gets authenticated.
168
+ const snapshot = {
169
+ body: ownedCopy(raw.body),
170
+ headers: { ...raw.headers },
171
+ idempotencyKey: raw.idempotencyKey
172
+ };
173
+ return Effect.gen(function* () {
174
+ const channels = yield* ControlChannels.Channels;
175
+ return yield* channels.ingest({ channel: config.name, raw: snapshot });
176
+ }).pipe(Effect.mapError((error) => error instanceof Unauthorized
177
+ ? new TriggerError({
178
+ code: "verification_failed",
179
+ message: `webhook ${config.name} did not verify the request`,
180
+ cause: error
181
+ })
182
+ : error));
183
+ }
184
+ };
185
+ };
186
+ //# sourceMappingURL=Webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Webhook.js","sourceRoot":"","sources":["../../src/Webhook.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,eAAe,MAAM,0BAA0B,CAAA;AAE3D,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAA;AAGzE,OAAO,KAAK,cAAc,MAAM,gCAAgC,CAAA;AAChE,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,QAAoB,EAAE,QAAoB,EAAW,EAAE;IACvF,IAAI,UAAU,GAAG,QAAQ,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAA;IAClD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC;QAC/C,UAAU,IAAI,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAA;IAC7C,CAAC;IACD,OAAO,UAAU,KAAK,CAAC,CAAA;AACzB,CAAC,CAAA;AAwBD;;;;;;;;;GASG;AACH,MAAM,SAAS,GAAG,CAAC,IAAgB,EAAc,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEzE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,MAAuB,EAAkB,EAAE,CAAC,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE;IACpG,MAAM,MAAM,GAAG,CAAC,KAAoB,EAAE,EAAE,CACtC,MAAM,CAAC,IAAI,CACT,IAAI,YAAY,CAAC;QACf,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,wBAAwB,MAAM,CAAC,MAAM,iBAAiB;QAC/D,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;KAC1C,CAAC,CACH,CAAA;IACH,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,EAAE;QACzB,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;QACvF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,OAAO,MAAM,EAAE,CAAA;QACjB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACjD,2EAA2E;QAC3E,4DAA4D;QAC5D,OAAO,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,CAAC,IAAI,CAC1D,MAAM,CAAC,WAAW,CAAC;YACjB,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC;YACnC,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CACtB,QAAQ,CAAC,MAAM,GAAG,CAAC,IAAI,iBAAiB,CAAC,QAAQ,EAAE,MAAM,CAAC;gBACxD,CAAC,CAAC,MAAM,CAAC,IAAI;gBACb,CAAC,CAAC,MAAM,EAAE;SACf,CAAC,CACH,CAAA;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAA;AA+BD,MAAM,YAAY,GAAG,CAAC,KAAa,EAAgB,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;AAEjF,MAAM,gBAAgB,GAAG,CACvB,MAAiC,EACC,EAAE;IACpC,MAAM,OAAO,GAAG,cAAc,CAAC,IAAI,CAAC;QAClC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,uEAAuE;QACvE,yEAAyE;QACzE,kEAAkE;QAClE,uEAAuE;QACvE,gBAAgB;QAChB,MAAM,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE,EAAE,CAC1B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,IAAI,CACjC,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,YAAY,CAAC,EAAE,OAAO,EAAE,WAAW,MAAM,CAAC,IAAI,6BAA6B,EAAE,CAAC,CAAC,CAC1G;QACH,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YACvC,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;gBACvB,OAAO,MAAM,CAAC,OAAO,CAAC;oBACpB,IAAI,EAAE,OAAgB;oBACtB,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM;oBAC5B,KAAK,EAAE,OAAO,CAAC,KAAK,CAAC,KAAK;iBAC3B,CAAC,CAAA;YACJ,CAAC;YACD,OAAO,MAAM,CAAC,mBAAmB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CACvE,MAAM,CAAC,GAAG,CACR,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;gBACV,IAAI,EAAE,QAAiB;gBACvB,KAAK,EAAE,OAAO,CAAC,MAAM,CAAC,KAAK;gBAC3B,MAAM,EAAE;oBACN,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,MAAM;oBAC3B,OAAO,EAAE,KAAK;iBACf;aACF,CAAC,CACH,EACD,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CACxD,CAAA;QACH,CAAC;QACD,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YACf,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAClC,OAAO;oBACL,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;oBAC7B,SAAS,EAAE,MAAe;oBAC1B,OAAO,EAAE,IAAI;iBACd,CAAA;YACH,CAAC;YACD,OAAO;gBACL,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;gBAC7B,SAAS,EAAE,MAAe;gBAC1B,OAAO,EAAE,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC;aAC9B,CAAA;QACH,CAAC;KACF,CAAC,CAAA;IACF,OAAO,OAAO,CAAA;AAChB,CAAC,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAClB,MAAiC,EACxB,EAAE;IACX,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAA;IACnG,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,QAAQ;QACR,MAAM,EAAE,CAAC,GAAG,EAAE,EAAE;YACd,yEAAyE;YACzE,kEAAkE;YAClE,qEAAqE;YACrE,mCAAmC;YACnC,MAAM,QAAQ,GAAuB;gBACnC,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;gBACzB,OAAO,EAAE,EAAE,GAAG,GAAG,CAAC,OAAO,EAAE;gBAC3B,cAAc,EAAE,GAAG,CAAC,cAAc;aACnC,CAAA;YACD,OAAO,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACzB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,eAAe,CAAC,QAAQ,CAAA;gBAChD,OAAO,KAAK,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAA;YACxE,CAAC,CAAC,CAAC,IAAI,CACL,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CACxB,KAAK,YAAY,YAAY;gBAC3B,CAAC,CAAC,IAAI,YAAY,CAAC;oBACjB,IAAI,EAAE,qBAAqB;oBAC3B,OAAO,EAAE,WAAW,MAAM,CAAC,IAAI,6BAA6B;oBAC5D,KAAK,EAAE,KAAK;iBACb,CAAC;gBACF,CAAC,CAAC,KAAK,CACV,CACF,CAAA;QACH,CAAC;KACF,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Verified webhook ingestion through the authoritative Control boundary.\n *\n * @see packages/smithers/agent/triggers/docs/api.md\n * @since 0.1.0\n */\n\nimport * as ControlChannels from \"@smthrs/control/Channels\"\nimport type { ControlError } from \"@smthrs/control/ControlError\"\nimport { InvalidInput, Unauthorized } from \"@smthrs/control/ControlError\"\nimport type { Receipt, RunSummary } from \"@smthrs/control/ControlSchema\"\nimport type { CredentialRef } from \"@smthrs/control/Credential\"\nimport * as ControlWebhook from \"@smthrs/control/WebhookChannel\"\nimport * as Effect from \"effect/Effect\"\nimport type * as Redacted from \"effect/Redacted\"\nimport * as Schema from \"effect/Schema\"\nimport type * as Channel from \"./Channel.ts\"\nimport { TriggerError } from \"./TriggerError.ts\"\n\n/**\n * Compares a supplied byte string against the expected one without returning\n * early on a mismatch.\n *\n * The loop runs exactly `expected.length` times, so its iteration count is\n * fixed by the secret side of the comparison and never by the caller's. Reading\n * the longer of the two instead let a caller lengthen its input until the work\n * stopped growing, which reports the expected signature's length. The length\n * difference is folded into the result, so inputs of unequal length always\n * disagree.\n *\n * @category verification\n * @since 0.1.0\n */\nexport const constantTimeEqual = (expected: Uint8Array, supplied: Uint8Array): boolean => {\n let difference = expected.length ^ supplied.length\n for (const [index, byte] of expected.entries()) {\n difference |= byte ^ (supplied[index] ?? 0)\n }\n return difference === 0\n}\n\n/**\n * Configuration for a raw-byte signature verifier.\n *\n * `expected` receives a private copy of the request bytes and the redacted\n * credential reference the channel was declared with, and answers with the\n * signature bytes the request must carry in `header`. It returns an Effect so\n * the secret is resolved through the host's resolver per request rather than\n * captured in a closure at declaration time, and so a resolution or HMAC\n * failure arrives as a typed `verification_failed` instead of a defect that\n * kills the fiber.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface SignatureConfig {\n readonly header: string\n readonly expected: (\n body: Uint8Array,\n credential: Redacted.Redacted<CredentialRef>\n ) => Effect.Effect<Uint8Array, TriggerError>\n}\n\n/**\n * Copies request bytes into memory nobody else holds.\n *\n * `body.slice()` is not enough: a Node `Buffer` is a `Uint8Array` whose\n * `slice` is an alias of `subarray` and answers with a view over the same\n * memory, so a caller that reused its `Buffer` after handing it over changed\n * what got verified, and a verifier that edited the bytes it was handed edited\n * the caller's. `Uint8Array.from` always allocates, and it copies only the\n * viewed range of an offset view and only a snapshot of a `SharedArrayBuffer`.\n */\nconst ownedCopy = (body: Uint8Array): Uint8Array => Uint8Array.from(body)\n\n/**\n * Builds a verifier that compares a signature header in constant time.\n *\n * An absent or empty header is refused before `expected` runs, and an\n * `expected` that answers with zero bytes is refused after it. Neither is a\n * comparison the constant-time equality can decide: it agrees on two empty\n * byte strings, so a request carrying no signature at all would have\n * authenticated against a secret that resolved to the empty string. A\n * zero-length expected signature is a misconfigured credential, never a valid\n * one, so the door stays closed instead of opening for everyone.\n *\n * Every refusal carries the same message, `webhook signature in <header> did\n * not verify`, whatever the reason. A failure raised by `expected` is kept in\n * `cause` only: its message names the credential reference or the resolver\n * that could not produce a secret, which is a host detail, and the refusal\n * that answers a bad signature answers a broken resolver the same way.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const makeSignatureVerifier = (config: SignatureConfig): Channel.Verify => (raw, credential) => {\n const refuse = (cause?: TriggerError) =>\n Effect.fail(\n new TriggerError({\n code: \"verification_failed\",\n message: `webhook signature in ${config.header} did not verify`,\n ...(cause === undefined ? {} : { cause })\n })\n )\n return Effect.suspend(() => {\n const supplied = raw.headers[config.header.toLowerCase()] ?? raw.headers[config.header]\n if (supplied === undefined || supplied.length === 0) {\n return refuse()\n }\n const actual = new TextEncoder().encode(supplied)\n // The verifier gets its own copy: nothing it does to these bytes can reach\n // the buffer that is about to be fingerprinted and decoded.\n return config.expected(ownedCopy(raw.body), credential).pipe(\n Effect.matchEffect({\n onFailure: (error) => refuse(error),\n onSuccess: (expected) =>\n expected.length > 0 && constantTimeEqual(expected, actual)\n ? Effect.void\n : refuse()\n })\n )\n })\n}\n\n/**\n * Webhook declaration configuration.\n *\n * `credential` is required. It used to default to a reference named after the\n * channel, so a webhook declared without one verified against whatever\n * credential happened to share its name instead of being refused, and two\n * declarations differing only in credential verified identically.\n *\n * @category models\n * @since 0.1.0\n */\nexport interface Config<Payload, Outbound = never> extends Channel.Config<Payload, RunSummary, Outbound> {\n readonly credential: Redacted.Redacted<CredentialRef>\n}\n\n/**\n * A webhook door that can only ingest through `Channels` and `Control`.\n *\n * @category services\n * @since 0.1.0\n */\nexport interface Webhook {\n readonly name: string\n readonly register: Effect.Effect<void, never, ControlChannels.Channels>\n readonly ingest: (\n raw: Channel.RawInbound\n ) => Effect.Effect<Receipt, ControlError | TriggerError, ControlChannels.Channels>\n}\n\nconst invalidInput = (issue: string): InvalidInput => new InvalidInput({ issue })\n\nconst toControlChannel = <Payload, Outbound>(\n config: Config<Payload, Outbound>\n): ControlChannels.Channel<Payload> => {\n const channel = ControlWebhook.make({\n name: config.name,\n schema: config.schema,\n credential: config.credential,\n // The message is fixed here rather than forwarded from the verifier: a\n // custom `verify` may say why it refused, and that reason travels toward\n // the unauthenticated sender on the same error that answers a bad\n // signature. `Unauthorized` carries no cause, so nothing else survives\n // the crossing.\n verify: (raw, credential) =>\n config.verify(raw, credential).pipe(\n Effect.mapError(() => new Unauthorized({ message: `webhook ${config.name} did not verify the request` }))\n ),\n map: (payload) => {\n const inbound = config.inbound(payload)\n if (\"start\" in inbound) {\n return Effect.succeed({\n _tag: \"Start\" as const,\n flowId: inbound.start.flowId,\n input: inbound.start.input\n })\n }\n return Schema.decodeUnknownEffect(Schema.Json)(inbound.signal.value).pipe(\n Effect.map(\n (value) => ({\n _tag: \"Signal\" as const,\n runId: inbound.signal.runId,\n signal: {\n name: inbound.signal.stepId,\n payload: value\n }\n })\n ),\n Effect.mapError((error) => invalidInput(String(error)))\n )\n },\n project: (run) => {\n if (config.outbound === undefined) {\n return {\n cursor: String(run.updatedAt),\n operation: \"noop\" as const,\n message: null\n }\n }\n return {\n cursor: String(run.updatedAt),\n operation: \"post\" as const,\n message: config.outbound(run)\n }\n }\n })\n return channel\n}\n\n/**\n * Builds a webhook door whose only dispatch path is the Control channel\n * coordinator.\n *\n * Verification occurs inside `Channels.ingest` before the adapter's JSON or\n * schema decoder and before any Control operation.\n *\n * `ingest` does not register: a channel is registered once, deliberately,\n * through {@link Webhook.register}, so traffic to an unregistered channel is\n * reported as unavailable rather than silently self-registering the door it\n * arrived at.\n *\n * `ingest` also copies `body`, `headers`, and `idempotencyKey` before anything\n * reads them. Verification, delivery fingerprinting, and decoding then all see\n * one private snapshot, so a verifier that edits the bytes it was handed, or a\n * caller that mutates its own object between building this Effect and running\n * it, cannot authenticate one payload and have another decoded. A\n * `SharedArrayBuffer`-backed view is copied out of shared memory by the same\n * step.\n *\n * @category constructors\n * @since 0.1.0\n */\nexport const make = <Payload, Outbound = never>(\n config: Config<Payload, Outbound>\n): Webhook => {\n const channel = toControlChannel(config)\n const register = Effect.flatMap(ControlChannels.Channels, (channels) => channels.register(channel))\n return {\n name: config.name,\n register,\n ingest: (raw) => {\n // Copied here rather than inside the Effect: the copy has to happen when\n // the caller hands the request over, not when the returned Effect\n // eventually runs, or a caller that reuses its own buffer in between\n // changes what gets authenticated.\n const snapshot: Channel.RawInbound = {\n body: ownedCopy(raw.body),\n headers: { ...raw.headers },\n idempotencyKey: raw.idempotencyKey\n }\n return Effect.gen(function*() {\n const channels = yield* ControlChannels.Channels\n return yield* channels.ingest({ channel: config.name, raw: snapshot })\n }).pipe(\n Effect.mapError((error) =>\n error instanceof Unauthorized\n ? new TriggerError({\n code: \"verification_failed\",\n message: `webhook ${config.name} did not verify the request`,\n cause: error\n })\n : error\n )\n )\n }\n }\n}\n"]}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `/triggers` — durable schedules and verified inbound channels.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ /** @category errors @since 0.1.0 */
7
+ export * as TriggerError from "./TriggerError.ts";
8
+ /** @category schedules @since 0.1.0 */
9
+ export * as Cron from "./Cron.ts";
10
+ /** @category declarations @since 0.1.0 */
11
+ export * as Trigger from "./Trigger.ts";
12
+ /** @category declarations @since 0.1.0 */
13
+ export * as Schedule from "./Schedule.ts";
14
+ /** @category policies @since 0.1.0 */
15
+ export * as Overlap from "./Overlap.ts";
16
+ /** @category policies @since 0.1.0 */
17
+ export * as CatchUp from "./CatchUp.ts";
18
+ /** @category services @since 0.1.0 */
19
+ export * as TriggerStore from "./TriggerStore.ts";
20
+ /** @category services @since 0.1.0 */
21
+ export * as SqlTriggerStore from "./SqlTriggerStore.ts";
22
+ /** @category scheduling @since 0.1.0 */
23
+ export * as Scheduler from "./Scheduler.ts";
24
+ /** @category services @since 1.0.0-rc.0 */
25
+ export * as DispatchReader from "./DispatchReader.ts";
26
+ /** @category channels @since 0.1.0 */
27
+ export * as Channel from "./Channel.ts";
28
+ /** @category webhooks @since 0.1.0 */
29
+ export * as Webhook from "./Webhook.ts";
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oCAAoC;AACpC,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,uCAAuC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,0CAA0C;AAC1C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,0CAA0C;AAC1C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,sCAAsC;AACtC,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD,wCAAwC;AACxC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,2CAA2C;AAC3C,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `/triggers` — durable schedules and verified inbound channels.
3
+ *
4
+ * @since 0.1.0
5
+ */
6
+ /** @category errors @since 0.1.0 */
7
+ export * as TriggerError from "./TriggerError.js";
8
+ /** @category schedules @since 0.1.0 */
9
+ export * as Cron from "./Cron.js";
10
+ /** @category declarations @since 0.1.0 */
11
+ export * as Trigger from "./Trigger.js";
12
+ /** @category declarations @since 0.1.0 */
13
+ export * as Schedule from "./Schedule.js";
14
+ /** @category policies @since 0.1.0 */
15
+ export * as Overlap from "./Overlap.js";
16
+ /** @category policies @since 0.1.0 */
17
+ export * as CatchUp from "./CatchUp.js";
18
+ /** @category services @since 0.1.0 */
19
+ export * as TriggerStore from "./TriggerStore.js";
20
+ /** @category services @since 0.1.0 */
21
+ export * as SqlTriggerStore from "./SqlTriggerStore.js";
22
+ /** @category scheduling @since 0.1.0 */
23
+ export * as Scheduler from "./Scheduler.js";
24
+ /** @category services @since 1.0.0-rc.0 */
25
+ export * as DispatchReader from "./DispatchReader.js";
26
+ /** @category channels @since 0.1.0 */
27
+ export * as Channel from "./Channel.js";
28
+ /** @category webhooks @since 0.1.0 */
29
+ export * as Webhook from "./Webhook.js";
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,oCAAoC;AACpC,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,uCAAuC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAA;AACjC,0CAA0C;AAC1C,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,0CAA0C;AAC1C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAA;AACzC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,YAAY,MAAM,mBAAmB,CAAA;AACjD,sCAAsC;AACtC,OAAO,KAAK,eAAe,MAAM,sBAAsB,CAAA;AACvD,wCAAwC;AACxC,OAAO,KAAK,SAAS,MAAM,gBAAgB,CAAA;AAC3C,2CAA2C;AAC3C,OAAO,KAAK,cAAc,MAAM,qBAAqB,CAAA;AACrD,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA;AACvC,sCAAsC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAA","sourcesContent":["/**\n * `/triggers` — durable schedules and verified inbound channels.\n *\n * @since 0.1.0\n */\n\n/** @category errors @since 0.1.0 */\nexport * as TriggerError from \"./TriggerError.ts\"\n/** @category schedules @since 0.1.0 */\nexport * as Cron from \"./Cron.ts\"\n/** @category declarations @since 0.1.0 */\nexport * as Trigger from \"./Trigger.ts\"\n/** @category declarations @since 0.1.0 */\nexport * as Schedule from \"./Schedule.ts\"\n/** @category policies @since 0.1.0 */\nexport * as Overlap from \"./Overlap.ts\"\n/** @category policies @since 0.1.0 */\nexport * as CatchUp from \"./CatchUp.ts\"\n/** @category services @since 0.1.0 */\nexport * as TriggerStore from \"./TriggerStore.ts\"\n/** @category services @since 0.1.0 */\nexport * as SqlTriggerStore from \"./SqlTriggerStore.ts\"\n/** @category scheduling @since 0.1.0 */\nexport * as Scheduler from \"./Scheduler.ts\"\n/** @category services @since 1.0.0-rc.0 */\nexport * as DispatchReader from \"./DispatchReader.ts\"\n/** @category channels @since 0.1.0 */\nexport * as Channel from \"./Channel.ts\"\n/** @category webhooks @since 0.1.0 */\nexport * as Webhook from \"./Webhook.ts\"\n"]}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What one scheduler process knows about the run each trigger owns.
3
+ *
4
+ * @since 1.0.0-rc.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import type * as Fiber from "effect/Fiber";
8
+ /**
9
+ * What this process knows about the run one trigger currently owns: which
10
+ * occurrence claimed it, the reservation or run id the store holds for it, and
11
+ * the monitor fiber when this process is the one watching it.
12
+ *
13
+ * `runId` is always known. A claim that hands out work names the reservation it
14
+ * wrote, and a recovered entry is read back from the store, so an entry with
15
+ * nothing to release cannot be constructed.
16
+ *
17
+ * @private
18
+ * @since 1.0.0-rc.0
19
+ */
20
+ export interface Active {
21
+ readonly occurrence: number;
22
+ readonly runId: string;
23
+ readonly fiber?: Fiber.Fiber<void> | undefined;
24
+ }
25
+ /**
26
+ * The entries of one scheduler, keyed by trigger id.
27
+ *
28
+ * `take` is the only unfenced write: an occurrence that claimed the trigger,
29
+ * or a run recovered from the store, replaces whatever entry was there. Every
30
+ * later write is fenced on the occurrence that took the entry, so one guard
31
+ * states the rule once: a launch that has been superseded, or whose run
32
+ * already settled, no longer owns the entry and must not write to it. Spelling
33
+ * the fence out at each site is how the three copies of it drifted apart.
34
+ *
35
+ * @private
36
+ * @since 1.0.0-rc.0
37
+ */
38
+ export interface ActiveRuns {
39
+ readonly get: (triggerId: string) => Effect.Effect<Active | undefined>;
40
+ readonly take: (triggerId: string, entry: Active) => Effect.Effect<void>;
41
+ readonly update: (triggerId: string, occurrence: number, change: (entry: Active) => Active) => Effect.Effect<void>;
42
+ readonly remove: (triggerId: string, occurrence: number) => Effect.Effect<void>;
43
+ }
44
+ /**
45
+ * Allocates an empty set of entries.
46
+ *
47
+ * @private
48
+ * @since 1.0.0-rc.0
49
+ */
50
+ export declare const make: Effect.Effect<ActiveRuns>;
51
+ //# sourceMappingURL=ActiveRuns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActiveRuns.d.ts","sourceRoot":"","sources":["../../../src/internal/ActiveRuns.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AACvC,OAAO,KAAK,KAAK,KAAK,MAAM,cAAc,CAAA;AAG1C;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,MAAM;IACrB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,CAAA;CAC/C;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACtE,QAAQ,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxE,QAAQ,CAAC,MAAM,EAAE,CACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,KAC9B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;IACxB,QAAQ,CAAC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;CAChF;AAED;;;;;GAKG;AACH,eAAO,MAAM,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,UAAU,CAwB1C,CAAA"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * What one scheduler process knows about the run each trigger owns.
3
+ *
4
+ * @since 1.0.0-rc.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import * as Ref from "effect/Ref";
8
+ /**
9
+ * Allocates an empty set of entries.
10
+ *
11
+ * @private
12
+ * @since 1.0.0-rc.0
13
+ */
14
+ export const make = Effect.map(Ref.make(new Map()), (entries) => {
15
+ const fenced = (triggerId, occurrence, change) => Ref.update(entries, (current) => {
16
+ const entry = current.get(triggerId);
17
+ if (entry?.occurrence !== occurrence)
18
+ return current;
19
+ const next = new Map(current);
20
+ const updated = change(entry);
21
+ if (updated === undefined)
22
+ next.delete(triggerId);
23
+ else
24
+ next.set(triggerId, updated);
25
+ return next;
26
+ });
27
+ return {
28
+ get: (triggerId) => Effect.map(Ref.get(entries), (current) => current.get(triggerId)),
29
+ take: (triggerId, entry) => Ref.update(entries, (current) => new Map(current).set(triggerId, entry)),
30
+ update: fenced,
31
+ remove: (triggerId, occurrence) => fenced(triggerId, occurrence, () => undefined)
32
+ };
33
+ });
34
+ //# sourceMappingURL=ActiveRuns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActiveRuns.js","sourceRoot":"","sources":["../../../src/internal/ActiveRuns.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAEvC,OAAO,KAAK,GAAG,MAAM,YAAY,CAAA;AA4CjC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,IAAI,GAA8B,MAAM,CAAC,GAAG,CACvD,GAAG,CAAC,IAAI,CAA8B,IAAI,GAAG,EAAE,CAAC,EAChD,CAAC,OAAO,EAAE,EAAE;IACV,MAAM,MAAM,GAAG,CACb,SAAiB,EACjB,UAAkB,EAClB,MAA6C,EACxB,EAAE,CACvB,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE;QAC9B,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;QACpC,IAAI,KAAK,EAAE,UAAU,KAAK,UAAU;YAAE,OAAO,OAAO,CAAA;QACpD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAA;QAC7B,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;QAC7B,IAAI,OAAO,KAAK,SAAS;YAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;;YAC5C,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;QACjC,OAAO,IAAI,CAAA;IACb,CAAC,CAAC,CAAA;IACJ,OAAO;QACL,GAAG,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrF,IAAI,EAAE,CAAC,SAAS,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC;QACpG,MAAM,EAAE,MAAM;QACd,MAAM,EAAE,CAAC,SAAS,EAAE,UAAU,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC;KAClF,CAAA;AACH,CAAC,CACF,CAAA","sourcesContent":["/**\n * What one scheduler process knows about the run each trigger owns.\n *\n * @since 1.0.0-rc.0\n */\n\nimport * as Effect from \"effect/Effect\"\nimport type * as Fiber from \"effect/Fiber\"\nimport * as Ref from \"effect/Ref\"\n\n/**\n * What this process knows about the run one trigger currently owns: which\n * occurrence claimed it, the reservation or run id the store holds for it, and\n * the monitor fiber when this process is the one watching it.\n *\n * `runId` is always known. A claim that hands out work names the reservation it\n * wrote, and a recovered entry is read back from the store, so an entry with\n * nothing to release cannot be constructed.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface Active {\n readonly occurrence: number\n readonly runId: string\n readonly fiber?: Fiber.Fiber<void> | undefined\n}\n\n/**\n * The entries of one scheduler, keyed by trigger id.\n *\n * `take` is the only unfenced write: an occurrence that claimed the trigger,\n * or a run recovered from the store, replaces whatever entry was there. Every\n * later write is fenced on the occurrence that took the entry, so one guard\n * states the rule once: a launch that has been superseded, or whose run\n * already settled, no longer owns the entry and must not write to it. Spelling\n * the fence out at each site is how the three copies of it drifted apart.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport interface ActiveRuns {\n readonly get: (triggerId: string) => Effect.Effect<Active | undefined>\n readonly take: (triggerId: string, entry: Active) => Effect.Effect<void>\n readonly update: (\n triggerId: string,\n occurrence: number,\n change: (entry: Active) => Active\n ) => Effect.Effect<void>\n readonly remove: (triggerId: string, occurrence: number) => Effect.Effect<void>\n}\n\n/**\n * Allocates an empty set of entries.\n *\n * @private\n * @since 1.0.0-rc.0\n */\nexport const make: Effect.Effect<ActiveRuns> = Effect.map(\n Ref.make<ReadonlyMap<string, Active>>(new Map()),\n (entries) => {\n const fenced = (\n triggerId: string,\n occurrence: number,\n change: (entry: Active) => Active | undefined\n ): Effect.Effect<void> =>\n Ref.update(entries, (current) => {\n const entry = current.get(triggerId)\n if (entry?.occurrence !== occurrence) return current\n const next = new Map(current)\n const updated = change(entry)\n if (updated === undefined) next.delete(triggerId)\n else next.set(triggerId, updated)\n return next\n })\n return {\n get: (triggerId) => Effect.map(Ref.get(entries), (current) => current.get(triggerId)),\n take: (triggerId, entry) => Ref.update(entries, (current) => new Map(current).set(triggerId, entry)),\n update: fenced,\n remove: (triggerId, occurrence) => fenced(triggerId, occurrence, () => undefined)\n }\n }\n)\n"]}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Which occurrences one scheduler tick owes a trigger.
3
+ *
4
+ * @since 1.0.0-rc.0
5
+ */
6
+ import * as Effect from "effect/Effect";
7
+ import type { TriggerError } from "../TriggerError.ts";
8
+ import type { Registered } from "../TriggerStore.ts";
9
+ /**
10
+ * The occurrences to claim, oldest first, and the watermark that stands once
11
+ * every one of them has been dispatched.
12
+ *
13
+ * @private
14
+ * @since 1.0.0-rc.0
15
+ */
16
+ export interface Due {
17
+ readonly occurrences: ReadonlyArray<number>;
18
+ readonly watermark: number;
19
+ }
20
+ /**
21
+ * Computes what a trigger owes at `now`, given the watermark this process has
22
+ * already dispatched through, or `undefined` on first sight of the trigger.
23
+ *
24
+ * The answer depends on nothing but its arguments. The watermark is the
25
+ * caller's to keep: it only moves forward, and only past occurrences the
26
+ * caller finished dispatching.
27
+ *
28
+ * @private
29
+ * @since 1.0.0-rc.0
30
+ */
31
+ export declare const compute: (trigger: Registered, now: number, observed: number | undefined) => Effect.Effect<Due, TriggerError>;
32
+ //# sourceMappingURL=DueOccurrences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DueOccurrences.d.ts","sourceRoot":"","sources":["../../../src/internal/DueOccurrences.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,MAAM,MAAM,eAAe,CAAA;AAGvC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpD;;;;;;GAMG;AACH,MAAM,WAAW,GAAG;IAClB,QAAQ,CAAC,WAAW,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC3C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAoBD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,OAAO,GAClB,SAAS,UAAU,EACnB,KAAK,MAAM,EACX,UAAU,MAAM,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,YAAY,CA2C9B,CAAA"}