@shware/workflow 5.0.0 → 6.0.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 (487) hide show
  1. package/README.md +165 -42
  2. package/dist/_virtual/_rolldown/runtime.cjs +1 -2
  3. package/dist/apns-relay.cjs +1 -2
  4. package/dist/apns-relay.d.cts +0 -2
  5. package/dist/apns-relay.d.cts.map +1 -1
  6. package/dist/apns-relay.d.mts +0 -2
  7. package/dist/apns-relay.d.mts.map +1 -1
  8. package/dist/apns-relay.mjs +1 -2
  9. package/dist/apns.cjs +1 -2
  10. package/dist/apns.d.cts +0 -2
  11. package/dist/apns.d.cts.map +1 -1
  12. package/dist/apns.d.mts +0 -2
  13. package/dist/apns.d.mts.map +1 -1
  14. package/dist/apns.mjs +1 -2
  15. package/dist/aws/engine-step.cjs +1 -2
  16. package/dist/aws/engine-step.d.cts +0 -2
  17. package/dist/aws/engine-step.d.cts.map +1 -1
  18. package/dist/aws/engine-step.d.mts +0 -2
  19. package/dist/aws/engine-step.d.mts.map +1 -1
  20. package/dist/aws/engine-step.mjs +1 -2
  21. package/dist/aws/function-url.cjs +1 -2
  22. package/dist/aws/function-url.d.cts +0 -2
  23. package/dist/aws/function-url.d.cts.map +1 -1
  24. package/dist/aws/function-url.d.mts +0 -2
  25. package/dist/aws/function-url.d.mts.map +1 -1
  26. package/dist/aws/function-url.mjs +1 -2
  27. package/dist/aws/index.cjs +2 -3
  28. package/dist/aws/index.d.cts +0 -2
  29. package/dist/aws/index.d.cts.map +1 -1
  30. package/dist/aws/index.d.mts +0 -2
  31. package/dist/aws/index.d.mts.map +1 -1
  32. package/dist/aws/index.mjs +2 -3
  33. package/dist/aws/instances.cjs +1 -2
  34. package/dist/aws/instances.d.cts +0 -2
  35. package/dist/aws/instances.d.cts.map +1 -1
  36. package/dist/aws/instances.d.mts +0 -2
  37. package/dist/aws/instances.d.mts.map +1 -1
  38. package/dist/aws/instances.mjs +1 -2
  39. package/dist/aws/lambda-api.cjs +1 -2
  40. package/dist/aws/lambda-api.d.cts +0 -2
  41. package/dist/aws/lambda-api.d.cts.map +1 -1
  42. package/dist/aws/lambda-api.d.mts +0 -2
  43. package/dist/aws/lambda-api.d.mts.map +1 -1
  44. package/dist/aws/lambda-api.mjs +1 -2
  45. package/dist/client.cjs +46 -6
  46. package/dist/client.cjs.map +1 -1
  47. package/dist/client.d.cts +25 -5
  48. package/dist/client.d.cts.map +1 -1
  49. package/dist/client.d.mts +25 -5
  50. package/dist/client.d.mts.map +1 -1
  51. package/dist/client.mjs +46 -6
  52. package/dist/client.mjs.map +1 -1
  53. package/dist/cloudflare/bindings.cjs +1 -2
  54. package/dist/cloudflare/bindings.d.cts +0 -2
  55. package/dist/cloudflare/bindings.d.cts.map +1 -1
  56. package/dist/cloudflare/bindings.d.mts +0 -2
  57. package/dist/cloudflare/bindings.d.mts.map +1 -1
  58. package/dist/cloudflare/bindings.mjs +1 -2
  59. package/dist/cloudflare/index.cjs +2 -2
  60. package/dist/cloudflare/index.mjs +2 -2
  61. package/dist/cloudflare/runner.cjs +2 -3
  62. package/dist/cloudflare/runner.d.cts +1 -3
  63. package/dist/cloudflare/runner.d.cts.map +1 -1
  64. package/dist/cloudflare/runner.d.mts +1 -3
  65. package/dist/cloudflare/runner.d.mts.map +1 -1
  66. package/dist/cloudflare/runner.mjs +2 -3
  67. package/dist/cloudflare/worker.cjs +1 -2
  68. package/dist/cloudflare/worker.d.cts +0 -2
  69. package/dist/cloudflare/worker.d.cts.map +1 -1
  70. package/dist/cloudflare/worker.d.mts +0 -2
  71. package/dist/cloudflare/worker.d.mts.map +1 -1
  72. package/dist/cloudflare/worker.mjs +1 -2
  73. package/dist/cloudflare/workers.d.cts +0 -2
  74. package/dist/cloudflare/workers.d.cts.map +1 -1
  75. package/dist/cloudflare/workers.d.mts +0 -2
  76. package/dist/cloudflare/workers.d.mts.map +1 -1
  77. package/dist/config.cjs +25 -5
  78. package/dist/config.cjs.map +1 -1
  79. package/dist/config.d.cts +15 -11
  80. package/dist/config.d.cts.map +1 -1
  81. package/dist/config.d.mts +15 -11
  82. package/dist/config.d.mts.map +1 -1
  83. package/dist/config.mjs +26 -6
  84. package/dist/config.mjs.map +1 -1
  85. package/dist/contract.cjs +12 -2
  86. package/dist/contract.cjs.map +1 -1
  87. package/dist/contract.d.cts +28 -9
  88. package/dist/contract.d.cts.map +1 -1
  89. package/dist/contract.d.mts +28 -9
  90. package/dist/contract.d.mts.map +1 -1
  91. package/dist/contract.mjs +12 -2
  92. package/dist/contract.mjs.map +1 -1
  93. package/dist/dsl/action.cjs +1 -2
  94. package/dist/dsl/action.d.cts +0 -2
  95. package/dist/dsl/action.d.cts.map +1 -1
  96. package/dist/dsl/action.d.mts +0 -2
  97. package/dist/dsl/action.d.mts.map +1 -1
  98. package/dist/dsl/action.mjs +1 -2
  99. package/dist/dsl/base.cjs +1 -2
  100. package/dist/dsl/base.d.cts +0 -2
  101. package/dist/dsl/base.d.cts.map +1 -1
  102. package/dist/dsl/base.d.mts +0 -2
  103. package/dist/dsl/base.d.mts.map +1 -1
  104. package/dist/dsl/base.mjs +1 -2
  105. package/dist/dsl/bundle.cjs +5 -5
  106. package/dist/dsl/bundle.cjs.map +1 -1
  107. package/dist/dsl/bundle.d.cts +5 -3
  108. package/dist/dsl/bundle.d.cts.map +1 -1
  109. package/dist/dsl/bundle.d.mts +5 -3
  110. package/dist/dsl/bundle.d.mts.map +1 -1
  111. package/dist/dsl/bundle.mjs +5 -5
  112. package/dist/dsl/bundle.mjs.map +1 -1
  113. package/dist/dsl/condition.cjs +1 -2
  114. package/dist/dsl/condition.d.cts +0 -2
  115. package/dist/dsl/condition.d.cts.map +1 -1
  116. package/dist/dsl/condition.d.mts +0 -2
  117. package/dist/dsl/condition.d.mts.map +1 -1
  118. package/dist/dsl/condition.mjs +1 -2
  119. package/dist/dsl/flow.cjs +3 -3
  120. package/dist/dsl/flow.cjs.map +1 -1
  121. package/dist/dsl/flow.d.cts +1 -3
  122. package/dist/dsl/flow.d.cts.map +1 -1
  123. package/dist/dsl/flow.d.mts +1 -3
  124. package/dist/dsl/flow.d.mts.map +1 -1
  125. package/dist/dsl/flow.mjs +3 -3
  126. package/dist/dsl/flow.mjs.map +1 -1
  127. package/dist/dsl/refs.cjs +1 -2
  128. package/dist/dsl/refs.d.cts +0 -2
  129. package/dist/dsl/refs.d.cts.map +1 -1
  130. package/dist/dsl/refs.d.mts +0 -2
  131. package/dist/dsl/refs.d.mts.map +1 -1
  132. package/dist/dsl/refs.mjs +1 -2
  133. package/dist/dsl/template.cjs +1 -2
  134. package/dist/dsl/template.d.cts +0 -2
  135. package/dist/dsl/template.d.cts.map +1 -1
  136. package/dist/dsl/template.d.mts +0 -2
  137. package/dist/dsl/template.d.mts.map +1 -1
  138. package/dist/dsl/template.mjs +1 -2
  139. package/dist/dsl/trigger.cjs +1 -2
  140. package/dist/dsl/trigger.d.cts +0 -2
  141. package/dist/dsl/trigger.d.cts.map +1 -1
  142. package/dist/dsl/trigger.d.mts +0 -2
  143. package/dist/dsl/trigger.d.mts.map +1 -1
  144. package/dist/dsl/trigger.mjs +1 -2
  145. package/dist/dsl/workflow.cjs +3 -4
  146. package/dist/dsl/workflow.d.cts +0 -2
  147. package/dist/dsl/workflow.d.cts.map +1 -1
  148. package/dist/dsl/workflow.d.mts +0 -2
  149. package/dist/dsl/workflow.d.mts.map +1 -1
  150. package/dist/dsl/workflow.mjs +3 -4
  151. package/dist/email-context.cjs +1 -2
  152. package/dist/email-context.d.cts +0 -2
  153. package/dist/email-context.d.cts.map +1 -1
  154. package/dist/email-context.d.mts +0 -2
  155. package/dist/email-context.d.mts.map +1 -1
  156. package/dist/email-context.mjs +1 -2
  157. package/dist/email-sender.cjs +2 -3
  158. package/dist/email-sender.cjs.map +1 -1
  159. package/dist/email-sender.d.cts +1 -3
  160. package/dist/email-sender.d.cts.map +1 -1
  161. package/dist/email-sender.d.mts +1 -3
  162. package/dist/email-sender.d.mts.map +1 -1
  163. package/dist/email-sender.mjs +2 -3
  164. package/dist/email-sender.mjs.map +1 -1
  165. package/dist/email.cjs +33 -7
  166. package/dist/email.cjs.map +1 -1
  167. package/dist/email.d.cts +38 -11
  168. package/dist/email.d.cts.map +1 -1
  169. package/dist/email.d.mts +38 -11
  170. package/dist/email.d.mts.map +1 -1
  171. package/dist/email.mjs +33 -8
  172. package/dist/email.mjs.map +1 -1
  173. package/dist/engine/actions.cjs +1 -2
  174. package/dist/engine/actions.d.cts +0 -2
  175. package/dist/engine/actions.d.cts.map +1 -1
  176. package/dist/engine/actions.d.mts +0 -2
  177. package/dist/engine/actions.d.mts.map +1 -1
  178. package/dist/engine/actions.mjs +1 -2
  179. package/dist/engine/bucket.cjs +1 -2
  180. package/dist/engine/bucket.d.cts +0 -2
  181. package/dist/engine/bucket.d.cts.map +1 -1
  182. package/dist/engine/bucket.d.mts +0 -2
  183. package/dist/engine/bucket.d.mts.map +1 -1
  184. package/dist/engine/bucket.mjs +1 -2
  185. package/dist/engine/condition.cjs +1 -2
  186. package/dist/engine/condition.cjs.map +1 -1
  187. package/dist/engine/condition.d.cts +0 -2
  188. package/dist/engine/condition.d.cts.map +1 -1
  189. package/dist/engine/condition.d.mts +0 -2
  190. package/dist/engine/condition.d.mts.map +1 -1
  191. package/dist/engine/condition.mjs +1 -2
  192. package/dist/engine/condition.mjs.map +1 -1
  193. package/dist/engine/duration.cjs +1 -2
  194. package/dist/engine/duration.d.cts +0 -2
  195. package/dist/engine/duration.d.cts.map +1 -1
  196. package/dist/engine/duration.d.mts +0 -2
  197. package/dist/engine/duration.d.mts.map +1 -1
  198. package/dist/engine/duration.mjs +1 -2
  199. package/dist/engine/index.cjs +1 -1
  200. package/dist/engine/index.mjs +1 -1
  201. package/dist/engine/interpreter.cjs +22 -6
  202. package/dist/engine/interpreter.cjs.map +1 -1
  203. package/dist/engine/interpreter.d.cts +0 -2
  204. package/dist/engine/interpreter.d.cts.map +1 -1
  205. package/dist/engine/interpreter.d.mts +0 -2
  206. package/dist/engine/interpreter.d.mts.map +1 -1
  207. package/dist/engine/interpreter.mjs +22 -6
  208. package/dist/engine/interpreter.mjs.map +1 -1
  209. package/dist/engine/ports.d.cts +8 -4
  210. package/dist/engine/ports.d.cts.map +1 -1
  211. package/dist/engine/ports.d.mts +8 -4
  212. package/dist/engine/ports.d.mts.map +1 -1
  213. package/dist/engine/recipient.cjs +1 -2
  214. package/dist/engine/recipient.d.cts +0 -2
  215. package/dist/engine/recipient.d.cts.map +1 -1
  216. package/dist/engine/recipient.d.mts +0 -2
  217. package/dist/engine/recipient.d.mts.map +1 -1
  218. package/dist/engine/recipient.mjs +1 -2
  219. package/dist/engine/retry.cjs +1 -2
  220. package/dist/engine/retry.d.cts +0 -2
  221. package/dist/engine/retry.d.cts.map +1 -1
  222. package/dist/engine/retry.d.mts +0 -2
  223. package/dist/engine/retry.d.mts.map +1 -1
  224. package/dist/engine/retry.mjs +1 -2
  225. package/dist/engine/time-scale.cjs +1 -2
  226. package/dist/engine/time-scale.d.cts +0 -2
  227. package/dist/engine/time-scale.d.cts.map +1 -1
  228. package/dist/engine/time-scale.d.mts +0 -2
  229. package/dist/engine/time-scale.d.mts.map +1 -1
  230. package/dist/engine/time-scale.mjs +1 -2
  231. package/dist/engine/time-window.cjs +1 -2
  232. package/dist/engine/time-window.d.cts +0 -2
  233. package/dist/engine/time-window.d.cts.map +1 -1
  234. package/dist/engine/time-window.d.mts +0 -2
  235. package/dist/engine/time-window.d.mts.map +1 -1
  236. package/dist/engine/time-window.mjs +1 -2
  237. package/dist/fcm.cjs +1 -2
  238. package/dist/fcm.d.cts +0 -2
  239. package/dist/fcm.d.cts.map +1 -1
  240. package/dist/fcm.d.mts +0 -2
  241. package/dist/fcm.d.mts.map +1 -1
  242. package/dist/fcm.mjs +1 -2
  243. package/dist/hash.cjs +1 -2
  244. package/dist/hash.d.cts +0 -2
  245. package/dist/hash.d.cts.map +1 -1
  246. package/dist/hash.d.mts +0 -2
  247. package/dist/hash.d.mts.map +1 -1
  248. package/dist/hash.mjs +1 -2
  249. package/dist/host/auth.cjs +1 -2
  250. package/dist/host/auth.d.cts +0 -2
  251. package/dist/host/auth.d.cts.map +1 -1
  252. package/dist/host/auth.d.mts +0 -2
  253. package/dist/host/auth.d.mts.map +1 -1
  254. package/dist/host/auth.mjs +1 -2
  255. package/dist/host/channels.cjs +2 -3
  256. package/dist/host/channels.d.cts +0 -2
  257. package/dist/host/channels.d.cts.map +1 -1
  258. package/dist/host/channels.d.mts +0 -2
  259. package/dist/host/channels.d.mts.map +1 -1
  260. package/dist/host/channels.mjs +2 -3
  261. package/dist/host/execute.cjs +3 -4
  262. package/dist/host/execute.d.cts +1 -3
  263. package/dist/host/execute.d.cts.map +1 -1
  264. package/dist/host/execute.d.mts +1 -3
  265. package/dist/host/execute.d.mts.map +1 -1
  266. package/dist/host/execute.mjs +3 -4
  267. package/dist/host/fresh-read.cjs +1 -2
  268. package/dist/host/fresh-read.d.cts +0 -2
  269. package/dist/host/fresh-read.d.cts.map +1 -1
  270. package/dist/host/fresh-read.d.mts +0 -2
  271. package/dist/host/fresh-read.d.mts.map +1 -1
  272. package/dist/host/fresh-read.mjs +1 -2
  273. package/dist/host/http.cjs +39 -5
  274. package/dist/host/http.cjs.map +1 -1
  275. package/dist/host/http.d.cts +2 -4
  276. package/dist/host/http.d.cts.map +1 -1
  277. package/dist/host/http.d.mts +2 -4
  278. package/dist/host/http.d.mts.map +1 -1
  279. package/dist/host/http.mjs +40 -6
  280. package/dist/host/http.mjs.map +1 -1
  281. package/dist/host/ingest.cjs +129 -6
  282. package/dist/host/ingest.cjs.map +1 -1
  283. package/dist/host/ingest.d.cts +71 -2
  284. package/dist/host/ingest.d.cts.map +1 -1
  285. package/dist/host/ingest.d.mts +71 -2
  286. package/dist/host/ingest.d.mts.map +1 -1
  287. package/dist/host/ingest.mjs +126 -7
  288. package/dist/host/ingest.mjs.map +1 -1
  289. package/dist/host/journey-host.cjs +11 -10
  290. package/dist/host/journey-host.cjs.map +1 -1
  291. package/dist/host/journey-host.d.cts +0 -2
  292. package/dist/host/journey-host.d.cts.map +1 -1
  293. package/dist/host/journey-host.d.mts +0 -2
  294. package/dist/host/journey-host.d.mts.map +1 -1
  295. package/dist/host/journey-host.mjs +11 -10
  296. package/dist/host/journey-host.mjs.map +1 -1
  297. package/dist/host/platform.cjs +1 -2
  298. package/dist/host/platform.d.cts +0 -2
  299. package/dist/host/platform.d.cts.map +1 -1
  300. package/dist/host/platform.d.mts +0 -2
  301. package/dist/host/platform.d.mts.map +1 -1
  302. package/dist/host/platform.mjs +1 -2
  303. package/dist/host/senders.cjs +1 -2
  304. package/dist/host/senders.d.cts +0 -2
  305. package/dist/host/senders.d.cts.map +1 -1
  306. package/dist/host/senders.d.mts +0 -2
  307. package/dist/host/senders.d.mts.map +1 -1
  308. package/dist/host/senders.mjs +1 -2
  309. package/dist/http2-client.cjs +1 -2
  310. package/dist/http2-client.d.cts +0 -2
  311. package/dist/http2-client.d.cts.map +1 -1
  312. package/dist/http2-client.d.mts +0 -2
  313. package/dist/http2-client.d.mts.map +1 -1
  314. package/dist/http2-client.mjs +1 -2
  315. package/dist/index.cjs +5 -2
  316. package/dist/index.d.cts +5 -5
  317. package/dist/index.d.mts +5 -5
  318. package/dist/index.mjs +5 -5
  319. package/dist/ir.cjs +12 -4
  320. package/dist/ir.cjs.map +1 -1
  321. package/dist/ir.d.cts +17 -2
  322. package/dist/ir.d.cts.map +1 -1
  323. package/dist/ir.d.mts +17 -2
  324. package/dist/ir.d.mts.map +1 -1
  325. package/dist/ir.mjs +12 -5
  326. package/dist/ir.mjs.map +1 -1
  327. package/dist/jwt.cjs +1 -2
  328. package/dist/jwt.d.cts +0 -2
  329. package/dist/jwt.d.cts.map +1 -1
  330. package/dist/jwt.d.mts +0 -2
  331. package/dist/jwt.d.mts.map +1 -1
  332. package/dist/jwt.mjs +1 -2
  333. package/dist/plan.cjs +1 -2
  334. package/dist/plan.d.cts +0 -2
  335. package/dist/plan.d.cts.map +1 -1
  336. package/dist/plan.d.mts +0 -2
  337. package/dist/plan.d.mts.map +1 -1
  338. package/dist/plan.mjs +1 -2
  339. package/dist/postgres-js.cjs +1 -2
  340. package/dist/postgres-js.d.cts +0 -2
  341. package/dist/postgres-js.d.cts.map +1 -1
  342. package/dist/postgres-js.d.mts +0 -2
  343. package/dist/postgres-js.d.mts.map +1 -1
  344. package/dist/postgres-js.mjs +1 -2
  345. package/dist/provenance.cjs +1 -2
  346. package/dist/provenance.d.cts +0 -2
  347. package/dist/provenance.d.cts.map +1 -1
  348. package/dist/provenance.d.mts +0 -2
  349. package/dist/provenance.d.mts.map +1 -1
  350. package/dist/provenance.mjs +1 -2
  351. package/dist/push.cjs +1 -2
  352. package/dist/push.d.cts +0 -2
  353. package/dist/push.d.cts.map +1 -1
  354. package/dist/push.d.mts +0 -2
  355. package/dist/push.d.mts.map +1 -1
  356. package/dist/push.mjs +1 -2
  357. package/dist/react-email.cjs +1 -2
  358. package/dist/react-email.d.cts +0 -2
  359. package/dist/react-email.d.cts.map +1 -1
  360. package/dist/react-email.d.mts +0 -2
  361. package/dist/react-email.d.mts.map +1 -1
  362. package/dist/react-email.mjs +1 -2
  363. package/dist/reporting/classify.cjs +1 -2
  364. package/dist/reporting/classify.d.cts +0 -2
  365. package/dist/reporting/classify.d.cts.map +1 -1
  366. package/dist/reporting/classify.d.mts +0 -2
  367. package/dist/reporting/classify.d.mts.map +1 -1
  368. package/dist/reporting/classify.mjs +1 -2
  369. package/dist/reporting/observer.cjs +2 -2
  370. package/dist/reporting/observer.cjs.map +1 -1
  371. package/dist/reporting/observer.d.cts +0 -2
  372. package/dist/reporting/observer.d.cts.map +1 -1
  373. package/dist/reporting/observer.d.mts +0 -2
  374. package/dist/reporting/observer.d.mts.map +1 -1
  375. package/dist/reporting/observer.mjs +2 -2
  376. package/dist/reporting/observer.mjs.map +1 -1
  377. package/dist/reporting/tracking.cjs +29 -12
  378. package/dist/reporting/tracking.cjs.map +1 -1
  379. package/dist/reporting/tracking.d.cts +13 -8
  380. package/dist/reporting/tracking.d.cts.map +1 -1
  381. package/dist/reporting/tracking.d.mts +13 -8
  382. package/dist/reporting/tracking.d.mts.map +1 -1
  383. package/dist/reporting/tracking.mjs +29 -12
  384. package/dist/reporting/tracking.mjs.map +1 -1
  385. package/dist/runtime.cjs +1 -2
  386. package/dist/runtime.d.cts +0 -2
  387. package/dist/runtime.d.cts.map +1 -1
  388. package/dist/runtime.d.mts +0 -2
  389. package/dist/runtime.d.mts.map +1 -1
  390. package/dist/runtime.mjs +1 -2
  391. package/dist/store/erasure.cjs +1 -2
  392. package/dist/store/erasure.d.cts +0 -2
  393. package/dist/store/erasure.d.cts.map +1 -1
  394. package/dist/store/erasure.d.mts +0 -2
  395. package/dist/store/erasure.d.mts.map +1 -1
  396. package/dist/store/erasure.mjs +1 -2
  397. package/dist/store/facts.cjs +4 -5
  398. package/dist/store/facts.cjs.map +1 -1
  399. package/dist/store/facts.d.cts +2 -4
  400. package/dist/store/facts.d.cts.map +1 -1
  401. package/dist/store/facts.d.mts +2 -4
  402. package/dist/store/facts.d.mts.map +1 -1
  403. package/dist/store/facts.mjs +4 -5
  404. package/dist/store/facts.mjs.map +1 -1
  405. package/dist/store/index.cjs +2 -3
  406. package/dist/store/index.cjs.map +1 -1
  407. package/dist/store/index.d.cts +44 -15
  408. package/dist/store/index.d.cts.map +1 -1
  409. package/dist/store/index.d.mts +44 -15
  410. package/dist/store/index.d.mts.map +1 -1
  411. package/dist/store/index.mjs +2 -3
  412. package/dist/store/index.mjs.map +1 -1
  413. package/dist/store/postgres-reporting.cjs +31 -12
  414. package/dist/store/postgres-reporting.cjs.map +1 -1
  415. package/dist/store/postgres-reporting.d.cts +4 -4
  416. package/dist/store/postgres-reporting.d.cts.map +1 -1
  417. package/dist/store/postgres-reporting.d.mts +4 -4
  418. package/dist/store/postgres-reporting.d.mts.map +1 -1
  419. package/dist/store/postgres-reporting.mjs +31 -12
  420. package/dist/store/postgres-reporting.mjs.map +1 -1
  421. package/dist/store/postgres.cjs +40 -5
  422. package/dist/store/postgres.cjs.map +1 -1
  423. package/dist/store/postgres.d.cts +4 -4
  424. package/dist/store/postgres.d.cts.map +1 -1
  425. package/dist/store/postgres.d.mts +4 -4
  426. package/dist/store/postgres.d.mts.map +1 -1
  427. package/dist/store/postgres.mjs +41 -6
  428. package/dist/store/postgres.mjs.map +1 -1
  429. package/dist/store/sql.cjs +1 -2
  430. package/dist/store/sql.d.cts +0 -2
  431. package/dist/store/sql.d.cts.map +1 -1
  432. package/dist/store/sql.d.mts +0 -2
  433. package/dist/store/sql.d.mts.map +1 -1
  434. package/dist/store/sql.mjs +1 -2
  435. package/dist/store/tables.cjs +76 -13
  436. package/dist/store/tables.cjs.map +1 -1
  437. package/dist/store/tables.d.cts +17 -3
  438. package/dist/store/tables.d.cts.map +1 -1
  439. package/dist/store/tables.d.mts +17 -3
  440. package/dist/store/tables.d.mts.map +1 -1
  441. package/dist/store/tables.mjs +75 -14
  442. package/dist/store/tables.mjs.map +1 -1
  443. package/dist/store/user-lock.cjs +1 -2
  444. package/dist/store/user-lock.d.cts +0 -2
  445. package/dist/store/user-lock.d.cts.map +1 -1
  446. package/dist/store/user-lock.d.mts +0 -2
  447. package/dist/store/user-lock.d.mts.map +1 -1
  448. package/dist/store/user-lock.mjs +1 -2
  449. package/dist/templates.cjs +61 -9
  450. package/dist/templates.cjs.map +1 -1
  451. package/dist/templates.d.cts +101 -15
  452. package/dist/templates.d.cts.map +1 -1
  453. package/dist/templates.d.mts +101 -15
  454. package/dist/templates.d.mts.map +1 -1
  455. package/dist/templates.mjs +62 -11
  456. package/dist/templates.mjs.map +1 -1
  457. package/dist/transports/cloudflare-email.cjs +1 -2
  458. package/dist/transports/cloudflare-email.d.cts +0 -2
  459. package/dist/transports/cloudflare-email.d.cts.map +1 -1
  460. package/dist/transports/cloudflare-email.d.mts +0 -2
  461. package/dist/transports/cloudflare-email.d.mts.map +1 -1
  462. package/dist/transports/cloudflare-email.mjs +1 -2
  463. package/dist/transports/ses.cjs +1 -2
  464. package/dist/transports/ses.d.cts +0 -2
  465. package/dist/transports/ses.d.cts.map +1 -1
  466. package/dist/transports/ses.d.mts +0 -2
  467. package/dist/transports/ses.d.mts.map +1 -1
  468. package/dist/transports/ses.mjs +1 -2
  469. package/dist/transports/sigv4.cjs +1 -2
  470. package/dist/transports/sigv4.d.cts +0 -2
  471. package/dist/transports/sigv4.d.cts.map +1 -1
  472. package/dist/transports/sigv4.d.mts +0 -2
  473. package/dist/transports/sigv4.d.mts.map +1 -1
  474. package/dist/transports/sigv4.mjs +1 -2
  475. package/dist/webhooks.cjs +1 -2
  476. package/dist/webhooks.d.cts +0 -2
  477. package/dist/webhooks.d.cts.map +1 -1
  478. package/dist/webhooks.d.mts +0 -2
  479. package/dist/webhooks.d.mts.map +1 -1
  480. package/dist/webhooks.mjs +1 -2
  481. package/dist/webpush.cjs +1 -2
  482. package/dist/webpush.d.cts +0 -2
  483. package/dist/webpush.d.cts.map +1 -1
  484. package/dist/webpush.d.mts +0 -2
  485. package/dist/webpush.d.mts.map +1 -1
  486. package/dist/webpush.mjs +1 -2
  487. package/package.json +5 -5
