@shware/workflow 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (442) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -2
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/apns-relay.cjs +105 -0
  5. package/dist/apns-relay.cjs.map +1 -0
  6. package/dist/apns-relay.d.cts +28 -0
  7. package/dist/apns-relay.d.cts.map +1 -0
  8. package/dist/apns-relay.d.mts +28 -0
  9. package/dist/apns-relay.d.mts.map +1 -0
  10. package/dist/apns-relay.mjs +104 -0
  11. package/dist/apns-relay.mjs.map +1 -0
  12. package/dist/apns.cjs +121 -0
  13. package/dist/apns.cjs.map +1 -0
  14. package/dist/apns.d.cts +63 -0
  15. package/dist/apns.d.cts.map +1 -0
  16. package/dist/apns.d.mts +63 -0
  17. package/dist/apns.d.mts.map +1 -0
  18. package/dist/apns.mjs +116 -0
  19. package/dist/apns.mjs.map +1 -0
  20. package/dist/client.cjs +125 -0
  21. package/dist/client.cjs.map +1 -0
  22. package/dist/client.d.cts +108 -0
  23. package/dist/client.d.cts.map +1 -0
  24. package/dist/client.d.mts +108 -0
  25. package/dist/client.d.mts.map +1 -0
  26. package/dist/client.mjs +122 -0
  27. package/dist/client.mjs.map +1 -0
  28. package/dist/cloudflare/bindings.cjs +28 -0
  29. package/dist/cloudflare/bindings.cjs.map +1 -0
  30. package/dist/cloudflare/bindings.d.cts +144 -0
  31. package/dist/cloudflare/bindings.d.cts.map +1 -0
  32. package/dist/cloudflare/bindings.d.mts +144 -0
  33. package/dist/cloudflare/bindings.d.mts.map +1 -0
  34. package/dist/cloudflare/bindings.mjs +25 -0
  35. package/dist/cloudflare/bindings.mjs.map +1 -0
  36. package/dist/cloudflare/channels.cjs +164 -0
  37. package/dist/cloudflare/channels.cjs.map +1 -0
  38. package/dist/cloudflare/channels.d.cts +29 -0
  39. package/dist/cloudflare/channels.d.cts.map +1 -0
  40. package/dist/cloudflare/channels.d.mts +29 -0
  41. package/dist/cloudflare/channels.d.mts.map +1 -0
  42. package/dist/cloudflare/channels.mjs +161 -0
  43. package/dist/cloudflare/channels.mjs.map +1 -0
  44. package/dist/cloudflare/d1-env.cjs +17 -0
  45. package/dist/cloudflare/d1-env.cjs.map +1 -0
  46. package/dist/cloudflare/d1-env.d.cts +12 -0
  47. package/dist/cloudflare/d1-env.d.cts.map +1 -0
  48. package/dist/cloudflare/d1-env.d.mts +12 -0
  49. package/dist/cloudflare/d1-env.d.mts.map +1 -0
  50. package/dist/cloudflare/d1-env.mjs +16 -0
  51. package/dist/cloudflare/d1-env.mjs.map +1 -0
  52. package/dist/cloudflare/index.cjs +54 -0
  53. package/dist/cloudflare/index.d.cts +19 -0
  54. package/dist/cloudflare/index.d.mts +19 -0
  55. package/dist/cloudflare/index.mjs +16 -0
  56. package/dist/cloudflare/router.cjs +516 -0
  57. package/dist/cloudflare/router.cjs.map +1 -0
  58. package/dist/cloudflare/router.d.cts +157 -0
  59. package/dist/cloudflare/router.d.cts.map +1 -0
  60. package/dist/cloudflare/router.d.mts +157 -0
  61. package/dist/cloudflare/router.d.mts.map +1 -0
  62. package/dist/cloudflare/router.mjs +503 -0
  63. package/dist/cloudflare/router.mjs.map +1 -0
  64. package/dist/cloudflare/runner.cjs +225 -0
  65. package/dist/cloudflare/runner.cjs.map +1 -0
  66. package/dist/cloudflare/runner.d.cts +73 -0
  67. package/dist/cloudflare/runner.d.cts.map +1 -0
  68. package/dist/cloudflare/runner.d.mts +73 -0
  69. package/dist/cloudflare/runner.d.mts.map +1 -0
  70. package/dist/cloudflare/runner.mjs +224 -0
  71. package/dist/cloudflare/runner.mjs.map +1 -0
  72. package/dist/cloudflare/schema.sql +127 -0
  73. package/dist/cloudflare/senders.cjs +114 -0
  74. package/dist/cloudflare/senders.cjs.map +1 -0
  75. package/dist/cloudflare/senders.d.cts +68 -0
  76. package/dist/cloudflare/senders.d.cts.map +1 -0
  77. package/dist/cloudflare/senders.d.mts +68 -0
  78. package/dist/cloudflare/senders.d.mts.map +1 -0
  79. package/dist/cloudflare/senders.mjs +108 -0
  80. package/dist/cloudflare/senders.mjs.map +1 -0
  81. package/dist/cloudflare/worker.cjs +228 -0
  82. package/dist/cloudflare/worker.cjs.map +1 -0
  83. package/dist/cloudflare/worker.d.cts +92 -0
  84. package/dist/cloudflare/worker.d.cts.map +1 -0
  85. package/dist/cloudflare/worker.d.mts +92 -0
  86. package/dist/cloudflare/worker.d.mts.map +1 -0
  87. package/dist/cloudflare/worker.mjs +224 -0
  88. package/dist/cloudflare/worker.mjs.map +1 -0
  89. package/dist/cloudflare/workers.d.cjs +0 -0
  90. package/dist/cloudflare/workers.d.cts +44 -0
  91. package/dist/cloudflare/workers.d.cts.map +1 -0
  92. package/dist/cloudflare/workers.d.mts +44 -0
  93. package/dist/cloudflare/workers.d.mts.map +1 -0
  94. package/dist/config.cjs +151 -0
  95. package/dist/config.cjs.map +1 -0
  96. package/dist/config.d.cts +149 -0
  97. package/dist/config.d.cts.map +1 -0
  98. package/dist/config.d.mts +149 -0
  99. package/dist/config.d.mts.map +1 -0
  100. package/dist/config.mjs +143 -0
  101. package/dist/config.mjs.map +1 -0
  102. package/dist/contract.cjs +212 -0
  103. package/dist/contract.cjs.map +1 -0
  104. package/dist/contract.d.cts +90 -0
  105. package/dist/contract.d.cts.map +1 -0
  106. package/dist/contract.d.mts +90 -0
  107. package/dist/contract.d.mts.map +1 -0
  108. package/dist/contract.mjs +206 -0
  109. package/dist/contract.mjs.map +1 -0
  110. package/dist/dsl/action.cjs +29 -0
  111. package/dist/dsl/action.cjs.map +1 -0
  112. package/dist/dsl/action.d.cts +64 -0
  113. package/dist/dsl/action.d.cts.map +1 -0
  114. package/dist/dsl/action.d.mts +64 -0
  115. package/dist/dsl/action.d.mts.map +1 -0
  116. package/dist/dsl/action.mjs +28 -0
  117. package/dist/dsl/action.mjs.map +1 -0
  118. package/dist/dsl/base.cjs +34 -0
  119. package/dist/dsl/base.cjs.map +1 -0
  120. package/dist/dsl/base.d.cts +45 -0
  121. package/dist/dsl/base.d.cts.map +1 -0
  122. package/dist/dsl/base.d.mts +45 -0
  123. package/dist/dsl/base.d.mts.map +1 -0
  124. package/dist/dsl/base.mjs +30 -0
  125. package/dist/dsl/base.mjs.map +1 -0
  126. package/dist/dsl/bundle.cjs +207 -0
  127. package/dist/dsl/bundle.cjs.map +1 -0
  128. package/dist/dsl/bundle.d.cts +19 -0
  129. package/dist/dsl/bundle.d.cts.map +1 -0
  130. package/dist/dsl/bundle.d.mts +19 -0
  131. package/dist/dsl/bundle.d.mts.map +1 -0
  132. package/dist/dsl/bundle.mjs +206 -0
  133. package/dist/dsl/bundle.mjs.map +1 -0
  134. package/dist/dsl/condition.cjs +177 -0
  135. package/dist/dsl/condition.cjs.map +1 -0
  136. package/dist/dsl/condition.d.cts +140 -0
  137. package/dist/dsl/condition.d.cts.map +1 -0
  138. package/dist/dsl/condition.d.mts +140 -0
  139. package/dist/dsl/condition.d.mts.map +1 -0
  140. package/dist/dsl/condition.mjs +156 -0
  141. package/dist/dsl/condition.mjs.map +1 -0
  142. package/dist/dsl/flow.cjs +228 -0
  143. package/dist/dsl/flow.cjs.map +1 -0
  144. package/dist/dsl/flow.d.cts +223 -0
  145. package/dist/dsl/flow.d.cts.map +1 -0
  146. package/dist/dsl/flow.d.mts +223 -0
  147. package/dist/dsl/flow.d.mts.map +1 -0
  148. package/dist/dsl/flow.mjs +226 -0
  149. package/dist/dsl/flow.mjs.map +1 -0
  150. package/dist/dsl/index.cjs +38 -0
  151. package/dist/dsl/index.d.cts +10 -0
  152. package/dist/dsl/index.d.mts +10 -0
  153. package/dist/dsl/index.mjs +9 -0
  154. package/dist/dsl/refs.cjs +59 -0
  155. package/dist/dsl/refs.cjs.map +1 -0
  156. package/dist/dsl/refs.d.cts +88 -0
  157. package/dist/dsl/refs.d.cts.map +1 -0
  158. package/dist/dsl/refs.d.mts +88 -0
  159. package/dist/dsl/refs.d.mts.map +1 -0
  160. package/dist/dsl/refs.mjs +55 -0
  161. package/dist/dsl/refs.mjs.map +1 -0
  162. package/dist/dsl/template.cjs +65 -0
  163. package/dist/dsl/template.cjs.map +1 -0
  164. package/dist/dsl/template.d.cts +75 -0
  165. package/dist/dsl/template.d.cts.map +1 -0
  166. package/dist/dsl/template.d.mts +75 -0
  167. package/dist/dsl/template.d.mts.map +1 -0
  168. package/dist/dsl/template.mjs +63 -0
  169. package/dist/dsl/template.mjs.map +1 -0
  170. package/dist/dsl/trigger.cjs +40 -0
  171. package/dist/dsl/trigger.cjs.map +1 -0
  172. package/dist/dsl/trigger.d.cts +65 -0
  173. package/dist/dsl/trigger.d.cts.map +1 -0
  174. package/dist/dsl/trigger.d.mts +65 -0
  175. package/dist/dsl/trigger.d.mts.map +1 -0
  176. package/dist/dsl/trigger.mjs +39 -0
  177. package/dist/dsl/trigger.mjs.map +1 -0
  178. package/dist/dsl/workflow.cjs +121 -0
  179. package/dist/dsl/workflow.cjs.map +1 -0
  180. package/dist/dsl/workflow.d.cts +59 -0
  181. package/dist/dsl/workflow.d.cts.map +1 -0
  182. package/dist/dsl/workflow.d.mts +59 -0
  183. package/dist/dsl/workflow.d.mts.map +1 -0
  184. package/dist/dsl/workflow.mjs +120 -0
  185. package/dist/dsl/workflow.mjs.map +1 -0
  186. package/dist/engine/actions.cjs +27 -0
  187. package/dist/engine/actions.cjs.map +1 -0
  188. package/dist/engine/actions.d.cts +40 -0
  189. package/dist/engine/actions.d.cts.map +1 -0
  190. package/dist/engine/actions.d.mts +40 -0
  191. package/dist/engine/actions.d.mts.map +1 -0
  192. package/dist/engine/actions.mjs +26 -0
  193. package/dist/engine/actions.mjs.map +1 -0
  194. package/dist/engine/bucket.cjs +72 -0
  195. package/dist/engine/bucket.cjs.map +1 -0
  196. package/dist/engine/bucket.d.cts +32 -0
  197. package/dist/engine/bucket.d.cts.map +1 -0
  198. package/dist/engine/bucket.d.mts +32 -0
  199. package/dist/engine/bucket.d.mts.map +1 -0
  200. package/dist/engine/bucket.mjs +69 -0
  201. package/dist/engine/bucket.mjs.map +1 -0
  202. package/dist/engine/condition.cjs +166 -0
  203. package/dist/engine/condition.cjs.map +1 -0
  204. package/dist/engine/condition.d.cts +52 -0
  205. package/dist/engine/condition.d.cts.map +1 -0
  206. package/dist/engine/condition.d.mts +52 -0
  207. package/dist/engine/condition.d.mts.map +1 -0
  208. package/dist/engine/condition.mjs +161 -0
  209. package/dist/engine/condition.mjs.map +1 -0
  210. package/dist/engine/index.cjs +20 -0
  211. package/dist/engine/index.d.cts +8 -0
  212. package/dist/engine/index.d.mts +8 -0
  213. package/dist/engine/index.mjs +7 -0
  214. package/dist/engine/interpreter.cjs +428 -0
  215. package/dist/engine/interpreter.cjs.map +1 -0
  216. package/dist/engine/interpreter.d.cts +6 -0
  217. package/dist/engine/interpreter.d.cts.map +1 -0
  218. package/dist/engine/interpreter.d.mts +6 -0
  219. package/dist/engine/interpreter.d.mts.map +1 -0
  220. package/dist/engine/interpreter.mjs +427 -0
  221. package/dist/engine/interpreter.mjs.map +1 -0
  222. package/dist/engine/ports.cjs +0 -0
  223. package/dist/engine/ports.d.cts +252 -0
  224. package/dist/engine/ports.d.cts.map +1 -0
  225. package/dist/engine/ports.d.mts +252 -0
  226. package/dist/engine/ports.d.mts.map +1 -0
  227. package/dist/engine/ports.mjs +1 -0
  228. package/dist/engine/recipient.cjs +23 -0
  229. package/dist/engine/recipient.cjs.map +1 -0
  230. package/dist/engine/recipient.d.cts +13 -0
  231. package/dist/engine/recipient.d.cts.map +1 -0
  232. package/dist/engine/recipient.d.mts +13 -0
  233. package/dist/engine/recipient.d.mts.map +1 -0
  234. package/dist/engine/recipient.mjs +22 -0
  235. package/dist/engine/recipient.mjs.map +1 -0
  236. package/dist/engine/time-scale.cjs +54 -0
  237. package/dist/engine/time-scale.cjs.map +1 -0
  238. package/dist/engine/time-scale.d.cts +31 -0
  239. package/dist/engine/time-scale.d.cts.map +1 -0
  240. package/dist/engine/time-scale.d.mts +31 -0
  241. package/dist/engine/time-scale.d.mts.map +1 -0
  242. package/dist/engine/time-scale.mjs +52 -0
  243. package/dist/engine/time-scale.mjs.map +1 -0
  244. package/dist/engine/time-window.cjs +149 -0
  245. package/dist/engine/time-window.cjs.map +1 -0
  246. package/dist/engine/time-window.d.cts +22 -0
  247. package/dist/engine/time-window.d.cts.map +1 -0
  248. package/dist/engine/time-window.d.mts +22 -0
  249. package/dist/engine/time-window.d.mts.map +1 -0
  250. package/dist/engine/time-window.mjs +147 -0
  251. package/dist/engine/time-window.mjs.map +1 -0
  252. package/dist/fcm.cjs +125 -0
  253. package/dist/fcm.cjs.map +1 -0
  254. package/dist/fcm.d.cts +48 -0
  255. package/dist/fcm.d.cts.map +1 -0
  256. package/dist/fcm.d.mts +48 -0
  257. package/dist/fcm.d.mts.map +1 -0
  258. package/dist/fcm.mjs +121 -0
  259. package/dist/fcm.mjs.map +1 -0
  260. package/dist/hash.cjs +245 -0
  261. package/dist/hash.cjs.map +1 -0
  262. package/dist/hash.d.cts +30 -0
  263. package/dist/hash.d.cts.map +1 -0
  264. package/dist/hash.d.mts +30 -0
  265. package/dist/hash.d.mts.map +1 -0
  266. package/dist/hash.mjs +239 -0
  267. package/dist/hash.mjs.map +1 -0
  268. package/dist/index.cjs +100 -0
  269. package/dist/index.d.cts +18 -0
  270. package/dist/index.d.mts +18 -0
  271. package/dist/index.mjs +17 -0
  272. package/dist/ir.cjs +366 -0
  273. package/dist/ir.cjs.map +1 -0
  274. package/dist/ir.d.cts +486 -0
  275. package/dist/ir.d.cts.map +1 -0
  276. package/dist/ir.d.mts +486 -0
  277. package/dist/ir.d.mts.map +1 -0
  278. package/dist/ir.mjs +344 -0
  279. package/dist/ir.mjs.map +1 -0
  280. package/dist/jwt.cjs +22 -0
  281. package/dist/jwt.cjs.map +1 -0
  282. package/dist/jwt.d.cts +9 -0
  283. package/dist/jwt.d.cts.map +1 -0
  284. package/dist/jwt.d.mts +9 -0
  285. package/dist/jwt.d.mts.map +1 -0
  286. package/dist/jwt.mjs +20 -0
  287. package/dist/jwt.mjs.map +1 -0
  288. package/dist/plan.cjs +223 -0
  289. package/dist/plan.cjs.map +1 -0
  290. package/dist/plan.d.cts +68 -0
  291. package/dist/plan.d.cts.map +1 -0
  292. package/dist/plan.d.mts +68 -0
  293. package/dist/plan.d.mts.map +1 -0
  294. package/dist/plan.mjs +221 -0
  295. package/dist/plan.mjs.map +1 -0
  296. package/dist/postgres-js.cjs +42 -0
  297. package/dist/postgres-js.cjs.map +1 -0
  298. package/dist/postgres-js.d.cts +28 -0
  299. package/dist/postgres-js.d.cts.map +1 -0
  300. package/dist/postgres-js.d.mts +28 -0
  301. package/dist/postgres-js.d.mts.map +1 -0
  302. package/dist/postgres-js.mjs +38 -0
  303. package/dist/postgres-js.mjs.map +1 -0
  304. package/dist/provenance.cjs +67 -0
  305. package/dist/provenance.cjs.map +1 -0
  306. package/dist/provenance.d.cts +41 -0
  307. package/dist/provenance.d.cts.map +1 -0
  308. package/dist/provenance.d.mts +41 -0
  309. package/dist/provenance.d.mts.map +1 -0
  310. package/dist/provenance.mjs +65 -0
  311. package/dist/provenance.mjs.map +1 -0
  312. package/dist/push.cjs +14 -0
  313. package/dist/push.cjs.map +1 -0
  314. package/dist/push.d.cts +16 -0
  315. package/dist/push.d.cts.map +1 -0
  316. package/dist/push.d.mts +16 -0
  317. package/dist/push.d.mts.map +1 -0
  318. package/dist/push.mjs +13 -0
  319. package/dist/push.mjs.map +1 -0
  320. package/dist/react-email.cjs +48 -0
  321. package/dist/react-email.cjs.map +1 -0
  322. package/dist/react-email.d.cts +7 -0
  323. package/dist/react-email.d.cts.map +1 -0
  324. package/dist/react-email.d.mts +7 -0
  325. package/dist/react-email.d.mts.map +1 -0
  326. package/dist/react-email.mjs +47 -0
  327. package/dist/react-email.mjs.map +1 -0
  328. package/dist/reporting/classify.cjs +42 -0
  329. package/dist/reporting/classify.cjs.map +1 -0
  330. package/dist/reporting/classify.d.cts +60 -0
  331. package/dist/reporting/classify.d.cts.map +1 -0
  332. package/dist/reporting/classify.d.mts +60 -0
  333. package/dist/reporting/classify.d.mts.map +1 -0
  334. package/dist/reporting/classify.mjs +38 -0
  335. package/dist/reporting/classify.mjs.map +1 -0
  336. package/dist/reporting/observer.cjs +63 -0
  337. package/dist/reporting/observer.cjs.map +1 -0
  338. package/dist/reporting/observer.d.cts +18 -0
  339. package/dist/reporting/observer.d.cts.map +1 -0
  340. package/dist/reporting/observer.d.mts +18 -0
  341. package/dist/reporting/observer.d.mts.map +1 -0
  342. package/dist/reporting/observer.mjs +62 -0
  343. package/dist/reporting/observer.mjs.map +1 -0
  344. package/dist/reporting/tracking.cjs +153 -0
  345. package/dist/reporting/tracking.cjs.map +1 -0
  346. package/dist/reporting/tracking.d.cts +43 -0
  347. package/dist/reporting/tracking.d.cts.map +1 -0
  348. package/dist/reporting/tracking.d.mts +43 -0
  349. package/dist/reporting/tracking.d.mts.map +1 -0
  350. package/dist/reporting/tracking.mjs +151 -0
  351. package/dist/reporting/tracking.mjs.map +1 -0
  352. package/dist/runtime.cjs +19 -0
  353. package/dist/runtime.cjs.map +1 -0
  354. package/dist/runtime.d.cts +30 -0
  355. package/dist/runtime.d.cts.map +1 -0
  356. package/dist/runtime.d.mts +30 -0
  357. package/dist/runtime.d.mts.map +1 -0
  358. package/dist/runtime.mjs +18 -0
  359. package/dist/runtime.mjs.map +1 -0
  360. package/dist/store/d1.cjs +185 -0
  361. package/dist/store/d1.cjs.map +1 -0
  362. package/dist/store/d1.d.cts +55 -0
  363. package/dist/store/d1.d.cts.map +1 -0
  364. package/dist/store/d1.d.mts +55 -0
  365. package/dist/store/d1.d.mts.map +1 -0
  366. package/dist/store/d1.mjs +184 -0
  367. package/dist/store/d1.mjs.map +1 -0
  368. package/dist/store/facts.cjs +60 -0
  369. package/dist/store/facts.cjs.map +1 -0
  370. package/dist/store/facts.d.cts +38 -0
  371. package/dist/store/facts.d.cts.map +1 -0
  372. package/dist/store/facts.d.mts +38 -0
  373. package/dist/store/facts.d.mts.map +1 -0
  374. package/dist/store/facts.mjs +58 -0
  375. package/dist/store/facts.mjs.map +1 -0
  376. package/dist/store/index.cjs +31 -0
  377. package/dist/store/index.cjs.map +1 -0
  378. package/dist/store/index.d.cts +291 -0
  379. package/dist/store/index.d.cts.map +1 -0
  380. package/dist/store/index.d.mts +291 -0
  381. package/dist/store/index.d.mts.map +1 -0
  382. package/dist/store/index.mjs +22 -0
  383. package/dist/store/index.mjs.map +1 -0
  384. package/dist/store/postgres-reporting.cjs +213 -0
  385. package/dist/store/postgres-reporting.cjs.map +1 -0
  386. package/dist/store/postgres-reporting.d.cts +63 -0
  387. package/dist/store/postgres-reporting.d.cts.map +1 -0
  388. package/dist/store/postgres-reporting.d.mts +63 -0
  389. package/dist/store/postgres-reporting.d.mts.map +1 -0
  390. package/dist/store/postgres-reporting.mjs +212 -0
  391. package/dist/store/postgres-reporting.mjs.map +1 -0
  392. package/dist/store/postgres.cjs +309 -0
  393. package/dist/store/postgres.cjs.map +1 -0
  394. package/dist/store/postgres.d.cts +77 -0
  395. package/dist/store/postgres.d.cts.map +1 -0
  396. package/dist/store/postgres.d.mts +77 -0
  397. package/dist/store/postgres.d.mts.map +1 -0
  398. package/dist/store/postgres.mjs +306 -0
  399. package/dist/store/postgres.mjs.map +1 -0
  400. package/dist/store/sql.cjs +24 -0
  401. package/dist/store/sql.cjs.map +1 -0
  402. package/dist/store/sql.d.cts +25 -0
  403. package/dist/store/sql.d.cts.map +1 -0
  404. package/dist/store/sql.d.mts +25 -0
  405. package/dist/store/sql.d.mts.map +1 -0
  406. package/dist/store/sql.mjs +23 -0
  407. package/dist/store/sql.mjs.map +1 -0
  408. package/dist/store/tables.cjs +404 -0
  409. package/dist/store/tables.cjs.map +1 -0
  410. package/dist/store/tables.d.cts +81 -0
  411. package/dist/store/tables.d.cts.map +1 -0
  412. package/dist/store/tables.d.mts +81 -0
  413. package/dist/store/tables.d.mts.map +1 -0
  414. package/dist/store/tables.mjs +399 -0
  415. package/dist/store/tables.mjs.map +1 -0
  416. package/dist/templates.cjs +247 -0
  417. package/dist/templates.cjs.map +1 -0
  418. package/dist/templates.d.cts +187 -0
  419. package/dist/templates.d.cts.map +1 -0
  420. package/dist/templates.d.mts +187 -0
  421. package/dist/templates.d.mts.map +1 -0
  422. package/dist/templates.mjs +230 -0
  423. package/dist/templates.mjs.map +1 -0
  424. package/dist/webhooks.cjs +96 -0
  425. package/dist/webhooks.cjs.map +1 -0
  426. package/dist/webhooks.d.cts +25 -0
  427. package/dist/webhooks.d.cts.map +1 -0
  428. package/dist/webhooks.d.mts +25 -0
  429. package/dist/webhooks.d.mts.map +1 -0
  430. package/dist/webhooks.mjs +94 -0
  431. package/dist/webhooks.mjs.map +1 -0
  432. package/dist/webpush.cjs +180 -0
  433. package/dist/webpush.cjs.map +1 -0
  434. package/dist/webpush.d.cts +53 -0
  435. package/dist/webpush.d.cts.map +1 -0
  436. package/dist/webpush.d.mts +53 -0
  437. package/dist/webpush.d.mts.map +1 -0
  438. package/dist/webpush.mjs +175 -0
  439. package/dist/webpush.mjs.map +1 -0
  440. package/package.json +189 -3
  441. package/src/cloudflare/schema.sql +127 -0
  442. package/src/cloudflare/workers.d.ts +44 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Shware Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,155 @@
