@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
@@ -0,0 +1,47 @@
1
+ import { parseTemplateProps } from "./templates.mjs";
2
+ import { createElement } from "react";
3
+ import { renderToReadableStream } from "react-dom/server";
4
+ //#region src/react-email.ts
5
+ /**
6
+ * `@shware/workflow/react-email` — an EmailRenderer over the project's email
7
+ * registry (`react-dom` is an optional peer dependency; import this subpath
8
+ * only when you use it).
9
+ *
10
+ * The registry maps keys to `emailTemplate({...})` definitions. Props are
11
+ * parsed against the template's schema before the component renders — a
12
+ * failure throws TemplateValidationError and nothing renders — and the
13
+ * component renders as an element, so hooks and context work. Rendering
14
+ * streams and waits for every Suspense boundary (`allReady`) — react-email's
15
+ * `<Tailwind>` suspends while it compiles the classes, which synchronous
16
+ * static markup cannot wait for — then prepends the XHTML doctype react-email
17
+ * uses. @react-email/render itself is avoided because its Workers build
18
+ * statically imports prettier and html-to-text for options never used here.
19
+ */
20
+ const DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
21
+ function registryRenderer(emails) {
22
+ return async (key, props) => {
23
+ if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);
24
+ const template = emails[key];
25
+ const parsed = parseTemplateProps(template, key, props);
26
+ const element = createElement(template.component, parsed);
27
+ return {
28
+ subject: template.subject,
29
+ html: DOCTYPE + await renderToHtml(element)
30
+ };
31
+ };
32
+ }
33
+ /** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */
34
+ async function renderToHtml(element) {
35
+ let failure;
36
+ const stream = await renderToReadableStream(element, { onError: (error) => {
37
+ failure = error;
38
+ } });
39
+ await stream.allReady;
40
+ const html = await new Response(stream).text();
41
+ if (failure !== void 0) throw failure instanceof Error ? failure : new Error("email render failed", { cause: failure });
42
+ return html.replace(/^<!DOCTYPE html>/i, "").replace(/<link rel="preload" as="image"[^>]*\/>/g, "");
43
+ }
44
+ //#endregion
45
+ export { registryRenderer };
46
+
47
+ //# sourceMappingURL=react-email.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-email.mjs","names":[],"sources":["../src/react-email.ts"],"sourcesContent":["import { type ComponentType, type ReactElement, createElement } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport type { EmailRenderer } from './cloudflare';\nimport { type EmailTemplate, parseTemplateProps } from './templates';\n\n/**\n * `@shware/workflow/react-email` — an EmailRenderer over the project's email\n * registry (`react-dom` is an optional peer dependency; import this subpath\n * only when you use it).\n *\n * The registry maps keys to `emailTemplate({...})` definitions. Props are\n * parsed against the template's schema before the component renders — a\n * failure throws TemplateValidationError and nothing renders — and the\n * component renders as an element, so hooks and context work. Rendering\n * streams and waits for every Suspense boundary (`allReady`) — react-email's\n * `<Tailwind>` suspends while it compiles the classes, which synchronous\n * static markup cannot wait for — then prepends the XHTML doctype react-email\n * uses. @react-email/render itself is avoided because its Workers build\n * statically imports prettier and html-to-text for options never used here.\n */\n\nconst DOCTYPE =\n '<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">';\n\nexport function registryRenderer(emails: Record<string, EmailTemplate>): EmailRenderer {\n return async (key, props) => {\n if (!Object.hasOwn(emails, key)) throw new Error(`unknown email template: ${key}`);\n const template = emails[key];\n const parsed = parseTemplateProps(template, key, props);\n const element = createElement(template.component as ComponentType<object>, parsed);\n return { subject: template.subject, html: DOCTYPE + (await renderToHtml(element)) };\n };\n}\n\n/** Render to a string once every Suspense boundary has resolved; a rendering error rejects. */\nasync function renderToHtml(element: ReactElement): Promise<string> {\n let failure: unknown;\n const stream = await renderToReadableStream(element, {\n onError: (error) => {\n failure = error;\n },\n });\n await stream.allReady;\n const html = await new Response(stream).text();\n if (failure !== undefined) {\n throw failure instanceof Error ? failure : new Error('email render failed', { cause: failure });\n }\n // Streaming a document adds what static markup did not: an HTML5 doctype (the caller prepends\n // the XHTML one email clients expect) and React 19's image preload hints, which mean nothing in\n // an email. Both are dropped, as @react-email/render does.\n return html\n .replace(/^<!DOCTYPE html>/i, '')\n .replace(/<link rel=\"preload\" as=\"image\"[^>]*\\/>/g, '');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,MAAM,UACJ;AAEF,SAAgB,iBAAiB,QAAsD;CACrF,OAAO,OAAO,KAAK,UAAU;EAC3B,IAAI,CAAC,OAAO,OAAO,QAAQ,GAAG,GAAG,MAAM,IAAI,MAAM,2BAA2B,KAAK;EACjF,MAAM,WAAW,OAAO;EACxB,MAAM,SAAS,mBAAmB,UAAU,KAAK,KAAK;EACtD,MAAM,UAAU,cAAc,SAAS,WAAoC,MAAM;EACjF,OAAO;GAAE,SAAS,SAAS;GAAS,MAAM,UAAW,MAAM,aAAa,OAAO;EAAG;CACpF;AACF;;AAGA,eAAe,aAAa,SAAwC;CAClE,IAAI;CACJ,MAAM,SAAS,MAAM,uBAAuB,SAAS,EACnD,UAAU,UAAU;EAClB,UAAU;CACZ,EACF,CAAC;CACD,MAAM,OAAO;CACb,MAAM,OAAO,MAAM,IAAI,SAAS,MAAM,CAAC,CAAC,KAAK;CAC7C,IAAI,YAAY,KAAA,GACd,MAAM,mBAAmB,QAAQ,UAAU,IAAI,MAAM,uBAAuB,EAAE,OAAO,QAAQ,CAAC;CAKhG,OAAO,KACJ,QAAQ,qBAAqB,EAAE,CAAC,CAChC,QAAQ,2CAA2C,EAAE;AAC1D"}
@@ -0,0 +1,42 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/reporting/classify.ts
3
+ /** How close together clicks on different links of one message must be to count as one walk. */
4
+ const BURST_WINDOW_MS = 5e3;
5
+ const DEFAULT_QUICK_MS = 1e3;
6
+ const SCANNER = /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\/|wget\/|python|go-http-client|java\/|okhttp/i;
7
+ const DATACENTER = /\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\b/i;
8
+ const GOOGLE_PROXY = /GoogleImageProxy|ggpht\.com/i;
9
+ const human = {
10
+ machine: false,
11
+ reason: null
12
+ };
13
+ const machine = (reason) => ({
14
+ machine: true,
15
+ reason
16
+ });
17
+ const scanner = (facts) => SCANNER.test(facts.userAgent?.trim() ?? "");
18
+ function classifyOpen(facts, options = {}) {
19
+ const quickMs = options.quickMs ?? 1e3;
20
+ const agent = facts.userAgent?.trim() ?? "";
21
+ if (agent === "Mozilla/5.0" || /apple/i.test(facts.organization ?? "")) return machine("apple_privacy_proxy");
22
+ if (GOOGLE_PROXY.test(agent)) return facts.sinceDeliveredMs < quickMs ? machine("gmail_prefetch") : human;
23
+ return scanner(facts) ? machine("scanner") : human;
24
+ }
25
+ function classifyClick(facts, options = {}) {
26
+ const quickMs = options.quickMs ?? 1e3;
27
+ if (facts.method === "HEAD") return machine("head_request");
28
+ if (scanner(facts)) return machine("scanner");
29
+ if (DATACENTER.test(facts.organization ?? "")) return machine("datacenter");
30
+ if (facts.linksInBurst >= 3 || facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount) return machine("link_burst");
31
+ const quick = facts.sinceDeliveredMs < quickMs;
32
+ const pair = facts.linksInBurst >= 2;
33
+ if (quick && pair) return machine("quick+link_pair");
34
+ return human;
35
+ }
36
+ //#endregion
37
+ exports.BURST_WINDOW_MS = BURST_WINDOW_MS;
38
+ exports.DEFAULT_QUICK_MS = DEFAULT_QUICK_MS;
39
+ exports.classifyClick = classifyClick;
40
+ exports.classifyOpen = classifyOpen;
41
+
42
+ //# sourceMappingURL=classify.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.cjs","names":[],"sources":["../../src/reporting/classify.ts"],"sourcesContent":["/**\n * Who opened or clicked: a person, or a machine acting on the message without one — the split\n * between human and machine engagement that open and click rates are read through. No single\n * signal decides on its own unless it is strong; weak ones only count together, the way the\n * ESPs that publish their approach describe it (a quick click alone is a person on a phone as\n * often as a scanner). Every verdict names the rule that made it, and the request's user agent and\n * network are stored beside it, so the rules can be revisited against history.\n *\n * Strong, alone enough:\n * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,\n * Proofpoint, Safe Links, curl, a headless browser…);\n * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,\n * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates\n * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not\n * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when\n * the person opens the message;\n * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on\n * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;\n * - `head_request` (clicks): a person's click is a GET;\n * - `link_burst` (clicks): three or more different links of one message within a few seconds, or\n * every link of it — the way a scanner walks a message. Earlier clicks of the burst are\n * reclassified too (see `recordClick`).\n *\n * Weak, machine only together:\n * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);\n * - `link_pair`: another link of the same message clicked within a few seconds.\n *\n * Gmail's image proxy fetches when the person opens the message, so its opens count as human —\n * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).\n */\n\nexport interface Verdict {\n machine: boolean;\n reason: string | null;\n}\n\nexport interface RequestFacts {\n method: string;\n userAgent: string | null;\n /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */\n organization: string | null;\n /** Milliseconds between delivering the message and this request. */\n sinceDeliveredMs: number;\n}\n\nexport interface ClassifyOptions {\n /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */\n quickMs?: number;\n}\n\nexport interface ClickFacts extends RequestFacts {\n /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */\n linksInBurst: number;\n /** How many tracked links the message has. */\n linkCount: number;\n}\n\n/** How close together clicks on different links of one message must be to count as one walk. */\nexport const BURST_WINDOW_MS = 5_000;\nexport const DEFAULT_QUICK_MS = 1_000;\n\n// Whole words only: \"San Francisco\" is not Cisco, a CUBOT phone is not a bot\nconst SCANNER =\n /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\\/|wget\\/|python|go-http-client|java\\/|okhttp/i;\nconst DATACENTER =\n /\\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\\b/i;\nconst GOOGLE_PROXY = /GoogleImageProxy|ggpht\\.com/i;\n\nconst human: Verdict = { machine: false, reason: null };\nconst machine = (reason: string): Verdict => ({ machine: true, reason });\n\nconst scanner = (facts: RequestFacts) => SCANNER.test(facts.userAgent?.trim() ?? '');\n\nexport function classifyOpen(facts: RequestFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n const agent = facts.userAgent?.trim() ?? '';\n if (agent === 'Mozilla/5.0' || /apple/i.test(facts.organization ?? '')) {\n return machine('apple_privacy_proxy');\n }\n if (GOOGLE_PROXY.test(agent)) {\n return facts.sinceDeliveredMs < quickMs ? machine('gmail_prefetch') : human;\n }\n return scanner(facts) ? machine('scanner') : human;\n}\n\nexport function classifyClick(facts: ClickFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n if (facts.method === 'HEAD') return machine('head_request');\n if (scanner(facts)) return machine('scanner');\n if (DATACENTER.test(facts.organization ?? '')) return machine('datacenter');\n if (facts.linksInBurst >= 3 || (facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount)) {\n return machine('link_burst');\n }\n const quick = facts.sinceDeliveredMs < quickMs;\n const pair = facts.linksInBurst >= 2;\n if (quick && pair) return machine('quick+link_pair');\n return human;\n}\n"],"mappings":";;;AA0DA,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAGhC,MAAM,UACJ;AACF,MAAM,aACJ;AACF,MAAM,eAAe;AAErB,MAAM,QAAiB;CAAE,SAAS;CAAO,QAAQ;AAAK;AACtD,MAAM,WAAW,YAA6B;CAAE,SAAS;CAAM;AAAO;AAEtE,MAAM,WAAW,UAAwB,QAAQ,KAAK,MAAM,WAAW,KAAK,KAAK,EAAE;AAEnF,SAAgB,aAAa,OAAqB,UAA2B,CAAC,GAAY;CACxF,MAAM,UAAU,QAAQ,WAAA;CACxB,MAAM,QAAQ,MAAM,WAAW,KAAK,KAAK;CACzC,IAAI,UAAU,iBAAiB,SAAS,KAAK,MAAM,gBAAgB,EAAE,GACnE,OAAO,QAAQ,qBAAqB;CAEtC,IAAI,aAAa,KAAK,KAAK,GACzB,OAAO,MAAM,mBAAmB,UAAU,QAAQ,gBAAgB,IAAI;CAExE,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI;AAC/C;AAEA,SAAgB,cAAc,OAAmB,UAA2B,CAAC,GAAY;CACvF,MAAM,UAAU,QAAQ,WAAA;CACxB,IAAI,MAAM,WAAW,QAAQ,OAAO,QAAQ,cAAc;CAC1D,IAAI,QAAQ,KAAK,GAAG,OAAO,QAAQ,SAAS;CAC5C,IAAI,WAAW,KAAK,MAAM,gBAAgB,EAAE,GAAG,OAAO,QAAQ,YAAY;CAC1E,IAAI,MAAM,gBAAgB,KAAM,MAAM,aAAa,KAAK,MAAM,gBAAgB,MAAM,WAClF,OAAO,QAAQ,YAAY;CAE7B,MAAM,QAAQ,MAAM,mBAAmB;CACvC,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,SAAS,MAAM,OAAO,QAAQ,iBAAiB;CACnD,OAAO;AACT"}
@@ -0,0 +1,60 @@
1
+ //#region src/reporting/classify.d.ts
2
+ /**
3
+ * Who opened or clicked: a person, or a machine acting on the message without one — the split
4
+ * between human and machine engagement that open and click rates are read through. No single
5
+ * signal decides on its own unless it is strong; weak ones only count together, the way the
6
+ * ESPs that publish their approach describe it (a quick click alone is a person on a phone as
7
+ * often as a scanner). Every verdict names the rule that made it, and the request's user agent and
8
+ * network are stored beside it, so the rules can be revisited against history.
9
+ *
10
+ * Strong, alone enough:
11
+ * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,
12
+ * Proofpoint, Safe Links, curl, a headless browser…);
13
+ * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,
14
+ * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates
15
+ * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not
16
+ * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when
17
+ * the person opens the message;
18
+ * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on
19
+ * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;
20
+ * - `head_request` (clicks): a person's click is a GET;
21
+ * - `link_burst` (clicks): three or more different links of one message within a few seconds, or
22
+ * every link of it — the way a scanner walks a message. Earlier clicks of the burst are
23
+ * reclassified too (see `recordClick`).
24
+ *
25
+ * Weak, machine only together:
26
+ * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);
27
+ * - `link_pair`: another link of the same message clicked within a few seconds.
28
+ *
29
+ * Gmail's image proxy fetches when the person opens the message, so its opens count as human —
30
+ * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).
31
+ */
32
+ export interface Verdict {
33
+ machine: boolean;
34
+ reason: string | null;
35
+ }
36
+ export interface RequestFacts {
37
+ method: string;
38
+ userAgent: string | null;
39
+ /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */
40
+ organization: string | null;
41
+ /** Milliseconds between delivering the message and this request. */
42
+ sinceDeliveredMs: number;
43
+ }
44
+ export interface ClassifyOptions {
45
+ /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */
46
+ quickMs?: number;
47
+ }
48
+ export interface ClickFacts extends RequestFacts {
49
+ /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */
50
+ linksInBurst: number;
51
+ /** How many tracked links the message has. */
52
+ linkCount: number;
53
+ }
54
+ /** How close together clicks on different links of one message must be to count as one walk. */
55
+ export declare const BURST_WINDOW_MS = 5000;
56
+ export declare const DEFAULT_QUICK_MS = 1000;
57
+ export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
58
+ export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
59
+ //#endregion
60
+ //# sourceMappingURL=classify.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.cts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BiB;EACf;EACA;;iBAGe;EACf;EACA;;EAEA;;EAEA;;iBAGe;;EAEf;;iBAGe,mBAAmB;;EAElC;;EAEA;;;qBAIW;qBACA;wBAcG,aAAa,OAAO,cAAc,UAAS,kBAAuB;wBAYlE,cAAc,OAAO,YAAY,UAAS,kBAAuB"}
@@ -0,0 +1,60 @@
1
+ //#region src/reporting/classify.d.ts
2
+ /**
3
+ * Who opened or clicked: a person, or a machine acting on the message without one — the split
4
+ * between human and machine engagement that open and click rates are read through. No single
5
+ * signal decides on its own unless it is strong; weak ones only count together, the way the
6
+ * ESPs that publish their approach describe it (a quick click alone is a person on a phone as
7
+ * often as a scanner). Every verdict names the rule that made it, and the request's user agent and
8
+ * network are stored beside it, so the rules can be revisited against history.
9
+ *
10
+ * Strong, alone enough:
11
+ * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,
12
+ * Proofpoint, Safe Links, curl, a headless browser…);
13
+ * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,
14
+ * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates
15
+ * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not
16
+ * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when
17
+ * the person opens the message;
18
+ * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on
19
+ * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;
20
+ * - `head_request` (clicks): a person's click is a GET;
21
+ * - `link_burst` (clicks): three or more different links of one message within a few seconds, or
22
+ * every link of it — the way a scanner walks a message. Earlier clicks of the burst are
23
+ * reclassified too (see `recordClick`).
24
+ *
25
+ * Weak, machine only together:
26
+ * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);
27
+ * - `link_pair`: another link of the same message clicked within a few seconds.
28
+ *
29
+ * Gmail's image proxy fetches when the person opens the message, so its opens count as human —
30
+ * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).
31
+ */
32
+ export interface Verdict {
33
+ machine: boolean;
34
+ reason: string | null;
35
+ }
36
+ export interface RequestFacts {
37
+ method: string;
38
+ userAgent: string | null;
39
+ /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */
40
+ organization: string | null;
41
+ /** Milliseconds between delivering the message and this request. */
42
+ sinceDeliveredMs: number;
43
+ }
44
+ export interface ClassifyOptions {
45
+ /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */
46
+ quickMs?: number;
47
+ }
48
+ export interface ClickFacts extends RequestFacts {
49
+ /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */
50
+ linksInBurst: number;
51
+ /** How many tracked links the message has. */
52
+ linkCount: number;
53
+ }
54
+ /** How close together clicks on different links of one message must be to count as one walk. */
55
+ export declare const BURST_WINDOW_MS = 5000;
56
+ export declare const DEFAULT_QUICK_MS = 1000;
57
+ export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
58
+ export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
59
+ //#endregion
60
+ //# sourceMappingURL=classify.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.d.mts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+BiB;EACf;EACA;;iBAGe;EACf;EACA;;EAEA;;EAEA;;iBAGe;;EAEf;;iBAGe,mBAAmB;;EAElC;;EAEA;;;qBAIW;qBACA;wBAcG,aAAa,OAAO,cAAc,UAAS,kBAAuB;wBAYlE,cAAc,OAAO,YAAY,UAAS,kBAAuB"}
@@ -0,0 +1,38 @@
1
+ //#region src/reporting/classify.ts
2
+ /** How close together clicks on different links of one message must be to count as one walk. */
3
+ const BURST_WINDOW_MS = 5e3;
4
+ const DEFAULT_QUICK_MS = 1e3;
5
+ const SCANNER = /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\/|wget\/|python|go-http-client|java\/|okhttp/i;
6
+ const DATACENTER = /\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\b/i;
7
+ const GOOGLE_PROXY = /GoogleImageProxy|ggpht\.com/i;
8
+ const human = {
9
+ machine: false,
10
+ reason: null
11
+ };
12
+ const machine = (reason) => ({
13
+ machine: true,
14
+ reason
15
+ });
16
+ const scanner = (facts) => SCANNER.test(facts.userAgent?.trim() ?? "");
17
+ function classifyOpen(facts, options = {}) {
18
+ const quickMs = options.quickMs ?? 1e3;
19
+ const agent = facts.userAgent?.trim() ?? "";
20
+ if (agent === "Mozilla/5.0" || /apple/i.test(facts.organization ?? "")) return machine("apple_privacy_proxy");
21
+ if (GOOGLE_PROXY.test(agent)) return facts.sinceDeliveredMs < quickMs ? machine("gmail_prefetch") : human;
22
+ return scanner(facts) ? machine("scanner") : human;
23
+ }
24
+ function classifyClick(facts, options = {}) {
25
+ const quickMs = options.quickMs ?? 1e3;
26
+ if (facts.method === "HEAD") return machine("head_request");
27
+ if (scanner(facts)) return machine("scanner");
28
+ if (DATACENTER.test(facts.organization ?? "")) return machine("datacenter");
29
+ if (facts.linksInBurst >= 3 || facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount) return machine("link_burst");
30
+ const quick = facts.sinceDeliveredMs < quickMs;
31
+ const pair = facts.linksInBurst >= 2;
32
+ if (quick && pair) return machine("quick+link_pair");
33
+ return human;
34
+ }
35
+ //#endregion
36
+ export { BURST_WINDOW_MS, DEFAULT_QUICK_MS, classifyClick, classifyOpen };
37
+
38
+ //# sourceMappingURL=classify.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classify.mjs","names":[],"sources":["../../src/reporting/classify.ts"],"sourcesContent":["/**\n * Who opened or clicked: a person, or a machine acting on the message without one — the split\n * between human and machine engagement that open and click rates are read through. No single\n * signal decides on its own unless it is strong; weak ones only count together, the way the\n * ESPs that publish their approach describe it (a quick click alone is a person on a phone as\n * often as a scanner). Every verdict names the rule that made it, and the request's user agent and\n * network are stored beside it, so the rules can be revisited against history.\n *\n * Strong, alone enough:\n * - `scanner`: a security gateway or link checker announcing itself (Barracuda, Mimecast,\n * Proofpoint, Safe Links, curl, a headless browser…);\n * - `datacenter` (clicks): the request came from a cloud or security vendor's network (AWS,\n * Azure, Google Cloud, Proofpoint…), where people do not browse — Microsoft Defender detonates\n * links from Azure. Not Cloudflare, Akamai or Fastly: iCloud Private Relay egresses there. Not\n * for opens: Gmail's, Outlook's and Yahoo's image proxies fetch from their own networks when\n * the person opens the message;\n * - `apple_privacy_proxy` (opens): Apple Mail Privacy Protection, which fetches every image on\n * delivery through Apple's proxies with the bare `Mozilla/5.0` user agent, read or not;\n * - `head_request` (clicks): a person's click is a GET;\n * - `link_burst` (clicks): three or more different links of one message within a few seconds, or\n * every link of it — the way a scanner walks a message. Earlier clicks of the burst are\n * reclassified too (see `recordClick`).\n *\n * Weak, machine only together:\n * - `quick`: within `quickMs` of delivery (1 s by default, `emails.tracking.quickClickMs`);\n * - `link_pair`: another link of the same message clicked within a few seconds.\n *\n * Gmail's image proxy fetches when the person opens the message, so its opens count as human —\n * unless it fetched within `quickMs` of delivery (`gmail_prefetch`).\n */\n\nexport interface Verdict {\n machine: boolean;\n reason: string | null;\n}\n\nexport interface RequestFacts {\n method: string;\n userAgent: string | null;\n /** Cloudflare's `request.cf.asOrganization`: who owns the address the request came from. */\n organization: string | null;\n /** Milliseconds between delivering the message and this request. */\n sinceDeliveredMs: number;\n}\n\nexport interface ClassifyOptions {\n /** A request this soon after delivery is a weak machine signal; 1000 ms by default. */\n quickMs?: number;\n}\n\nexport interface ClickFacts extends RequestFacts {\n /** The different links of the message clicked within `BURST_WINDOW_MS`, this one included. */\n linksInBurst: number;\n /** How many tracked links the message has. */\n linkCount: number;\n}\n\n/** How close together clicks on different links of one message must be to count as one walk. */\nexport const BURST_WINDOW_MS = 5_000;\nexport const DEFAULT_QUICK_MS = 1_000;\n\n// Whole words only: \"San Francisco\" is not Cisco, a CUBOT phone is not a bot\nconst SCANNER =\n /(?<![a-z])(?:(?!cubot)[a-z]*bot|crawler|spider|headlesschrome|phantomjs|barracuda|mimecast|proofpoint|safelinks|symantec|trend ?micro|forcepoint|zscaler|cisco|fortinet|sophos|messagelabs)(?![a-z])|curl\\/|wget\\/|python|go-http-client|java\\/|okhttp/i;\nconst DATACENTER =\n /\\b(?:amazon|aws|microsoft|azure|google cloud|digitalocean|ovh|hetzner|linode|akamai connected cloud|vultr|oracle|alibaba|tencent|proofpoint|mimecast|barracuda|forcepoint|zscaler|cisco|fortinet|sophos|trend ?micro|symantec|broadcom)\\b/i;\nconst GOOGLE_PROXY = /GoogleImageProxy|ggpht\\.com/i;\n\nconst human: Verdict = { machine: false, reason: null };\nconst machine = (reason: string): Verdict => ({ machine: true, reason });\n\nconst scanner = (facts: RequestFacts) => SCANNER.test(facts.userAgent?.trim() ?? '');\n\nexport function classifyOpen(facts: RequestFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n const agent = facts.userAgent?.trim() ?? '';\n if (agent === 'Mozilla/5.0' || /apple/i.test(facts.organization ?? '')) {\n return machine('apple_privacy_proxy');\n }\n if (GOOGLE_PROXY.test(agent)) {\n return facts.sinceDeliveredMs < quickMs ? machine('gmail_prefetch') : human;\n }\n return scanner(facts) ? machine('scanner') : human;\n}\n\nexport function classifyClick(facts: ClickFacts, options: ClassifyOptions = {}): Verdict {\n const quickMs = options.quickMs ?? DEFAULT_QUICK_MS;\n if (facts.method === 'HEAD') return machine('head_request');\n if (scanner(facts)) return machine('scanner');\n if (DATACENTER.test(facts.organization ?? '')) return machine('datacenter');\n if (facts.linksInBurst >= 3 || (facts.linkCount >= 2 && facts.linksInBurst >= facts.linkCount)) {\n return machine('link_burst');\n }\n const quick = facts.sinceDeliveredMs < quickMs;\n const pair = facts.linksInBurst >= 2;\n if (quick && pair) return machine('quick+link_pair');\n return human;\n}\n"],"mappings":";;AA0DA,MAAa,kBAAkB;AAC/B,MAAa,mBAAmB;AAGhC,MAAM,UACJ;AACF,MAAM,aACJ;AACF,MAAM,eAAe;AAErB,MAAM,QAAiB;CAAE,SAAS;CAAO,QAAQ;AAAK;AACtD,MAAM,WAAW,YAA6B;CAAE,SAAS;CAAM;AAAO;AAEtE,MAAM,WAAW,UAAwB,QAAQ,KAAK,MAAM,WAAW,KAAK,KAAK,EAAE;AAEnF,SAAgB,aAAa,OAAqB,UAA2B,CAAC,GAAY;CACxF,MAAM,UAAU,QAAQ,WAAA;CACxB,MAAM,QAAQ,MAAM,WAAW,KAAK,KAAK;CACzC,IAAI,UAAU,iBAAiB,SAAS,KAAK,MAAM,gBAAgB,EAAE,GACnE,OAAO,QAAQ,qBAAqB;CAEtC,IAAI,aAAa,KAAK,KAAK,GACzB,OAAO,MAAM,mBAAmB,UAAU,QAAQ,gBAAgB,IAAI;CAExE,OAAO,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI;AAC/C;AAEA,SAAgB,cAAc,OAAmB,UAA2B,CAAC,GAAY;CACvF,MAAM,UAAU,QAAQ,WAAA;CACxB,IAAI,MAAM,WAAW,QAAQ,OAAO,QAAQ,cAAc;CAC1D,IAAI,QAAQ,KAAK,GAAG,OAAO,QAAQ,SAAS;CAC5C,IAAI,WAAW,KAAK,MAAM,gBAAgB,EAAE,GAAG,OAAO,QAAQ,YAAY;CAC1E,IAAI,MAAM,gBAAgB,KAAM,MAAM,aAAa,KAAK,MAAM,gBAAgB,MAAM,WAClF,OAAO,QAAQ,YAAY;CAE7B,MAAM,QAAQ,MAAM,mBAAmB;CACvC,MAAM,OAAO,MAAM,gBAAgB;CACnC,IAAI,SAAS,MAAM,OAAO,QAAQ,iBAAiB;CACnD,OAAO;AACT"}
@@ -0,0 +1,63 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region src/reporting/observer.ts
3
+ /**
4
+ * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
5
+ * and outcome recorded through `store.reporting`.
6
+ *
7
+ * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
8
+ * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
9
+ * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
10
+ * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
11
+ * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
12
+ * The links are recorded when the sender commits the rewritten email, before it goes out.
13
+ */
14
+ function reportingObserver(reporting, origin) {
15
+ const base = origin?.replace(/\/$/, "");
16
+ return {
17
+ started: (event) => reporting.instanceStarted(event),
18
+ nodeReached: (event) => reporting.nodeReached(event),
19
+ async messagePrepared(message) {
20
+ const id = await reporting.recordMessage({
21
+ idempotencyKey: message.idempotencyKey,
22
+ workflow: message.workflow,
23
+ instanceId: message.instanceId,
24
+ nodeId: message.nodeId,
25
+ template: message.template,
26
+ channel: channelOf(message),
27
+ userId: message.userId,
28
+ sentAt: Date.now()
29
+ });
30
+ if (message.channel === "email" && base !== void 0) {
31
+ const urls = [];
32
+ return {
33
+ link: (url) => {
34
+ urls.push(url);
35
+ return `${base}/t/c/${id}/${urls.length - 1}`;
36
+ },
37
+ pixel: `${base}/t/o/${id}`,
38
+ commit: () => reporting.recordLinks(id, urls)
39
+ };
40
+ }
41
+ if (message.channel === "push") return { data: {
42
+ message_id: id,
43
+ ...base === void 0 ? {} : {
44
+ delivered_url: `${base}/t/d/${id}`,
45
+ opened_url: `${base}/t/p/${id}`
46
+ }
47
+ } };
48
+ },
49
+ async messageSent(message, result) {
50
+ await reporting.settleMessage(message.idempotencyKey, result, Date.now());
51
+ },
52
+ finished: (event) => reporting.instanceFinished(event)
53
+ };
54
+ }
55
+ /** Where the copy lands — the Studio's metric channel: a push is reported per platform. */
56
+ function channelOf(message) {
57
+ if (message.channel !== "push") return message.channel;
58
+ return `push_${message.device?.platform ?? "unknown"}`;
59
+ }
60
+ //#endregion
61
+ exports.reportingObserver = reportingObserver;
62
+
63
+ //# sourceMappingURL=observer.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.cjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel\n * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url) => {\n urls.push(url);\n return `${base}/t/c/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,QAAQ;MACb,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,SAAS;KAC5C;KACA,OAAO,GAAG,KAAK,OAAO;KAEtB,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}
@@ -0,0 +1,18 @@
1
+ import { JourneyObserver } from "../engine/ports.cjs";
2
+ import "../engine/index.cjs";
3
+ import { JourneyReporting } from "../store/index.cjs";
4
+ //#region src/reporting/observer.d.ts
5
+ /**
6
+ * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
7
+ * and outcome recorded through `store.reporting`.
8
+ *
9
+ * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
10
+ * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
11
+ * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
12
+ * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
13
+ * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
14
+ * The links are recorded when the sender commits the rewritten email, before it goes out.
15
+ */
16
+ export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
17
+ //#endregion
18
+ //# sourceMappingURL=observer.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.d.cts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAcgB,kBACd,WAAW,kBACX,6BACC"}
@@ -0,0 +1,18 @@
1
+ import { JourneyObserver } from "../engine/ports.mjs";
2
+ import "../engine/index.mjs";
3
+ import { JourneyReporting } from "../store/index.mjs";
4
+ //#region src/reporting/observer.d.ts
5
+ /**
6
+ * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
7
+ * and outcome recorded through `store.reporting`.
8
+ *
9
+ * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
10
+ * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
11
+ * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
12
+ * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
13
+ * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
14
+ * The links are recorded when the sender commits the rewritten email, before it goes out.
15
+ */
16
+ export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
17
+ //#endregion
18
+ //# sourceMappingURL=observer.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.d.mts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAcgB,kBACd,WAAW,kBACX,6BACC"}
@@ -0,0 +1,62 @@
1
+ //#region src/reporting/observer.ts
2
+ /**
3
+ * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
4
+ * and outcome recorded through `store.reporting`.
5
+ *
6
+ * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,
7
+ * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel
8
+ * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries
9
+ * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),
10
+ * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.
11
+ * The links are recorded when the sender commits the rewritten email, before it goes out.
12
+ */
13
+ function reportingObserver(reporting, origin) {
14
+ const base = origin?.replace(/\/$/, "");
15
+ return {
16
+ started: (event) => reporting.instanceStarted(event),
17
+ nodeReached: (event) => reporting.nodeReached(event),
18
+ async messagePrepared(message) {
19
+ const id = await reporting.recordMessage({
20
+ idempotencyKey: message.idempotencyKey,
21
+ workflow: message.workflow,
22
+ instanceId: message.instanceId,
23
+ nodeId: message.nodeId,
24
+ template: message.template,
25
+ channel: channelOf(message),
26
+ userId: message.userId,
27
+ sentAt: Date.now()
28
+ });
29
+ if (message.channel === "email" && base !== void 0) {
30
+ const urls = [];
31
+ return {
32
+ link: (url) => {
33
+ urls.push(url);
34
+ return `${base}/t/c/${id}/${urls.length - 1}`;
35
+ },
36
+ pixel: `${base}/t/o/${id}`,
37
+ commit: () => reporting.recordLinks(id, urls)
38
+ };
39
+ }
40
+ if (message.channel === "push") return { data: {
41
+ message_id: id,
42
+ ...base === void 0 ? {} : {
43
+ delivered_url: `${base}/t/d/${id}`,
44
+ opened_url: `${base}/t/p/${id}`
45
+ }
46
+ } };
47
+ },
48
+ async messageSent(message, result) {
49
+ await reporting.settleMessage(message.idempotencyKey, result, Date.now());
50
+ },
51
+ finished: (event) => reporting.instanceFinished(event)
52
+ };
53
+ }
54
+ /** Where the copy lands — the Studio's metric channel: a push is reported per platform. */
55
+ function channelOf(message) {
56
+ if (message.channel !== "push") return message.channel;
57
+ return `push_${message.device?.platform ?? "unknown"}`;
58
+ }
59
+ //#endregion
60
+ export { reportingObserver };
61
+
62
+ //# sourceMappingURL=observer.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observer.mjs","names":[],"sources":["../../src/reporting/observer.ts"],"sourcesContent":["import type { JourneyObserver, OutboundMessage } from '../engine';\nimport type { JourneyReporting } from '../store';\n\n/**\n * The observer journeyWorker installs when the store keeps reporting: every node arrival, copy\n * and outcome recorded through `store.reporting`.\n *\n * A copy is recorded before it goes out, so its id can travel with it: with a tracking origin,\n * an email's links become `<origin>/t/c/<message>/<position>` and an open pixel\n * `<origin>/t/o/<message>` (served by the Worker, ./tracking.ts), and a push carries\n * `message_id` in its payload for the app to report the tap (`JourneyClient.messageOpened`),\n * plus `delivered_url` / `opened_url` for a client that reports straight to the Worker.\n * The links are recorded when the sender commits the rewritten email, before it goes out.\n */\nexport function reportingObserver(\n reporting: JourneyReporting,\n origin: string | undefined\n): JourneyObserver {\n const base = origin?.replace(/\\/$/, '');\n\n return {\n started: (event) => reporting.instanceStarted(event),\n\n nodeReached: (event) => reporting.nodeReached(event),\n\n async messagePrepared(message) {\n const id = await reporting.recordMessage({\n idempotencyKey: message.idempotencyKey,\n workflow: message.workflow,\n instanceId: message.instanceId,\n nodeId: message.nodeId,\n template: message.template,\n channel: channelOf(message),\n userId: message.userId,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url) => {\n urls.push(url);\n return `${base}/t/c/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n // Before the transport takes it, so no click can reach a link not yet recorded\n commit: () => reporting.recordLinks(id, urls),\n };\n }\n if (message.channel === 'push') {\n // The app reports the tap with `message_id`; a client that can reach the Worker on its\n // own (iOS's notification service extension, a browser's service worker) posts to these\n return {\n data: {\n message_id: id,\n ...(base === undefined\n ? {}\n : { delivered_url: `${base}/t/d/${id}`, opened_url: `${base}/t/p/${id}` }),\n },\n };\n }\n return undefined;\n },\n\n async messageSent(message, result) {\n await reporting.settleMessage(message.idempotencyKey, result, Date.now());\n },\n\n finished: (event) => reporting.instanceFinished(event),\n };\n}\n\n/** Where the copy lands — the Studio's metric channel: a push is reported per platform. */\nfunction channelOf(message: OutboundMessage): string {\n if (message.channel !== 'push') return message.channel;\n return `push_${message.device?.platform ?? 'unknown'}`;\n}\n"],"mappings":";;;;;;;;;;;;AAcA,SAAgB,kBACd,WACA,QACiB;CACjB,MAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;CAEtC,OAAO;EACL,UAAU,UAAU,UAAU,gBAAgB,KAAK;EAEnD,cAAc,UAAU,UAAU,YAAY,KAAK;EAEnD,MAAM,gBAAgB,SAAS;GAC7B,MAAM,KAAK,MAAM,UAAU,cAAc;IACvC,gBAAgB,QAAQ;IACxB,UAAU,QAAQ;IAClB,YAAY,QAAQ;IACpB,QAAQ,QAAQ;IAChB,UAAU,QAAQ;IAClB,SAAS,UAAU,OAAO;IAC1B,QAAQ,QAAQ;IAChB,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,QAAQ;MACb,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,OAAO,GAAG,GAAG,KAAK,SAAS;KAC5C;KACA,OAAO,GAAG,KAAK,OAAO;KAEtB,cAAc,UAAU,YAAY,IAAI,IAAI;IAC9C;GACF;GACA,IAAI,QAAQ,YAAY,QAGtB,OAAO,EACL,MAAM;IACJ,YAAY;IACZ,GAAI,SAAS,KAAA,IACT,CAAC,IACD;KAAE,eAAe,GAAG,KAAK,OAAO;KAAM,YAAY,GAAG,KAAK,OAAO;IAAK;GAC5E,EACF;EAGJ;EAEA,MAAM,YAAY,SAAS,QAAQ;GACjC,MAAM,UAAU,cAAc,QAAQ,gBAAgB,QAAQ,KAAK,IAAI,CAAC;EAC1E;EAEA,WAAW,UAAU,UAAU,iBAAiB,KAAK;CACvD;AACF;;AAGA,SAAS,UAAU,SAAkC;CACnD,IAAI,QAAQ,YAAY,QAAQ,OAAO,QAAQ;CAC/C,OAAO,QAAQ,QAAQ,QAAQ,YAAY;AAC7C"}