@@ -1,5 +1,4 @@
1
1
  import { JourneyReporting } from "../store/index.cjs";
2
- //#region src/reporting/tracking.d.ts
3
2
  /**
4
3
  * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker
5
4
  * without a bearer — recipients open them:
@@ -9,7 +8,9 @@ import { JourneyReporting } from "../store/index.cjs";
9
8
  * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm
10
9
  * GET /t/u/<message> the List-Unsubscribe header's URL, opened: asks to confirm
11
10
  * POST /t/u/<message> the unsubscribe confirmation, and the header's one-click
12
- * unsubscribe (RFC 8058: a mail client's button posts here)
11
+ * unsubscribe (RFC 8058: a mail client's button posts here):
12
+ * opts out of the email's topic; `?scope=all` (the page's
13
+ * second button) of every email
13
14
  * POST /t/d/<message> a push the device received (iOS's notification service
14
15
  * extension, a browser's service worker)
15
16
  * POST /t/p/<message> a push opened, reported by a browser's service worker
@@ -19,8 +20,10 @@ import { JourneyReporting } from "../store/index.cjs";
19
20
  * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An
20
21
  * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was
21
22
  * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,
22
- * which no link scanner sends — counts. Changing a template's or the config's unsubscribe URL, or
23
- * removing the template, changes nothing for emails already sent.
23
+ * which no link scanner sends — counts. The page offers the email's topic (the default, and what
24
+ * a mail client's one-click POST does) and every email. Changing a template's or the config's
25
+ * unsubscribe URL, or removing the template, changes nothing for emails already sent. A
26
+ * transactional email (no topic) never carried the link: every `/t/u` variant answers 404 for it.
24
27
  */
