@shware/workflow 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +154 -2
- package/dist/_virtual/_rolldown/runtime.cjs +23 -0
- package/dist/apns-relay.cjs +105 -0
- package/dist/apns-relay.cjs.map +1 -0
- package/dist/apns-relay.d.cts +28 -0
- package/dist/apns-relay.d.cts.map +1 -0
- package/dist/apns-relay.d.mts +28 -0
- package/dist/apns-relay.d.mts.map +1 -0
- package/dist/apns-relay.mjs +104 -0
- package/dist/apns-relay.mjs.map +1 -0
- package/dist/apns.cjs +121 -0
- package/dist/apns.cjs.map +1 -0
- package/dist/apns.d.cts +63 -0
- package/dist/apns.d.cts.map +1 -0
- package/dist/apns.d.mts +63 -0
- package/dist/apns.d.mts.map +1 -0
- package/dist/apns.mjs +116 -0
- package/dist/apns.mjs.map +1 -0
- package/dist/client.cjs +125 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +108 -0
- package/dist/client.d.cts.map +1 -0
- package/dist/client.d.mts +108 -0
- package/dist/client.d.mts.map +1 -0
- package/dist/client.mjs +122 -0
- package/dist/client.mjs.map +1 -0
- package/dist/cloudflare/bindings.cjs +28 -0
- package/dist/cloudflare/bindings.cjs.map +1 -0
- package/dist/cloudflare/bindings.d.cts +144 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -0
- package/dist/cloudflare/bindings.d.mts +144 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -0
- package/dist/cloudflare/bindings.mjs +25 -0
- package/dist/cloudflare/bindings.mjs.map +1 -0
- package/dist/cloudflare/channels.cjs +164 -0
- package/dist/cloudflare/channels.cjs.map +1 -0
- package/dist/cloudflare/channels.d.cts +29 -0
- package/dist/cloudflare/channels.d.cts.map +1 -0
- package/dist/cloudflare/channels.d.mts +29 -0
- package/dist/cloudflare/channels.d.mts.map +1 -0
- package/dist/cloudflare/channels.mjs +161 -0
- package/dist/cloudflare/channels.mjs.map +1 -0
- package/dist/cloudflare/d1-env.cjs +17 -0
- package/dist/cloudflare/d1-env.cjs.map +1 -0
- package/dist/cloudflare/d1-env.d.cts +12 -0
- package/dist/cloudflare/d1-env.d.cts.map +1 -0
- package/dist/cloudflare/d1-env.d.mts +12 -0
- package/dist/cloudflare/d1-env.d.mts.map +1 -0
- package/dist/cloudflare/d1-env.mjs +16 -0
- package/dist/cloudflare/d1-env.mjs.map +1 -0
- package/dist/cloudflare/index.cjs +54 -0
- package/dist/cloudflare/index.d.cts +19 -0
- package/dist/cloudflare/index.d.mts +19 -0
- package/dist/cloudflare/index.mjs +16 -0
- package/dist/cloudflare/router.cjs +516 -0
- package/dist/cloudflare/router.cjs.map +1 -0
- package/dist/cloudflare/router.d.cts +157 -0
- package/dist/cloudflare/router.d.cts.map +1 -0
- package/dist/cloudflare/router.d.mts +157 -0
- package/dist/cloudflare/router.d.mts.map +1 -0
- package/dist/cloudflare/router.mjs +503 -0
- package/dist/cloudflare/router.mjs.map +1 -0
- package/dist/cloudflare/runner.cjs +225 -0
- package/dist/cloudflare/runner.cjs.map +1 -0
- package/dist/cloudflare/runner.d.cts +73 -0
- package/dist/cloudflare/runner.d.cts.map +1 -0
- package/dist/cloudflare/runner.d.mts +73 -0
- package/dist/cloudflare/runner.d.mts.map +1 -0
- package/dist/cloudflare/runner.mjs +224 -0
- package/dist/cloudflare/runner.mjs.map +1 -0
- package/dist/cloudflare/schema.sql +127 -0
- package/dist/cloudflare/senders.cjs +114 -0
- package/dist/cloudflare/senders.cjs.map +1 -0
- package/dist/cloudflare/senders.d.cts +68 -0
- package/dist/cloudflare/senders.d.cts.map +1 -0
- package/dist/cloudflare/senders.d.mts +68 -0
- package/dist/cloudflare/senders.d.mts.map +1 -0
- package/dist/cloudflare/senders.mjs +108 -0
- package/dist/cloudflare/senders.mjs.map +1 -0
- package/dist/cloudflare/worker.cjs +228 -0
- package/dist/cloudflare/worker.cjs.map +1 -0
- package/dist/cloudflare/worker.d.cts +92 -0
- package/dist/cloudflare/worker.d.cts.map +1 -0
- package/dist/cloudflare/worker.d.mts +92 -0
- package/dist/cloudflare/worker.d.mts.map +1 -0
- package/dist/cloudflare/worker.mjs +224 -0
- package/dist/cloudflare/worker.mjs.map +1 -0
- package/dist/cloudflare/workers.d.cjs +0 -0
- package/dist/cloudflare/workers.d.cts +44 -0
- package/dist/cloudflare/workers.d.cts.map +1 -0
- package/dist/cloudflare/workers.d.mts +44 -0
- package/dist/cloudflare/workers.d.mts.map +1 -0
- package/dist/config.cjs +151 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +149 -0
- package/dist/config.d.cts.map +1 -0
- package/dist/config.d.mts +149 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +143 -0
- package/dist/config.mjs.map +1 -0
- package/dist/contract.cjs +212 -0
- package/dist/contract.cjs.map +1 -0
- package/dist/contract.d.cts +90 -0
- package/dist/contract.d.cts.map +1 -0
- package/dist/contract.d.mts +90 -0
- package/dist/contract.d.mts.map +1 -0
- package/dist/contract.mjs +206 -0
- package/dist/contract.mjs.map +1 -0
- package/dist/dsl/action.cjs +29 -0
- package/dist/dsl/action.cjs.map +1 -0
- package/dist/dsl/action.d.cts +64 -0
- package/dist/dsl/action.d.cts.map +1 -0
- package/dist/dsl/action.d.mts +64 -0
- package/dist/dsl/action.d.mts.map +1 -0
- package/dist/dsl/action.mjs +28 -0
- package/dist/dsl/action.mjs.map +1 -0
- package/dist/dsl/base.cjs +34 -0
- package/dist/dsl/base.cjs.map +1 -0
- package/dist/dsl/base.d.cts +45 -0
- package/dist/dsl/base.d.cts.map +1 -0
- package/dist/dsl/base.d.mts +45 -0
- package/dist/dsl/base.d.mts.map +1 -0
- package/dist/dsl/base.mjs +30 -0
- package/dist/dsl/base.mjs.map +1 -0
- package/dist/dsl/bundle.cjs +207 -0
- package/dist/dsl/bundle.cjs.map +1 -0
- package/dist/dsl/bundle.d.cts +19 -0
- package/dist/dsl/bundle.d.cts.map +1 -0
- package/dist/dsl/bundle.d.mts +19 -0
- package/dist/dsl/bundle.d.mts.map +1 -0
- package/dist/dsl/bundle.mjs +206 -0
- package/dist/dsl/bundle.mjs.map +1 -0
- package/dist/dsl/condition.cjs +177 -0
- package/dist/dsl/condition.cjs.map +1 -0
- package/dist/dsl/condition.d.cts +140 -0
- package/dist/dsl/condition.d.cts.map +1 -0
- package/dist/dsl/condition.d.mts +140 -0
- package/dist/dsl/condition.d.mts.map +1 -0
- package/dist/dsl/condition.mjs +156 -0
- package/dist/dsl/condition.mjs.map +1 -0
- package/dist/dsl/flow.cjs +228 -0
- package/dist/dsl/flow.cjs.map +1 -0
- package/dist/dsl/flow.d.cts +223 -0
- package/dist/dsl/flow.d.cts.map +1 -0
- package/dist/dsl/flow.d.mts +223 -0
- package/dist/dsl/flow.d.mts.map +1 -0
- package/dist/dsl/flow.mjs +226 -0
- package/dist/dsl/flow.mjs.map +1 -0
- package/dist/dsl/index.cjs +38 -0
- package/dist/dsl/index.d.cts +10 -0
- package/dist/dsl/index.d.mts +10 -0
- package/dist/dsl/index.mjs +9 -0
- package/dist/dsl/refs.cjs +59 -0
- package/dist/dsl/refs.cjs.map +1 -0
- package/dist/dsl/refs.d.cts +88 -0
- package/dist/dsl/refs.d.cts.map +1 -0
- package/dist/dsl/refs.d.mts +88 -0
- package/dist/dsl/refs.d.mts.map +1 -0
- package/dist/dsl/refs.mjs +55 -0
- package/dist/dsl/refs.mjs.map +1 -0
- package/dist/dsl/template.cjs +65 -0
- package/dist/dsl/template.cjs.map +1 -0
- package/dist/dsl/template.d.cts +75 -0
- package/dist/dsl/template.d.cts.map +1 -0
- package/dist/dsl/template.d.mts +75 -0
- package/dist/dsl/template.d.mts.map +1 -0
- package/dist/dsl/template.mjs +63 -0
- package/dist/dsl/template.mjs.map +1 -0
- package/dist/dsl/trigger.cjs +40 -0
- package/dist/dsl/trigger.cjs.map +1 -0
- package/dist/dsl/trigger.d.cts +65 -0
- package/dist/dsl/trigger.d.cts.map +1 -0
- package/dist/dsl/trigger.d.mts +65 -0
- package/dist/dsl/trigger.d.mts.map +1 -0
- package/dist/dsl/trigger.mjs +39 -0
- package/dist/dsl/trigger.mjs.map +1 -0
- package/dist/dsl/workflow.cjs +121 -0
- package/dist/dsl/workflow.cjs.map +1 -0
- package/dist/dsl/workflow.d.cts +59 -0
- package/dist/dsl/workflow.d.cts.map +1 -0
- package/dist/dsl/workflow.d.mts +59 -0
- package/dist/dsl/workflow.d.mts.map +1 -0
- package/dist/dsl/workflow.mjs +120 -0
- package/dist/dsl/workflow.mjs.map +1 -0
- package/dist/engine/actions.cjs +27 -0
- package/dist/engine/actions.cjs.map +1 -0
- package/dist/engine/actions.d.cts +40 -0
- package/dist/engine/actions.d.cts.map +1 -0
- package/dist/engine/actions.d.mts +40 -0
- package/dist/engine/actions.d.mts.map +1 -0
- package/dist/engine/actions.mjs +26 -0
- package/dist/engine/actions.mjs.map +1 -0
- package/dist/engine/bucket.cjs +72 -0
- package/dist/engine/bucket.cjs.map +1 -0
- package/dist/engine/bucket.d.cts +32 -0
- package/dist/engine/bucket.d.cts.map +1 -0
- package/dist/engine/bucket.d.mts +32 -0
- package/dist/engine/bucket.d.mts.map +1 -0
- package/dist/engine/bucket.mjs +69 -0
- package/dist/engine/bucket.mjs.map +1 -0
- package/dist/engine/condition.cjs +166 -0
- package/dist/engine/condition.cjs.map +1 -0
- package/dist/engine/condition.d.cts +52 -0
- package/dist/engine/condition.d.cts.map +1 -0
- package/dist/engine/condition.d.mts +52 -0
- package/dist/engine/condition.d.mts.map +1 -0
- package/dist/engine/condition.mjs +161 -0
- package/dist/engine/condition.mjs.map +1 -0
- package/dist/engine/index.cjs +20 -0
- package/dist/engine/index.d.cts +8 -0
- package/dist/engine/index.d.mts +8 -0
- package/dist/engine/index.mjs +7 -0
- package/dist/engine/interpreter.cjs +428 -0
- package/dist/engine/interpreter.cjs.map +1 -0
- package/dist/engine/interpreter.d.cts +6 -0
- package/dist/engine/interpreter.d.cts.map +1 -0
- package/dist/engine/interpreter.d.mts +6 -0
- package/dist/engine/interpreter.d.mts.map +1 -0
- package/dist/engine/interpreter.mjs +427 -0
- package/dist/engine/interpreter.mjs.map +1 -0
- package/dist/engine/ports.cjs +0 -0
- package/dist/engine/ports.d.cts +252 -0
- package/dist/engine/ports.d.cts.map +1 -0
- package/dist/engine/ports.d.mts +252 -0
- package/dist/engine/ports.d.mts.map +1 -0
- package/dist/engine/ports.mjs +1 -0
- package/dist/engine/recipient.cjs +23 -0
- package/dist/engine/recipient.cjs.map +1 -0
- package/dist/engine/recipient.d.cts +13 -0
- package/dist/engine/recipient.d.cts.map +1 -0
- package/dist/engine/recipient.d.mts +13 -0
- package/dist/engine/recipient.d.mts.map +1 -0
- package/dist/engine/recipient.mjs +22 -0
- package/dist/engine/recipient.mjs.map +1 -0
- package/dist/engine/time-scale.cjs +54 -0
- package/dist/engine/time-scale.cjs.map +1 -0
- package/dist/engine/time-scale.d.cts +31 -0
- package/dist/engine/time-scale.d.cts.map +1 -0
- package/dist/engine/time-scale.d.mts +31 -0
- package/dist/engine/time-scale.d.mts.map +1 -0
- package/dist/engine/time-scale.mjs +52 -0
- package/dist/engine/time-scale.mjs.map +1 -0
- package/dist/engine/time-window.cjs +149 -0
- package/dist/engine/time-window.cjs.map +1 -0
- package/dist/engine/time-window.d.cts +22 -0
- package/dist/engine/time-window.d.cts.map +1 -0
- package/dist/engine/time-window.d.mts +22 -0
- package/dist/engine/time-window.d.mts.map +1 -0
- package/dist/engine/time-window.mjs +147 -0
- package/dist/engine/time-window.mjs.map +1 -0
- package/dist/fcm.cjs +125 -0
- package/dist/fcm.cjs.map +1 -0
- package/dist/fcm.d.cts +48 -0
- package/dist/fcm.d.cts.map +1 -0
- package/dist/fcm.d.mts +48 -0
- package/dist/fcm.d.mts.map +1 -0
- package/dist/fcm.mjs +121 -0
- package/dist/fcm.mjs.map +1 -0
- package/dist/hash.cjs +245 -0
- package/dist/hash.cjs.map +1 -0
- package/dist/hash.d.cts +30 -0
- package/dist/hash.d.cts.map +1 -0
- package/dist/hash.d.mts +30 -0
- package/dist/hash.d.mts.map +1 -0
- package/dist/hash.mjs +239 -0
- package/dist/hash.mjs.map +1 -0
- package/dist/index.cjs +100 -0
- package/dist/index.d.cts +18 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.mjs +17 -0
- package/dist/ir.cjs +366 -0
- package/dist/ir.cjs.map +1 -0
- package/dist/ir.d.cts +486 -0
- package/dist/ir.d.cts.map +1 -0
- package/dist/ir.d.mts +486 -0
- package/dist/ir.d.mts.map +1 -0
- package/dist/ir.mjs +344 -0
- package/dist/ir.mjs.map +1 -0
- package/dist/jwt.cjs +22 -0
- package/dist/jwt.cjs.map +1 -0
- package/dist/jwt.d.cts +9 -0
- package/dist/jwt.d.cts.map +1 -0
- package/dist/jwt.d.mts +9 -0
- package/dist/jwt.d.mts.map +1 -0
- package/dist/jwt.mjs +20 -0
- package/dist/jwt.mjs.map +1 -0
- package/dist/plan.cjs +223 -0
- package/dist/plan.cjs.map +1 -0
- package/dist/plan.d.cts +68 -0
- package/dist/plan.d.cts.map +1 -0
- package/dist/plan.d.mts +68 -0
- package/dist/plan.d.mts.map +1 -0
- package/dist/plan.mjs +221 -0
- package/dist/plan.mjs.map +1 -0
- package/dist/postgres-js.cjs +42 -0
- package/dist/postgres-js.cjs.map +1 -0
- package/dist/postgres-js.d.cts +28 -0
- package/dist/postgres-js.d.cts.map +1 -0
- package/dist/postgres-js.d.mts +28 -0
- package/dist/postgres-js.d.mts.map +1 -0
- package/dist/postgres-js.mjs +38 -0
- package/dist/postgres-js.mjs.map +1 -0
- package/dist/provenance.cjs +67 -0
- package/dist/provenance.cjs.map +1 -0
- package/dist/provenance.d.cts +41 -0
- package/dist/provenance.d.cts.map +1 -0
- package/dist/provenance.d.mts +41 -0
- package/dist/provenance.d.mts.map +1 -0
- package/dist/provenance.mjs +65 -0
- package/dist/provenance.mjs.map +1 -0
- package/dist/push.cjs +14 -0
- package/dist/push.cjs.map +1 -0
- package/dist/push.d.cts +16 -0
- package/dist/push.d.cts.map +1 -0
- package/dist/push.d.mts +16 -0
- package/dist/push.d.mts.map +1 -0
- package/dist/push.mjs +13 -0
- package/dist/push.mjs.map +1 -0
- package/dist/react-email.cjs +48 -0
- package/dist/react-email.cjs.map +1 -0
- package/dist/react-email.d.cts +7 -0
- package/dist/react-email.d.cts.map +1 -0
- package/dist/react-email.d.mts +7 -0
- package/dist/react-email.d.mts.map +1 -0
- package/dist/react-email.mjs +47 -0
- package/dist/react-email.mjs.map +1 -0
- package/dist/reporting/classify.cjs +42 -0
- package/dist/reporting/classify.cjs.map +1 -0
- package/dist/reporting/classify.d.cts +60 -0
- package/dist/reporting/classify.d.cts.map +1 -0
- package/dist/reporting/classify.d.mts +60 -0
- package/dist/reporting/classify.d.mts.map +1 -0
- package/dist/reporting/classify.mjs +38 -0
- package/dist/reporting/classify.mjs.map +1 -0
- package/dist/reporting/observer.cjs +63 -0
- package/dist/reporting/observer.cjs.map +1 -0
- package/dist/reporting/observer.d.cts +18 -0
- package/dist/reporting/observer.d.cts.map +1 -0
- package/dist/reporting/observer.d.mts +18 -0
- package/dist/reporting/observer.d.mts.map +1 -0
- package/dist/reporting/observer.mjs +62 -0
- package/dist/reporting/observer.mjs.map +1 -0
- package/dist/reporting/tracking.cjs +153 -0
- package/dist/reporting/tracking.cjs.map +1 -0
- package/dist/reporting/tracking.d.cts +43 -0
- package/dist/reporting/tracking.d.cts.map +1 -0
- package/dist/reporting/tracking.d.mts +43 -0
- package/dist/reporting/tracking.d.mts.map +1 -0
- package/dist/reporting/tracking.mjs +151 -0
- package/dist/reporting/tracking.mjs.map +1 -0
- package/dist/runtime.cjs +19 -0
- package/dist/runtime.cjs.map +1 -0
- package/dist/runtime.d.cts +30 -0
- package/dist/runtime.d.cts.map +1 -0
- package/dist/runtime.d.mts +30 -0
- package/dist/runtime.d.mts.map +1 -0
- package/dist/runtime.mjs +18 -0
- package/dist/runtime.mjs.map +1 -0
- package/dist/store/d1.cjs +185 -0
- package/dist/store/d1.cjs.map +1 -0
- package/dist/store/d1.d.cts +55 -0
- package/dist/store/d1.d.cts.map +1 -0
- package/dist/store/d1.d.mts +55 -0
- package/dist/store/d1.d.mts.map +1 -0
- package/dist/store/d1.mjs +184 -0
- package/dist/store/d1.mjs.map +1 -0
- package/dist/store/facts.cjs +60 -0
- package/dist/store/facts.cjs.map +1 -0
- package/dist/store/facts.d.cts +38 -0
- package/dist/store/facts.d.cts.map +1 -0
- package/dist/store/facts.d.mts +38 -0
- package/dist/store/facts.d.mts.map +1 -0
- package/dist/store/facts.mjs +58 -0
- package/dist/store/facts.mjs.map +1 -0
- package/dist/store/index.cjs +31 -0
- package/dist/store/index.cjs.map +1 -0
- package/dist/store/index.d.cts +291 -0
- package/dist/store/index.d.cts.map +1 -0
- package/dist/store/index.d.mts +291 -0
- package/dist/store/index.d.mts.map +1 -0
- package/dist/store/index.mjs +22 -0
- package/dist/store/index.mjs.map +1 -0
- package/dist/store/postgres-reporting.cjs +213 -0
- package/dist/store/postgres-reporting.cjs.map +1 -0
- package/dist/store/postgres-reporting.d.cts +63 -0
- package/dist/store/postgres-reporting.d.cts.map +1 -0
- package/dist/store/postgres-reporting.d.mts +63 -0
- package/dist/store/postgres-reporting.d.mts.map +1 -0
- package/dist/store/postgres-reporting.mjs +212 -0
- package/dist/store/postgres-reporting.mjs.map +1 -0
- package/dist/store/postgres.cjs +309 -0
- package/dist/store/postgres.cjs.map +1 -0
- package/dist/store/postgres.d.cts +77 -0
- package/dist/store/postgres.d.cts.map +1 -0
- package/dist/store/postgres.d.mts +77 -0
- package/dist/store/postgres.d.mts.map +1 -0
- package/dist/store/postgres.mjs +306 -0
- package/dist/store/postgres.mjs.map +1 -0
- package/dist/store/sql.cjs +24 -0
- package/dist/store/sql.cjs.map +1 -0
- package/dist/store/sql.d.cts +25 -0
- package/dist/store/sql.d.cts.map +1 -0
- package/dist/store/sql.d.mts +25 -0
- package/dist/store/sql.d.mts.map +1 -0
- package/dist/store/sql.mjs +23 -0
- package/dist/store/sql.mjs.map +1 -0
- package/dist/store/tables.cjs +404 -0
- package/dist/store/tables.cjs.map +1 -0
- package/dist/store/tables.d.cts +81 -0
- package/dist/store/tables.d.cts.map +1 -0
- package/dist/store/tables.d.mts +81 -0
- package/dist/store/tables.d.mts.map +1 -0
- package/dist/store/tables.mjs +399 -0
- package/dist/store/tables.mjs.map +1 -0
- package/dist/templates.cjs +247 -0
- package/dist/templates.cjs.map +1 -0
- package/dist/templates.d.cts +187 -0
- package/dist/templates.d.cts.map +1 -0
- package/dist/templates.d.mts +187 -0
- package/dist/templates.d.mts.map +1 -0
- package/dist/templates.mjs +230 -0
- package/dist/templates.mjs.map +1 -0
- package/dist/webhooks.cjs +96 -0
- package/dist/webhooks.cjs.map +1 -0
- package/dist/webhooks.d.cts +25 -0
- package/dist/webhooks.d.cts.map +1 -0
- package/dist/webhooks.d.mts +25 -0
- package/dist/webhooks.d.mts.map +1 -0
- package/dist/webhooks.mjs +94 -0
- package/dist/webhooks.mjs.map +1 -0
- package/dist/webpush.cjs +180 -0
- package/dist/webpush.cjs.map +1 -0
- package/dist/webpush.d.cts +53 -0
- package/dist/webpush.d.cts.map +1 -0
- package/dist/webpush.d.mts +53 -0
- package/dist/webpush.d.mts.map +1 -0
- package/dist/webpush.mjs +175 -0
- package/dist/webpush.mjs.map +1 -0
- package/package.json +189 -3
- package/src/cloudflare/schema.sql +127 -0
- package/src/cloudflare/workers.d.ts +44 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
//#region src/webhooks.ts
|
|
2
|
+
/**
|
|
3
|
+
* `@shware/workflow/webhooks` — messages posted to a team channel through an incoming webhook:
|
|
4
|
+
* Discord (`discord-api-types` bodies) and Slack (Block Kit). Plain fetch, no SDKs. A template
|
|
5
|
+
* names its webhook through the Worker's environment (`webhook: (env) => env.WEBHOOK_DISCORD_…`), so the
|
|
6
|
+
* renderer hands the sender both the URL and the message.
|
|
7
|
+
*
|
|
8
|
+
* A request the channel refuses for good — the webhook deleted, the channel archived, a message
|
|
9
|
+
* Discord or Slack rejects (4xx) — settles the copy undelivered instead of retrying into it, so a
|
|
10
|
+
* team-channel post never holds up the customer messages after it. Rate limits (429), timeouts
|
|
11
|
+
* (408) and server errors throw, and the step retries.
|
|
12
|
+
*/
|
|
13
|
+
/** A 4xx that retrying cannot fix. */
|
|
14
|
+
const permanent = (status) => status >= 400 && status < 500 && status !== 408 && status !== 429;
|
|
15
|
+
async function post(doFetch, url, body) {
|
|
16
|
+
const response = await doFetch(url, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "content-type": "application/json" },
|
|
19
|
+
body: JSON.stringify(body)
|
|
20
|
+
});
|
|
21
|
+
return {
|
|
22
|
+
status: response.status,
|
|
23
|
+
text: response.ok ? "" : await response.text()
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function requireChannel(message, channel) {
|
|
27
|
+
if (message.channel !== channel) throw new Error(`${channel} webhook sender: unsupported channel '${message.channel}'`);
|
|
28
|
+
}
|
|
29
|
+
var DiscordWebhookSender = class {
|
|
30
|
+
options;
|
|
31
|
+
constructor(options) {
|
|
32
|
+
this.options = options;
|
|
33
|
+
}
|
|
34
|
+
async send(message) {
|
|
35
|
+
requireChannel(message, "discord");
|
|
36
|
+
const { url, content } = await this.options.render(message.template, message.props);
|
|
37
|
+
const { status, text } = await post(this.options.fetch ?? fetch, url, content);
|
|
38
|
+
if (status >= 200 && status < 300) return;
|
|
39
|
+
if (status === 404 || status === 401) {
|
|
40
|
+
console.warn(`[discord] webhook gone for template ${message.template}: ${status} ${text}`);
|
|
41
|
+
return {
|
|
42
|
+
delivered: false,
|
|
43
|
+
reason: "UnknownWebhook"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (permanent(status)) {
|
|
47
|
+
console.warn(`[discord] refused ${message.template}: ${status} ${text}`);
|
|
48
|
+
return {
|
|
49
|
+
delivered: false,
|
|
50
|
+
reason: `HTTP ${status}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`[discord] ${status}: ${text}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
/** Slack's answers for a webhook that will never deliver again. */
|
|
57
|
+
const SLACK_GONE = /* @__PURE__ */ new Set([
|
|
58
|
+
"no_service",
|
|
59
|
+
"channel_not_found",
|
|
60
|
+
"channel_is_archived",
|
|
61
|
+
"action_prohibited"
|
|
62
|
+
]);
|
|
63
|
+
var SlackWebhookSender = class {
|
|
64
|
+
options;
|
|
65
|
+
constructor(options) {
|
|
66
|
+
this.options = options;
|
|
67
|
+
}
|
|
68
|
+
async send(message) {
|
|
69
|
+
requireChannel(message, "slack");
|
|
70
|
+
const { url, content } = await this.options.render(message.template, message.props);
|
|
71
|
+
const { status, text } = await post(this.options.fetch ?? fetch, url, content);
|
|
72
|
+
if (status >= 200 && status < 300) return;
|
|
73
|
+
const reason = text.trim();
|
|
74
|
+
if (SLACK_GONE.has(reason)) {
|
|
75
|
+
console.warn(`[slack] webhook gone for template ${message.template}: ${reason}`);
|
|
76
|
+
return {
|
|
77
|
+
delivered: false,
|
|
78
|
+
reason
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (permanent(status)) {
|
|
82
|
+
console.warn(`[slack] refused ${message.template}: ${status} ${reason}`);
|
|
83
|
+
return {
|
|
84
|
+
delivered: false,
|
|
85
|
+
reason: reason || `HTTP ${status}`
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`[slack] ${status}: ${reason}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
//#endregion
|
|
92
|
+
export { DiscordWebhookSender, SlackWebhookSender };
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=webhooks.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.mjs","names":[],"sources":["../src/webhooks.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport type { DiscordContent, SlackContent } from './templates';\n\n/**\n * `@shware/workflow/webhooks` — messages posted to a team channel through an incoming webhook:\n * Discord (`discord-api-types` bodies) and Slack (Block Kit). Plain fetch, no SDKs. A template\n * names its webhook through the Worker's environment (`webhook: (env) => env.WEBHOOK_DISCORD_…`), so the\n * renderer hands the sender both the URL and the message.\n *\n * A request the channel refuses for good — the webhook deleted, the channel archived, a message\n * Discord or Slack rejects (4xx) — settles the copy undelivered instead of retrying into it, so a\n * team-channel post never holds up the customer messages after it. Rate limits (429), timeouts\n * (408) and server errors throw, and the step retries.\n */\n\n/** A 4xx that retrying cannot fix. */\nconst permanent = (status: number) =>\n status >= 400 && status < 500 && status !== 408 && status !== 429;\n\nexport interface WebhookMessage<C> {\n url: string;\n content: C;\n}\n\nexport interface WebhookSenderOptions<C> {\n render: (\n template: string,\n props: OutboundMessage['props']\n ) => Promise<WebhookMessage<C>> | WebhookMessage<C>;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n}\n\nasync function post(\n doFetch: typeof fetch,\n url: string,\n body: unknown\n): Promise<{ status: number; text: string }> {\n const response = await doFetch(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n });\n return { status: response.status, text: response.ok ? '' : await response.text() };\n}\n\nfunction requireChannel(message: OutboundMessage, channel: 'discord' | 'slack') {\n if (message.channel !== channel) {\n throw new Error(`${channel} webhook sender: unsupported channel '${message.channel}'`);\n }\n}\n\nexport class DiscordWebhookSender implements MessageSender {\n constructor(private readonly options: WebhookSenderOptions<DiscordContent>) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n requireChannel(message, 'discord');\n const { url, content } = await this.options.render(message.template, message.props);\n const { status, text } = await post(this.options.fetch ?? fetch, url, content);\n if (status >= 200 && status < 300) return;\n // 10015 Unknown Webhook: deleted, or its channel is gone — it will never deliver again\n if (status === 404 || status === 401) {\n console.warn(`[discord] webhook gone for template ${message.template}: ${status} ${text}`);\n return { delivered: false, reason: 'UnknownWebhook' };\n }\n if (permanent(status)) {\n console.warn(`[discord] refused ${message.template}: ${status} ${text}`);\n return { delivered: false, reason: `HTTP ${status}` };\n }\n throw new Error(`[discord] ${status}: ${text}`);\n }\n}\n\n/** Slack's answers for a webhook that will never deliver again. */\nconst SLACK_GONE = new Set([\n 'no_service',\n 'channel_not_found',\n 'channel_is_archived',\n 'action_prohibited',\n]);\n\nexport class SlackWebhookSender implements MessageSender {\n constructor(private readonly options: WebhookSenderOptions<SlackContent>) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n requireChannel(message, 'slack');\n const { url, content } = await this.options.render(message.template, message.props);\n const { status, text } = await post(this.options.fetch ?? fetch, url, content);\n if (status >= 200 && status < 300) return;\n const reason = text.trim();\n if (SLACK_GONE.has(reason)) {\n console.warn(`[slack] webhook gone for template ${message.template}: ${reason}`);\n return { delivered: false, reason };\n }\n if (permanent(status)) {\n console.warn(`[slack] refused ${message.template}: ${status} ${reason}`);\n return { delivered: false, reason: reason || `HTTP ${status}` };\n }\n throw new Error(`[slack] ${status}: ${reason}`);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAgBA,MAAM,aAAa,WACjB,UAAU,OAAO,SAAS,OAAO,WAAW,OAAO,WAAW;AAgBhE,eAAe,KACb,SACA,KACA,MAC2C;CAC3C,MAAM,WAAW,MAAM,QAAQ,KAAK;EAClC,QAAQ;EACR,SAAS,EAAE,gBAAgB,mBAAmB;EAC9C,MAAM,KAAK,UAAU,IAAI;CAC3B,CAAC;CACD,OAAO;EAAE,QAAQ,SAAS;EAAQ,MAAM,SAAS,KAAK,KAAK,MAAM,SAAS,KAAK;CAAE;AACnF;AAEA,SAAS,eAAe,SAA0B,SAA8B;CAC9E,IAAI,QAAQ,YAAY,SACtB,MAAM,IAAI,MAAM,GAAG,QAAQ,wCAAwC,QAAQ,QAAQ,EAAE;AAEzF;AAEA,IAAa,uBAAb,MAA2D;CAC5B;CAA7B,YAAY,SAAgE;EAA/C,KAAA,UAAA;CAAgD;CAE7E,MAAM,KAAK,SAAsD;EAC/D,eAAe,SAAS,SAAS;EACjC,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,QAAQ,OAAO,QAAQ,UAAU,QAAQ,KAAK;EAClF,MAAM,EAAE,QAAQ,SAAS,MAAM,KAAK,KAAK,QAAQ,SAAS,OAAO,KAAK,OAAO;EAC7E,IAAI,UAAU,OAAO,SAAS,KAAK;EAEnC,IAAI,WAAW,OAAO,WAAW,KAAK;GACpC,QAAQ,KAAK,uCAAuC,QAAQ,SAAS,IAAI,OAAO,GAAG,MAAM;GACzF,OAAO;IAAE,WAAW;IAAO,QAAQ;GAAiB;EACtD;EACA,IAAI,UAAU,MAAM,GAAG;GACrB,QAAQ,KAAK,qBAAqB,QAAQ,SAAS,IAAI,OAAO,GAAG,MAAM;GACvE,OAAO;IAAE,WAAW;IAAO,QAAQ,QAAQ;GAAS;EACtD;EACA,MAAM,IAAI,MAAM,aAAa,OAAO,IAAI,MAAM;CAChD;AACF;;AAGA,MAAM,6BAAa,IAAI,IAAI;CACzB;CACA;CACA;CACA;AACF,CAAC;AAED,IAAa,qBAAb,MAAyD;CAC1B;CAA7B,YAAY,SAA8D;EAA7C,KAAA,UAAA;CAA8C;CAE3E,MAAM,KAAK,SAAsD;EAC/D,eAAe,SAAS,OAAO;EAC/B,MAAM,EAAE,KAAK,YAAY,MAAM,KAAK,QAAQ,OAAO,QAAQ,UAAU,QAAQ,KAAK;EAClF,MAAM,EAAE,QAAQ,SAAS,MAAM,KAAK,KAAK,QAAQ,SAAS,OAAO,KAAK,OAAO;EAC7E,IAAI,UAAU,OAAO,SAAS,KAAK;EACnC,MAAM,SAAS,KAAK,KAAK;EACzB,IAAI,WAAW,IAAI,MAAM,GAAG;GAC1B,QAAQ,KAAK,qCAAqC,QAAQ,SAAS,IAAI,QAAQ;GAC/E,OAAO;IAAE,WAAW;IAAO;GAAO;EACpC;EACA,IAAI,UAAU,MAAM,GAAG;GACrB,QAAQ,KAAK,mBAAmB,QAAQ,SAAS,IAAI,OAAO,GAAG,QAAQ;GACvE,OAAO;IAAE,WAAW;IAAO,QAAQ,UAAU,QAAQ;GAAS;EAChE;EACA,MAAM,IAAI,MAAM,WAAW,OAAO,IAAI,QAAQ;CAChD;AACF"}
|
package/dist/webpush.cjs
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_jwt = require("./jwt.cjs");
|
|
3
|
+
//#region src/webpush.ts
|
|
4
|
+
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
5
|
+
async function generateVapidKeys() {
|
|
6
|
+
const pair = await crypto.subtle.generateKey({
|
|
7
|
+
name: "ECDSA",
|
|
8
|
+
namedCurve: "P-256"
|
|
9
|
+
}, true, ["sign", "verify"]);
|
|
10
|
+
const raw = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
11
|
+
const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
12
|
+
return {
|
|
13
|
+
publicKey: require_jwt.base64url(raw),
|
|
14
|
+
privateKey: jwk.d ?? ""
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Records are one per message here: the payload stays far below the 4096-byte record size. */
|
|
18
|
+
const RECORD_SIZE = 4096;
|
|
19
|
+
const encoder = new TextEncoder();
|
|
20
|
+
const utf8 = (text) => encoder.encode(text);
|
|
21
|
+
function fromBase64url(text) {
|
|
22
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
23
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
24
|
+
}
|
|
25
|
+
function concat(...parts) {
|
|
26
|
+
const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (const part of parts) {
|
|
29
|
+
out.set(part, offset);
|
|
30
|
+
offset += part.length;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
async function hmac(key, data) {
|
|
35
|
+
const imported = await crypto.subtle.importKey("raw", key, {
|
|
36
|
+
name: "HMAC",
|
|
37
|
+
hash: "SHA-256"
|
|
38
|
+
}, false, ["sign"]);
|
|
39
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", imported, data));
|
|
40
|
+
}
|
|
41
|
+
/** HKDF-SHA256 with a single output block (every length here is ≤ 32 bytes). */
|
|
42
|
+
async function hkdf(salt, ikm, info, length) {
|
|
43
|
+
return (await hmac(await hmac(salt, ikm), concat(info, new Uint8Array([1])))).slice(0, length);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's
|
|
47
|
+
* ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the
|
|
48
|
+
* subscription's auth secret.
|
|
49
|
+
*/
|
|
50
|
+
async function encryptPayload(payload, p256dh, authSecret, salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))) {
|
|
51
|
+
const ephemeral = await crypto.subtle.generateKey({
|
|
52
|
+
name: "ECDH",
|
|
53
|
+
namedCurve: "P-256"
|
|
54
|
+
}, true, ["deriveBits"]);
|
|
55
|
+
const senderPublic = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
|
|
56
|
+
const receiver = await crypto.subtle.importKey("raw", p256dh, {
|
|
57
|
+
name: "ECDH",
|
|
58
|
+
namedCurve: "P-256"
|
|
59
|
+
}, false, []);
|
|
60
|
+
const ikm = await hkdf(authSecret, new Uint8Array(await crypto.subtle.deriveBits({
|
|
61
|
+
name: "ECDH",
|
|
62
|
+
public: receiver
|
|
63
|
+
}, ephemeral.privateKey, 256)), concat(utf8("WebPush: info\0"), p256dh, senderPublic), 32);
|
|
64
|
+
const cek = await hkdf(salt, ikm, utf8("Content-Encoding: aes128gcm\0"), 16);
|
|
65
|
+
const nonce = await hkdf(salt, ikm, utf8("Content-Encoding: nonce\0"), 12);
|
|
66
|
+
const key = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
67
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({
|
|
68
|
+
name: "AES-GCM",
|
|
69
|
+
iv: nonce
|
|
70
|
+
}, key, concat(payload, new Uint8Array([2]))));
|
|
71
|
+
const header = /* @__PURE__ */ new Uint8Array(21);
|
|
72
|
+
header.set(salt, 0);
|
|
73
|
+
new DataView(header.buffer).setUint32(16, RECORD_SIZE);
|
|
74
|
+
header[20] = senderPublic.length;
|
|
75
|
+
return concat(header, senderPublic, sealed);
|
|
76
|
+
}
|
|
77
|
+
/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */
|
|
78
|
+
async function vapidAuthorization(endpoint, vapid, now = Date.now()) {
|
|
79
|
+
const publicKey = fromBase64url(vapid.publicKey);
|
|
80
|
+
const key = await crypto.subtle.importKey("jwk", {
|
|
81
|
+
kty: "EC",
|
|
82
|
+
crv: "P-256",
|
|
83
|
+
d: vapid.privateKey,
|
|
84
|
+
x: require_jwt.base64url(publicKey.slice(1, 33)),
|
|
85
|
+
y: require_jwt.base64url(publicKey.slice(33, 65)),
|
|
86
|
+
ext: true
|
|
87
|
+
}, {
|
|
88
|
+
name: "ECDSA",
|
|
89
|
+
namedCurve: "P-256"
|
|
90
|
+
}, false, ["sign"]);
|
|
91
|
+
const header = require_jwt.base64url(JSON.stringify({
|
|
92
|
+
typ: "JWT",
|
|
93
|
+
alg: "ES256"
|
|
94
|
+
}));
|
|
95
|
+
const claims = require_jwt.base64url(JSON.stringify({
|
|
96
|
+
aud: new URL(endpoint).origin,
|
|
97
|
+
exp: Math.floor(now / 1e3) + 43200,
|
|
98
|
+
sub: vapid.subject
|
|
99
|
+
}));
|
|
100
|
+
const signature = new Uint8Array(await crypto.subtle.sign({
|
|
101
|
+
name: "ECDSA",
|
|
102
|
+
hash: "SHA-256"
|
|
103
|
+
}, key, utf8(`${header}.${claims}`)));
|
|
104
|
+
return `vapid t=${header}.${claims}.${require_jwt.base64url(signature)}, k=${vapid.publicKey}`;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A browser subscription as a sender can use it: an https endpoint, a 65-byte uncompressed P-256
|
|
108
|
+
* key and a 16-byte auth secret. Anything else — not JSON, missing keys, a key of the wrong size,
|
|
109
|
+
* an endpoint that is not https — is undefined.
|
|
110
|
+
*/
|
|
111
|
+
function parseSubscription(token) {
|
|
112
|
+
try {
|
|
113
|
+
const value = JSON.parse(token);
|
|
114
|
+
if (typeof value.endpoint !== "string" || new URL(value.endpoint).protocol !== "https:") return;
|
|
115
|
+
const p256dh = fromBase64url(value.keys?.p256dh ?? "");
|
|
116
|
+
const auth = fromBase64url(value.keys?.auth ?? "");
|
|
117
|
+
if (p256dh.length !== 65 || p256dh[0] !== 4 || auth.length !== 16) return void 0;
|
|
118
|
+
return {
|
|
119
|
+
endpoint: value.endpoint,
|
|
120
|
+
p256dh,
|
|
121
|
+
auth
|
|
122
|
+
};
|
|
123
|
+
} catch {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
var WebPushSender = class {
|
|
128
|
+
options;
|
|
129
|
+
constructor(options) {
|
|
130
|
+
this.options = options;
|
|
131
|
+
}
|
|
132
|
+
async send(message) {
|
|
133
|
+
if (message.channel !== "push") throw new Error(`WebPushSender: unsupported channel '${message.channel}'`);
|
|
134
|
+
if (message.recipient === void 0) throw new Error(`WebPushSender: no subscription for user '${message.userId}'`);
|
|
135
|
+
const subscription = parseSubscription(message.recipient);
|
|
136
|
+
if (subscription === void 0) {
|
|
137
|
+
await this.options.onUnregistered?.(message);
|
|
138
|
+
return {
|
|
139
|
+
delivered: false,
|
|
140
|
+
reason: "InvalidSubscription"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const content = await this.options.render(message.template, message.props);
|
|
144
|
+
const body = await encryptPayload(utf8(JSON.stringify({
|
|
145
|
+
...content,
|
|
146
|
+
template: message.template,
|
|
147
|
+
idempotencyKey: message.idempotencyKey,
|
|
148
|
+
...message.tracking?.data
|
|
149
|
+
})), subscription.p256dh, subscription.auth);
|
|
150
|
+
const response = await (this.options.fetch ?? fetch)(subscription.endpoint, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
authorization: await vapidAuthorization(subscription.endpoint, this.options.vapid),
|
|
154
|
+
"content-encoding": "aes128gcm",
|
|
155
|
+
"content-type": "application/octet-stream",
|
|
156
|
+
ttl: String(this.options.ttl ?? 86400),
|
|
157
|
+
urgency: "normal"
|
|
158
|
+
},
|
|
159
|
+
body
|
|
160
|
+
});
|
|
161
|
+
if (response.ok) return;
|
|
162
|
+
if (response.status === 404 || response.status === 410) {
|
|
163
|
+
console.warn(`[webpush] subscription gone for user ${message.userId}: ${response.status}`);
|
|
164
|
+
await this.options.onUnregistered?.(message);
|
|
165
|
+
return {
|
|
166
|
+
delivered: false,
|
|
167
|
+
reason: response.status === 410 ? "Gone" : "NotFound"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
throw new Error(`[webpush] ${response.status}: ${await response.text()}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
exports.WebPushSender = WebPushSender;
|
|
175
|
+
exports.encryptPayload = encryptPayload;
|
|
176
|
+
exports.fromBase64url = fromBase64url;
|
|
177
|
+
exports.generateVapidKeys = generateVapidKeys;
|
|
178
|
+
exports.vapidAuthorization = vapidAuthorization;
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=webpush.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpush.cjs","names":["base64url"],"sources":["../src/webpush.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { base64url } from './jwt';\nimport type { PushRenderer } from './push';\n\n/**\n * `@shware/workflow/webpush` — browser push through the standard Web Push protocol: VAPID\n * (RFC 8292) identifies the sender, the payload is encrypted for the subscription (RFC 8291,\n * `aes128gcm`, RFC 8188). One sender reaches Chrome, Edge, Firefox and Safari 16+ — each\n * browser's push service accepts the same request, over HTTP/1.1 — with nothing but WebCrypto.\n *\n * The recipient is a browser's `PushSubscription` as JSON (`JSON.stringify(subscription)`),\n * registered like an app token (`registerDevice(userId, { token, platform: 'web' })`). The\n * service worker receives `{ title, body, image?, url?, template, …tracking data }` as the push\n * event's JSON.\n */\n\n/** A VAPID key pair: the uncompressed P-256 public key and the private scalar, base64url. */\nexport interface VapidKeys {\n publicKey: string;\n privateKey: string;\n}\n\nexport interface WebPushOptions {\n vapid: VapidKeys & {\n /** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */\n subject: string;\n };\n render: PushRenderer;\n /** Seconds the push service keeps an undelivered message; a day by default. */\n ttl?: number;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n /** Called for a subscription the push service reports as gone (404 / 410). */\n onUnregistered?: (message: OutboundMessage) => Promise<void> | void;\n}\n\ninterface Subscription {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n}\n\n/** A fresh VAPID key pair (once per project; the private key is a secret). */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [\n 'sign',\n 'verify',\n ]);\n const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));\n const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);\n return { publicKey: base64url(raw), privateKey: jwk.d ?? '' };\n}\n\n/** Records are one per message here: the payload stays far below the 4096-byte record size. */\nconst RECORD_SIZE = 4096;\n/** Bytes WebCrypto accepts (backed by a plain ArrayBuffer). */\ntype Bytes = Uint8Array<ArrayBuffer>;\nconst encoder = new TextEncoder();\nconst utf8 = (text: string): Bytes => encoder.encode(text);\n\nexport function fromBase64url(text: string): Bytes {\n const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n}\n\nfunction concat(...parts: Uint8Array[]): Bytes {\n const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nasync function hmac(key: Bytes, data: Bytes): Promise<Bytes> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return new Uint8Array(await crypto.subtle.sign('HMAC', imported, data));\n}\n\n/** HKDF-SHA256 with a single output block (every length here is ≤ 32 bytes). */\nasync function hkdf(salt: Bytes, ikm: Bytes, info: Bytes, length: number): Promise<Bytes> {\n const prk = await hmac(salt, ikm);\n return (await hmac(prk, concat(info, new Uint8Array([1])))).slice(0, length);\n}\n\n/**\n * The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's\n * ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the\n * subscription's auth secret.\n */\nexport async function encryptPayload(\n payload: Bytes,\n p256dh: Bytes,\n authSecret: Bytes,\n salt: Bytes = crypto.getRandomValues(new Uint8Array(16))\n): Promise<Bytes> {\n const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [\n 'deriveBits',\n ]);\n const senderPublic = new Uint8Array(await crypto.subtle.exportKey('raw', ephemeral.publicKey));\n const receiver = await crypto.subtle.importKey(\n 'raw',\n p256dh,\n { name: 'ECDH', namedCurve: 'P-256' },\n false,\n []\n );\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: 'ECDH', public: receiver }, ephemeral.privateKey, 256)\n );\n const ikm = await hkdf(\n authSecret,\n shared,\n concat(utf8('WebPush: info\\0'), p256dh, senderPublic),\n 32\n );\n const cek = await hkdf(salt, ikm, utf8('Content-Encoding: aes128gcm\\0'), 16);\n const nonce = await hkdf(salt, ikm, utf8('Content-Encoding: nonce\\0'), 12);\n const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);\n // The single, last record: the payload and the 0x02 delimiter, no padding\n const sealed = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: nonce },\n key,\n concat(payload, new Uint8Array([2]))\n )\n );\n const header = new Uint8Array(16 + 4 + 1);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE);\n header[20] = senderPublic.length;\n return concat(header, senderPublic, sealed);\n}\n\n/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */\nexport async function vapidAuthorization(\n endpoint: string,\n vapid: WebPushOptions['vapid'],\n now = Date.now()\n): Promise<string> {\n const publicKey = fromBase64url(vapid.publicKey);\n const key = await crypto.subtle.importKey(\n 'jwk',\n {\n kty: 'EC',\n crv: 'P-256',\n d: vapid.privateKey,\n x: base64url(publicKey.slice(1, 33)),\n y: base64url(publicKey.slice(33, 65)),\n ext: true,\n },\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['sign']\n );\n const header = base64url(JSON.stringify({ typ: 'JWT', alg: 'ES256' }));\n const claims = base64url(\n JSON.stringify({\n aud: new URL(endpoint).origin,\n exp: Math.floor(now / 1000) + 12 * 3600,\n sub: vapid.subject,\n })\n );\n const signature = new Uint8Array(\n await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, utf8(`${header}.${claims}`))\n );\n return `vapid t=${header}.${claims}.${base64url(signature)}, k=${vapid.publicKey}`;\n}\n\n/**\n * A browser subscription as a sender can use it: an https endpoint, a 65-byte uncompressed P-256\n * key and a 16-byte auth secret. Anything else — not JSON, missing keys, a key of the wrong size,\n * an endpoint that is not https — is undefined.\n */\nfunction parseSubscription(\n token: string\n): { endpoint: string; p256dh: Bytes; auth: Bytes } | undefined {\n try {\n const value = JSON.parse(token) as Partial<Subscription>;\n if (typeof value.endpoint !== 'string' || new URL(value.endpoint).protocol !== 'https:') {\n return undefined;\n }\n const p256dh = fromBase64url(value.keys?.p256dh ?? '');\n const auth = fromBase64url(value.keys?.auth ?? '');\n if (p256dh.length !== 65 || p256dh[0] !== 4 || auth.length !== 16) return undefined;\n return { endpoint: value.endpoint, p256dh, auth };\n } catch {\n return undefined;\n }\n}\n\nexport class WebPushSender implements MessageSender {\n constructor(private readonly options: WebPushOptions) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n if (message.channel !== 'push') {\n throw new Error(`WebPushSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`WebPushSender: no subscription for user '${message.userId}'`);\n }\n const subscription = parseSubscription(message.recipient);\n if (subscription === undefined) {\n // Not a usable subscription: nothing will ever deliver to it, so do not retry into it\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: 'InvalidSubscription' };\n }\n const content = await this.options.render(message.template, message.props);\n const payload = utf8(\n JSON.stringify({\n ...content,\n template: message.template,\n idempotencyKey: message.idempotencyKey,\n ...message.tracking?.data,\n })\n );\n const body = await encryptPayload(payload, subscription.p256dh, subscription.auth);\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(subscription.endpoint, {\n method: 'POST',\n headers: {\n authorization: await vapidAuthorization(subscription.endpoint, this.options.vapid),\n 'content-encoding': 'aes128gcm',\n 'content-type': 'application/octet-stream',\n ttl: String(this.options.ttl ?? 86_400),\n urgency: 'normal',\n },\n body,\n });\n if (response.ok) return;\n if (response.status === 404 || response.status === 410) {\n // The browser unsubscribed or the subscription expired: it will never deliver again\n console.warn(`[webpush] subscription gone for user ${message.userId}: ${response.status}`);\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: response.status === 410 ? 'Gone' : 'NotFound' };\n }\n throw new Error(`[webpush] ${response.status}: ${await response.text()}`);\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MessageSender, OutboundMessage, SendResult } from "./engine/ports.cjs";
|
|
2
|
+
import "./engine/index.cjs";
|
|
3
|
+
import { PushRenderer } from "./push.cjs";
|
|
4
|
+
//#region src/webpush.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@shware/workflow/webpush` — browser push through the standard Web Push protocol: VAPID
|
|
7
|
+
* (RFC 8292) identifies the sender, the payload is encrypted for the subscription (RFC 8291,
|
|
8
|
+
* `aes128gcm`, RFC 8188). One sender reaches Chrome, Edge, Firefox and Safari 16+ — each
|
|
9
|
+
* browser's push service accepts the same request, over HTTP/1.1 — with nothing but WebCrypto.
|
|
10
|
+
*
|
|
11
|
+
* The recipient is a browser's `PushSubscription` as JSON (`JSON.stringify(subscription)`),
|
|
12
|
+
* registered like an app token (`registerDevice(userId, { token, platform: 'web' })`). The
|
|
13
|
+
* service worker receives `{ title, body, image?, url?, template, …tracking data }` as the push
|
|
14
|
+
* event's JSON.
|
|
15
|
+
*/
|
|
16
|
+
/** A VAPID key pair: the uncompressed P-256 public key and the private scalar, base64url. */
|
|
17
|
+
export interface VapidKeys {
|
|
18
|
+
publicKey: string;
|
|
19
|
+
privateKey: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WebPushOptions {
|
|
22
|
+
vapid: VapidKeys & {
|
|
23
|
+
/** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */
|
|
24
|
+
subject: string;
|
|
25
|
+
};
|
|
26
|
+
render: PushRenderer;
|
|
27
|
+
/** Seconds the push service keeps an undelivered message; a day by default. */
|
|
28
|
+
ttl?: number;
|
|
29
|
+
/** Fetch implementation (defaults to the global one). */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** Called for a subscription the push service reports as gone (404 / 410). */
|
|
32
|
+
onUnregistered?: (message: OutboundMessage) => Promise<void> | void;
|
|
33
|
+
}
|
|
34
|
+
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
35
|
+
export declare function generateVapidKeys(): Promise<VapidKeys>;
|
|
36
|
+
/** Bytes WebCrypto accepts (backed by a plain ArrayBuffer). */
|
|
37
|
+
type Bytes = Uint8Array<ArrayBuffer>;
|
|
38
|
+
export declare function fromBase64url(text: string): Bytes;
|
|
39
|
+
/**
|
|
40
|
+
* The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's
|
|
41
|
+
* ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the
|
|
42
|
+
* subscription's auth secret.
|
|
43
|
+
*/
|
|
44
|
+
export declare function encryptPayload(payload: Bytes, p256dh: Bytes, authSecret: Bytes, salt?: Bytes): Promise<Bytes>;
|
|
45
|
+
/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */
|
|
46
|
+
export declare function vapidAuthorization(endpoint: string, vapid: WebPushOptions['vapid'], now?: number): Promise<string>;
|
|
47
|
+
export declare class WebPushSender implements MessageSender {
|
|
48
|
+
private readonly options;
|
|
49
|
+
constructor(options: WebPushOptions);
|
|
50
|
+
send(message: OutboundMessage): Promise<void | SendResult>;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//# sourceMappingURL=webpush.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpush.d.cts","names":[],"sources":["../src/webpush.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;EACf;EACA;;iBAGe;EACf,OAAO;;IAEL;;EAEF,QAAQ;;EAER;;EAEA,eAAe;;EAEf,kBAAkB,SAAS,oBAAoB;;;wBAS3B,qBAAqB,QAAQ;;KAa9C,QAAQ,WAAW;wBAIR,cAAc,eAAe;;;;;;wBAqCvB,eACpB,SAAS,OACT,QAAQ,OACR,YAAY,OACZ,OAAM,QACL,QAAQ;;wBAwCW,mBACpB,kBACA,OAAO,yBACP,eACC;qBAoDU,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB,eAAe"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { MessageSender, OutboundMessage, SendResult } from "./engine/ports.mjs";
|
|
2
|
+
import "./engine/index.mjs";
|
|
3
|
+
import { PushRenderer } from "./push.mjs";
|
|
4
|
+
//#region src/webpush.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@shware/workflow/webpush` — browser push through the standard Web Push protocol: VAPID
|
|
7
|
+
* (RFC 8292) identifies the sender, the payload is encrypted for the subscription (RFC 8291,
|
|
8
|
+
* `aes128gcm`, RFC 8188). One sender reaches Chrome, Edge, Firefox and Safari 16+ — each
|
|
9
|
+
* browser's push service accepts the same request, over HTTP/1.1 — with nothing but WebCrypto.
|
|
10
|
+
*
|
|
11
|
+
* The recipient is a browser's `PushSubscription` as JSON (`JSON.stringify(subscription)`),
|
|
12
|
+
* registered like an app token (`registerDevice(userId, { token, platform: 'web' })`). The
|
|
13
|
+
* service worker receives `{ title, body, image?, url?, template, …tracking data }` as the push
|
|
14
|
+
* event's JSON.
|
|
15
|
+
*/
|
|
16
|
+
/** A VAPID key pair: the uncompressed P-256 public key and the private scalar, base64url. */
|
|
17
|
+
export interface VapidKeys {
|
|
18
|
+
publicKey: string;
|
|
19
|
+
privateKey: string;
|
|
20
|
+
}
|
|
21
|
+
export interface WebPushOptions {
|
|
22
|
+
vapid: VapidKeys & {
|
|
23
|
+
/** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */
|
|
24
|
+
subject: string;
|
|
25
|
+
};
|
|
26
|
+
render: PushRenderer;
|
|
27
|
+
/** Seconds the push service keeps an undelivered message; a day by default. */
|
|
28
|
+
ttl?: number;
|
|
29
|
+
/** Fetch implementation (defaults to the global one). */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** Called for a subscription the push service reports as gone (404 / 410). */
|
|
32
|
+
onUnregistered?: (message: OutboundMessage) => Promise<void> | void;
|
|
33
|
+
}
|
|
34
|
+
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
35
|
+
export declare function generateVapidKeys(): Promise<VapidKeys>;
|
|
36
|
+
/** Bytes WebCrypto accepts (backed by a plain ArrayBuffer). */
|
|
37
|
+
type Bytes = Uint8Array<ArrayBuffer>;
|
|
38
|
+
export declare function fromBase64url(text: string): Bytes;
|
|
39
|
+
/**
|
|
40
|
+
* The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's
|
|
41
|
+
* ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the
|
|
42
|
+
* subscription's auth secret.
|
|
43
|
+
*/
|
|
44
|
+
export declare function encryptPayload(payload: Bytes, p256dh: Bytes, authSecret: Bytes, salt?: Bytes): Promise<Bytes>;
|
|
45
|
+
/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */
|
|
46
|
+
export declare function vapidAuthorization(endpoint: string, vapid: WebPushOptions['vapid'], now?: number): Promise<string>;
|
|
47
|
+
export declare class WebPushSender implements MessageSender {
|
|
48
|
+
private readonly options;
|
|
49
|
+
constructor(options: WebPushOptions);
|
|
50
|
+
send(message: OutboundMessage): Promise<void | SendResult>;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
//# sourceMappingURL=webpush.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpush.d.mts","names":[],"sources":["../src/webpush.ts"],"mappings":";;;;;;;;;;;;;;;;iBAiBiB;EACf;EACA;;iBAGe;EACf,OAAO;;IAEL;;EAEF,QAAQ;;EAER;;EAEA,eAAe;;EAEf,kBAAkB,SAAS,oBAAoB;;;wBAS3B,qBAAqB,QAAQ;;KAa9C,QAAQ,WAAW;wBAIR,cAAc,eAAe;;;;;;wBAqCvB,eACpB,SAAS,OACT,QAAQ,OACR,YAAY,OACZ,OAAM,QACL,QAAQ;;wBAwCW,mBACpB,kBACA,OAAO,yBACP,eACC;qBAoDU,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB,eAAe"}
|
package/dist/webpush.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { base64url } from "./jwt.mjs";
|
|
2
|
+
//#region src/webpush.ts
|
|
3
|
+
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
4
|
+
async function generateVapidKeys() {
|
|
5
|
+
const pair = await crypto.subtle.generateKey({
|
|
6
|
+
name: "ECDSA",
|
|
7
|
+
namedCurve: "P-256"
|
|
8
|
+
}, true, ["sign", "verify"]);
|
|
9
|
+
const raw = new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey));
|
|
10
|
+
const jwk = await crypto.subtle.exportKey("jwk", pair.privateKey);
|
|
11
|
+
return {
|
|
12
|
+
publicKey: base64url(raw),
|
|
13
|
+
privateKey: jwk.d ?? ""
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Records are one per message here: the payload stays far below the 4096-byte record size. */
|
|
17
|
+
const RECORD_SIZE = 4096;
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
const utf8 = (text) => encoder.encode(text);
|
|
20
|
+
function fromBase64url(text) {
|
|
21
|
+
const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
|
|
22
|
+
return Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
23
|
+
}
|
|
24
|
+
function concat(...parts) {
|
|
25
|
+
const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
out.set(part, offset);
|
|
29
|
+
offset += part.length;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
async function hmac(key, data) {
|
|
34
|
+
const imported = await crypto.subtle.importKey("raw", key, {
|
|
35
|
+
name: "HMAC",
|
|
36
|
+
hash: "SHA-256"
|
|
37
|
+
}, false, ["sign"]);
|
|
38
|
+
return new Uint8Array(await crypto.subtle.sign("HMAC", imported, data));
|
|
39
|
+
}
|
|
40
|
+
/** HKDF-SHA256 with a single output block (every length here is ≤ 32 bytes). */
|
|
41
|
+
async function hkdf(salt, ikm, info, length) {
|
|
42
|
+
return (await hmac(await hmac(salt, ikm), concat(info, new Uint8Array([1])))).slice(0, length);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's
|
|
46
|
+
* ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the
|
|
47
|
+
* subscription's auth secret.
|
|
48
|
+
*/
|
|
49
|
+
async function encryptPayload(payload, p256dh, authSecret, salt = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))) {
|
|
50
|
+
const ephemeral = await crypto.subtle.generateKey({
|
|
51
|
+
name: "ECDH",
|
|
52
|
+
namedCurve: "P-256"
|
|
53
|
+
}, true, ["deriveBits"]);
|
|
54
|
+
const senderPublic = new Uint8Array(await crypto.subtle.exportKey("raw", ephemeral.publicKey));
|
|
55
|
+
const receiver = await crypto.subtle.importKey("raw", p256dh, {
|
|
56
|
+
name: "ECDH",
|
|
57
|
+
namedCurve: "P-256"
|
|
58
|
+
}, false, []);
|
|
59
|
+
const ikm = await hkdf(authSecret, new Uint8Array(await crypto.subtle.deriveBits({
|
|
60
|
+
name: "ECDH",
|
|
61
|
+
public: receiver
|
|
62
|
+
}, ephemeral.privateKey, 256)), concat(utf8("WebPush: info\0"), p256dh, senderPublic), 32);
|
|
63
|
+
const cek = await hkdf(salt, ikm, utf8("Content-Encoding: aes128gcm\0"), 16);
|
|
64
|
+
const nonce = await hkdf(salt, ikm, utf8("Content-Encoding: nonce\0"), 12);
|
|
65
|
+
const key = await crypto.subtle.importKey("raw", cek, "AES-GCM", false, ["encrypt"]);
|
|
66
|
+
const sealed = new Uint8Array(await crypto.subtle.encrypt({
|
|
67
|
+
name: "AES-GCM",
|
|
68
|
+
iv: nonce
|
|
69
|
+
}, key, concat(payload, new Uint8Array([2]))));
|
|
70
|
+
const header = /* @__PURE__ */ new Uint8Array(21);
|
|
71
|
+
header.set(salt, 0);
|
|
72
|
+
new DataView(header.buffer).setUint32(16, RECORD_SIZE);
|
|
73
|
+
header[20] = senderPublic.length;
|
|
74
|
+
return concat(header, senderPublic, sealed);
|
|
75
|
+
}
|
|
76
|
+
/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */
|
|
77
|
+
async function vapidAuthorization(endpoint, vapid, now = Date.now()) {
|
|
78
|
+
const publicKey = fromBase64url(vapid.publicKey);
|
|
79
|
+
const key = await crypto.subtle.importKey("jwk", {
|
|
80
|
+
kty: "EC",
|
|
81
|
+
crv: "P-256",
|
|
82
|
+
d: vapid.privateKey,
|
|
83
|
+
x: base64url(publicKey.slice(1, 33)),
|
|
84
|
+
y: base64url(publicKey.slice(33, 65)),
|
|
85
|
+
ext: true
|
|
86
|
+
}, {
|
|
87
|
+
name: "ECDSA",
|
|
88
|
+
namedCurve: "P-256"
|
|
89
|
+
}, false, ["sign"]);
|
|
90
|
+
const header = base64url(JSON.stringify({
|
|
91
|
+
typ: "JWT",
|
|
92
|
+
alg: "ES256"
|
|
93
|
+
}));
|
|
94
|
+
const claims = base64url(JSON.stringify({
|
|
95
|
+
aud: new URL(endpoint).origin,
|
|
96
|
+
exp: Math.floor(now / 1e3) + 43200,
|
|
97
|
+
sub: vapid.subject
|
|
98
|
+
}));
|
|
99
|
+
const signature = new Uint8Array(await crypto.subtle.sign({
|
|
100
|
+
name: "ECDSA",
|
|
101
|
+
hash: "SHA-256"
|
|
102
|
+
}, key, utf8(`${header}.${claims}`)));
|
|
103
|
+
return `vapid t=${header}.${claims}.${base64url(signature)}, k=${vapid.publicKey}`;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* A browser subscription as a sender can use it: an https endpoint, a 65-byte uncompressed P-256
|
|
107
|
+
* key and a 16-byte auth secret. Anything else — not JSON, missing keys, a key of the wrong size,
|
|
108
|
+
* an endpoint that is not https — is undefined.
|
|
109
|
+
*/
|
|
110
|
+
function parseSubscription(token) {
|
|
111
|
+
try {
|
|
112
|
+
const value = JSON.parse(token);
|
|
113
|
+
if (typeof value.endpoint !== "string" || new URL(value.endpoint).protocol !== "https:") return;
|
|
114
|
+
const p256dh = fromBase64url(value.keys?.p256dh ?? "");
|
|
115
|
+
const auth = fromBase64url(value.keys?.auth ?? "");
|
|
116
|
+
if (p256dh.length !== 65 || p256dh[0] !== 4 || auth.length !== 16) return void 0;
|
|
117
|
+
return {
|
|
118
|
+
endpoint: value.endpoint,
|
|
119
|
+
p256dh,
|
|
120
|
+
auth
|
|
121
|
+
};
|
|
122
|
+
} catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
var WebPushSender = class {
|
|
127
|
+
options;
|
|
128
|
+
constructor(options) {
|
|
129
|
+
this.options = options;
|
|
130
|
+
}
|
|
131
|
+
async send(message) {
|
|
132
|
+
if (message.channel !== "push") throw new Error(`WebPushSender: unsupported channel '${message.channel}'`);
|
|
133
|
+
if (message.recipient === void 0) throw new Error(`WebPushSender: no subscription for user '${message.userId}'`);
|
|
134
|
+
const subscription = parseSubscription(message.recipient);
|
|
135
|
+
if (subscription === void 0) {
|
|
136
|
+
await this.options.onUnregistered?.(message);
|
|
137
|
+
return {
|
|
138
|
+
delivered: false,
|
|
139
|
+
reason: "InvalidSubscription"
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const content = await this.options.render(message.template, message.props);
|
|
143
|
+
const body = await encryptPayload(utf8(JSON.stringify({
|
|
144
|
+
...content,
|
|
145
|
+
template: message.template,
|
|
146
|
+
idempotencyKey: message.idempotencyKey,
|
|
147
|
+
...message.tracking?.data
|
|
148
|
+
})), subscription.p256dh, subscription.auth);
|
|
149
|
+
const response = await (this.options.fetch ?? fetch)(subscription.endpoint, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
authorization: await vapidAuthorization(subscription.endpoint, this.options.vapid),
|
|
153
|
+
"content-encoding": "aes128gcm",
|
|
154
|
+
"content-type": "application/octet-stream",
|
|
155
|
+
ttl: String(this.options.ttl ?? 86400),
|
|
156
|
+
urgency: "normal"
|
|
157
|
+
},
|
|
158
|
+
body
|
|
159
|
+
});
|
|
160
|
+
if (response.ok) return;
|
|
161
|
+
if (response.status === 404 || response.status === 410) {
|
|
162
|
+
console.warn(`[webpush] subscription gone for user ${message.userId}: ${response.status}`);
|
|
163
|
+
await this.options.onUnregistered?.(message);
|
|
164
|
+
return {
|
|
165
|
+
delivered: false,
|
|
166
|
+
reason: response.status === 410 ? "Gone" : "NotFound"
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`[webpush] ${response.status}: ${await response.text()}`);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
//#endregion
|
|
173
|
+
export { WebPushSender, encryptPayload, fromBase64url, generateVapidKeys, vapidAuthorization };
|
|
174
|
+
|
|
175
|
+
//# sourceMappingURL=webpush.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webpush.mjs","names":[],"sources":["../src/webpush.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { base64url } from './jwt';\nimport type { PushRenderer } from './push';\n\n/**\n * `@shware/workflow/webpush` — browser push through the standard Web Push protocol: VAPID\n * (RFC 8292) identifies the sender, the payload is encrypted for the subscription (RFC 8291,\n * `aes128gcm`, RFC 8188). One sender reaches Chrome, Edge, Firefox and Safari 16+ — each\n * browser's push service accepts the same request, over HTTP/1.1 — with nothing but WebCrypto.\n *\n * The recipient is a browser's `PushSubscription` as JSON (`JSON.stringify(subscription)`),\n * registered like an app token (`registerDevice(userId, { token, platform: 'web' })`). The\n * service worker receives `{ title, body, image?, url?, template, …tracking data }` as the push\n * event's JSON.\n */\n\n/** A VAPID key pair: the uncompressed P-256 public key and the private scalar, base64url. */\nexport interface VapidKeys {\n publicKey: string;\n privateKey: string;\n}\n\nexport interface WebPushOptions {\n vapid: VapidKeys & {\n /** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */\n subject: string;\n };\n render: PushRenderer;\n /** Seconds the push service keeps an undelivered message; a day by default. */\n ttl?: number;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n /** Called for a subscription the push service reports as gone (404 / 410). */\n onUnregistered?: (message: OutboundMessage) => Promise<void> | void;\n}\n\ninterface Subscription {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n}\n\n/** A fresh VAPID key pair (once per project; the private key is a secret). */\nexport async function generateVapidKeys(): Promise<VapidKeys> {\n const pair = await crypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, [\n 'sign',\n 'verify',\n ]);\n const raw = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey));\n const jwk = await crypto.subtle.exportKey('jwk', pair.privateKey);\n return { publicKey: base64url(raw), privateKey: jwk.d ?? '' };\n}\n\n/** Records are one per message here: the payload stays far below the 4096-byte record size. */\nconst RECORD_SIZE = 4096;\n/** Bytes WebCrypto accepts (backed by a plain ArrayBuffer). */\ntype Bytes = Uint8Array<ArrayBuffer>;\nconst encoder = new TextEncoder();\nconst utf8 = (text: string): Bytes => encoder.encode(text);\n\nexport function fromBase64url(text: string): Bytes {\n const binary = atob(text.replace(/-/g, '+').replace(/_/g, '/'));\n return Uint8Array.from(binary, (char) => char.charCodeAt(0));\n}\n\nfunction concat(...parts: Uint8Array[]): Bytes {\n const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nasync function hmac(key: Bytes, data: Bytes): Promise<Bytes> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return new Uint8Array(await crypto.subtle.sign('HMAC', imported, data));\n}\n\n/** HKDF-SHA256 with a single output block (every length here is ≤ 32 bytes). */\nasync function hkdf(salt: Bytes, ikm: Bytes, info: Bytes, length: number): Promise<Bytes> {\n const prk = await hmac(salt, ikm);\n return (await hmac(prk, concat(info, new Uint8Array([1])))).slice(0, length);\n}\n\n/**\n * The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's\n * ephemeral public key, then the payload sealed under a key both sides derive from ECDH and the\n * subscription's auth secret.\n */\nexport async function encryptPayload(\n payload: Bytes,\n p256dh: Bytes,\n authSecret: Bytes,\n salt: Bytes = crypto.getRandomValues(new Uint8Array(16))\n): Promise<Bytes> {\n const ephemeral = await crypto.subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, true, [\n 'deriveBits',\n ]);\n const senderPublic = new Uint8Array(await crypto.subtle.exportKey('raw', ephemeral.publicKey));\n const receiver = await crypto.subtle.importKey(\n 'raw',\n p256dh,\n { name: 'ECDH', namedCurve: 'P-256' },\n false,\n []\n );\n const shared = new Uint8Array(\n await crypto.subtle.deriveBits({ name: 'ECDH', public: receiver }, ephemeral.privateKey, 256)\n );\n const ikm = await hkdf(\n authSecret,\n shared,\n concat(utf8('WebPush: info\\0'), p256dh, senderPublic),\n 32\n );\n const cek = await hkdf(salt, ikm, utf8('Content-Encoding: aes128gcm\\0'), 16);\n const nonce = await hkdf(salt, ikm, utf8('Content-Encoding: nonce\\0'), 12);\n const key = await crypto.subtle.importKey('raw', cek, 'AES-GCM', false, ['encrypt']);\n // The single, last record: the payload and the 0x02 delimiter, no padding\n const sealed = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: nonce },\n key,\n concat(payload, new Uint8Array([2]))\n )\n );\n const header = new Uint8Array(16 + 4 + 1);\n header.set(salt, 0);\n new DataView(header.buffer).setUint32(16, RECORD_SIZE);\n header[20] = senderPublic.length;\n return concat(header, senderPublic, sealed);\n}\n\n/** The VAPID `Authorization` header for one push service (RFC 8292 §3). */\nexport async function vapidAuthorization(\n endpoint: string,\n vapid: WebPushOptions['vapid'],\n now = Date.now()\n): Promise<string> {\n const publicKey = fromBase64url(vapid.publicKey);\n const key = await crypto.subtle.importKey(\n 'jwk',\n {\n kty: 'EC',\n crv: 'P-256',\n d: vapid.privateKey,\n x: base64url(publicKey.slice(1, 33)),\n y: base64url(publicKey.slice(33, 65)),\n ext: true,\n },\n { name: 'ECDSA', namedCurve: 'P-256' },\n false,\n ['sign']\n );\n const header = base64url(JSON.stringify({ typ: 'JWT', alg: 'ES256' }));\n const claims = base64url(\n JSON.stringify({\n aud: new URL(endpoint).origin,\n exp: Math.floor(now / 1000) + 12 * 3600,\n sub: vapid.subject,\n })\n );\n const signature = new Uint8Array(\n await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, key, utf8(`${header}.${claims}`))\n );\n return `vapid t=${header}.${claims}.${base64url(signature)}, k=${vapid.publicKey}`;\n}\n\n/**\n * A browser subscription as a sender can use it: an https endpoint, a 65-byte uncompressed P-256\n * key and a 16-byte auth secret. Anything else — not JSON, missing keys, a key of the wrong size,\n * an endpoint that is not https — is undefined.\n */\nfunction parseSubscription(\n token: string\n): { endpoint: string; p256dh: Bytes; auth: Bytes } | undefined {\n try {\n const value = JSON.parse(token) as Partial<Subscription>;\n if (typeof value.endpoint !== 'string' || new URL(value.endpoint).protocol !== 'https:') {\n return undefined;\n }\n const p256dh = fromBase64url(value.keys?.p256dh ?? '');\n const auth = fromBase64url(value.keys?.auth ?? '');\n if (p256dh.length !== 65 || p256dh[0] !== 4 || auth.length !== 16) return undefined;\n return { endpoint: value.endpoint, p256dh, auth };\n } catch {\n return undefined;\n }\n}\n\nexport class WebPushSender implements MessageSender {\n constructor(private readonly options: WebPushOptions) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n if (message.channel !== 'push') {\n throw new Error(`WebPushSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`WebPushSender: no subscription for user '${message.userId}'`);\n }\n const subscription = parseSubscription(message.recipient);\n if (subscription === undefined) {\n // Not a usable subscription: nothing will ever deliver to it, so do not retry into it\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: 'InvalidSubscription' };\n }\n const content = await this.options.render(message.template, message.props);\n const payload = utf8(\n JSON.stringify({\n ...content,\n template: message.template,\n idempotencyKey: message.idempotencyKey,\n ...message.tracking?.data,\n })\n );\n const body = await encryptPayload(payload, subscription.p256dh, subscription.auth);\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(subscription.endpoint, {\n method: 'POST',\n headers: {\n authorization: await vapidAuthorization(subscription.endpoint, this.options.vapid),\n 'content-encoding': 'aes128gcm',\n 'content-type': 'application/octet-stream',\n ttl: String(this.options.ttl ?? 86_400),\n urgency: 'normal',\n },\n body,\n });\n if (response.ok) return;\n if (response.status === 404 || response.status === 410) {\n // The browser unsubscribed or the subscription expired: it will never deliver again\n console.warn(`[webpush] subscription gone for user ${message.userId}: ${response.status}`);\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: response.status === 410 ? 'Gone' : 'NotFound' };\n }\n throw new Error(`[webpush] ${response.status}: ${await response.text()}`);\n }\n}\n"],"mappings":"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"}
|