@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.
- package/README.md +165 -42
- package/dist/_virtual/_rolldown/runtime.cjs +1 -2
- package/dist/apns-relay.cjs +1 -2
- package/dist/apns-relay.d.cts +0 -2
- package/dist/apns-relay.d.cts.map +1 -1
- package/dist/apns-relay.d.mts +0 -2
- package/dist/apns-relay.d.mts.map +1 -1
- package/dist/apns-relay.mjs +1 -2
- package/dist/apns.cjs +1 -2
- package/dist/apns.d.cts +0 -2
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +0 -2
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +1 -2
- package/dist/aws/engine-step.cjs +1 -2
- package/dist/aws/engine-step.d.cts +0 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +0 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +1 -2
- package/dist/aws/function-url.cjs +1 -2
- package/dist/aws/function-url.d.cts +0 -2
- package/dist/aws/function-url.d.cts.map +1 -1
- package/dist/aws/function-url.d.mts +0 -2
- package/dist/aws/function-url.d.mts.map +1 -1
- package/dist/aws/function-url.mjs +1 -2
- package/dist/aws/index.cjs +2 -3
- package/dist/aws/index.d.cts +0 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +0 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +2 -3
- package/dist/aws/instances.cjs +1 -2
- package/dist/aws/instances.d.cts +0 -2
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +0 -2
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +1 -2
- package/dist/aws/lambda-api.cjs +1 -2
- package/dist/aws/lambda-api.d.cts +0 -2
- package/dist/aws/lambda-api.d.cts.map +1 -1
- package/dist/aws/lambda-api.d.mts +0 -2
- package/dist/aws/lambda-api.d.mts.map +1 -1
- package/dist/aws/lambda-api.mjs +1 -2
- package/dist/client.cjs +46 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +25 -5
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +25 -5
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +46 -6
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +1 -2
- package/dist/cloudflare/bindings.d.cts +0 -2
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +0 -2
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +1 -2
- package/dist/cloudflare/index.cjs +2 -2
- package/dist/cloudflare/index.mjs +2 -2
- package/dist/cloudflare/runner.cjs +2 -3
- package/dist/cloudflare/runner.d.cts +1 -3
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +1 -3
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +2 -3
- package/dist/cloudflare/worker.cjs +1 -2
- package/dist/cloudflare/worker.d.cts +0 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +0 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +1 -2
- package/dist/cloudflare/workers.d.cts +0 -2
- package/dist/cloudflare/workers.d.cts.map +1 -1
- package/dist/cloudflare/workers.d.mts +0 -2
- package/dist/cloudflare/workers.d.mts.map +1 -1
- package/dist/config.cjs +25 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +15 -11
- package/dist/config.d.cts.map +1 -1
- package/dist/config.d.mts +15 -11
- package/dist/config.d.mts.map +1 -1
- package/dist/config.mjs +26 -6
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs +12 -2
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +28 -9
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +28 -9
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs +12 -2
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/action.cjs +1 -2
- package/dist/dsl/action.d.cts +0 -2
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +0 -2
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +1 -2
- package/dist/dsl/base.cjs +1 -2
- package/dist/dsl/base.d.cts +0 -2
- package/dist/dsl/base.d.cts.map +1 -1
- package/dist/dsl/base.d.mts +0 -2
- package/dist/dsl/base.d.mts.map +1 -1
- package/dist/dsl/base.mjs +1 -2
- package/dist/dsl/bundle.cjs +5 -5
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +5 -3
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts +5 -3
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +5 -5
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +1 -2
- package/dist/dsl/condition.d.cts +0 -2
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +0 -2
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +1 -2
- package/dist/dsl/flow.cjs +3 -3
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +1 -3
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +1 -3
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +3 -3
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/refs.cjs +1 -2
- package/dist/dsl/refs.d.cts +0 -2
- package/dist/dsl/refs.d.cts.map +1 -1
- package/dist/dsl/refs.d.mts +0 -2
- package/dist/dsl/refs.d.mts.map +1 -1
- package/dist/dsl/refs.mjs +1 -2
- package/dist/dsl/template.cjs +1 -2
- package/dist/dsl/template.d.cts +0 -2
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +0 -2
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.mjs +1 -2
- package/dist/dsl/trigger.cjs +1 -2
- package/dist/dsl/trigger.d.cts +0 -2
- package/dist/dsl/trigger.d.cts.map +1 -1
- package/dist/dsl/trigger.d.mts +0 -2
- package/dist/dsl/trigger.d.mts.map +1 -1
- package/dist/dsl/trigger.mjs +1 -2
- package/dist/dsl/workflow.cjs +3 -4
- package/dist/dsl/workflow.d.cts +0 -2
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts +0 -2
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/email-context.cjs +1 -2
- package/dist/email-context.d.cts +0 -2
- package/dist/email-context.d.cts.map +1 -1
- package/dist/email-context.d.mts +0 -2
- package/dist/email-context.d.mts.map +1 -1
- package/dist/email-context.mjs +1 -2
- package/dist/email-sender.cjs +2 -3
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +1 -3
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +1 -3
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +2 -3
- package/dist/email-sender.mjs.map +1 -1
- package/dist/email.cjs +33 -7
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +38 -11
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +38 -11
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +33 -8
- package/dist/email.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -2
- package/dist/engine/actions.d.cts +0 -2
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +0 -2
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -2
- package/dist/engine/bucket.cjs +1 -2
- package/dist/engine/bucket.d.cts +0 -2
- package/dist/engine/bucket.d.cts.map +1 -1
- package/dist/engine/bucket.d.mts +0 -2
- package/dist/engine/bucket.d.mts.map +1 -1
- package/dist/engine/bucket.mjs +1 -2
- package/dist/engine/condition.cjs +1 -2
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +0 -2
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +0 -2
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +1 -2
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/duration.cjs +1 -2
- package/dist/engine/duration.d.cts +0 -2
- package/dist/engine/duration.d.cts.map +1 -1
- package/dist/engine/duration.d.mts +0 -2
- package/dist/engine/duration.d.mts.map +1 -1
- package/dist/engine/duration.mjs +1 -2
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +22 -6
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts +0 -2
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts +0 -2
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +22 -6
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +8 -4
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +8 -4
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/recipient.cjs +1 -2
- package/dist/engine/recipient.d.cts +0 -2
- package/dist/engine/recipient.d.cts.map +1 -1
- package/dist/engine/recipient.d.mts +0 -2
- package/dist/engine/recipient.d.mts.map +1 -1
- package/dist/engine/recipient.mjs +1 -2
- package/dist/engine/retry.cjs +1 -2
- package/dist/engine/retry.d.cts +0 -2
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +0 -2
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +1 -2
- package/dist/engine/time-scale.cjs +1 -2
- package/dist/engine/time-scale.d.cts +0 -2
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +0 -2
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +1 -2
- package/dist/engine/time-window.cjs +1 -2
- package/dist/engine/time-window.d.cts +0 -2
- package/dist/engine/time-window.d.cts.map +1 -1
- package/dist/engine/time-window.d.mts +0 -2
- package/dist/engine/time-window.d.mts.map +1 -1
- package/dist/engine/time-window.mjs +1 -2
- package/dist/fcm.cjs +1 -2
- package/dist/fcm.d.cts +0 -2
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +0 -2
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +1 -2
- package/dist/hash.cjs +1 -2
- package/dist/hash.d.cts +0 -2
- package/dist/hash.d.cts.map +1 -1
- package/dist/hash.d.mts +0 -2
- package/dist/hash.d.mts.map +1 -1
- package/dist/hash.mjs +1 -2
- package/dist/host/auth.cjs +1 -2
- package/dist/host/auth.d.cts +0 -2
- package/dist/host/auth.d.cts.map +1 -1
- package/dist/host/auth.d.mts +0 -2
- package/dist/host/auth.d.mts.map +1 -1
- package/dist/host/auth.mjs +1 -2
- package/dist/host/channels.cjs +2 -3
- package/dist/host/channels.d.cts +0 -2
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +0 -2
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +2 -3
- package/dist/host/execute.cjs +3 -4
- package/dist/host/execute.d.cts +1 -3
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +1 -3
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +3 -4
- package/dist/host/fresh-read.cjs +1 -2
- package/dist/host/fresh-read.d.cts +0 -2
- package/dist/host/fresh-read.d.cts.map +1 -1
- package/dist/host/fresh-read.d.mts +0 -2
- package/dist/host/fresh-read.d.mts.map +1 -1
- package/dist/host/fresh-read.mjs +1 -2
- package/dist/host/http.cjs +39 -5
- package/dist/host/http.cjs.map +1 -1
- package/dist/host/http.d.cts +2 -4
- package/dist/host/http.d.cts.map +1 -1
- package/dist/host/http.d.mts +2 -4
- package/dist/host/http.d.mts.map +1 -1
- package/dist/host/http.mjs +40 -6
- package/dist/host/http.mjs.map +1 -1
- package/dist/host/ingest.cjs +129 -6
- package/dist/host/ingest.cjs.map +1 -1
- package/dist/host/ingest.d.cts +71 -2
- package/dist/host/ingest.d.cts.map +1 -1
- package/dist/host/ingest.d.mts +71 -2
- package/dist/host/ingest.d.mts.map +1 -1
- package/dist/host/ingest.mjs +126 -7
- package/dist/host/ingest.mjs.map +1 -1
- package/dist/host/journey-host.cjs +11 -10
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +0 -2
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +0 -2
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +11 -10
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +1 -2
- package/dist/host/platform.d.cts +0 -2
- package/dist/host/platform.d.cts.map +1 -1
- package/dist/host/platform.d.mts +0 -2
- package/dist/host/platform.d.mts.map +1 -1
- package/dist/host/platform.mjs +1 -2
- package/dist/host/senders.cjs +1 -2
- package/dist/host/senders.d.cts +0 -2
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +0 -2
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +1 -2
- package/dist/http2-client.cjs +1 -2
- package/dist/http2-client.d.cts +0 -2
- package/dist/http2-client.d.cts.map +1 -1
- package/dist/http2-client.d.mts +0 -2
- package/dist/http2-client.d.mts.map +1 -1
- package/dist/http2-client.mjs +1 -2
- package/dist/index.cjs +5 -2
- package/dist/index.d.cts +5 -5
- package/dist/index.d.mts +5 -5
- package/dist/index.mjs +5 -5
- package/dist/ir.cjs +12 -4
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +17 -2
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +17 -2
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +12 -5
- package/dist/ir.mjs.map +1 -1
- package/dist/jwt.cjs +1 -2
- package/dist/jwt.d.cts +0 -2
- package/dist/jwt.d.cts.map +1 -1
- package/dist/jwt.d.mts +0 -2
- package/dist/jwt.d.mts.map +1 -1
- package/dist/jwt.mjs +1 -2
- package/dist/plan.cjs +1 -2
- package/dist/plan.d.cts +0 -2
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +0 -2
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +1 -2
- package/dist/postgres-js.cjs +1 -2
- package/dist/postgres-js.d.cts +0 -2
- package/dist/postgres-js.d.cts.map +1 -1
- package/dist/postgres-js.d.mts +0 -2
- package/dist/postgres-js.d.mts.map +1 -1
- package/dist/postgres-js.mjs +1 -2
- package/dist/provenance.cjs +1 -2
- package/dist/provenance.d.cts +0 -2
- package/dist/provenance.d.cts.map +1 -1
- package/dist/provenance.d.mts +0 -2
- package/dist/provenance.d.mts.map +1 -1
- package/dist/provenance.mjs +1 -2
- package/dist/push.cjs +1 -2
- package/dist/push.d.cts +0 -2
- package/dist/push.d.cts.map +1 -1
- package/dist/push.d.mts +0 -2
- package/dist/push.d.mts.map +1 -1
- package/dist/push.mjs +1 -2
- package/dist/react-email.cjs +1 -2
- package/dist/react-email.d.cts +0 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +0 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs +1 -2
- package/dist/reporting/classify.cjs +1 -2
- package/dist/reporting/classify.d.cts +0 -2
- package/dist/reporting/classify.d.cts.map +1 -1
- package/dist/reporting/classify.d.mts +0 -2
- package/dist/reporting/classify.d.mts.map +1 -1
- package/dist/reporting/classify.mjs +1 -2
- package/dist/reporting/observer.cjs +2 -2
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.d.cts +0 -2
- package/dist/reporting/observer.d.cts.map +1 -1
- package/dist/reporting/observer.d.mts +0 -2
- package/dist/reporting/observer.d.mts.map +1 -1
- package/dist/reporting/observer.mjs +2 -2
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +29 -12
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +13 -8
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +13 -8
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +29 -12
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/runtime.cjs +1 -2
- package/dist/runtime.d.cts +0 -2
- package/dist/runtime.d.cts.map +1 -1
- package/dist/runtime.d.mts +0 -2
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs +1 -2
- package/dist/store/erasure.cjs +1 -2
- package/dist/store/erasure.d.cts +0 -2
- package/dist/store/erasure.d.cts.map +1 -1
- package/dist/store/erasure.d.mts +0 -2
- package/dist/store/erasure.d.mts.map +1 -1
- package/dist/store/erasure.mjs +1 -2
- package/dist/store/facts.cjs +4 -5
- package/dist/store/facts.cjs.map +1 -1
- package/dist/store/facts.d.cts +2 -4
- package/dist/store/facts.d.cts.map +1 -1
- package/dist/store/facts.d.mts +2 -4
- package/dist/store/facts.d.mts.map +1 -1
- package/dist/store/facts.mjs +4 -5
- package/dist/store/facts.mjs.map +1 -1
- package/dist/store/index.cjs +2 -3
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +44 -15
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +44 -15
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -3
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +31 -12
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +4 -4
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +4 -4
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +31 -12
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +40 -5
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -4
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -4
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +41 -6
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/sql.cjs +1 -2
- package/dist/store/sql.d.cts +0 -2
- package/dist/store/sql.d.cts.map +1 -1
- package/dist/store/sql.d.mts +0 -2
- package/dist/store/sql.d.mts.map +1 -1
- package/dist/store/sql.mjs +1 -2
- package/dist/store/tables.cjs +76 -13
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +17 -3
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +17 -3
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +75 -14
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +1 -2
- package/dist/store/user-lock.d.cts +0 -2
- package/dist/store/user-lock.d.cts.map +1 -1
- package/dist/store/user-lock.d.mts +0 -2
- package/dist/store/user-lock.d.mts.map +1 -1
- package/dist/store/user-lock.mjs +1 -2
- package/dist/templates.cjs +61 -9
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.d.cts +101 -15
- package/dist/templates.d.cts.map +1 -1
- package/dist/templates.d.mts +101 -15
- package/dist/templates.d.mts.map +1 -1
- package/dist/templates.mjs +62 -11
- package/dist/templates.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +1 -2
- package/dist/transports/cloudflare-email.d.cts +0 -2
- package/dist/transports/cloudflare-email.d.cts.map +1 -1
- package/dist/transports/cloudflare-email.d.mts +0 -2
- package/dist/transports/cloudflare-email.d.mts.map +1 -1
- package/dist/transports/cloudflare-email.mjs +1 -2
- package/dist/transports/ses.cjs +1 -2
- package/dist/transports/ses.d.cts +0 -2
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +0 -2
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +1 -2
- package/dist/transports/sigv4.cjs +1 -2
- package/dist/transports/sigv4.d.cts +0 -2
- package/dist/transports/sigv4.d.cts.map +1 -1
- package/dist/transports/sigv4.d.mts +0 -2
- package/dist/transports/sigv4.d.mts.map +1 -1
- package/dist/transports/sigv4.mjs +1 -2
- package/dist/webhooks.cjs +1 -2
- package/dist/webhooks.d.cts +0 -2
- package/dist/webhooks.d.cts.map +1 -1
- package/dist/webhooks.d.mts +0 -2
- package/dist/webhooks.d.mts.map +1 -1
- package/dist/webhooks.mjs +1 -2
- package/dist/webpush.cjs +1 -2
- package/dist/webpush.d.cts +0 -2
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +0 -2
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +1 -2
- package/package.json +5 -5
package/dist/provenance.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
2
|
+
// src/provenance.ts
|
|
3
3
|
/**
|
|
4
4
|
* V8-style frame: `at fn (file:line:col)` / `at file:line:col`, where file may
|
|
5
5
|
* be an absolute path, a file:// URL (Node ESM) or an http(s) URL (Vite dev).
|
|
@@ -60,7 +60,6 @@ function toPosix(path) {
|
|
|
60
60
|
const slashed = path.replaceAll("\\", "/");
|
|
61
61
|
return /^[a-z]:/.test(slashed) ? slashed[0].toUpperCase() + slashed.slice(1) : slashed;
|
|
62
62
|
}
|
|
63
|
-
//#endregion
|
|
64
63
|
exports.captureLoc = captureLoc;
|
|
65
64
|
exports.normalizeFile = normalizeFile;
|
|
66
65
|
|
package/dist/provenance.d.cts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SourceLocIR } from "./ir.cjs";
|
|
2
|
-
//#region src/provenance.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* Provenance — where in source code a definition came from.
|
|
5
4
|
*
|
|
@@ -37,6 +36,5 @@ export declare function captureLoc(boundary: AnyFunction): SourceLocIR | undefin
|
|
|
37
36
|
* line, and the platform it gets wrong is not the one the suite runs on.
|
|
38
37
|
*/
|
|
39
38
|
export declare function normalizeFile(rawFile: string, cwd: string | undefined): string;
|
|
40
|
-
//#endregion
|
|
41
39
|
export {};
|
|
42
40
|
//# sourceMappingURL=provenance.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provenance.d.cts","names":[],"sources":["../src/provenance.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"provenance.d.cts","names":[],"sources":["../src/provenance.ts"],"mappings":";;;;;;;;;;;;;;;;;KAmBK,kBAAkB;;;;;wBAYP,WAAW,UAAU,cAAc;;;;;;;;;;;;;;;wBAyCnC,cAAc,iBAAiB"}
|
package/dist/provenance.d.mts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { SourceLocIR } from "./ir.mjs";
|
|
2
|
-
//#region src/provenance.d.ts
|
|
3
2
|
/**
|
|
4
3
|
* Provenance — where in source code a definition came from.
|
|
5
4
|
*
|
|
@@ -37,6 +36,5 @@ export declare function captureLoc(boundary: AnyFunction): SourceLocIR | undefin
|
|
|
37
36
|
* line, and the platform it gets wrong is not the one the suite runs on.
|
|
38
37
|
*/
|
|
39
38
|
export declare function normalizeFile(rawFile: string, cwd: string | undefined): string;
|
|
40
|
-
//#endregion
|
|
41
39
|
export {};
|
|
42
40
|
//# sourceMappingURL=provenance.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"provenance.d.mts","names":[],"sources":["../src/provenance.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"provenance.d.mts","names":[],"sources":["../src/provenance.ts"],"mappings":";;;;;;;;;;;;;;;;;KAmBK,kBAAkB;;;;;wBAYP,WAAW,UAAU,cAAc;;;;;;;;;;;;;;;wBAyCnC,cAAc,iBAAiB"}
|
package/dist/provenance.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/provenance.ts
|
|
2
2
|
/**
|
|
3
3
|
* V8-style frame: `at fn (file:line:col)` / `at file:line:col`, where file may
|
|
4
4
|
* be an absolute path, a file:// URL (Node ESM) or an http(s) URL (Vite dev).
|
|
@@ -59,7 +59,6 @@ function toPosix(path) {
|
|
|
59
59
|
const slashed = path.replaceAll("\\", "/");
|
|
60
60
|
return /^[a-z]:/.test(slashed) ? slashed[0].toUpperCase() + slashed.slice(1) : slashed;
|
|
61
61
|
}
|
|
62
|
-
//#endregion
|
|
63
62
|
export { captureLoc, normalizeFile };
|
|
64
63
|
|
|
65
64
|
//# sourceMappingURL=provenance.mjs.map
|
package/dist/push.cjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_templates = require("./templates.cjs");
|
|
3
|
-
|
|
3
|
+
// src/push.ts
|
|
4
4
|
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
5
5
|
function registryPushRenderer(pushes) {
|
|
6
6
|
const index = require_templates.indexTemplates("push", pushes);
|
|
7
7
|
return async (id, props) => require_templates.renderPushContent(require_templates.lookupTemplate(index, "push", id), props);
|
|
8
8
|
}
|
|
9
|
-
//#endregion
|
|
10
9
|
exports.registryPushRenderer = registryPushRenderer;
|
|
11
10
|
|
|
12
11
|
//# sourceMappingURL=push.cjs.map
|
package/dist/push.d.cts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ScalarIR } from "./ir.cjs";
|
|
2
2
|
import { PushContent, PushTemplate } from "./templates.cjs";
|
|
3
|
-
//#region src/push.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* `@shware/workflow/push` — push-notification content. A push is data, not a
|
|
6
5
|
* component: a title and a body the OS renders in its own chrome, built by a
|
|
@@ -12,5 +11,4 @@ export type RenderedPush = PushContent;
|
|
|
12
11
|
export type PushRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedPush>;
|
|
13
12
|
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
14
13
|
export declare function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer;
|
|
15
|
-
//#endregion
|
|
16
14
|
//# sourceMappingURL=push.d.cts.map
|
package/dist/push.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.d.cts","names":[],"sources":["../src/push.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"push.d.cts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;YAiBY,eAAe;YAEf,gBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;;wBAGG,qBAAqB,QAAQ,SAAS,eAAe,iBAAiB"}
|
package/dist/push.d.mts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ScalarIR } from "./ir.mjs";
|
|
2
2
|
import { PushContent, PushTemplate } from "./templates.mjs";
|
|
3
|
-
//#region src/push.d.ts
|
|
4
3
|
/**
|
|
5
4
|
* `@shware/workflow/push` — push-notification content. A push is data, not a
|
|
6
5
|
* component: a title and a body the OS renders in its own chrome, built by a
|
|
@@ -12,5 +11,4 @@ export type RenderedPush = PushContent;
|
|
|
12
11
|
export type PushRenderer = (template: string, props: Record<string, ScalarIR | undefined>) => Promise<RenderedPush>;
|
|
13
12
|
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
14
13
|
export declare function registryPushRenderer(pushes: Readonly<Record<string, PushTemplate>>): PushRenderer;
|
|
15
|
-
//#endregion
|
|
16
14
|
//# sourceMappingURL=push.d.mts.map
|
package/dist/push.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"push.d.mts","names":[],"sources":["../src/push.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"push.d.mts","names":[],"sources":["../src/push.ts"],"mappings":";;;;;;;;;YAiBY,eAAe;YAEf,gBACV,kBACA,OAAO,eAAe,0BACnB,QAAQ;;wBAGG,qBAAqB,QAAQ,SAAS,eAAe,iBAAiB"}
|
package/dist/push.mjs
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { indexTemplates, lookupTemplate, renderPushContent } from "./templates.mjs";
|
|
2
|
-
|
|
2
|
+
// src/push.ts
|
|
3
3
|
/** A renderer over the registry, by template id; a TemplateValidationError means nothing may be sent. */
|
|
4
4
|
function registryPushRenderer(pushes) {
|
|
5
5
|
const index = indexTemplates("push", pushes);
|
|
6
6
|
return async (id, props) => renderPushContent(lookupTemplate(index, "push", id), props);
|
|
7
7
|
}
|
|
8
|
-
//#endregion
|
|
9
8
|
export { registryPushRenderer };
|
|
10
9
|
|
|
11
10
|
//# sourceMappingURL=push.mjs.map
|
package/dist/react-email.cjs
CHANGED
|
@@ -4,7 +4,7 @@ const require_templates = require("./templates.cjs");
|
|
|
4
4
|
const require_email_context = require("./email-context.cjs");
|
|
5
5
|
let react = require("react");
|
|
6
6
|
let react_dom_server = require("react-dom/server");
|
|
7
|
-
|
|
7
|
+
// src/react-email.ts
|
|
8
8
|
/**
|
|
9
9
|
* `@shware/workflow/react-email` — an EmailRenderer over the project's email
|
|
10
10
|
* registry (`react-dom` is an optional peer dependency; import this subpath
|
|
@@ -50,7 +50,6 @@ async function renderToHtml(element) {
|
|
|
50
50
|
if (failure !== void 0) throw failure instanceof Error ? failure : new Error("email render failed", { cause: failure });
|
|
51
51
|
return html.replace(/^<!DOCTYPE html>/i, "").replace(/<link rel="preload" as="image"[^>]*\/>/g, "");
|
|
52
52
|
}
|
|
53
|
-
//#endregion
|
|
54
53
|
exports.registryRenderer = registryRenderer;
|
|
55
54
|
|
|
56
55
|
//# sourceMappingURL=react-email.cjs.map
|
package/dist/react-email.d.cts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { EmailConfig } from "./email.cjs";
|
|
2
2
|
import { EmailTemplate } from "./templates.cjs";
|
|
3
3
|
import { EmailRenderer } from "./email-sender.cjs";
|
|
4
|
-
//#region src/react-email.d.ts
|
|
5
4
|
export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>, config?: EmailConfig): EmailRenderer;
|
|
6
|
-
//#endregion
|
|
7
5
|
//# sourceMappingURL=react-email.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-email.d.cts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;wBAmCgB,iBACd,QAAQ,SAAS,eAAe,iBAChC,SAAS,cACR"}
|
package/dist/react-email.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { EmailConfig } from "./email.mjs";
|
|
2
2
|
import { EmailTemplate } from "./templates.mjs";
|
|
3
3
|
import { EmailRenderer } from "./email-sender.mjs";
|
|
4
|
-
//#region src/react-email.d.ts
|
|
5
4
|
export declare function registryRenderer(emails: Readonly<Record<string, EmailTemplate>>, config?: EmailConfig): EmailRenderer;
|
|
6
|
-
//#endregion
|
|
7
5
|
//# sourceMappingURL=react-email.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"react-email.d.mts","names":[],"sources":["../src/react-email.ts"],"mappings":";;;wBAmCgB,iBACd,QAAQ,SAAS,eAAe,iBAChC,SAAS,cACR"}
|
package/dist/react-email.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { indexTemplates, lookupTemplate, parseTemplateProps } from "./templates.
|
|
|
3
3
|
import { EmailProvider } from "./email-context.mjs";
|
|
4
4
|
import { createElement } from "react";
|
|
5
5
|
import { renderToReadableStream } from "react-dom/server";
|
|
6
|
-
|
|
6
|
+
// src/react-email.ts
|
|
7
7
|
/**
|
|
8
8
|
* `@shware/workflow/react-email` — an EmailRenderer over the project's email
|
|
9
9
|
* registry (`react-dom` is an optional peer dependency; import this subpath
|
|
@@ -49,7 +49,6 @@ async function renderToHtml(element) {
|
|
|
49
49
|
if (failure !== void 0) throw failure instanceof Error ? failure : new Error("email render failed", { cause: failure });
|
|
50
50
|
return html.replace(/^<!DOCTYPE html>/i, "").replace(/<link rel="preload" as="image"[^>]*\/>/g, "");
|
|
51
51
|
}
|
|
52
|
-
//#endregion
|
|
53
52
|
export { registryRenderer };
|
|
54
53
|
|
|
55
54
|
//# sourceMappingURL=react-email.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
2
|
+
// src/reporting/classify.ts
|
|
3
3
|
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
4
4
|
const BURST_WINDOW_MS = 5e3;
|
|
5
5
|
const DEFAULT_QUICK_MS = 1e3;
|
|
@@ -33,7 +33,6 @@ function classifyClick(facts, options = {}) {
|
|
|
33
33
|
if (quick && pair) return machine("quick+link_pair");
|
|
34
34
|
return human;
|
|
35
35
|
}
|
|
36
|
-
//#endregion
|
|
37
36
|
exports.BURST_WINDOW_MS = BURST_WINDOW_MS;
|
|
38
37
|
exports.DEFAULT_QUICK_MS = DEFAULT_QUICK_MS;
|
|
39
38
|
exports.classifyClick = classifyClick;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/reporting/classify.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* Who opened or clicked: a person, or a machine acting on the message without one — the split
|
|
4
3
|
* between human and machine engagement that open and click rates are read through. No single
|
|
@@ -56,5 +55,4 @@ export declare const BURST_WINDOW_MS = 5000;
|
|
|
56
55
|
export declare const DEFAULT_QUICK_MS = 1000;
|
|
57
56
|
export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
|
|
58
57
|
export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
|
|
59
|
-
//#endregion
|
|
60
58
|
//# sourceMappingURL=classify.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.cts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":"
|
|
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"}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region src/reporting/classify.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* Who opened or clicked: a person, or a machine acting on the message without one — the split
|
|
4
3
|
* between human and machine engagement that open and click rates are read through. No single
|
|
@@ -56,5 +55,4 @@ export declare const BURST_WINDOW_MS = 5000;
|
|
|
56
55
|
export declare const DEFAULT_QUICK_MS = 1000;
|
|
57
56
|
export declare function classifyOpen(facts: RequestFacts, options?: ClassifyOptions): Verdict;
|
|
58
57
|
export declare function classifyClick(facts: ClickFacts, options?: ClassifyOptions): Verdict;
|
|
59
|
-
//#endregion
|
|
60
58
|
//# sourceMappingURL=classify.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classify.d.mts","names":[],"sources":["../../src/reporting/classify.ts"],"mappings":"
|
|
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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/reporting/classify.ts
|
|
2
2
|
/** How close together clicks on different links of one message must be to count as one walk. */
|
|
3
3
|
const BURST_WINDOW_MS = 5e3;
|
|
4
4
|
const DEFAULT_QUICK_MS = 1e3;
|
|
@@ -32,7 +32,6 @@ function classifyClick(facts, options = {}) {
|
|
|
32
32
|
if (quick && pair) return machine("quick+link_pair");
|
|
33
33
|
return human;
|
|
34
34
|
}
|
|
35
|
-
//#endregion
|
|
36
35
|
export { BURST_WINDOW_MS, DEFAULT_QUICK_MS, classifyClick, classifyOpen };
|
|
37
36
|
|
|
38
37
|
//# sourceMappingURL=classify.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
2
|
+
// src/reporting/observer.ts
|
|
3
3
|
/**
|
|
4
4
|
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
5
5
|
* and outcome recorded through `store.reporting`.
|
|
@@ -26,6 +26,7 @@ function reportingObserver(reporting, origin) {
|
|
|
26
26
|
template: message.template,
|
|
27
27
|
channel: channelOf(message),
|
|
28
28
|
userId: message.userId,
|
|
29
|
+
topic: message.channel === "email" || message.channel === "push" ? message.topic ?? null : null,
|
|
29
30
|
sentAt: Date.now()
|
|
30
31
|
});
|
|
31
32
|
if (message.channel === "email" && base !== void 0) {
|
|
@@ -59,7 +60,6 @@ function channelOf(message) {
|
|
|
59
60
|
if (message.channel !== "push") return message.channel;
|
|
60
61
|
return `push_${message.device?.platform ?? "unknown"}`;
|
|
61
62
|
}
|
|
62
|
-
//#endregion
|
|
63
63
|
exports.reportingObserver = reportingObserver;
|
|
64
64
|
|
|
65
65
|
//# sourceMappingURL=observer.cjs.map
|
|
@@ -1 +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/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (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, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${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":";;;;;;;;;;;;;;AAeA,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,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,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"}
|
|
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/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (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 topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${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":";;;;;;;;;;;;;;AAeA,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,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,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"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { JourneyObserver } from "../engine/ports.cjs";
|
|
2
2
|
import "../engine/index.cjs";
|
|
3
3
|
import { JourneyReporting } from "../store/index.cjs";
|
|
4
|
-
//#region src/reporting/observer.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
7
6
|
* and outcome recorded through `store.reporting`.
|
|
@@ -15,5 +14,4 @@ import { JourneyReporting } from "../store/index.cjs";
|
|
|
15
14
|
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
16
15
|
*/
|
|
17
16
|
export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
|
|
18
|
-
//#endregion
|
|
19
17
|
//# sourceMappingURL=observer.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer.d.cts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"observer.d.cts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAegB,kBACd,WAAW,kBACX,6BACC"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { JourneyObserver } from "../engine/ports.mjs";
|
|
2
2
|
import "../engine/index.mjs";
|
|
3
3
|
import { JourneyReporting } from "../store/index.mjs";
|
|
4
|
-
//#region src/reporting/observer.d.ts
|
|
5
4
|
/**
|
|
6
5
|
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
7
6
|
* and outcome recorded through `store.reporting`.
|
|
@@ -15,5 +14,4 @@ import { JourneyReporting } from "../store/index.mjs";
|
|
|
15
14
|
* The links are recorded when the sender commits the rewritten email, before it goes out.
|
|
16
15
|
*/
|
|
17
16
|
export declare function reportingObserver(reporting: JourneyReporting, origin: string | undefined): JourneyObserver;
|
|
18
|
-
//#endregion
|
|
19
17
|
//# sourceMappingURL=observer.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observer.d.mts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"observer.d.mts","names":[],"sources":["../../src/reporting/observer.ts"],"mappings":";;;;;;;;;;;;;;;wBAegB,kBACd,WAAW,kBACX,6BACC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
// src/reporting/observer.ts
|
|
2
2
|
/**
|
|
3
3
|
* The observer journeyWorker installs when the store keeps reporting: every node arrival, copy
|
|
4
4
|
* and outcome recorded through `store.reporting`.
|
|
@@ -25,6 +25,7 @@ function reportingObserver(reporting, origin) {
|
|
|
25
25
|
template: message.template,
|
|
26
26
|
channel: channelOf(message),
|
|
27
27
|
userId: message.userId,
|
|
28
|
+
topic: message.channel === "email" || message.channel === "push" ? message.topic ?? null : null,
|
|
28
29
|
sentAt: Date.now()
|
|
29
30
|
});
|
|
30
31
|
if (message.channel === "email" && base !== void 0) {
|
|
@@ -58,7 +59,6 @@ function channelOf(message) {
|
|
|
58
59
|
if (message.channel !== "push") return message.channel;
|
|
59
60
|
return `push_${message.device?.platform ?? "unknown"}`;
|
|
60
61
|
}
|
|
61
|
-
//#endregion
|
|
62
62
|
export { reportingObserver };
|
|
63
63
|
|
|
64
64
|
//# sourceMappingURL=observer.mjs.map
|
|
@@ -1 +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/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (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, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${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":";;;;;;;;;;;;;AAeA,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,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,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"}
|
|
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/l/<message>/<position>` — `/t/u/...` for its unsubscribe\n * link, so what a click means is fixed when it is sent — and an open pixel `<origin>/t/o/<message>`\n * (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 topic:\n message.channel === 'email' || message.channel === 'push'\n ? (message.topic ?? null)\n : null,\n sentAt: Date.now(),\n });\n if (message.channel === 'email' && base !== undefined) {\n const urls: string[] = [];\n return {\n link: (url, purpose) => {\n urls.push(url);\n return `${base}/t/${purpose === 'unsubscribe' ? 'u' : 'l'}/${id}/${urls.length - 1}`;\n },\n pixel: `${base}/t/o/${id}`,\n unsubscribe: `${base}/t/u/${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":";;;;;;;;;;;;;AAeA,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,OACE,QAAQ,YAAY,WAAW,QAAQ,YAAY,SAC9C,QAAQ,SAAS,OAClB;IACN,QAAQ,KAAK,IAAI;GACnB,CAAC;GACD,IAAI,QAAQ,YAAY,WAAW,SAAS,KAAA,GAAW;IACrD,MAAM,OAAiB,CAAC;IACxB,OAAO;KACL,OAAO,KAAK,YAAY;MACtB,KAAK,KAAK,GAAG;MACb,OAAO,GAAG,KAAK,KAAK,YAAY,gBAAgB,MAAM,IAAI,GAAG,GAAG,GAAG,KAAK,SAAS;KACnF;KACA,OAAO,GAAG,KAAK,OAAO;KACtB,aAAa,GAAG,KAAK,OAAO;KAE5B,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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
const require_reporting_classify = require("./classify.cjs");
|
|
3
|
-
|
|
3
|
+
// src/reporting/tracking.ts
|
|
4
4
|
const GIF = Uint8Array.from(atob("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"), (char) => char.charCodeAt(0));
|
|
5
5
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
6
|
function requester(request) {
|
|
@@ -15,7 +15,8 @@ function requester(request) {
|
|
|
15
15
|
}
|
|
16
16
|
/** A `/t/...` request's response, or undefined when the path is not a tracking route. */
|
|
17
17
|
async function handleTracking(request, reporting, options = {}) {
|
|
18
|
-
const
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
const path = url.pathname.split("/");
|
|
19
20
|
const root = path.at(1);
|
|
20
21
|
const kind = path.at(2);
|
|
21
22
|
const id = path.at(3);
|
|
@@ -33,10 +34,10 @@ async function handleTracking(request, reporting, options = {}) {
|
|
|
33
34
|
if ((kind === "l" || kind === "u") && position !== void 0 && read) {
|
|
34
35
|
const target = await recordClick(reporting, id, Number(position), who, options);
|
|
35
36
|
if (target === void 0) return page(options, "This link has expired.", 404);
|
|
36
|
-
if (kind === "u") return confirmUnsubscribe(options, id);
|
|
37
|
+
if (kind === "u") return confirmUnsubscribe(reporting, options, id);
|
|
37
38
|
return Response.redirect(target, 302);
|
|
38
39
|
}
|
|
39
|
-
if (kind === "u" && position === void 0 && read) return confirmUnsubscribe(options, id);
|
|
40
|
+
if (kind === "u" && position === void 0 && read) return confirmUnsubscribe(reporting, options, id);
|
|
40
41
|
if (kind === "d" && who.method === "POST") {
|
|
41
42
|
const known = await reporting.recordDeviceDelivered(id, Date.now());
|
|
42
43
|
return new Response(null, { status: known ? 204 : 404 });
|
|
@@ -66,23 +67,40 @@ async function handleTracking(request, reporting, options = {}) {
|
|
|
66
67
|
}
|
|
67
68
|
if (kind === "u" && who.method === "POST") {
|
|
68
69
|
const at = Date.now();
|
|
69
|
-
const
|
|
70
|
+
const all = url.searchParams.get("scope") === "all";
|
|
71
|
+
const unsubscribed = await reporting.recordUnsubscribe(id, at, all ? "all" : "topic");
|
|
70
72
|
if (unsubscribed === void 0) return page(options, "You won't get these emails anymore.");
|
|
73
|
+
if (unsubscribed.topic === null) return page(options, "This link has expired.", 404);
|
|
71
74
|
if (options.onUnsubscribed !== void 0) try {
|
|
72
75
|
await options.onUnsubscribed({
|
|
73
76
|
messageId: id,
|
|
74
77
|
userId: unsubscribed.userId,
|
|
75
|
-
at
|
|
78
|
+
at,
|
|
79
|
+
topic: unsubscribed.topic,
|
|
80
|
+
all
|
|
76
81
|
});
|
|
77
82
|
} catch (error) {
|
|
78
83
|
console.error(`[workflow] acting on the unsubscribe of ${id} failed`, error);
|
|
79
84
|
}
|
|
80
|
-
|
|
85
|
+
const from = options.product === void 0 ? "" : ` from ${escape(options.product)}`;
|
|
86
|
+
return page(options, all ? `You won't get emails${from} anymore.` : `You won't get ${escape(topicName(unsubscribed.topic))} emails${from} anymore.`);
|
|
81
87
|
}
|
|
82
88
|
}
|
|
83
|
-
/**
|
|
84
|
-
function
|
|
85
|
-
return
|
|
89
|
+
/** A topic id as the page shows it: `product_updates` reads as "product updates". */
|
|
90
|
+
function topicName(topic) {
|
|
91
|
+
return topic.replace(/[_-]+/g, " ");
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The question an unsubscribe link, or the List-Unsubscribe URL opened in a browser, asks: the
|
|
95
|
+
* email's topic (the default), or every email. A copy nobody knows, or a transactional one (the
|
|
96
|
+
* link was never in it), is an expired link.
|
|
97
|
+
*/
|
|
98
|
+
async function confirmUnsubscribe(reporting, options, id) {
|
|
99
|
+
const message = await reporting.lookupMessage(id);
|
|
100
|
+
if (message?.channel !== "email" || message.topic === null) return page(options, "This link has expired.", 404);
|
|
101
|
+
const product = options.product === void 0 ? "" : ` from ${escape(options.product)}`;
|
|
102
|
+
const topic = escape(topicName(message.topic));
|
|
103
|
+
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>`);
|
|
86
104
|
}
|
|
87
105
|
/**
|
|
88
106
|
* A journey push tapped open, as the app reports it (`JourneyClient.messageOpened`): an open on
|
|
@@ -150,13 +168,12 @@ function escape(text) {
|
|
|
150
168
|
return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
151
169
|
}
|
|
152
170
|
function page(options, message, status = 200, extra = "") {
|
|
153
|
-
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>`;
|
|
171
|
+
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>`;
|
|
154
172
|
return new Response(html, {
|
|
155
173
|
status,
|
|
156
174
|
headers: { "content-type": "text/html; charset=utf-8" }
|
|
157
175
|
});
|
|
158
176
|
}
|
|
159
|
-
//#endregion
|
|
160
177
|
exports.handleTracking = handleTracking;
|
|
161
178
|
exports.recordPushOpen = recordPushOpen;
|
|
162
179
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracking.cjs","names":["classifyOpen","BURST_WINDOW_MS","classifyClick"],"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.cjs","names":["classifyOpen","BURST_WINDOW_MS","classifyClick"],"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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"}
|