25
28
  export interface TrackingOptions {
26
29
  /** The name the confirmation pages show (the config's `title`). */
@@ -34,14 +37,17 @@ export interface TrackingOptions {
34
37
  at: number;
35
38
  }) => Promise<void>) | undefined;
36
39
  /**
37
- * An unsubscribe was recorded (`POST /t/u`): the host wakes the user's waits that read email
38
- * reachability, so an `exitWhen: not(reachable('email'))` takes effect at once. Its failure is
39
- * logged, never a reason to fail the unsubscribe.
40
+ * An unsubscribe was recorded (`POST /t/u`): from the email's `topic`, or with `all` from every
41
+ * email — then the host wakes the user's waits that read email reachability, so an
42
+ * `exitWhen: not(reachable('email'))` takes effect at once. Its failure is logged, never a
43
+ * reason to fail the unsubscribe.
40
44
  */
41
45
  onUnsubscribed?: ((unsubscribe: {
42
46
  messageId: string;
43
47
  userId: string;
44
48
  at: number;
49
+ topic: string;
50
+ all: boolean;
45
51
  }) => Promise<void>) | undefined;
46
52
  }
47
53
  /** A `/t/...` request's response, or undefined when the path is not a tracking route. */
@@ -52,5 +58,4 @@ export declare function handleTracking(request: Request, reporting: JourneyRepor
52
58
  * notification. False when the message is unknown or not the user's.
53
59
  */
54
60
  export declare function recordPushOpen(reporting: JourneyReporting, userId: string, messageId: string, at?: number): Promise<boolean>;
55
- //#endregion
56
61
  //# sourceMappingURL=tracking.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracking.d.cts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;iBA0BiB;;EAEf;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;;;;EAOlE,mBACM;IAAe;IAAmB;IAAgB;QAAiB;;;wBAgCrD,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAoFW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
1
+ {"version":3,"file":"tracking.d.cts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;EAEf;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;;;;;EAQlE,mBACM;IACA;IACA;IACA;IACA;IACA;QACI;;;wBAgCU,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAoHW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
@@ -1,5 +1,4 @@
1
1
  import { JourneyReporting } from "../store/index.mjs";
2
- //#region src/reporting/tracking.d.ts
3
2
  /**
4
3
  * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker
5
4
  * without a bearer — recipients open them:
@@ -9,7 +8,9 @@ import { JourneyReporting } from "../store/index.mjs";
9
8
  * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm
10
9
  * GET /t/u/<message> the List-Unsubscribe header's URL, opened: asks to confirm
11
10
  * POST /t/u/<message> the unsubscribe confirmation, and the header's one-click
12
- * unsubscribe (RFC 8058: a mail client's button posts here)
11
+ * unsubscribe (RFC 8058: a mail client's button posts here):
12
+ * opts out of the email's topic; `?scope=all` (the page's
13
+ * second button) of every email
13
14
  * POST /t/d/<message> a push the device received (iOS's notification service
14
15
  * extension, a browser's service worker)
15
16
  * POST /t/p/<message> a push opened, reported by a browser's service worker
@@ -19,8 +20,10 @@ import { JourneyReporting } from "../store/index.mjs";
19
20
  * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An
20
21
  * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was
21
22
  * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,
22
- * which no link scanner sends — counts. Changing a template's or the config's unsubscribe URL, or
23
- * removing the template, changes nothing for emails already sent.
23
+ * which no link scanner sends — counts. The page offers the email's topic (the default, and what
24
+ * a mail client's one-click POST does) and every email. Changing a template's or the config's
25
+ * unsubscribe URL, or removing the template, changes nothing for emails already sent. A
26
+ * transactional email (no topic) never carried the link: every `/t/u` variant answers 404 for it.
24
27
  */
25
28
  export interface TrackingOptions {
26
29
  /** The name the confirmation pages show (the config's `title`). */
@@ -34,14 +37,17 @@ export interface TrackingOptions {
34
37
  at: number;
35
38
  }) => Promise<void>) | undefined;
36
39
  /**
37
- * An unsubscribe was recorded (`POST /t/u`): the host wakes the user's waits that read email
38
- * reachability, so an `exitWhen: not(reachable('email'))` takes effect at once. Its failure is
39
- * logged, never a reason to fail the unsubscribe.
40
+ * An unsubscribe was recorded (`POST /t/u`): from the email's `topic`, or with `all` from every
41
+ * email — then the host wakes the user's waits that read email reachability, so an
42
+ * `exitWhen: not(reachable('email'))` takes effect at once. Its failure is logged, never a
43
+ * reason to fail the unsubscribe.
40
44
  */
41
45
  onUnsubscribed?: ((unsubscribe: {
42
46
  messageId: string;
43
47
  userId: string;
44
48
  at: number;
49
+ topic: string;
50
+ all: boolean;
45
51
  }) => Promise<void>) | undefined;
46
52
  }