1
- # Temporary Holding Version
1
+ # @shware/workflow
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Code-first customer journeys. Workflows are written as TypeScript, compiled to a
4
+ versioned IR, and executed by a single generic runner: the IR is data, so one
5
+ deployed Worker runs every journey.
6
+
7
+ ```ts
8
+ const welcome = workflow({ id: 'welcome', trigger: trigger.event(e.sign_up) })
9
+ .email(gettingStarted)
10
+ .delay('3 days')
11
+ .waitUntil(performed(e.activated), {
12
+ timeout: '7 days',
13
+ onTimeout: (w) => w.email(nudge),
14
+ });
15
+ ```
16
+
17
+ ## The three planes
18
+
19
+ | Plane | What it is | Where it lives |
20
+ | --------- | ------------------------------------------ | ----------------------------- |
21
+ | Authoring | The DSL, compiled to IR by `compileBundle` | `@shware/workflow` |
22
+ | Execution | The interpreter, over abstract ports | `@shware/workflow/engine` |
23
+ | Adapter | Cloudflare bindings, ingest, runner | `@shware/workflow/cloudflare` |
24
+
25
+ The interpreter never touches a database or a network. Everything it needs
26
+ arrives through the ports in `engine/ports.ts`, which is what makes a journey
27
+ testable without deploying one.
28
+
29
+ ## Versioning
30
+
31
+ Every workflow carries a `contentHash` over its own IR, with metadata excluded,
32
+ so a reworded description is not a new version but a changed delay is. The hash
33
+ is the KV address the IR is stored under, and a journey is pinned to the hash it
34
+ entered on. An in-flight journey therefore finishes on the version it started,
35
+ however many times you deploy underneath it.
36
+
37
+ `plan()` diffs a compiled bundle against what is deployed and reports which
38
+ workflows actually changed. Deploy recomputes every declared hash and refuses a
39
+ bundle whose hashes do not describe its contents.
40
+
41
+ ## Cloudflare setup
42
+
43
+ The adapter needs three bindings, named as in `JourneyEnv`:
44
+
45
+ | Binding | Type | Holds |
46
+ | ------------- | ------------ | ----------------------------------------- |
47
+ | `WORKFLOW_KV` | KV namespace | Compiled IR, content-addressed by hash |
48
+ | `DB` | D1 database | Events, profiles, routing, entries, wakes |
49
+ | `JOURNEY` | Workflow | The `JourneyRunner` entrypoint |
50
+
51
+ Set `API_TOKEN` in production. Without it every mutating endpoint is open, and
52
+ `/deploy` rewrites the whole routing table.
53
+
54
+ The D1 schema ships with the package. Apply it before the first deploy:
55
+
56
+ ```bash
57
+ wrangler d1 execute <database> --file node_modules/@shware/workflow/dist/cloudflare/schema.sql
58
+ ```
59
+
60
+ Nothing in the adapter creates these tables at runtime, so this step is not
61
+ optional. The same file lives at `src/cloudflare/schema.sql` in this repo and is
62
+ the single source for both.
63
+
64
+ There are no migrations, and re-applying the file is not one: every statement is
65
+ `CREATE TABLE IF NOT EXISTS`, so an existing database keeps its old tables and
66
+ the first insert needing a new column fails. Recreate it instead. The demo has
67
+ `pnpm db:reset`.
68
+
69
+ ## Postgres
70
+
71
+ A host on Postgres (through Hyperdrive, say) keeps Cloudflare Workflows for
72
+ execution and the data in its own database:
73
+
74
+ ```ts
75
+ const journeys = journeyWorker<Env>({
76
+ config,
77
+ store: (env, database) => postgresJsStore(env.HYPERDRIVE.connectionString, database),
78
+ });
79
+ export default { fetch: journeys.fetch, scheduled: journeys.scheduled };
80
+ ```
81
+
82
+ The engine's tables live in one Postgres schema, `workflow` unless the config
83
+ says `database: { schema }`, so their short names never meet the app's. The
84
+ host owns the migrations. `npx workflow-ui generate --out <file>` (from
85
+ `@shware/workflow-ui`) writes the tables as one drizzle file on that schema;
86
+ re-export it from your drizzle schema and let drizzle-kit write the migration.
87
+ Run `--check` in CI to catch an SDK upgrade that changed them. Without drizzle,
88
+ `renderPostgresDdl()` returns the same tables as plain DDL. Nothing creates or
89
+ alters them at runtime: `POST /bundle/deploy` checks the live database first
90
+ and answers 503 with every missing table or column.
91
+
92
+ On Postgres the engine also keeps:
93
+
94
+ - **Push devices.** The app registers each token with
95
+ `JourneyClient.registerDevice(userId, { token, platform })` and forgets one
96
+ with `removeDevice(token)`. Tokens APNs / FCM report as gone are dropped.
97
+ - **Reporting.** Instances, every message copy, opens and clicks, recorded
98
+ by the default observer. With `WORKFLOW_ORIGIN` set, email links and an open
99
+ pixel are tracked through the Worker's `/t/...` routes. Each open and click
100
+ is classified as a person's or a machine's (Apple Mail Privacy Protection,
101
+ link scanners, prefetches). `emails.unsubscribe` gets a confirmation page.
102
+ A push carries `message_id`; report the tap with `messageOpened`.
103
+ - **Segments.** The cron trigger runs `sweepSegments`, which keeps every
104
+ segment's membership and size and starts the journeys of segments entered by
105
+ the clock alone. `POST /segments/sweep` runs it on demand.
106
+
107
+ `@shware/workflow-ui/postgres` reads all of it for the studio.
108
+
109
+ ## HTTP surface
110
+
111
+ `handleRequest` serves four endpoints.
112
+
113
+ - `GET /health` is the only unauthenticated one.
114
+ - `POST /events` takes `{ userId, event, payload?, ts?, id? }` and answers with
115
+ the id the occurrence was recorded under. Event names beginning with `$` are
116
+ reserved for internal use.
117
+ - `POST /identify` takes `{ userId, props }` and merges the properties into the
118
+ profile. A property set to `null` is removed, which is the only way to unset
119
+ one.
120
+ - `POST /deploy` takes a compiled bundle and swaps the routing table atomically.
121
+
122
+ ### Give each event an `id`
123
+
124
+ `id` is the occurrence's identity, and sending the same one twice records it
125
+ once. This matters because every count- and window-based condition reads the
126
+ event log, so a second copy of one event silently changes what a journey
127
+ decides.
128
+
129
+ Only the sender knows whether two deliveries describe one occurrence or two, so
130
+ the engine never guesses. An event that arrives without an id is given a fresh
131
+ one, which records it as its own occurrence. If you retry a request, send an id
132
+ you can reproduce and the retry costs nothing.
133
+
134
+ ```json
135
+ { "id": "order-8841-confirmed", "userId": "u_123", "event": "purchase" }
136
+ ```
137
+
138
+ Use something that identifies the occurrence, not the event name: a row id from
139
+ your own system, an upstream webhook's delivery id, or a message id from your
140
+ queue. Workflows emitting events through `send_event` already do this, passing
141
+ the node's identity within the running instance.
142
+
143
+ ## Entry policy
144
+
145
+ A user enters a workflow at most once. The entry ledger's primary key enforces
146
+ it, which also makes the check atomic under concurrent ingests. The one
147
+ exception is an entry whose instance died: that row records a failure, not a
148
+ journey the user received, so the next trigger reclaims it. Re-entry beyond that
149
+ is a design question this package does not yet answer.
150
+
151
+ ## Testing
152
+
153
+ See [TESTING.md](./TESTING.md). The interpreter runs against in-memory ports and
154
+ the Cloudflare adapter against fakes that dispatch on the exact SQL strings the
155
+ real code sends, so changing a query means updating the fake, deliberately.
@@ -0,0 +1,23 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ exports.__toESM = __toESM;
@@ -0,0 +1,105 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_http = require("node:http");
3
+ let node_http2 = require("node:http2");
4
+ //#region src/apns-relay.ts
5
+ const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
6
+ /** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
7
+ const FORWARDED = [
8
+ "authorization",
9
+ "apns-topic",
10
+ "apns-push-type",
11
+ "apns-priority",
12
+ "apns-expiration",
13
+ "apns-collapse-id",
14
+ "apns-id",
15
+ "content-type"
16
+ ];
17
+ function startApnsRelay(options) {
18
+ const allow = new Set(options.allow ?? DEFAULT_ALLOW);
19
+ const sessions = /* @__PURE__ */ new Map();
20
+ function session(upstream) {
21
+ const existing = sessions.get(upstream);
22
+ if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
23
+ const created = (0, node_http2.connect)(upstream);
24
+ const forget = () => {
25
+ if (sessions.get(upstream) === created) sessions.delete(upstream);
26
+ };
27
+ created.on("error", forget);
28
+ created.on("close", forget);
29
+ sessions.set(upstream, created);
30
+ return created;
31
+ }
32
+ function relay(req, res) {
33
+ const upstreamHeader = req.headers["apns-relay-upstream"];
34
+ const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
35
+ if (upstream === void 0 || !allow.has(upstream)) {
36
+ res.writeHead(400, { "content-type": "application/json" });
37
+ res.end(JSON.stringify({
38
+ reason: "RelayUpstreamNotAllowed",
39
+ upstream: upstream ?? null
40
+ }));
41
+ return;
42
+ }
43
+ const chunks = [];
44
+ req.on("data", (chunk) => chunks.push(chunk));
45
+ req.on("end", () => {
46
+ const headers = {
47
+ ":method": req.method ?? "POST",
48
+ ":path": req.url ?? "/"
49
+ };
50
+ for (const name of FORWARDED) {
51
+ const value = req.headers[name];
52
+ if (typeof value === "string") headers[name] = value;
53
+ }
54
+ let stream;
55
+ try {
56
+ stream = session(upstream).request(headers);
57
+ } catch (error) {
58
+ res.writeHead(502, { "content-type": "application/json" });
59
+ res.end(JSON.stringify({
60
+ reason: "RelayConnectFailed",
61
+ error: String(error)
62
+ }));
63
+ return;
64
+ }
65
+ const body = [];
66
+ stream.on("response", (responseHeaders) => {
67
+ const status = responseHeaders[":status"] ?? 502;
68
+ const apnsId = responseHeaders["apns-id"];
69
+ res.writeHead(status, {
70
+ "content-type": "application/json",
71
+ ...typeof apnsId === "string" ? { "apns-id": apnsId } : {}
72
+ });
73
+ });
74
+ stream.on("data", (chunk) => body.push(chunk));
75
+ stream.on("end", () => res.end(Buffer.concat(body)));
76
+ stream.on("error", (error) => {
77
+ if (!res.headersSent) res.writeHead(502, { "content-type": "application/json" });
78
+ res.end(JSON.stringify({
79
+ reason: "RelayUpstreamError",
80
+ error: String(error)
81
+ }));
82
+ });
83
+ stream.end(Buffer.concat(chunks));
84
+ });
85
+ }
86
+ const server = (0, node_http.createServer)(relay);
87
+ return new Promise((resolve, reject) => {
88
+ server.once("error", reject);
89
+ server.listen(options.port, options.hostname ?? "127.0.0.1", () => {
90
+ const address = server.address();
91
+ resolve({
92
+ port: typeof address === "object" && address !== null ? address.port : options.port,
93
+ close: () => new Promise((done) => {
94
+ for (const open of sessions.values()) open.close();
95
+ sessions.clear();
96
+ server.close(() => done());
97
+ })
98
+ });
99
+ });
100
+ });
101
+ }
102
+ //#endregion
103
+ exports.startApnsRelay = startApnsRelay;
104
+
105
+ //# sourceMappingURL=apns-relay.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apns-relay.cjs","names":["connect","createServer"],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = new Map<string, ClientHttp2Session>();\n\n function session(upstream: string): ClientHttp2Session {\n const existing = sessions.get(upstream);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(upstream);\n const forget = () => {\n if (sessions.get(upstream) === created) sessions.delete(upstream);\n };\n created.on('error', forget);\n created.on('close', forget);\n sessions.set(upstream, created);\n return created;\n }\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ //#region src/apns-relay.d.ts
2
+ /**
3
+ * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay
4
+ * for APNs, run under Node next to `wrangler dev`.
5
+ *
6
+ * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot
7
+ * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a
8
+ * local Worker has nowhere to go. A deployed Worker needs none of this: its
9
+ * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the
10
+ * origin named in its `apns-relay-upstream` header (the sender sets it along
11
+ * with `origin`), keeping one HTTP/2 session per upstream.
12
+ *
13
+ * Only Apple's two hosts are relayed by default; `allow` widens that for tests.
14
+ */
15
+ export interface ApnsRelayOptions {
16
+ port: number;
17
+ /** Interface to listen on; loopback by default — the relay has no auth of its own. */
18
+ hostname?: string;
19
+ /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */
20
+ allow?: readonly string[];
21
+ }
22
+ export interface ApnsRelay {
23
+ port: number;
24
+ close(): Promise<void>;
25
+ }
26
+ export declare function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay>;
27
+ //#endregion
28
+ //# sourceMappingURL=apns-relay.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apns-relay.d.cts","names":[],"sources":["../src/apns-relay.ts"],"mappings":";;;;;;;;;;;;;;iBAiBiB;EACf;;EAEA;;EAEA;;iBAGe;EACf;EACA,SAAS;;wBAiBK,eAAe,SAAS,mBAAmB,QAAQ"}
@@ -0,0 +1,28 @@
1
+ //#region src/apns-relay.d.ts
2
+ /**
3
+ * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay
4
+ * for APNs, run under Node next to `wrangler dev`.
5
+ *
6
+ * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot
7
+ * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a
8
+ * local Worker has nowhere to go. A deployed Worker needs none of this: its
9
+ * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the
10
+ * origin named in its `apns-relay-upstream` header (the sender sets it along
11
+ * with `origin`), keeping one HTTP/2 session per upstream.
12
+ *
13
+ * Only Apple's two hosts are relayed by default; `allow` widens that for tests.
14
+ */
15
+ export interface ApnsRelayOptions {
16
+ port: number;
17
+ /** Interface to listen on; loopback by default — the relay has no auth of its own. */
18
+ hostname?: string;
19
+ /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */
20
+ allow?: readonly string[];
21
+ }
22
+ export interface ApnsRelay {
23
+ port: number;
24
+ close(): Promise<void>;
25
+ }
26
+ export declare function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay>;
27
+ //#endregion
28
+ //# sourceMappingURL=apns-relay.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apns-relay.d.mts","names":[],"sources":["../src/apns-relay.ts"],"mappings":";;;;;;;;;;;;;;iBAiBiB;EACf;;EAEA;;EAEA;;iBAGe;EACf;EACA,SAAS;;wBAiBK,eAAe,SAAS,mBAAmB,QAAQ"}
@@ -0,0 +1,104 @@
1
+ import { createServer } from "node:http";
2
+ import { connect } from "node:http2";
3
+ //#region src/apns-relay.ts
4
+ const DEFAULT_ALLOW = ["https://api.push.apple.com", "https://api.sandbox.push.apple.com"];
5
+ /** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */
6
+ const FORWARDED = [
7
+ "authorization",
8
+ "apns-topic",
9
+ "apns-push-type",
10
+ "apns-priority",
11
+ "apns-expiration",
12
+ "apns-collapse-id",
13
+ "apns-id",
14
+ "content-type"
15
+ ];
16
+ function startApnsRelay(options) {
17
+ const allow = new Set(options.allow ?? DEFAULT_ALLOW);
18
+ const sessions = /* @__PURE__ */ new Map();
19
+ function session(upstream) {
20
+ const existing = sessions.get(upstream);
21
+ if (existing !== void 0 && !existing.closed && !existing.destroyed) return existing;
22
+ const created = connect(upstream);
23
+ const forget = () => {
24
+ if (sessions.get(upstream) === created) sessions.delete(upstream);
25
+ };
26
+ created.on("error", forget);
27
+ created.on("close", forget);
28
+ sessions.set(upstream, created);
29
+ return created;
30
+ }
31
+ function relay(req, res) {
32
+ const upstreamHeader = req.headers["apns-relay-upstream"];
33
+ const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;
34
+ if (upstream === void 0 || !allow.has(upstream)) {
35
+ res.writeHead(400, { "content-type": "application/json" });
36
+ res.end(JSON.stringify({
37
+ reason: "RelayUpstreamNotAllowed",
38
+ upstream: upstream ?? null
39
+ }));
40
+ return;
41
+ }
42
+ const chunks = [];
43
+ req.on("data", (chunk) => chunks.push(chunk));
44
+ req.on("end", () => {
45
+ const headers = {
46
+ ":method": req.method ?? "POST",
47
+ ":path": req.url ?? "/"
48
+ };
49
+ for (const name of FORWARDED) {
50
+ const value = req.headers[name];
51
+ if (typeof value === "string") headers[name] = value;
52
+ }
53
+ let stream;
54
+ try {
55
+ stream = session(upstream).request(headers);
56
+ } catch (error) {
57
+ res.writeHead(502, { "content-type": "application/json" });
58
+ res.end(JSON.stringify({
59
+ reason: "RelayConnectFailed",
60
+ error: String(error)
61
+ }));
62
+ return;
63
+ }
64
+ const body = [];
65
+ stream.on("response", (responseHeaders) => {
66
+ const status = responseHeaders[":status"] ?? 502;
67
+ const apnsId = responseHeaders["apns-id"];
68
+ res.writeHead(status, {
69
+ "content-type": "application/json",
70
+ ...typeof apnsId === "string" ? { "apns-id": apnsId } : {}
71
+ });
72
+ });
73
+ stream.on("data", (chunk) => body.push(chunk));
74
+ stream.on("end", () => res.end(Buffer.concat(body)));
75
+ stream.on("error", (error) => {
76
+ if (!res.headersSent) res.writeHead(502, { "content-type": "application/json" });
77
+ res.end(JSON.stringify({
78
+ reason: "RelayUpstreamError",
79
+ error: String(error)
80
+ }));
81
+ });
82
+ stream.end(Buffer.concat(chunks));
83
+ });
84
+ }
85
+ const server = createServer(relay);
86
+ return new Promise((resolve, reject) => {
87
+ server.once("error", reject);
88
+ server.listen(options.port, options.hostname ?? "127.0.0.1", () => {
89
+ const address = server.address();
90
+ resolve({
91
+ port: typeof address === "object" && address !== null ? address.port : options.port,
92
+ close: () => new Promise((done) => {
93
+ for (const open of sessions.values()) open.close();
94
+ sessions.clear();
95
+ server.close(() => done());
96
+ })
97
+ });
98
+ });
99
+ });
100
+ }
101
+ //#endregion
102
+ export { startApnsRelay };
103
+
104
+ //# sourceMappingURL=apns-relay.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apns-relay.mjs","names":[],"sources":["../src/apns-relay.ts"],"sourcesContent":["import { type IncomingMessage, type ServerResponse, createServer } from 'node:http';\nimport { type ClientHttp2Session, connect } from 'node:http2';\n\n/**\n * `@shware/workflow/apns-relay` — a development-only HTTP/1.1 → HTTP/2 relay\n * for APNs, run under Node next to `wrangler dev`.\n *\n * APNs accepts HTTP/2 only, and workerd (the local Workers runtime) cannot\n * make an HTTP/2 subrequest, so a journey that pushes to an iPhone from a\n * local Worker has nowhere to go. A deployed Worker needs none of this: its\n * `fetch` negotiates HTTP/2 with Apple. The relay forwards each request to the\n * origin named in its `apns-relay-upstream` header (the sender sets it along\n * with `origin`), keeping one HTTP/2 session per upstream.\n *\n * Only Apple's two hosts are relayed by default; `allow` widens that for tests.\n */\n\nexport interface ApnsRelayOptions {\n port: number;\n /** Interface to listen on; loopback by default — the relay has no auth of its own. */\n hostname?: string;\n /** Upstream origins that may be relayed; Apple's production and sandbox hosts by default. */\n allow?: readonly string[];\n}\n\nexport interface ApnsRelay {\n port: number;\n close(): Promise<void>;\n}\n\nconst DEFAULT_ALLOW = ['https://api.push.apple.com', 'https://api.sandbox.push.apple.com'];\n\n/** Request headers APNs reads; everything else (host, content-length, …) is the relay's own business. */\nconst FORWARDED = [\n 'authorization',\n 'apns-topic',\n 'apns-push-type',\n 'apns-priority',\n 'apns-expiration',\n 'apns-collapse-id',\n 'apns-id',\n 'content-type',\n];\n\nexport function startApnsRelay(options: ApnsRelayOptions): Promise<ApnsRelay> {\n const allow = new Set(options.allow ?? DEFAULT_ALLOW);\n const sessions = new Map<string, ClientHttp2Session>();\n\n function session(upstream: string): ClientHttp2Session {\n const existing = sessions.get(upstream);\n if (existing !== undefined && !existing.closed && !existing.destroyed) return existing;\n const created = connect(upstream);\n const forget = () => {\n if (sessions.get(upstream) === created) sessions.delete(upstream);\n };\n created.on('error', forget);\n created.on('close', forget);\n sessions.set(upstream, created);\n return created;\n }\n\n function relay(req: IncomingMessage, res: ServerResponse): void {\n const upstreamHeader = req.headers['apns-relay-upstream'];\n const upstream = Array.isArray(upstreamHeader) ? upstreamHeader[0] : upstreamHeader;\n if (upstream === undefined || !allow.has(upstream)) {\n res.writeHead(400, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamNotAllowed', upstream: upstream ?? null }));\n return;\n }\n\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n const headers: Record<string, string> = {\n ':method': req.method ?? 'POST',\n ':path': req.url ?? '/',\n };\n for (const name of FORWARDED) {\n const value = req.headers[name];\n if (typeof value === 'string') headers[name] = value;\n }\n let stream;\n try {\n stream = session(upstream).request(headers);\n } catch (error) {\n res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayConnectFailed', error: String(error) }));\n return;\n }\n const body: Buffer[] = [];\n stream.on('response', (responseHeaders) => {\n const status = responseHeaders[':status'] ?? 502;\n const apnsId = responseHeaders['apns-id'];\n res.writeHead(status, {\n 'content-type': 'application/json',\n ...(typeof apnsId === 'string' ? { 'apns-id': apnsId } : {}),\n });\n });\n stream.on('data', (chunk: Buffer) => body.push(chunk));\n stream.on('end', () => res.end(Buffer.concat(body)));\n stream.on('error', (error) => {\n if (!res.headersSent) res.writeHead(502, { 'content-type': 'application/json' });\n res.end(JSON.stringify({ reason: 'RelayUpstreamError', error: String(error) }));\n });\n stream.end(Buffer.concat(chunks));\n });\n }\n\n const server = createServer(relay);\n return new Promise((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port, options.hostname ?? '127.0.0.1', () => {\n const address = server.address();\n const port = typeof address === 'object' && address !== null ? address.port : options.port;\n resolve({\n port,\n close: () =>\n new Promise((done) => {\n for (const open of sessions.values()) open.close();\n sessions.clear();\n server.close(() => done());\n }),\n });\n });\n });\n}\n"],"mappings":"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"}
package/dist/apns.cjs ADDED
@@ -0,0 +1,121 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_jwt = require("./jwt.cjs");
3
+ //#region src/apns.ts
4
+ const APNS_HOSTS = {
5
+ sandbox: "https://api.sandbox.push.apple.com",
6
+ production: "https://api.push.apple.com"
7
+ };
8
+ /** Reasons APNs gives for a token that will never deliver again — not worth a retry. */
9
+ const DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
10
+ "Unregistered",
11
+ "BadDeviceToken",
12
+ "DeviceTokenNotForTopic",
13
+ "ExpiredToken"
14
+ ]);
15
+ /** Apple: reuse a provider token for at least 20 minutes, never past 60. */
16
+ const TOKEN_TTL_MS = 3e6;
17
+ var ApnsPushSender = class {
18
+ options;
19
+ token;
20
+ constructor(options) {
21
+ this.options = options;
22
+ }
23
+ async send(message) {
24
+ if (message.channel !== "push") throw new Error(`ApnsPushSender: unsupported channel '${message.channel}'`);
25
+ if (message.recipient === void 0) throw new Error(`ApnsPushSender: no push token for user '${message.userId}'`);
26
+ const { title, body, image, url } = await this.options.render(message.template, message.props);
27
+ const mutable = image !== void 0 || message.tracking?.data?.delivered_url !== void 0;
28
+ const upstream = APNS_HOSTS[this.options.environment];
29
+ const origin = this.options.origin ?? upstream;
30
+ const response = await (this.options.fetch ?? fetch)(`${origin}/3/device/${message.recipient}`, {
31
+ method: "POST",
32
+ headers: {
33
+ authorization: `bearer ${await this.providerToken()}`,
34
+ "apns-topic": this.options.topic,
35
+ "apns-push-type": "alert",
36
+ "apns-priority": "10",
37
+ "content-type": "application/json",
38
+ ...this.options.origin === void 0 ? {} : { "apns-relay-upstream": upstream }
39
+ },
40
+ body: JSON.stringify({
41
+ aps: {
42
+ alert: {
43
+ title,
44
+ body
45
+ },
46
+ sound: "default",
47
+ ...mutable ? { "mutable-content": 1 } : {}
48
+ },
49
+ template: message.template,
50
+ idempotencyKey: message.idempotencyKey,
51
+ ...image === void 0 ? {} : { image },
52
+ ...url === void 0 ? {} : { url },
53
+ ...message.tracking?.data
54
+ })
55
+ });
56
+ if (response.ok) return;
57
+ const text = await response.text();
58
+ const reason = apnsReason(text);
59
+ if (response.status === 410 || DEAD_TOKEN_REASONS.has(reason)) {
60
+ console.warn(`[apns] token gone for user ${message.userId}: ${reason || response.status}`);
61
+ await this.options.onUnregistered?.(message);
62
+ return {
63
+ delivered: false,
64
+ reason: reason || `HTTP ${response.status}`
65
+ };
66
+ }
67
+ throw new Error(`APNs send failed: ${response.status} ${text}`);
68
+ }
69
+ async providerToken() {
70
+ const now = Date.now();
71
+ if (this.token !== void 0 && now - this.token.issuedAt < TOKEN_TTL_MS) return this.token.value;
72
+ const value = await signApnsJwt(this.options, now);
73
+ this.token = {
74
+ value,
75
+ issuedAt: now
76
+ };
77
+ return value;
78
+ }
79
+ };
80
+ /** APNs error bodies are `{ "reason": "…" }`. */
81
+ function apnsReason(body) {
82
+ try {
83
+ return JSON.parse(body).reason ?? "";
84
+ } catch {
85
+ return "";
86
+ }
87
+ }
88
+ /** ES256-sign the APNs provider token: header `kid`, claims `iss` (team) and `iat`. */
89
+ async function signApnsJwt(key, nowMs = Date.now()) {
90
+ const header = require_jwt.base64url(JSON.stringify({
91
+ alg: "ES256",
92
+ kid: key.keyId
93
+ }));
94
+ const claims = require_jwt.base64url(JSON.stringify({
95
+ iss: key.teamId,
96
+ iat: Math.floor(nowMs / 1e3)
97
+ }));
98
+ const cryptoKey = await crypto.subtle.importKey("pkcs8", require_jwt.pemToDer(key.privateKey), {
99
+ name: "ECDSA",
100
+ namedCurve: "P-256"
101
+ }, false, ["sign"]);
102
+ const signature = await crypto.subtle.sign({
103
+ name: "ECDSA",
104
+ hash: "SHA-256"
105
+ }, cryptoKey, new TextEncoder().encode(`${header}.${claims}`));
106
+ return `${header}.${claims}.${require_jwt.base64url(new Uint8Array(signature))}`;
107
+ }
108
+ /** The `.p8` as it is usually shipped in a secret: base64 of the file. */
109
+ function apnsKeyFromBase64(value) {
110
+ const pem = atob(value);
111
+ if (!pem.includes("-----BEGIN PRIVATE KEY-----")) throw new Error("APNs key is not a PKCS #8 PEM (.p8) file");
112
+ return pem;
113
+ }
114
+ //#endregion
115
+ exports.APNS_HOSTS = APNS_HOSTS;
116
+ exports.ApnsPushSender = ApnsPushSender;
117
+ exports.apnsKeyFromBase64 = apnsKeyFromBase64;
118
+ exports.apnsReason = apnsReason;
119
+ exports.signApnsJwt = signApnsJwt;
120
+
121
+ //# sourceMappingURL=apns.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apns.cjs","names":["base64url","pemToDer"],"sources":["../src/apns.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { base64url, pemToDer } from './jwt';\nimport type { PushRenderer } from './push';\n\n/**\n * `@shware/workflow/apns` — iOS push delivery straight to the Apple Push\n * Notification service, token-based (an APNs auth key, the `.p8` file from the\n * developer portal). No Firebase in between: the app registers the APNs device\n * token in its own device table, and the host's DeviceDirectory hands it to the\n * engine, which sends one message per device.\n * No dependencies: the provider JWT is ES256-signed with WebCrypto.\n *\n * APNs speaks HTTP/2 only. A deployed Worker's `fetch` negotiates it, but\n * workerd under `wrangler dev` does not (the connection is dropped), so a local\n * host points `origin` at the Node relay in `@shware/workflow/apns-relay`.\n */\n\nexport interface ApnsPushOptions {\n /** The key's 10-character id, shown next to it in the developer portal. */\n keyId: string;\n /** The Apple Developer team id. */\n teamId: string;\n /** The `.p8` file's contents: PEM, `-----BEGIN PRIVATE KEY-----` (PKCS #8, P-256). */\n privateKey: string;\n /** The app's bundle id — APNs's `apns-topic`. */\n topic: string;\n /**\n * Which APNs environment the device tokens come from: `sandbox` for builds\n * signed with `aps-environment: development` (Xcode, the simulator),\n * `production` for App Store and TestFlight builds. A token from one is\n * `BadDeviceToken` on the other.\n */\n environment: 'sandbox' | 'production';\n render: PushRenderer;\n /**\n * POST here instead of Apple's host — the dev relay, which forwards over\n * HTTP/2. The real host still travels along (`apns-relay-upstream`), so the\n * relay needs no configuration of its own.\n */\n origin?: string;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n /** Called for a token APNs reports as gone (`Unregistered`, `BadDeviceToken`); `channelsFromConfig` passes it on to the DeviceDirectory's `disable`. */\n onUnregistered?: (message: OutboundMessage) => Promise<void> | void;\n}\n\nexport const APNS_HOSTS = {\n sandbox: 'https://api.sandbox.push.apple.com',\n production: 'https://api.push.apple.com',\n} as const;\n\n/** Reasons APNs gives for a token that will never deliver again — not worth a retry. */\nconst DEAD_TOKEN_REASONS = new Set([\n 'Unregistered',\n 'BadDeviceToken',\n 'DeviceTokenNotForTopic',\n 'ExpiredToken',\n]);\n\n/** Apple: reuse a provider token for at least 20 minutes, never past 60. */\nconst TOKEN_TTL_MS = 50 * 60_000;\n\nexport class ApnsPushSender implements MessageSender {\n private token: { value: string; issuedAt: number } | undefined;\n\n constructor(private readonly options: ApnsPushOptions) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n if (message.channel !== 'push') {\n throw new Error(`ApnsPushSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`ApnsPushSender: no push token for user '${message.userId}'`);\n }\n const { title, body, image, url } = await this.options.render(message.template, message.props);\n // A notification service extension runs only for a mutable push: it attaches the image and\n // reports the device delivery (`delivered_url`)\n const mutable = image !== undefined || message.tracking?.data?.delivered_url !== undefined;\n const upstream = APNS_HOSTS[this.options.environment];\n const origin = this.options.origin ?? upstream;\n const doFetch = this.options.fetch ?? fetch;\n\n const response = await doFetch(`${origin}/3/device/${message.recipient}`, {\n method: 'POST',\n headers: {\n authorization: `bearer ${await this.providerToken()}`,\n 'apns-topic': this.options.topic,\n 'apns-push-type': 'alert',\n 'apns-priority': '10',\n 'content-type': 'application/json',\n ...(this.options.origin === undefined ? {} : { 'apns-relay-upstream': upstream }),\n },\n body: JSON.stringify({\n aps: {\n alert: { title, body },\n sound: 'default',\n ...(mutable ? { 'mutable-content': 1 } : {}),\n },\n // The template key travels so the app can route a tap; the idempotency\n // key lets the app de-duplicate a redelivered push.\n template: message.template,\n idempotencyKey: message.idempotencyKey,\n ...(image === undefined ? {} : { image }),\n ...(url === undefined ? {} : { url }),\n ...message.tracking?.data,\n }),\n });\n if (response.ok) return;\n\n const text = await response.text();\n const reason = apnsReason(text);\n if (response.status === 410 || DEAD_TOKEN_REASONS.has(reason)) {\n // A dead token is a fact about the recipient, not a transient failure:\n // report it and move on, so the step does not retry into the same wall.\n console.warn(`[apns] token gone for user ${message.userId}: ${reason || response.status}`);\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: reason || `HTTP ${response.status}` };\n }\n throw new Error(`APNs send failed: ${response.status} ${text}`);\n }\n\n private async providerToken(): Promise<string> {\n const now = Date.now();\n if (this.token !== undefined && now - this.token.issuedAt < TOKEN_TTL_MS) {\n return this.token.value;\n }\n const value = await signApnsJwt(this.options, now);\n this.token = { value, issuedAt: now };\n return value;\n }\n}\n\n/** APNs error bodies are `{ \"reason\": \"…\" }`. */\nexport function apnsReason(body: string): string {\n try {\n const parsed = JSON.parse(body) as { reason?: string };\n return parsed.reason ?? '';\n } catch {\n return '';\n }\n}\n\n/** ES256-sign the APNs provider token: header `kid`, claims `iss` (team) and `iat`. */\nexport async function signApnsJwt(\n key: Pick<ApnsPushOptions, 'keyId' | 'teamId' | 'privateKey'>,\n nowMs = Date.now()\n): Promise<string> {\n const header = base64url(JSON.stringify({ alg: 'ES256', kid: key.keyId }));\n const claims = base64url(JSON.stringify({ iss: key.teamId, iat: Math.floor(nowMs / 1000) }));\n const cryptoKey = await crypto.subtle.importKey(\n 'pkcs8',\n pemToDer(key.privateKey),\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['sign']\n );\n // WebCrypto's ECDSA signature is the raw r‖s pair, which is exactly JWS's ES256 encoding.\n const signature = await crypto.subtle.sign(\n { name: 'ECDSA', hash: 'SHA-256' },\n cryptoKey,\n new TextEncoder().encode(`${header}.${claims}`)\n );\n return `${header}.${claims}.${base64url(new Uint8Array(signature))}`;\n}\n\n/** The `.p8` as it is usually shipped in a secret: base64 of the file. */\nexport function apnsKeyFromBase64(value: string): string {\n const pem = atob(value);\n if (!pem.includes('-----BEGIN PRIVATE KEY-----')) {\n throw new Error('APNs key is not a PKCS #8 PEM (.p8) file');\n }\n return pem;\n}\n"],"mappings":";;;AA8CA,MAAa,aAAa;CACxB,SAAS;CACT,YAAY;AACd;;AAGA,MAAM,qCAAqB,IAAI,IAAI;CACjC;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,eAAe;AAErB,IAAa,iBAAb,MAAqD;CAGtB;CAF7B;CAEA,YAAY,SAA2C;EAA1B,KAAA,UAAA;CAA2B;CAExD,MAAM,KAAK,SAAsD;EAC/D,IAAI,QAAQ,YAAY,QACtB,MAAM,IAAI,MAAM,wCAAwC,QAAQ,QAAQ,EAAE;EAE5E,IAAI,QAAQ,cAAc,KAAA,GACxB,MAAM,IAAI,MAAM,2CAA2C,QAAQ,OAAO,EAAE;EAE9E,MAAM,EAAE,OAAO,MAAM,OAAO,QAAQ,MAAM,KAAK,QAAQ,OAAO,QAAQ,UAAU,QAAQ,KAAK;EAG7F,MAAM,UAAU,UAAU,KAAA,KAAa,QAAQ,UAAU,MAAM,kBAAkB,KAAA;EACjF,MAAM,WAAW,WAAW,KAAK,QAAQ;EACzC,MAAM,SAAS,KAAK,QAAQ,UAAU;EAGtC,MAAM,WAAW,OAFD,KAAK,QAAQ,SAAS,MAAA,CAEP,GAAG,OAAO,YAAY,QAAQ,aAAa;GACxE,QAAQ;GACR,SAAS;IACP,eAAe,UAAU,MAAM,KAAK,cAAc;IAClD,cAAc,KAAK,QAAQ;IAC3B,kBAAkB;IAClB,iBAAiB;IACjB,gBAAgB;IAChB,GAAI,KAAK,QAAQ,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,uBAAuB,SAAS;GACjF;GACA,MAAM,KAAK,UAAU;IACnB,KAAK;KACH,OAAO;MAAE;MAAO;KAAK;KACrB,OAAO;KACP,GAAI,UAAU,EAAE,mBAAmB,EAAE,IAAI,CAAC;IAC5C;IAGA,UAAU,QAAQ;IAClB,gBAAgB,QAAQ;IACxB,GAAI,UAAU,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM;IACvC,GAAI,QAAQ,KAAA,IAAY,CAAC,IAAI,EAAE,IAAI;IACnC,GAAG,QAAQ,UAAU;GACvB,CAAC;EACH,CAAC;EACD,IAAI,SAAS,IAAI;EAEjB,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,SAAS,WAAW,IAAI;EAC9B,IAAI,SAAS,WAAW,OAAO,mBAAmB,IAAI,MAAM,GAAG;GAG7D,QAAQ,KAAK,8BAA8B,QAAQ,OAAO,IAAI,UAAU,SAAS,QAAQ;GACzF,MAAM,KAAK,QAAQ,iBAAiB,OAAO;GAC3C,OAAO;IAAE,WAAW;IAAO,QAAQ,UAAU,QAAQ,SAAS;GAAS;EACzE;EACA,MAAM,IAAI,MAAM,qBAAqB,SAAS,OAAO,GAAG,MAAM;CAChE;CAEA,MAAc,gBAAiC;EAC7C,MAAM,MAAM,KAAK,IAAI;EACrB,IAAI,KAAK,UAAU,KAAA,KAAa,MAAM,KAAK,MAAM,WAAW,cAC1D,OAAO,KAAK,MAAM;EAEpB,MAAM,QAAQ,MAAM,YAAY,KAAK,SAAS,GAAG;EACjD,KAAK,QAAQ;GAAE;GAAO,UAAU;EAAI;EACpC,OAAO;CACT;AACF;;AAGA,SAAgB,WAAW,MAAsB;CAC/C,IAAI;EAEF,OADe,KAAK,MAAM,IACd,CAAC,CAAC,UAAU;CAC1B,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,eAAsB,YACpB,KACA,QAAQ,KAAK,IAAI,GACA;CACjB,MAAM,SAASA,YAAAA,UAAU,KAAK,UAAU;EAAE,KAAK;EAAS,KAAK,IAAI;CAAM,CAAC,CAAC;CACzE,MAAM,SAASA,YAAAA,UAAU,KAAK,UAAU;EAAE,KAAK,IAAI;EAAQ,KAAK,KAAK,MAAM,QAAQ,GAAI;CAAE,CAAC,CAAC;CAC3F,MAAM,YAAY,MAAM,OAAO,OAAO,UACpC,SACAC,YAAAA,SAAS,IAAI,UAAU,GACvB;EAAE,MAAM;EAAS,YAAY;CAAQ,GACrC,OACA,CAAC,MAAM,CACT;CAEA,MAAM,YAAY,MAAM,OAAO,OAAO,KACpC;EAAE,MAAM;EAAS,MAAM;CAAU,GACjC,WACA,IAAI,YAAY,CAAC,CAAC,OAAO,GAAG,OAAO,GAAG,QAAQ,CAChD;CACA,OAAO,GAAG,OAAO,GAAG,OAAO,GAAGD,YAAAA,UAAU,IAAI,WAAW,SAAS,CAAC;AACnE;;AAGA,SAAgB,kBAAkB,OAAuB;CACvD,MAAM,MAAM,KAAK,KAAK;CACtB,IAAI,CAAC,IAAI,SAAS,6BAA6B,GAC7C,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO;AACT"}