47
53
  /** A `/t/...` request's response, or undefined when the path is not a tracking route. */
@@ -52,5 +58,4 @@ export declare function handleTracking(request: Request, reporting: JourneyRepor
52
58
  * notification. False when the message is unknown or not the user's.
53
59
  */
54
60
  export declare function recordPushOpen(reporting: JourneyReporting, userId: string, messageId: string, at?: number): Promise<boolean>;
55
- //#endregion
56
61
  //# sourceMappingURL=tracking.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracking.d.mts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;iBA0BiB;;EAEf;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;;;;EAOlE,mBACM;IAAe;IAAmB;IAAgB;QAAiB;;;wBAgCrD,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAoFW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
1
+ {"version":3,"file":"tracking.d.mts","names":[],"sources":["../../src/reporting/tracking.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BiB;;EAEf;;EAEA;;EAEA,eACM;IAAQ;IAAmB;IAAgB;QAAiB;;;;;;;EAQlE,mBACM;IACA;IACA;IACA;IACA;IACA;QACI;;;wBAgCU,eACpB,SAAS,SACT,WAAW,kBACX,UAAS,kBACR,QAAQ;;;;;;wBAoHW,eACpB,WAAW,kBACX,gBACA,mBACA,cACC"}
@@ -1,5 +1,5 @@
1
1
  import { BURST_WINDOW_MS, classifyClick, classifyOpen } from "./classify.mjs";
2
- //#region src/reporting/tracking.ts
2
+ // src/reporting/tracking.ts
3
3
  const GIF = Uint8Array.from(atob("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"), (char) => char.charCodeAt(0));
4
4
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
5
  function requester(request) {
@@ -14,7 +14,8 @@ function requester(request) {
14
14
  }
15
15
  /** A `/t/...` request's response, or undefined when the path is not a tracking route. */
16
16
  async function handleTracking(request, reporting, options = {}) {
17
- const path = new URL(request.url).pathname.split("/");
17
+ const url = new URL(request.url);
18
+ const path = url.pathname.split("/");
18
19
  const root = path.at(1);
19
20
  const kind = path.at(2);
20
21
  const id = path.at(3);
@@ -32,10 +33,10 @@ async function handleTracking(request, reporting, options = {}) {
32
33
  if ((kind === "l" || kind === "u") && position !== void 0 && read) {
33
34
  const target = await recordClick(reporting, id, Number(position), who, options);
34
35
  if (target === void 0) return page(options, "This link has expired.", 404);
35
- if (kind === "u") return confirmUnsubscribe(options, id);
36
+ if (kind === "u") return confirmUnsubscribe(reporting, options, id);
36
37
  return Response.redirect(target, 302);
37
38
  }
38
- if (kind === "u" && position === void 0 && read) return confirmUnsubscribe(options, id);
39
+ if (kind === "u" && position === void 0 && read) return confirmUnsubscribe(reporting, options, id);
39
40
  if (kind === "d" && who.method === "POST") {
40
41
  const known = await reporting.recordDeviceDelivered(id, Date.now());
41
42
  return new Response(null, { status: known ? 204 : 404 });
@@ -65,23 +66,40 @@ async function handleTracking(request, reporting, options = {}) {
65
66
  }
66
67
  if (kind === "u" && who.method === "POST") {
67
68
  const at = Date.now();
68
- const unsubscribed = await reporting.recordUnsubscribe(id, at);
69
+ const all = url.searchParams.get("scope") === "all";
70
+ const unsubscribed = await reporting.recordUnsubscribe(id, at, all ? "all" : "topic");
69
71
  if (unsubscribed === void 0) return page(options, "You won't get these emails anymore.");
72
+ if (unsubscribed.topic === null) return page(options, "This link has expired.", 404);
70
73
  if (options.onUnsubscribed !== void 0) try {
71
74
  await options.onUnsubscribed({
72
75
  messageId: id,
73
76
  userId: unsubscribed.userId,
74
- at
77
+ at,
78
+ topic: unsubscribed.topic,
79
+ all
75
80
  });
76
81
  } catch (error) {
77
82
  console.error(`[workflow] acting on the unsubscribe of ${id} failed`, error);
78
83
  }
79
- return page(options, options.product === void 0 ? "You won't get these emails anymore." : `You won't get these emails from ${escape(options.product)} anymore.`);
84
+ const from = options.product === void 0 ? "" : ` from ${escape(options.product)}`;
85
+ return page(options, all ? `You won't get emails${from} anymore.` : `You won't get ${escape(topicName(unsubscribed.topic))} emails${from} anymore.`);
80
86
  }
81
87
  }
82
- /** The question an unsubscribe link, or the List-Unsubscribe URL opened in a browser, asks. */
83
- function confirmUnsubscribe(options, id) {
84
- return page(options, `Unsubscribe from ${escape(options.product ?? "these")} emails?`, 200, `<form method="post" action="/t/u/${id}"><button type="submit">Unsubscribe</button></form>`);
88
+ /** A topic id as the page shows it: `product_updates` reads as "product updates". */
89
+ function topicName(topic) {
90
+ return topic.replace(/[_-]+/g, " ");
91
+ }
92
+ /**
93
+ * The question an unsubscribe link, or the List-Unsubscribe URL opened in a browser, asks: the
94
+ * email's topic (the default), or every email. A copy nobody knows, or a transactional one (the
95
+ * link was never in it), is an expired link.
96
+ */
97
+ async function confirmUnsubscribe(reporting, options, id) {
98
+ const message = await reporting.lookupMessage(id);
99
+ if (message?.channel !== "email" || message.topic === null) return page(options, "This link has expired.", 404);
100
+ const product = options.product === void 0 ? "" : ` from ${escape(options.product)}`;
101
+ const topic = escape(topicName(message.topic));
102
+ return page(options, `Unsubscribe from ${topic} emails${product}?`, 200, `<form method="post" action="/t/u/${id}"><button type="submit">Unsubscribe from ${topic}</button></form><form method="post" action="/t/u/${id}?scope=all"><button type="submit" class="secondary">Unsubscribe from all emails</button></form>`);
85
103
  }
86
104
  /**
87
105
  * A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on
@@ -149,13 +167,12 @@ function escape(text) {
149
167
  return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
150
168
  }
151
169
  function page(options, message, status = 200, extra = "") {
152
- const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(options.product ?? "Email")}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;
170
+ const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(options.product ?? "Email")}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}form{margin:12px 0}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}button.secondary{background:#e4e4e7;color:#18181b}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;
153
171
  return new Response(html, {
154
172
  status,
155
173
  headers: { "content-type": "text/html; charset=utf-8" }
156
174
  });
157
175
  }
158
- //#endregion
159
176
  export { handleTracking, recordPushOpen };
160
177
 
161
178
  //# sourceMappingURL=tracking.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"tracking.mjs","names":[],"sources":["../../src/reporting/tracking.ts"],"sourcesContent":["import type { JourneyReporting } from '../store';\nimport { BURST_WINDOW_MS, classifyClick, classifyOpen } from './classify';\n\n/**\n * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker\n * without a bearer — recipients open them:\n *\n * GET /t/o/<message> the open pixel: records the open, answers a 1×1 GIF\n * GET /t/l/<message>/<position> a link: records the click, redirects to the recorded URL\n * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm\n * GET /t/u/<message> the List-Unsubscribe header's URL, opened: asks to confirm\n * POST /t/u/<message> the unsubscribe confirmation, and the header's one-click\n * unsubscribe (RFC 8058: a mail client's button posts here)\n * POST /t/d/<message> a push the device received (iOS's notification service\n * extension, a browser's service worker)\n * POST /t/p/<message> a push opened, reported by a browser's service worker\n *\n * Every open and click is recorded with its verdict (./classify.ts), and the message keeps its\n * first open and click of each kind. The redirect goes only to the URL recorded for that position,\n * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An\n * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was\n * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,\n * which no link scanner sends — counts. Changing a template's or the config's unsubscribe URL, or\n * removing the template, changes nothing for emails already sent.\n */\n\nexport interface TrackingOptions {\n /** The name the confirmation pages show (the config's `title`). */\n product?: string | undefined;\n /** `email.tracking.quickClickMs`: how soon after delivery an open or click is a weak machine signal. */\n quickMs?: number | undefined;\n /** A browser reported a push tapped (`/t/p`): the host records it as an event too (`webPush.openedEvent`). */\n pushOpened?:\n | ((open: { messageId: string; userId: string; at: number }) => Promise<void>)\n | undefined;\n /**\n * An unsubscribe was recorded (`POST /t/u`): the host wakes the user's waits that read email\n * reachability, so an `exitWhen: not(reachable('email'))` takes effect at once. Its failure is\n * logged, never a reason to fail the unsubscribe.\n */\n onUnsubscribed?:\n | ((unsubscribe: { messageId: string; userId: string; at: number }) => Promise<void>)\n | undefined;\n}\n\nconst GIF = Uint8Array.from(\n atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),\n (char) => char.charCodeAt(0)\n);\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Requester {\n method: string;\n userAgent: string | null;\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\nfunction requester(request: Request): Requester {\n const cf = (\n request as Request & { cf?: { asn?: number; asOrganization?: string; country?: string } }\n ).cf;\n return {\n method: request.method,\n userAgent: request.headers.get('user-agent'),\n asn: cf?.asn ?? null,\n organization: cf?.asOrganization ?? null,\n country: cf?.country ?? null,\n };\n}\n\n/** A `/t/...` request's response, or undefined when the path is not a tracking route. */\nexport async function handleTracking(\n request: Request,\n reporting: JourneyReporting,\n options: TrackingOptions = {}\n): Promise<Response | undefined> {\n const url = new URL(request.url);\n const path = url.pathname.split('/');\n const root = path.at(1);\n const kind = path.at(2);\n const id = path.at(3);\n const position = path.at(4);\n if (root !== 't' || id === undefined || !UUID.test(id)) return undefined;\n const who = requester(request);\n const read = who.method === 'GET' || who.method === 'HEAD';\n if (kind === 'o' && read) {\n await recordOpen(reporting, id, who, options);\n return new Response(who.method === 'HEAD' ? null : GIF, {\n headers: { 'content-type': 'image/gif', 'cache-control': 'no-store, max-age=0' },\n });\n }\n if ((kind === 'l' || kind === 'u') && position !== undefined && read) {\n const target = await recordClick(reporting, id, Number(position), who, options);\n if (target === undefined) return page(options, 'This link has expired.', 404);\n if (kind === 'u') return confirmUnsubscribe(options, id);\n return Response.redirect(target, 302);\n }\n // The List-Unsubscribe URL opened in a browser (a client without one-click): the same question\n if (kind === 'u' && position === undefined && read) return confirmUnsubscribe(options, id);\n if (kind === 'd' && who.method === 'POST') {\n const known = await reporting.recordDeviceDelivered(id, Date.now());\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'p' && who.method === 'POST') {\n // A notification tap: a person, as the message id is what proves it is theirs\n const at = Date.now();\n let userId: string | undefined;\n const known = await reporting.recordOpen(id, (message) => {\n userId = message.userId;\n return { at, machine: false, reason: null, ...network(who) };\n });\n if (known && userId !== undefined && options.pushOpened !== undefined) {\n try {\n await options.pushOpened({ messageId: id, userId, at });\n } catch (error) {\n // The open is recorded; the event is the host's extra, never a reason to fail the beacon\n console.error(`[workflow] recording push open ${id} as an event failed`, error);\n }\n }\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'u' && who.method === 'POST') {\n const at = Date.now();\n const unsubscribed = await reporting.recordUnsubscribe(id, at);\n // A mail client's one-click POST must succeed: for a copy no longer on record (its user\n // erased) there is nothing left to send to, so the answer is the same\n if (unsubscribed === undefined) return page(options, \"You won't get these emails anymore.\");\n if (options.onUnsubscribed !== undefined) {\n try {\n await options.onUnsubscribed({ messageId: id, userId: unsubscribed.userId, at });\n } catch (error) {\n console.error(`[workflow] acting on the unsubscribe of ${id} failed`, error);\n }\n }\n return page(\n options,\n options.product === undefined\n ? \"You won't get these emails anymore.\"\n : `You won't get these emails from ${escape(options.product)} anymore.`\n );\n }\n return undefined;\n}\n\n/** The question an unsubscribe link, or the List-Unsubscribe URL opened in a browser, asks. */\nfunction confirmUnsubscribe(options: TrackingOptions, id: string): Response {\n return page(\n options,\n `Unsubscribe from ${escape(options.product ?? 'these')} emails?`,\n 200,\n `<form method=\"post\" action=\"/t/u/${id}\"><button type=\"submit\">Unsubscribe</button></form>`\n );\n}\n\n/**\n * A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on\n * the copy — only the recipient's own, and never a machine, since no proxy or scanner taps a\n * notification. False when the message is unknown or not the user's.\n */\nexport async function recordPushOpen(\n reporting: JourneyReporting,\n userId: string,\n messageId: string,\n at = Date.now()\n): Promise<boolean> {\n if (!UUID.test(messageId)) return false;\n let recorded = false;\n await reporting.recordOpen(messageId, (message) => {\n if (message.userId !== userId) return undefined;\n recorded = true;\n return {\n at,\n machine: false,\n reason: null,\n userAgent: null,\n asn: null,\n organization: null,\n country: null,\n };\n });\n return recorded;\n}\n\nasync function recordOpen(\n reporting: JourneyReporting,\n id: string,\n who: Requester,\n options: TrackingOptions\n) {\n const at = Date.now();\n await reporting.recordOpen(id, (message) => ({\n at,\n ...classifyOpen(\n { ...who, sinceDeliveredMs: at - message.deliveredAt },\n { quickMs: options.quickMs }\n ),\n ...network(who),\n }));\n}\n\nconst network = (who: Requester) => ({\n userAgent: who.userAgent,\n asn: who.asn,\n organization: who.organization,\n country: who.country,\n});\n\n/** Records the click and answers the URL to send the visitor to, or undefined for an unknown link. */\nasync function recordClick(\n reporting: JourneyReporting,\n id: string,\n position: number,\n who: Requester,\n options: TrackingOptions\n): Promise<string | undefined> {\n if (!Number.isInteger(position) || position < 0) return undefined;\n const at = Date.now();\n const recorded = await reporting.recordClick(id, position, at, BURST_WINDOW_MS, (link) => {\n const linksInBurst = new Set([position, ...link.recent.map((click) => click.position)]).size;\n const verdict = classifyClick(\n { ...who, sinceDeliveredMs: at - link.deliveredAt, linksInBurst, linkCount: link.linkCount },\n { quickMs: options.quickMs }\n );\n return {\n engagement: { at, ...verdict, ...network(who) },\n backdate: verdict.reason === 'link_burst' || verdict.reason === 'quick+link_pair',\n };\n });\n return recorded?.url;\n}\n\nfunction escape(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\nfunction page(options: TrackingOptions, message: string, status = 200, extra = ''): Response {\n const title = escape(options.product ?? 'Email');\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;\n return new Response(html, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tracking.mjs","names":[],"sources":["../../src/reporting/tracking.ts"],"sourcesContent":["import type { JourneyReporting } from '../store';\nimport { BURST_WINDOW_MS, classifyClick, classifyOpen } from './classify';\n\n/**\n * The endpoints a journey email's tracking points at (see ./observer.ts), served by journeyWorker\n * without a bearer — recipients open them:\n *\n * GET /t/o/<message> the open pixel: records the open, answers a 1×1 GIF\n * GET /t/l/<message>/<position> a link: records the click, redirects to the recorded URL\n * GET /t/u/<message>/<position> the unsubscribe link: records the click, asks to confirm\n * GET /t/u/<message> the List-Unsubscribe header's URL, opened: asks to confirm\n * POST /t/u/<message> the unsubscribe confirmation, and the header's one-click\n * unsubscribe (RFC 8058: a mail client's button posts here):\n * opts out of the email's topic; `?scope=all` (the page's\n * second button) of every email\n * POST /t/d/<message> a push the device received (iOS's notification service\n * extension, a browser's service worker)\n * POST /t/p/<message> a push opened, reported by a browser's service worker\n *\n * Every open and click is recorded with its verdict (./classify.ts), and the message keeps its\n * first open and click of each kind. The redirect goes only to the URL recorded for that position,\n * never to one the request carries, so the endpoint cannot bounce visitors anywhere else. An\n * email's unsubscribe link is a tracked link too, and which link it is was fixed when the email was\n * sent (its route): following it shows a confirmation page, and only the confirmation — a POST,\n * which no link scanner sends — counts. The page offers the email's topic (the default, and what\n * a mail client's one-click POST does) and every email. Changing a template's or the config's\n * unsubscribe URL, or removing the template, changes nothing for emails already sent. A\n * transactional email (no topic) never carried the link: every `/t/u` variant answers 404 for it.\n */\n\nexport interface TrackingOptions {\n /** The name the confirmation pages show (the config's `title`). */\n product?: string | undefined;\n /** `email.tracking.quickClickMs`: how soon after delivery an open or click is a weak machine signal. */\n quickMs?: number | undefined;\n /** A browser reported a push tapped (`/t/p`): the host records it as an event too (`webPush.openedEvent`). */\n pushOpened?:\n | ((open: { messageId: string; userId: string; at: number }) => Promise<void>)\n | undefined;\n /**\n * An unsubscribe was recorded (`POST /t/u`): from the email's `topic`, or with `all` from every\n * email — then the host wakes the user's waits that read email reachability, so an\n * `exitWhen: not(reachable('email'))` takes effect at once. Its failure is logged, never a\n * reason to fail the unsubscribe.\n */\n onUnsubscribed?:\n | ((unsubscribe: {\n messageId: string;\n userId: string;\n at: number;\n topic: string;\n all: boolean;\n }) => Promise<void>)\n | undefined;\n}\n\nconst GIF = Uint8Array.from(\n atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),\n (char) => char.charCodeAt(0)\n);\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n\ninterface Requester {\n method: string;\n userAgent: string | null;\n asn: number | null;\n organization: string | null;\n country: string | null;\n}\n\nfunction requester(request: Request): Requester {\n const cf = (\n request as Request & { cf?: { asn?: number; asOrganization?: string; country?: string } }\n ).cf;\n return {\n method: request.method,\n userAgent: request.headers.get('user-agent'),\n asn: cf?.asn ?? null,\n organization: cf?.asOrganization ?? null,\n country: cf?.country ?? null,\n };\n}\n\n/** A `/t/...` request's response, or undefined when the path is not a tracking route. */\nexport async function handleTracking(\n request: Request,\n reporting: JourneyReporting,\n options: TrackingOptions = {}\n): Promise<Response | undefined> {\n const url = new URL(request.url);\n const path = url.pathname.split('/');\n const root = path.at(1);\n const kind = path.at(2);\n const id = path.at(3);\n const position = path.at(4);\n if (root !== 't' || id === undefined || !UUID.test(id)) return undefined;\n const who = requester(request);\n const read = who.method === 'GET' || who.method === 'HEAD';\n if (kind === 'o' && read) {\n await recordOpen(reporting, id, who, options);\n return new Response(who.method === 'HEAD' ? null : GIF, {\n headers: { 'content-type': 'image/gif', 'cache-control': 'no-store, max-age=0' },\n });\n }\n if ((kind === 'l' || kind === 'u') && position !== undefined && read) {\n const target = await recordClick(reporting, id, Number(position), who, options);\n if (target === undefined) return page(options, 'This link has expired.', 404);\n if (kind === 'u') return confirmUnsubscribe(reporting, options, id);\n return Response.redirect(target, 302);\n }\n // The List-Unsubscribe URL opened in a browser (a client without one-click): the same question\n if (kind === 'u' && position === undefined && read)\n return confirmUnsubscribe(reporting, options, id);\n if (kind === 'd' && who.method === 'POST') {\n const known = await reporting.recordDeviceDelivered(id, Date.now());\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'p' && who.method === 'POST') {\n // A notification tap: a person, as the message id is what proves it is theirs\n const at = Date.now();\n let userId: string | undefined;\n const known = await reporting.recordOpen(id, (message) => {\n userId = message.userId;\n return { at, machine: false, reason: null, ...network(who) };\n });\n if (known && userId !== undefined && options.pushOpened !== undefined) {\n try {\n await options.pushOpened({ messageId: id, userId, at });\n } catch (error) {\n // The open is recorded; the event is the host's extra, never a reason to fail the beacon\n console.error(`[workflow] recording push open ${id} as an event failed`, error);\n }\n }\n return new Response(null, { status: known ? 204 : 404 });\n }\n if (kind === 'u' && who.method === 'POST') {\n const at = Date.now();\n // The page's \"all emails\" button; a mail client's one-click POST carries no scope\n const all = url.searchParams.get('scope') === 'all';\n const unsubscribed = await reporting.recordUnsubscribe(id, at, all ? 'all' : 'topic');\n // A mail client's one-click POST must succeed: for a copy no longer on record (its user\n // erased) there is nothing left to send to, so the answer is the same\n if (unsubscribed === undefined) return page(options, \"You won't get these emails anymore.\");\n // A transactional email never carried the link\n if (unsubscribed.topic === null) return page(options, 'This link has expired.', 404);\n if (options.onUnsubscribed !== undefined) {\n try {\n await options.onUnsubscribed({\n messageId: id,\n userId: unsubscribed.userId,\n at,\n topic: unsubscribed.topic,\n all,\n });\n } catch (error) {\n console.error(`[workflow] acting on the unsubscribe of ${id} failed`, error);\n }\n }\n const from = options.product === undefined ? '' : ` from ${escape(options.product)}`;\n return page(\n options,\n all\n ? `You won't get emails${from} anymore.`\n : `You won't get ${escape(topicName(unsubscribed.topic))} emails${from} anymore.`\n );\n }\n return undefined;\n}\n\n/** A topic id as the page shows it: `product_updates` reads as \"product updates\". */\nfunction topicName(topic: string): string {\n return topic.replace(/[_-]+/g, ' ');\n}\n\n/**\n * The question an unsubscribe link, or the List-Unsubscribe URL opened in a browser, asks: the\n * email's topic (the default), or every email. A copy nobody knows, or a transactional one (the\n * link was never in it), is an expired link.\n */\nasync function confirmUnsubscribe(\n reporting: JourneyReporting,\n options: TrackingOptions,\n id: string\n): Promise<Response> {\n const message = await reporting.lookupMessage(id);\n if (message?.channel !== 'email' || message.topic === null) {\n return page(options, 'This link has expired.', 404);\n }\n const product = options.product === undefined ? '' : ` from ${escape(options.product)}`;\n const topic = escape(topicName(message.topic));\n return page(\n options,\n `Unsubscribe from ${topic} emails${product}?`,\n 200,\n `<form method=\"post\" action=\"/t/u/${id}\"><button type=\"submit\">Unsubscribe from ${topic}</button></form>` +\n `<form method=\"post\" action=\"/t/u/${id}?scope=all\"><button type=\"submit\" class=\"secondary\">Unsubscribe from all emails</button></form>`\n );\n}\n\n/**\n * A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on\n * the copy — only the recipient's own, and never a machine, since no proxy or scanner taps a\n * notification. False when the message is unknown or not the user's.\n */\nexport async function recordPushOpen(\n reporting: JourneyReporting,\n userId: string,\n messageId: string,\n at = Date.now()\n): Promise<boolean> {\n if (!UUID.test(messageId)) return false;\n let recorded = false;\n await reporting.recordOpen(messageId, (message) => {\n if (message.userId !== userId) return undefined;\n recorded = true;\n return {\n at,\n machine: false,\n reason: null,\n userAgent: null,\n asn: null,\n organization: null,\n country: null,\n };\n });\n return recorded;\n}\n\nasync function recordOpen(\n reporting: JourneyReporting,\n id: string,\n who: Requester,\n options: TrackingOptions\n) {\n const at = Date.now();\n await reporting.recordOpen(id, (message) => ({\n at,\n ...classifyOpen(\n { ...who, sinceDeliveredMs: at - message.deliveredAt },\n { quickMs: options.quickMs }\n ),\n ...network(who),\n }));\n}\n\nconst network = (who: Requester) => ({\n userAgent: who.userAgent,\n asn: who.asn,\n organization: who.organization,\n country: who.country,\n});\n\n/** Records the click and answers the URL to send the visitor to, or undefined for an unknown link. */\nasync function recordClick(\n reporting: JourneyReporting,\n id: string,\n position: number,\n who: Requester,\n options: TrackingOptions\n): Promise<string | undefined> {\n if (!Number.isInteger(position) || position < 0) return undefined;\n const at = Date.now();\n const recorded = await reporting.recordClick(id, position, at, BURST_WINDOW_MS, (link) => {\n const linksInBurst = new Set([position, ...link.recent.map((click) => click.position)]).size;\n const verdict = classifyClick(\n { ...who, sinceDeliveredMs: at - link.deliveredAt, linksInBurst, linkCount: link.linkCount },\n { quickMs: options.quickMs }\n );\n return {\n engagement: { at, ...verdict, ...network(who) },\n backdate: verdict.reason === 'link_burst' || verdict.reason === 'quick+link_pair',\n };\n });\n return recorded?.url;\n}\n\nfunction escape(text: string): string {\n return text.replace(/[&<>\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\nfunction page(options: TrackingOptions, message: string, status = 200, extra = ''): Response {\n const title = escape(options.product ?? 'Email');\n const html = `<!doctype html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>${title}</title><style>body{font:16px system-ui;margin:0;display:grid;place-items:center;min-height:100vh;background:#fafafa;color:#18181b}main{text-align:center;padding:24px}form{margin:12px 0}button{font:inherit;padding:10px 20px;border-radius:9999px;border:0;background:#18181b;color:#fff;cursor:pointer}button.secondary{background:#e4e4e7;color:#18181b}</style></head><body><main><p>${message}</p>${extra}</main></body></html>`;\n return new Response(html, { status, headers: { 'content-type': 'text/html; charset=utf-8' } });\n}\n"],"mappings":"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"}
package/dist/runtime.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/runtime.ts
2
+ // src/runtime.ts
3
3
  const DEFAULTS = {
4
4
  timeScale: 1,
5
5
  logMessages: false
@@ -13,7 +13,6 @@ function resolveRuntime(config, mode) {
13
13
  logMessages: entry.logMessages ?? DEFAULTS.logMessages
14
14
  };
15
15
  }
16
- //#endregion
17
16
  exports.resolveRuntime = resolveRuntime;
18
17
 
19
18
  //# sourceMappingURL=runtime.cjs.map
@@ -1,4 +1,3 @@
1
- //#region src/runtime.d.ts
2
1
  /**
3
2
  * Runtime options — how the engine should behave in a given environment, as
4
3
  * typed configuration rather than loose environment variables. A project keeps
@@ -26,5 +25,4 @@ export type JourneyMode = 'development' | 'production' | (string & {});
26
25
  export type JourneyRuntimeConfig = Partial<Record<JourneyMode, JourneyRuntimeOptions>>;
27
26
  /** The options for `mode`, defaults filled in; `config` may be absent altogether. */
28
27
  export declare function resolveRuntime(config: JourneyRuntimeConfig | undefined, mode: JourneyMode): Required<JourneyRuntimeOptions>;
29
- //#endregion
30
28
  //# sourceMappingURL=runtime.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.cts","names":[],"sources":["../src/runtime.ts"],"mappings":";;;;;;;;;;;iBAWiB;;;;;;;EAOf;;EAEA;;;YAIU;;YAGA,uBAAuB,QAAQ,OAAO,aAAa;;wBAK/C,eACd,QAAQ,kCACR,MAAM,cACL,SAAS"}
1
+ {"version":3,"file":"runtime.d.cts","names":[],"sources":["../src/runtime.ts"],"mappings":";;;;;;;;;;iBAWiB;;;;;;;EAOf;;EAEA;;;YAIU;;YAGA,uBAAuB,QAAQ,OAAO,aAAa;;wBAK/C,eACd,QAAQ,kCACR,MAAM,cACL,SAAS"}
@@ -1,4 +1,3 @@
1
- //#region src/runtime.d.ts
2
1
  /**
3
2
  * Runtime options — how the engine should behave in a given environment, as
4
3
  * typed configuration rather than loose environment variables. A project keeps
@@ -26,5 +25,4 @@ export type JourneyMode = 'development' | 'production' | (string & {});
26
25
  export type JourneyRuntimeConfig = Partial<Record<JourneyMode, JourneyRuntimeOptions>>;
27
26
  /** The options for `mode`, defaults filled in; `config` may be absent altogether. */
28
27
  export declare function resolveRuntime(config: JourneyRuntimeConfig | undefined, mode: JourneyMode): Required<JourneyRuntimeOptions>;
29
- //#endregion
30
28
  //# sourceMappingURL=runtime.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/runtime.ts"],"mappings":";;;;;;;;;;;iBAWiB;;;;;;;EAOf;;EAEA;;;YAIU;;YAGA,uBAAuB,QAAQ,OAAO,aAAa;;wBAK/C,eACd,QAAQ,kCACR,MAAM,cACL,SAAS"}
1
+ {"version":3,"file":"runtime.d.mts","names":[],"sources":["../src/runtime.ts"],"mappings":";;;;;;;;;;iBAWiB;;;;;;;EAOf;;EAEA;;;YAIU;;YAGA,uBAAuB,QAAQ,OAAO,aAAa;;wBAK/C,eACd,QAAQ,kCACR,MAAM,cACL,SAAS"}
package/dist/runtime.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region src/runtime.ts
1
+ // src/runtime.ts
2
2
  const DEFAULTS = {
3
3
  timeScale: 1,
4
4
  logMessages: false
@@ -12,7 +12,6 @@ function resolveRuntime(config, mode) {
12
12
  logMessages: entry.logMessages ?? DEFAULTS.logMessages
13
13
  };
14
14
  }
15
- //#endregion
16
15
  export { resolveRuntime };
17
16
 
18
17
  //# sourceMappingURL=runtime.mjs.map
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/store/erasure.ts
2
+ // src/store/erasure.ts
3
3
  /**
4
4
  * The user has an erasure tombstone — requested and under way, or carried out — so nothing may
5
5
  * be written about them: a write that got through would come back after the erase, or bring an
@@ -16,7 +16,6 @@ var UserErasedError = class extends Error {
16
16
  this.userId = userId;
17
17
  }
18
18
  };
19
- //#endregion
20
19
  exports.UserErasedError = UserErasedError;
21
20
 
22
21
  //# sourceMappingURL=erasure.cjs.map
@@ -1,4 +1,3 @@
1
- //#region src/store/erasure.d.ts
2
1
  /** Where a user stands with erasure: see `JourneyStore.erasureState`. */
3
2
  export type ErasureState = 'none' | 'erasing' | 'erased';
4
3
  /**
@@ -14,5 +13,4 @@ export declare class UserErasedError extends Error {
14
13
  readonly retryable = false;
15
14
  constructor(userId: string);
16
15
  }
17
- //#endregion
18
16
  //# sourceMappingURL=erasure.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"erasure.d.cts","names":[],"sources":["../../src/store/erasure.ts"],"mappings":";;YACY;;;;;;;qBAQC,wBAAwB;WAId;WAHH;;WAET;EACT,YAAqB"}
1
+ {"version":3,"file":"erasure.d.cts","names":[],"sources":["../../src/store/erasure.ts"],"mappings":";YACY;;;;;;;qBAQC,wBAAwB;WAId;WAHH;;WAET;EACT,YAAqB"}
@@ -1,4 +1,3 @@
1
- //#region src/store/erasure.d.ts
2
1
  /** Where a user stands with erasure: see `JourneyStore.erasureState`. */
3
2
  export type ErasureState = 'none' | 'erasing' | 'erased';
4
3
  /**
@@ -14,5 +13,4 @@ export declare class UserErasedError extends Error {
14
13
  readonly retryable = false;
15
14
  constructor(userId: string);
16
15
  }
17
- //#endregion
18
16
  //# sourceMappingURL=erasure.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"erasure.d.mts","names":[],"sources":["../../src/store/erasure.ts"],"mappings":";;YACY;;;;;;;qBAQC,wBAAwB;WAId;WAHH;;WAET;EACT,YAAqB"}
1
+ {"version":3,"file":"erasure.d.mts","names":[],"sources":["../../src/store/erasure.ts"],"mappings":";YACY;;;;;;;qBAQC,wBAAwB;WAId;WAHH;;WAET;EACT,YAAqB"}
@@ -1,4 +1,4 @@
1
- //#region src/store/erasure.ts
1
+ // src/store/erasure.ts
2
2
  /**
3
3
  * The user has an erasure tombstone — requested and under way, or carried out — so nothing may
4
4
  * be written about them: a write that got through would come back after the erase, or bring an
@@ -15,7 +15,6 @@ var UserErasedError = class extends Error {
15
15
  this.userId = userId;
16
16
  }
17
17
  };
18
- //#endregion
19
18
  export { UserErasedError };
20
19
 
21
20
  //# sourceMappingURL=erasure.mjs.map
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_engine_condition = require("../engine/condition.cjs");
3
- //#region src/store/facts.ts
3
+ // src/store/facts.ts
4
4
  /** The profile path that reads the user's own id (`u.userId`). */
5
5
  const USER_ID_PROPERTY = "userId";
6
6
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
@@ -45,15 +45,14 @@ var JourneyFactSource = class {
45
45
  getSegmentCondition(name) {
46
46
  return this.store.getSegmentCondition(name);
47
47
  }
48
- /** The user's registered devices; none, and push skips while `reachable('push')` is false. */
49
- async isSuppressed(channel) {
50
- return this.store.isSuppressed(this.userId, channel);
48
+ /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
49
+ async isSuppressed(channel, topic) {
50
+ return this.store.isSuppressed(this.userId, channel, topic);
51
51
  }
52
52
  listDevices() {
53
53
  return this.store.listDevices(this.userId);
54
54
  }
55
55
  };
56
- //#endregion
57
56
  exports.JourneyFactSource = JourneyFactSource;
58
57
  exports.USER_ID_PROPERTY = USER_ID_PROPERTY;
59
58
 
@@ -1 +1 @@
1
- {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's registered devices; none, and push skips while `reachable('push')` is false. */\n async isSuppressed(channel: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAYA,yBAAAA,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAmC;EACpD,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,OAAO;CACrD;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.cjs","names":["matchesWhere"],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAYA,yBAAAA,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
@@ -1,7 +1,6 @@
1
1
  import { ConditionIR, ScalarIR } from "../ir.cjs";
2
2
  import { FactSource, PushDevice } from "../engine/ports.cjs";
3
3
  import { JourneyStore } from "./index.cjs";
4
- //#region src/store/facts.d.ts
5
4
  /** The profile path that reads the user's own id (`u.userId`). */
6
5
  export declare const USER_ID_PROPERTY = "userId";
7
6
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
@@ -30,9 +29,8 @@ export declare class JourneyFactSource implements FactSource {
30
29
  */
31
30
  getProperty(path: string): Promise<ScalarIR | undefined>;
32
31
  getSegmentCondition(name: string): Promise<ConditionIR | undefined>;
33
- /** The user's registered devices; none, and push skips while `reachable('push')` is false. */
34
- isSuppressed(channel: string): Promise<boolean>;
32
+ /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
33
+ isSuppressed(channel: string, topic?: string): Promise<boolean>;
35
34
  listDevices(): Promise<PushDevice[]>;
36
35
  }
37
- //#endregion
38
36
  //# sourceMappingURL=facts.d.cts.map
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,kBAAkB;EAIrC,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.cts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
@@ -1,7 +1,6 @@
1
1
  import { ConditionIR, ScalarIR } from "../ir.mjs";
2
2
  import { FactSource, PushDevice } from "../engine/ports.mjs";
3
3
  import { JourneyStore } from "./index.mjs";
4
- //#region src/store/facts.d.ts
5
4
  /** The profile path that reads the user's own id (`u.userId`). */
6
5
  export declare const USER_ID_PROPERTY = "userId";
7
6
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
@@ -30,9 +29,8 @@ export declare class JourneyFactSource implements FactSource {
30
29
  */
31
30
  getProperty(path: string): Promise<ScalarIR | undefined>;
32
31
  getSegmentCondition(name: string): Promise<ConditionIR | undefined>;
33
- /** The user's registered devices; none, and push skips while `reachable('push')` is false. */
34
- isSuppressed(channel: string): Promise<boolean>;
32
+ /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
33
+ isSuppressed(channel: string, topic?: string): Promise<boolean>;
35
34
  listDevices(): Promise<PushDevice[]>;
36
35
  }
37
- //#endregion
38
36
  //# sourceMappingURL=facts.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,kBAAkB;EAIrC,eAAe,QAAQ"}
1
+ {"version":3,"file":"facts.d.mts","names":[],"sources":["../../src/store/facts.ts"],"mappings":";;;;qBAMa;;qBAGA,6BAA6B;mBAErB;mBACA;EAFnB,YACmB,OAAO,cACP;EAGb,YACJ,eACA;IAAS;IAAkB;IAAkB,QAAQ;MACpD;;;;;;;;;;;;;;;EA2BG,YAAY,eAAe,QAAQ;EAoBzC,oBAAoB,eAAe,QAAQ;;EAKrC,aAAa,iBAAiB,iBAAiB;EAIrD,eAAe,QAAQ"}
@@ -1,5 +1,5 @@
1
1
  import { matchesWhere } from "../engine/condition.mjs";
2
- //#region src/store/facts.ts
2
+ // src/store/facts.ts
3
3
  /** The profile path that reads the user's own id (`u.userId`). */
4
4
  const USER_ID_PROPERTY = "userId";
5
5
  /** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */
@@ -44,15 +44,14 @@ var JourneyFactSource = class {
44
44
  getSegmentCondition(name) {
45
45
  return this.store.getSegmentCondition(name);
46
46
  }
47
- /** The user's registered devices; none, and push skips while `reachable('push')` is false. */
48
- async isSuppressed(channel) {
49
- return this.store.isSuppressed(this.userId, channel);
47
+ /** The user's opt-outs: the channel's, or with a topic that topic's as well. */
48
+ async isSuppressed(channel, topic) {
49
+ return this.store.isSuppressed(this.userId, channel, topic);
50
50
  }
51
51
  listDevices() {
52
52
  return this.store.listDevices(this.userId);
53
53
  }
54
54
  };
55
- //#endregion
56
55
  export { JourneyFactSource, USER_ID_PROPERTY };
57
56
 
58
57
  //# sourceMappingURL=facts.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's registered devices; none, and push skips while `reachable('push')` is false. */\n async isSuppressed(channel: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAY,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAmC;EACpD,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,OAAO;CACrD;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
1
+ {"version":3,"file":"facts.mjs","names":[],"sources":["../../src/store/facts.ts"],"sourcesContent":["import { matchesWhere } from '../engine/condition';\nimport type { FactSource, PushDevice } from '../engine/ports';\nimport type { ConditionIR, ScalarIR } from '../ir';\nimport type { JourneyStore } from './index';\n\n/** The profile path that reads the user's own id (`u.userId`). */\nexport const USER_ID_PROPERTY = 'userId';\n\n/** Single-user fact source over a JourneyStore: evaluation lives in engine/condition.ts, this only reads. */\nexport class JourneyFactSource implements FactSource {\n constructor(\n private readonly store: JourneyStore,\n private readonly userId: string\n ) {}\n\n async countEvents(\n event: string,\n opts?: { sinceMs?: number; untilMs?: number; where?: ConditionIR }\n ): Promise<number> {\n const window = {\n ...(opts?.sinceMs === undefined ? {} : { sinceMs: opts.sinceMs }),\n ...(opts?.untilMs === undefined ? {} : { untilMs: opts.untilMs }),\n };\n if (opts?.where === undefined) return this.store.countEvents(this.userId, event, window);\n // Payload filtering happens in JS via the shared evaluator — one set of\n // where semantics with the in-memory FactSource; per-user row counts are small.\n const where = opts.where;\n const payloads = await this.store.listEventPayloads(this.userId, event, window);\n return payloads.filter((payload) => matchesWhere(payload, where)).length;\n }\n\n /**\n * The scalar boundary. Everything downstream — comparisons, subject\n * placeholders, resolved message props — is typed ScalarIR, but nothing so\n * far has checked that the stored JSON is one: /identify validates only that\n * `props` is a plain object, and the merge stores whatever nesting it is\n * handed. Left unchecked, `gt(score, 3)` on `['5']` coerces its way to true\n * while `eq(score, 5)` is false, and `String(recipient)` on an object\n * addresses an email to `[object Object]`.\n *\n * So a non-scalar value reads as absent, matching how matchesWhere treats a\n * non-scalar payload leaf. The port cannot say \"present but not a scalar\",\n * and guessing at a coercion is what produced the wrong branch in the first\n * place.\n */\n async getProperty(path: string): Promise<ScalarIR | undefined> {\n // The system field: the request's identity, never stored in the profile (contract.user.userId)\n if (path === USER_ID_PROPERTY) return this.userId;\n const props: Record<string, unknown> | undefined = await this.store.getProfile(this.userId);\n if (props === undefined) return undefined;\n // Own keys only: `props[path]` would otherwise find Object.prototype, so\n // `exists(u.toString)` held for every user with a profile row and a subject\n // placeholder rendered native-code text into the email.\n if (!Object.hasOwn(props, path)) return undefined;\n const value = props[path];\n // An explicit null means \"unset\" and behaves as not_exists. /identify\n // itself clears a property by removing the key (the merge drops nulls), so\n // this covers profiles written by other means — a direct SQL fixup, a\n // migration, an import.\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return undefined;\n }\n\n getSegmentCondition(name: string): Promise<ConditionIR | undefined> {\n return this.store.getSegmentCondition(name);\n }\n\n /** The user's opt-outs: the channel's, or with a topic that topic's as well. */\n async isSuppressed(channel: string, topic?: string): Promise<boolean> {\n return this.store.isSuppressed(this.userId, channel, topic);\n }\n\n listDevices(): Promise<PushDevice[]> {\n return this.store.listDevices(this.userId);\n }\n}\n"],"mappings":";;;AAMA,MAAa,mBAAmB;;AAGhC,IAAa,oBAAb,MAAqD;CAEhC;CACA;CAFnB,YACE,OACA,QACA;EAFiB,KAAA,QAAA;EACA,KAAA,SAAA;CAChB;CAEH,MAAM,YACJ,OACA,MACiB;EACjB,MAAM,SAAS;GACb,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;GAC/D,GAAI,MAAM,YAAY,KAAA,IAAY,CAAC,IAAI,EAAE,SAAS,KAAK,QAAQ;EACjE;EACA,IAAI,MAAM,UAAU,KAAA,GAAW,OAAO,KAAK,MAAM,YAAY,KAAK,QAAQ,OAAO,MAAM;EAGvF,MAAM,QAAQ,KAAK;EAEnB,QAAO,MADgB,KAAK,MAAM,kBAAkB,KAAK,QAAQ,OAAO,MAAM,EAAA,CAC9D,QAAQ,YAAY,aAAa,SAAS,KAAK,CAAC,CAAC,CAAC;CACpE;;;;;;;;;;;;;;;CAgBA,MAAM,YAAY,MAA6C;EAE7D,IAAI,SAAA,UAA2B,OAAO,KAAK;EAC3C,MAAM,QAA6C,MAAM,KAAK,MAAM,WAAW,KAAK,MAAM;EAC1F,IAAI,UAAU,KAAA,GAAW,OAAO,KAAA;EAIhC,IAAI,CAAC,OAAO,OAAO,OAAO,IAAI,GAAG,OAAO,KAAA;EACxC,MAAM,QAAQ,MAAM;EAKpB,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO;CAGX;CAEA,oBAAoB,MAAgD;EAClE,OAAO,KAAK,MAAM,oBAAoB,IAAI;CAC5C;;CAGA,MAAM,aAAa,SAAiB,OAAkC;EACpE,OAAO,KAAK,MAAM,aAAa,KAAK,QAAQ,SAAS,KAAK;CAC5D;CAEA,cAAqC;EACnC,OAAO,KAAK,MAAM,YAAY,KAAK,MAAM;CAC3C;AACF"}
@@ -1,10 +1,10 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_store_facts = require("./facts.cjs");
2
3
  const require_store_erasure = require("./erasure.cjs");
3
4
  const require_store_postgres_reporting = require("./postgres-reporting.cjs");
4
5
  const require_store_tables = require("./tables.cjs");
5
6
  const require_store_postgres = require("./postgres.cjs");
6
- const require_store_facts = require("./facts.cjs");
7
- //#region src/store/index.ts
7
+ // src/store/index.ts
8
8
  /**
9
9
  * The live database lacks tables or columns the engine reads (an SDK upgrade added them, or the
10
10
  * host never ran the migration). Raised before a deploy writes anything.
@@ -17,7 +17,6 @@ var SchemaMismatchError = class extends Error {
17
17
  this.problems = problems;
18
18
  }
19
19
  };
20
- //#endregion
21
20
  exports.DEFAULT_SCHEMA = require_store_tables.DEFAULT_SCHEMA;
22
21
  exports.JourneyFactSource = require_store_facts.JourneyFactSource;
23
22
  exports.PostgresJourneyStore = require_store_postgres.PostgresJourneyStore;