@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
package/dist/fcm.cjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_jwt = require("./jwt.cjs");
|
|
3
|
+
//#region src/fcm.ts
|
|
4
|
+
/** Errors FCM reports for a token that will never deliver again — not worth a retry. */
|
|
5
|
+
const DEAD_TOKEN_CODES = /* @__PURE__ */ new Set([
|
|
6
|
+
"UNREGISTERED",
|
|
7
|
+
"INVALID_ARGUMENT",
|
|
8
|
+
"SENDER_ID_MISMATCH"
|
|
9
|
+
]);
|
|
10
|
+
const SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
|
|
11
|
+
var FcmPushSender = class {
|
|
12
|
+
options;
|
|
13
|
+
token;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
async send(message) {
|
|
18
|
+
if (message.channel !== "push") throw new Error(`FcmPushSender: unsupported channel '${message.channel}'`);
|
|
19
|
+
if (message.recipient === void 0) throw new Error(`FcmPushSender: no push token for user '${message.userId}'`);
|
|
20
|
+
const { title, body, image, url } = await this.options.render(message.template, message.props);
|
|
21
|
+
const { serviceAccount, android, apns } = this.options;
|
|
22
|
+
const response = await (this.options.fetch ?? fetch)(`https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, {
|
|
23
|
+
method: "POST",
|
|
24
|
+
headers: {
|
|
25
|
+
authorization: `Bearer ${await this.accessToken()}`,
|
|
26
|
+
"content-type": "application/json"
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ message: {
|
|
29
|
+
token: message.recipient,
|
|
30
|
+
notification: {
|
|
31
|
+
title,
|
|
32
|
+
body,
|
|
33
|
+
...image === void 0 ? {} : { image }
|
|
34
|
+
},
|
|
35
|
+
data: {
|
|
36
|
+
template: message.template,
|
|
37
|
+
idempotencyKey: message.idempotencyKey,
|
|
38
|
+
...url === void 0 ? {} : { url },
|
|
39
|
+
...message.tracking?.data
|
|
40
|
+
},
|
|
41
|
+
...android === void 0 ? {} : { android: { notification: android } },
|
|
42
|
+
apns: { payload: { aps: {
|
|
43
|
+
sound: "default",
|
|
44
|
+
...apns
|
|
45
|
+
} } }
|
|
46
|
+
} })
|
|
47
|
+
});
|
|
48
|
+
if (response.ok) return;
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (DEAD_TOKEN_CODES.has(fcmErrorCode(text))) {
|
|
51
|
+
console.warn(`[fcm] token gone for user ${message.userId}: ${fcmErrorCode(text)}`);
|
|
52
|
+
await this.options.onUnregistered?.(message);
|
|
53
|
+
return {
|
|
54
|
+
delivered: false,
|
|
55
|
+
reason: fcmErrorCode(text)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
throw new Error(`FCM send failed: ${response.status} ${text}`);
|
|
59
|
+
}
|
|
60
|
+
async accessToken() {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (this.token !== void 0 && this.token.expiresAt - 6e4 > now) return this.token.value;
|
|
63
|
+
const { serviceAccount } = this.options;
|
|
64
|
+
const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token";
|
|
65
|
+
const assertion = await signJwt(serviceAccount, tokenUri, SCOPE, now);
|
|
66
|
+
const response = await (this.options.fetch ?? fetch)(tokenUri, {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
69
|
+
body: new URLSearchParams({
|
|
70
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
71
|
+
assertion
|
|
72
|
+
})
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) throw new Error(`FCM token exchange failed: ${response.status} ${await response.text()}`);
|
|
75
|
+
const json = await response.json();
|
|
76
|
+
this.token = {
|
|
77
|
+
value: json.access_token,
|
|
78
|
+
expiresAt: now + json.expires_in * 1e3
|
|
79
|
+
};
|
|
80
|
+
return json.access_token;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */
|
|
84
|
+
function fcmErrorCode(body) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(body);
|
|
87
|
+
return parsed.error?.details?.find((d) => d.errorCode !== void 0)?.errorCode ?? parsed.error?.status ?? "";
|
|
88
|
+
} catch {
|
|
89
|
+
return "";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** RS256-sign the OAuth2 JWT assertion for a service account. */
|
|
93
|
+
async function signJwt(account, audience, scope, nowMs = Date.now()) {
|
|
94
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
95
|
+
const header = require_jwt.base64url(JSON.stringify({
|
|
96
|
+
alg: "RS256",
|
|
97
|
+
typ: "JWT"
|
|
98
|
+
}));
|
|
99
|
+
const claims = require_jwt.base64url(JSON.stringify({
|
|
100
|
+
iss: account.client_email,
|
|
101
|
+
scope,
|
|
102
|
+
aud: audience,
|
|
103
|
+
iat,
|
|
104
|
+
exp: iat + 3600
|
|
105
|
+
}));
|
|
106
|
+
const key = await crypto.subtle.importKey("pkcs8", require_jwt.pemToDer(account.private_key), {
|
|
107
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
108
|
+
hash: "SHA-256"
|
|
109
|
+
}, false, ["sign"]);
|
|
110
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(`${header}.${claims}`));
|
|
111
|
+
return `${header}.${claims}.${require_jwt.base64url(new Uint8Array(signature))}`;
|
|
112
|
+
}
|
|
113
|
+
/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */
|
|
114
|
+
function serviceAccountFromBase64(value) {
|
|
115
|
+
const json = JSON.parse(atob(value));
|
|
116
|
+
if (!json.project_id || !json.client_email || !json.private_key) throw new Error("service account JSON is missing project_id, client_email or private_key");
|
|
117
|
+
return json;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
exports.FcmPushSender = FcmPushSender;
|
|
121
|
+
exports.fcmErrorCode = fcmErrorCode;
|
|
122
|
+
exports.serviceAccountFromBase64 = serviceAccountFromBase64;
|
|
123
|
+
exports.signJwt = signJwt;
|
|
124
|
+
|
|
125
|
+
//# sourceMappingURL=fcm.cjs.map
|
package/dist/fcm.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fcm.cjs","names":["base64url","pemToDer"],"sources":["../src/fcm.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { base64url, pemToDer } from './jwt';\nimport type { PushRenderer } from './push';\n\n/**\n * `@shware/workflow/fcm` — push delivery through Firebase Cloud Messaging's\n * HTTP v1 API, from a service account. No dependencies: the OAuth2 access\n * token is minted by signing a JWT with WebCrypto (Workers, Node 20+, Deno,\n * Bun), so this runs wherever the engine runs.\n *\n * The recipient is an FCM registration token the app registered for the user\n * (`registerDevice`); the engine sends one message per device.\n */\n\n/** The fields of a Google service-account JSON this sender needs. */\nexport interface FcmServiceAccount {\n project_id: string;\n client_email: string;\n /** PEM, `-----BEGIN PRIVATE KEY-----` (PKCS #8). */\n private_key: string;\n token_uri?: string;\n}\n\nexport interface FcmPushOptions {\n serviceAccount: FcmServiceAccount;\n render: PushRenderer;\n /** Android-specific delivery options, e.g. `{ channel_id: 'default' }` (FCM v1 `android.notification`). */\n android?: Record<string, unknown>;\n /** APNs-specific payload options merged into `aps`, e.g. `{ sound: 'default' }`. */\n apns?: Record<string, unknown>;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n /** Called for a token FCM reports as gone (`UNREGISTERED`); `channelsFromConfig` removes the device. */\n onUnregistered?: (message: OutboundMessage) => Promise<void> | void;\n}\n\n/** Errors FCM reports for a token that will never deliver again — not worth a retry. */\nconst DEAD_TOKEN_CODES = new Set(['UNREGISTERED', 'INVALID_ARGUMENT', 'SENDER_ID_MISMATCH']);\n\nconst SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';\n\nexport class FcmPushSender implements MessageSender {\n private token: { value: string; expiresAt: number } | undefined;\n\n constructor(private readonly options: FcmPushOptions) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n if (message.channel !== 'push') {\n throw new Error(`FcmPushSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`FcmPushSender: no push token for user '${message.userId}'`);\n }\n const { title, body, image, url } = await this.options.render(message.template, message.props);\n const { serviceAccount, android, apns } = this.options;\n const doFetch = this.options.fetch ?? fetch;\n\n const response = await doFetch(\n `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${await this.accessToken()}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n message: {\n token: message.recipient,\n notification: { title, body, ...(image === undefined ? {} : { image }) },\n // The template key travels as data so the app can route a tap; the\n // idempotency key lets the app de-duplicate a redelivered push.\n data: {\n template: message.template,\n idempotencyKey: message.idempotencyKey,\n ...(url === undefined ? {} : { url }),\n ...message.tracking?.data,\n },\n ...(android === undefined ? {} : { android: { notification: android } }),\n apns: { payload: { aps: { sound: 'default', ...apns } } },\n },\n }),\n }\n );\n if (response.ok) return;\n\n const text = await response.text();\n if (DEAD_TOKEN_CODES.has(fcmErrorCode(text))) {\n // A dead token is a fact about the recipient, not a transient failure:\n // report it and move on, so the step does not retry into the same wall.\n console.warn(`[fcm] token gone for user ${message.userId}: ${fcmErrorCode(text)}`);\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: fcmErrorCode(text) };\n }\n throw new Error(`FCM send failed: ${response.status} ${text}`);\n }\n\n private async accessToken(): Promise<string> {\n const now = Date.now();\n if (this.token !== undefined && this.token.expiresAt - 60_000 > now) return this.token.value;\n\n const { serviceAccount } = this.options;\n const tokenUri = serviceAccount.token_uri ?? 'https://oauth2.googleapis.com/token';\n const assertion = await signJwt(serviceAccount, tokenUri, SCOPE, now);\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(tokenUri, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion,\n }),\n });\n if (!response.ok) {\n throw new Error(`FCM token exchange failed: ${response.status} ${await response.text()}`);\n }\n const json = (await response.json()) as { access_token: string; expires_in: number };\n this.token = { value: json.access_token, expiresAt: now + json.expires_in * 1000 };\n return json.access_token;\n }\n}\n\n/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */\nexport function fcmErrorCode(body: string): string {\n try {\n const parsed = JSON.parse(body) as {\n error?: { status?: string; details?: { errorCode?: string }[] };\n };\n const detail = parsed.error?.details?.find((d) => d.errorCode !== undefined)?.errorCode;\n return detail ?? parsed.error?.status ?? '';\n } catch {\n return '';\n }\n}\n\n/** RS256-sign the OAuth2 JWT assertion for a service account. */\nexport async function signJwt(\n account: FcmServiceAccount,\n audience: string,\n scope: string,\n nowMs = Date.now()\n): Promise<string> {\n const iat = Math.floor(nowMs / 1000);\n const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const claims = base64url(\n JSON.stringify({ iss: account.client_email, scope, aud: audience, iat, exp: iat + 3600 })\n );\n const key = await crypto.subtle.importKey(\n 'pkcs8',\n pemToDer(account.private_key),\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const signature = await crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(`${header}.${claims}`)\n );\n return `${header}.${claims}.${base64url(new Uint8Array(signature))}`;\n}\n\n/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */\nexport function serviceAccountFromBase64(value: string): FcmServiceAccount {\n const json = JSON.parse(atob(value)) as Partial<FcmServiceAccount>;\n if (!json.project_id || !json.client_email || !json.private_key) {\n throw new Error('service account JSON is missing project_id, client_email or private_key');\n }\n return json as FcmServiceAccount;\n}\n"],"mappings":"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"}
|
package/dist/fcm.d.cts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MessageSender, OutboundMessage, SendResult } from "./engine/ports.cjs";
|
|
2
|
+
import "./engine/index.cjs";
|
|
3
|
+
import { PushRenderer } from "./push.cjs";
|
|
4
|
+
//#region src/fcm.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@shware/workflow/fcm` — push delivery through Firebase Cloud Messaging's
|
|
7
|
+
* HTTP v1 API, from a service account. No dependencies: the OAuth2 access
|
|
8
|
+
* token is minted by signing a JWT with WebCrypto (Workers, Node 20+, Deno,
|
|
9
|
+
* Bun), so this runs wherever the engine runs.
|
|
10
|
+
*
|
|
11
|
+
* The recipient is an FCM registration token the app registered for the user
|
|
12
|
+
* (`registerDevice`); the engine sends one message per device.
|
|
13
|
+
*/
|
|
14
|
+
/** The fields of a Google service-account JSON this sender needs. */
|
|
15
|
+
export interface FcmServiceAccount {
|
|
16
|
+
project_id: string;
|
|
17
|
+
client_email: string;
|
|
18
|
+
/** PEM, `-----BEGIN PRIVATE KEY-----` (PKCS #8). */
|
|
19
|
+
private_key: string;
|
|
20
|
+
token_uri?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface FcmPushOptions {
|
|
23
|
+
serviceAccount: FcmServiceAccount;
|
|
24
|
+
render: PushRenderer;
|
|
25
|
+
/** Android-specific delivery options, e.g. `{ channel_id: 'default' }` (FCM v1 `android.notification`). */
|
|
26
|
+
android?: Record<string, unknown>;
|
|
27
|
+
/** APNs-specific payload options merged into `aps`, e.g. `{ sound: 'default' }`. */
|
|
28
|
+
apns?: Record<string, unknown>;
|
|
29
|
+
/** Fetch implementation (defaults to the global one). */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** Called for a token FCM reports as gone (`UNREGISTERED`); `channelsFromConfig` removes the device. */
|
|
32
|
+
onUnregistered?: (message: OutboundMessage) => Promise<void> | void;
|
|
33
|
+
}
|
|
34
|
+
export declare class FcmPushSender implements MessageSender {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private token;
|
|
37
|
+
constructor(options: FcmPushOptions);
|
|
38
|
+
send(message: OutboundMessage): Promise<void | SendResult>;
|
|
39
|
+
private accessToken;
|
|
40
|
+
}
|
|
41
|
+
/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */
|
|
42
|
+
export declare function fcmErrorCode(body: string): string;
|
|
43
|
+
/** RS256-sign the OAuth2 JWT assertion for a service account. */
|
|
44
|
+
export declare function signJwt(account: FcmServiceAccount, audience: string, scope: string, nowMs?: number): Promise<string>;
|
|
45
|
+
/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */
|
|
46
|
+
export declare function serviceAccountFromBase64(value: string): FcmServiceAccount;
|
|
47
|
+
//#endregion
|
|
48
|
+
//# sourceMappingURL=fcm.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fcm.d.cts","names":[],"sources":["../src/fcm.ts"],"mappings":";;;;;;;;;;;;;;iBAeiB;EACf;EACA;;EAEA;EACA;;iBAGe;EACf,gBAAgB;EAChB,QAAQ;;EAER,UAAU;;EAEV,OAAO;;EAEP,eAAe;;EAEf,kBAAkB,SAAS,oBAAoB;;qBAQpC,yBAAyB;mBAGP;UAFrB;EAER,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB,eAAe;UAkDvC;;;wBA0BA,aAAa;;wBAaP,QACpB,SAAS,mBACT,kBACA,eACA,iBACC;;wBAsBa,yBAAyB,gBAAgB"}
|
package/dist/fcm.d.mts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { MessageSender, OutboundMessage, SendResult } from "./engine/ports.mjs";
|
|
2
|
+
import "./engine/index.mjs";
|
|
3
|
+
import { PushRenderer } from "./push.mjs";
|
|
4
|
+
//#region src/fcm.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* `@shware/workflow/fcm` — push delivery through Firebase Cloud Messaging's
|
|
7
|
+
* HTTP v1 API, from a service account. No dependencies: the OAuth2 access
|
|
8
|
+
* token is minted by signing a JWT with WebCrypto (Workers, Node 20+, Deno,
|
|
9
|
+
* Bun), so this runs wherever the engine runs.
|
|
10
|
+
*
|
|
11
|
+
* The recipient is an FCM registration token the app registered for the user
|
|
12
|
+
* (`registerDevice`); the engine sends one message per device.
|
|
13
|
+
*/
|
|
14
|
+
/** The fields of a Google service-account JSON this sender needs. */
|
|
15
|
+
export interface FcmServiceAccount {
|
|
16
|
+
project_id: string;
|
|
17
|
+
client_email: string;
|
|
18
|
+
/** PEM, `-----BEGIN PRIVATE KEY-----` (PKCS #8). */
|
|
19
|
+
private_key: string;
|
|
20
|
+
token_uri?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface FcmPushOptions {
|
|
23
|
+
serviceAccount: FcmServiceAccount;
|
|
24
|
+
render: PushRenderer;
|
|
25
|
+
/** Android-specific delivery options, e.g. `{ channel_id: 'default' }` (FCM v1 `android.notification`). */
|
|
26
|
+
android?: Record<string, unknown>;
|
|
27
|
+
/** APNs-specific payload options merged into `aps`, e.g. `{ sound: 'default' }`. */
|
|
28
|
+
apns?: Record<string, unknown>;
|
|
29
|
+
/** Fetch implementation (defaults to the global one). */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** Called for a token FCM reports as gone (`UNREGISTERED`); `channelsFromConfig` removes the device. */
|
|
32
|
+
onUnregistered?: (message: OutboundMessage) => Promise<void> | void;
|
|
33
|
+
}
|
|
34
|
+
export declare class FcmPushSender implements MessageSender {
|
|
35
|
+
private readonly options;
|
|
36
|
+
private token;
|
|
37
|
+
constructor(options: FcmPushOptions);
|
|
38
|
+
send(message: OutboundMessage): Promise<void | SendResult>;
|
|
39
|
+
private accessToken;
|
|
40
|
+
}
|
|
41
|
+
/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */
|
|
42
|
+
export declare function fcmErrorCode(body: string): string;
|
|
43
|
+
/** RS256-sign the OAuth2 JWT assertion for a service account. */
|
|
44
|
+
export declare function signJwt(account: FcmServiceAccount, audience: string, scope: string, nowMs?: number): Promise<string>;
|
|
45
|
+
/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */
|
|
46
|
+
export declare function serviceAccountFromBase64(value: string): FcmServiceAccount;
|
|
47
|
+
//#endregion
|
|
48
|
+
//# sourceMappingURL=fcm.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fcm.d.mts","names":[],"sources":["../src/fcm.ts"],"mappings":";;;;;;;;;;;;;;iBAeiB;EACf;EACA;;EAEA;EACA;;iBAGe;EACf,gBAAgB;EAChB,QAAQ;;EAER,UAAU;;EAEV,OAAO;;EAEP,eAAe;;EAEf,kBAAkB,SAAS,oBAAoB;;qBAQpC,yBAAyB;mBAGP;UAFrB;EAER,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB,eAAe;UAkDvC;;;wBA0BA,aAAa;;wBAaP,QACpB,SAAS,mBACT,kBACA,eACA,iBACC;;wBAsBa,yBAAyB,gBAAgB"}
|
package/dist/fcm.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { base64url, pemToDer } from "./jwt.mjs";
|
|
2
|
+
//#region src/fcm.ts
|
|
3
|
+
/** Errors FCM reports for a token that will never deliver again — not worth a retry. */
|
|
4
|
+
const DEAD_TOKEN_CODES = /* @__PURE__ */ new Set([
|
|
5
|
+
"UNREGISTERED",
|
|
6
|
+
"INVALID_ARGUMENT",
|
|
7
|
+
"SENDER_ID_MISMATCH"
|
|
8
|
+
]);
|
|
9
|
+
const SCOPE = "https://www.googleapis.com/auth/firebase.messaging";
|
|
10
|
+
var FcmPushSender = class {
|
|
11
|
+
options;
|
|
12
|
+
token;
|
|
13
|
+
constructor(options) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
async send(message) {
|
|
17
|
+
if (message.channel !== "push") throw new Error(`FcmPushSender: unsupported channel '${message.channel}'`);
|
|
18
|
+
if (message.recipient === void 0) throw new Error(`FcmPushSender: no push token for user '${message.userId}'`);
|
|
19
|
+
const { title, body, image, url } = await this.options.render(message.template, message.props);
|
|
20
|
+
const { serviceAccount, android, apns } = this.options;
|
|
21
|
+
const response = await (this.options.fetch ?? fetch)(`https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
authorization: `Bearer ${await this.accessToken()}`,
|
|
25
|
+
"content-type": "application/json"
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({ message: {
|
|
28
|
+
token: message.recipient,
|
|
29
|
+
notification: {
|
|
30
|
+
title,
|
|
31
|
+
body,
|
|
32
|
+
...image === void 0 ? {} : { image }
|
|
33
|
+
},
|
|
34
|
+
data: {
|
|
35
|
+
template: message.template,
|
|
36
|
+
idempotencyKey: message.idempotencyKey,
|
|
37
|
+
...url === void 0 ? {} : { url },
|
|
38
|
+
...message.tracking?.data
|
|
39
|
+
},
|
|
40
|
+
...android === void 0 ? {} : { android: { notification: android } },
|
|
41
|
+
apns: { payload: { aps: {
|
|
42
|
+
sound: "default",
|
|
43
|
+
...apns
|
|
44
|
+
} } }
|
|
45
|
+
} })
|
|
46
|
+
});
|
|
47
|
+
if (response.ok) return;
|
|
48
|
+
const text = await response.text();
|
|
49
|
+
if (DEAD_TOKEN_CODES.has(fcmErrorCode(text))) {
|
|
50
|
+
console.warn(`[fcm] token gone for user ${message.userId}: ${fcmErrorCode(text)}`);
|
|
51
|
+
await this.options.onUnregistered?.(message);
|
|
52
|
+
return {
|
|
53
|
+
delivered: false,
|
|
54
|
+
reason: fcmErrorCode(text)
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
throw new Error(`FCM send failed: ${response.status} ${text}`);
|
|
58
|
+
}
|
|
59
|
+
async accessToken() {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
if (this.token !== void 0 && this.token.expiresAt - 6e4 > now) return this.token.value;
|
|
62
|
+
const { serviceAccount } = this.options;
|
|
63
|
+
const tokenUri = serviceAccount.token_uri ?? "https://oauth2.googleapis.com/token";
|
|
64
|
+
const assertion = await signJwt(serviceAccount, tokenUri, SCOPE, now);
|
|
65
|
+
const response = await (this.options.fetch ?? fetch)(tokenUri, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
68
|
+
body: new URLSearchParams({
|
|
69
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
70
|
+
assertion
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
if (!response.ok) throw new Error(`FCM token exchange failed: ${response.status} ${await response.text()}`);
|
|
74
|
+
const json = await response.json();
|
|
75
|
+
this.token = {
|
|
76
|
+
value: json.access_token,
|
|
77
|
+
expiresAt: now + json.expires_in * 1e3
|
|
78
|
+
};
|
|
79
|
+
return json.access_token;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */
|
|
83
|
+
function fcmErrorCode(body) {
|
|
84
|
+
try {
|
|
85
|
+
const parsed = JSON.parse(body);
|
|
86
|
+
return parsed.error?.details?.find((d) => d.errorCode !== void 0)?.errorCode ?? parsed.error?.status ?? "";
|
|
87
|
+
} catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** RS256-sign the OAuth2 JWT assertion for a service account. */
|
|
92
|
+
async function signJwt(account, audience, scope, nowMs = Date.now()) {
|
|
93
|
+
const iat = Math.floor(nowMs / 1e3);
|
|
94
|
+
const header = base64url(JSON.stringify({
|
|
95
|
+
alg: "RS256",
|
|
96
|
+
typ: "JWT"
|
|
97
|
+
}));
|
|
98
|
+
const claims = base64url(JSON.stringify({
|
|
99
|
+
iss: account.client_email,
|
|
100
|
+
scope,
|
|
101
|
+
aud: audience,
|
|
102
|
+
iat,
|
|
103
|
+
exp: iat + 3600
|
|
104
|
+
}));
|
|
105
|
+
const key = await crypto.subtle.importKey("pkcs8", pemToDer(account.private_key), {
|
|
106
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
107
|
+
hash: "SHA-256"
|
|
108
|
+
}, false, ["sign"]);
|
|
109
|
+
const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(`${header}.${claims}`));
|
|
110
|
+
return `${header}.${claims}.${base64url(new Uint8Array(signature))}`;
|
|
111
|
+
}
|
|
112
|
+
/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */
|
|
113
|
+
function serviceAccountFromBase64(value) {
|
|
114
|
+
const json = JSON.parse(atob(value));
|
|
115
|
+
if (!json.project_id || !json.client_email || !json.private_key) throw new Error("service account JSON is missing project_id, client_email or private_key");
|
|
116
|
+
return json;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
export { FcmPushSender, fcmErrorCode, serviceAccountFromBase64, signJwt };
|
|
120
|
+
|
|
121
|
+
//# sourceMappingURL=fcm.mjs.map
|
package/dist/fcm.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fcm.mjs","names":[],"sources":["../src/fcm.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { base64url, pemToDer } from './jwt';\nimport type { PushRenderer } from './push';\n\n/**\n * `@shware/workflow/fcm` — push delivery through Firebase Cloud Messaging's\n * HTTP v1 API, from a service account. No dependencies: the OAuth2 access\n * token is minted by signing a JWT with WebCrypto (Workers, Node 20+, Deno,\n * Bun), so this runs wherever the engine runs.\n *\n * The recipient is an FCM registration token the app registered for the user\n * (`registerDevice`); the engine sends one message per device.\n */\n\n/** The fields of a Google service-account JSON this sender needs. */\nexport interface FcmServiceAccount {\n project_id: string;\n client_email: string;\n /** PEM, `-----BEGIN PRIVATE KEY-----` (PKCS #8). */\n private_key: string;\n token_uri?: string;\n}\n\nexport interface FcmPushOptions {\n serviceAccount: FcmServiceAccount;\n render: PushRenderer;\n /** Android-specific delivery options, e.g. `{ channel_id: 'default' }` (FCM v1 `android.notification`). */\n android?: Record<string, unknown>;\n /** APNs-specific payload options merged into `aps`, e.g. `{ sound: 'default' }`. */\n apns?: Record<string, unknown>;\n /** Fetch implementation (defaults to the global one). */\n fetch?: typeof fetch;\n /** Called for a token FCM reports as gone (`UNREGISTERED`); `channelsFromConfig` removes the device. */\n onUnregistered?: (message: OutboundMessage) => Promise<void> | void;\n}\n\n/** Errors FCM reports for a token that will never deliver again — not worth a retry. */\nconst DEAD_TOKEN_CODES = new Set(['UNREGISTERED', 'INVALID_ARGUMENT', 'SENDER_ID_MISMATCH']);\n\nconst SCOPE = 'https://www.googleapis.com/auth/firebase.messaging';\n\nexport class FcmPushSender implements MessageSender {\n private token: { value: string; expiresAt: number } | undefined;\n\n constructor(private readonly options: FcmPushOptions) {}\n\n async send(message: OutboundMessage): Promise<void | SendResult> {\n if (message.channel !== 'push') {\n throw new Error(`FcmPushSender: unsupported channel '${message.channel}'`);\n }\n if (message.recipient === undefined) {\n throw new Error(`FcmPushSender: no push token for user '${message.userId}'`);\n }\n const { title, body, image, url } = await this.options.render(message.template, message.props);\n const { serviceAccount, android, apns } = this.options;\n const doFetch = this.options.fetch ?? fetch;\n\n const response = await doFetch(\n `https://fcm.googleapis.com/v1/projects/${serviceAccount.project_id}/messages:send`,\n {\n method: 'POST',\n headers: {\n authorization: `Bearer ${await this.accessToken()}`,\n 'content-type': 'application/json',\n },\n body: JSON.stringify({\n message: {\n token: message.recipient,\n notification: { title, body, ...(image === undefined ? {} : { image }) },\n // The template key travels as data so the app can route a tap; the\n // idempotency key lets the app de-duplicate a redelivered push.\n data: {\n template: message.template,\n idempotencyKey: message.idempotencyKey,\n ...(url === undefined ? {} : { url }),\n ...message.tracking?.data,\n },\n ...(android === undefined ? {} : { android: { notification: android } }),\n apns: { payload: { aps: { sound: 'default', ...apns } } },\n },\n }),\n }\n );\n if (response.ok) return;\n\n const text = await response.text();\n if (DEAD_TOKEN_CODES.has(fcmErrorCode(text))) {\n // A dead token is a fact about the recipient, not a transient failure:\n // report it and move on, so the step does not retry into the same wall.\n console.warn(`[fcm] token gone for user ${message.userId}: ${fcmErrorCode(text)}`);\n await this.options.onUnregistered?.(message);\n return { delivered: false, reason: fcmErrorCode(text) };\n }\n throw new Error(`FCM send failed: ${response.status} ${text}`);\n }\n\n private async accessToken(): Promise<string> {\n const now = Date.now();\n if (this.token !== undefined && this.token.expiresAt - 60_000 > now) return this.token.value;\n\n const { serviceAccount } = this.options;\n const tokenUri = serviceAccount.token_uri ?? 'https://oauth2.googleapis.com/token';\n const assertion = await signJwt(serviceAccount, tokenUri, SCOPE, now);\n const doFetch = this.options.fetch ?? fetch;\n const response = await doFetch(tokenUri, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',\n assertion,\n }),\n });\n if (!response.ok) {\n throw new Error(`FCM token exchange failed: ${response.status} ${await response.text()}`);\n }\n const json = (await response.json()) as { access_token: string; expires_in: number };\n this.token = { value: json.access_token, expiresAt: now + json.expires_in * 1000 };\n return json.access_token;\n }\n}\n\n/** FCM v1 error bodies carry the code at `error.status` and, for messaging, in `error.details[].errorCode`. */\nexport function fcmErrorCode(body: string): string {\n try {\n const parsed = JSON.parse(body) as {\n error?: { status?: string; details?: { errorCode?: string }[] };\n };\n const detail = parsed.error?.details?.find((d) => d.errorCode !== undefined)?.errorCode;\n return detail ?? parsed.error?.status ?? '';\n } catch {\n return '';\n }\n}\n\n/** RS256-sign the OAuth2 JWT assertion for a service account. */\nexport async function signJwt(\n account: FcmServiceAccount,\n audience: string,\n scope: string,\n nowMs = Date.now()\n): Promise<string> {\n const iat = Math.floor(nowMs / 1000);\n const header = base64url(JSON.stringify({ alg: 'RS256', typ: 'JWT' }));\n const claims = base64url(\n JSON.stringify({ iss: account.client_email, scope, aud: audience, iat, exp: iat + 3600 })\n );\n const key = await crypto.subtle.importKey(\n 'pkcs8',\n pemToDer(account.private_key),\n { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },\n false,\n ['sign']\n );\n const signature = await crypto.subtle.sign(\n 'RSASSA-PKCS1-v1_5',\n key,\n new TextEncoder().encode(`${header}.${claims}`)\n );\n return `${header}.${claims}.${base64url(new Uint8Array(signature))}`;\n}\n\n/** Parse the service-account JSON as it is usually shipped: base64 of the file, in one secret. */\nexport function serviceAccountFromBase64(value: string): FcmServiceAccount {\n const json = JSON.parse(atob(value)) as Partial<FcmServiceAccount>;\n if (!json.project_id || !json.client_email || !json.private_key) {\n throw new Error('service account JSON is missing project_id, client_email or private_key');\n }\n return json as FcmServiceAccount;\n}\n"],"mappings":"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"}
|
package/dist/hash.cjs
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region src/hash.ts
|
|
3
|
+
/**
|
|
4
|
+
* Content hashing — contentHash covers execution semantics only.
|
|
5
|
+
*
|
|
6
|
+
* Human-facing metadata (name / description, node labels) is excluded,
|
|
7
|
+
* because it cannot change which path a user takes:
|
|
8
|
+
* - rewording a description must not invalidate the version an in-flight
|
|
9
|
+
* journey is pinned to;
|
|
10
|
+
* - nor should it surface as a change in `plan`.
|
|
11
|
+
*
|
|
12
|
+
* Storage, by contrast, always keeps the full IR (including the latest
|
|
13
|
+
* description). contentHash carries exactly two responsibilities: deciding
|
|
14
|
+
* whether semantics moved (plan / migration), and pinning a version for
|
|
15
|
+
* in-flight instances. So after a description edit: the UI shows the new text,
|
|
16
|
+
* plan reports no change, and in-flight users are untouched.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Keys excluded from hashing.
|
|
20
|
+
* - meta: metadata at any level — workflow (name / description) and
|
|
21
|
+
* node/segment provenance (`loc`, the callsite source map)
|
|
22
|
+
* - label: node and branch-arm names (UI titles / observability, never routing)
|
|
23
|
+
* - reason: the exit / filter audit string. It is written into the audit log
|
|
24
|
+
* and shown in the studio, but no user takes a different path because of it —
|
|
25
|
+
* the same test as a description, so rewording one must not repin in-flight
|
|
26
|
+
* journeys or report as a semantic change in plan.
|
|
27
|
+
* - contentHash: the hash itself
|
|
28
|
+
*
|
|
29
|
+
* Note that a cohort arm's `name` IS hashed: it becomes part of the node id
|
|
30
|
+
* (`{id}.{armName}.{j}`), and node ids are durable step names — renaming an arm
|
|
31
|
+
* changes execution identity.
|
|
32
|
+
*/
|
|
33
|
+
const UNHASHED_KEYS = /* @__PURE__ */ new Set([
|
|
34
|
+
"meta",
|
|
35
|
+
"label",
|
|
36
|
+
"reason",
|
|
37
|
+
"contentHash"
|
|
38
|
+
]);
|
|
39
|
+
/**
|
|
40
|
+
* Provenance only. `loc` is a source map, not content: it moves with the
|
|
41
|
+
* machine and with the directory the compile ran from, so plan compares full
|
|
42
|
+
* content with it removed (see plan.ts) — otherwise CI, compiling from the
|
|
43
|
+
* workspace root, reports every definition as metadata_only against a deploy
|
|
44
|
+
* compiled from a package directory.
|
|
45
|
+
*/
|
|
46
|
+
const PROVENANCE_KEYS = /* @__PURE__ */ new Set(["loc"]);
|
|
47
|
+
/**
|
|
48
|
+
* Fields whose values are maps the *author* keys, not the IR: message props,
|
|
49
|
+
* send_event payloads and action args. Everything inside them is execution
|
|
50
|
+
* data and is hashed whole.
|
|
51
|
+
*
|
|
52
|
+
* This is what keeps the exclusion above positional. Stripping by name at every
|
|
53
|
+
* depth used to reach inside these maps, so a prop or payload field that
|
|
54
|
+
* happened to be called `label` (or `meta`) vanished from the hash: two sends
|
|
55
|
+
* carrying different values hashed identically, `plan` reported a real change
|
|
56
|
+
* as metadata-only, and the deploy wrote different content under the same KV
|
|
57
|
+
* key that in-flight journeys are pinned to.
|
|
58
|
+
*
|
|
59
|
+
* These three are the only `z.record` fields in the IR (see ir.ts). A new one
|
|
60
|
+
* has to be added here, or its contents become strippable by name again.
|
|
61
|
+
*
|
|
62
|
+
* It matters for every key above: a send_event payload may well carry a field
|
|
63
|
+
* called `reason` (subscription_cancelled does), and dropping it would hash two
|
|
64
|
+
* different sends the same.
|
|
65
|
+
*/
|
|
66
|
+
const AUTHOR_KEYED_FIELDS = /* @__PURE__ */ new Set([
|
|
67
|
+
"props",
|
|
68
|
+
"payload",
|
|
69
|
+
"args"
|
|
70
|
+
]);
|
|
71
|
+
/** Drop `keys` at the positions the IR defines them, never inside an author-keyed map. */
|
|
72
|
+
function stripKeys(value, keys) {
|
|
73
|
+
if (Array.isArray(value)) return value.map((item) => stripKeys(item, keys));
|
|
74
|
+
if (value !== null && typeof value === "object") {
|
|
75
|
+
const out = {};
|
|
76
|
+
for (const [key, item] of Object.entries(value)) {
|
|
77
|
+
if (keys.has(key)) continue;
|
|
78
|
+
out[key] = AUTHOR_KEYED_FIELDS.has(key) ? item : stripKeys(item, keys);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
/** Drop metadata at the positions the IR defines it, leaving execution semantics. */
|
|
85
|
+
function stripMeta(value) {
|
|
86
|
+
return stripKeys(value, UNHASHED_KEYS);
|
|
87
|
+
}
|
|
88
|
+
/** Drop provenance, keeping the rest of the metadata — what plan compares. */
|
|
89
|
+
function stripProvenance(value) {
|
|
90
|
+
return stripKeys(value, PROVENANCE_KEYS);
|
|
91
|
+
}
|
|
92
|
+
/** Canonical JSON: sorted keys, no whitespace — a stable hash input. */
|
|
93
|
+
function canonicalJSON(value) {
|
|
94
|
+
if (value === void 0) return "null";
|
|
95
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJSON).join(",")}]`;
|
|
96
|
+
if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJSON(value[key])}`).join(",")}}`;
|
|
97
|
+
return JSON.stringify(value);
|
|
98
|
+
}
|
|
99
|
+
const K = new Uint32Array([
|
|
100
|
+
1116352408,
|
|
101
|
+
1899447441,
|
|
102
|
+
3049323471,
|
|
103
|
+
3921009573,
|
|
104
|
+
961987163,
|
|
105
|
+
1508970993,
|
|
106
|
+
2453635748,
|
|
107
|
+
2870763221,
|
|
108
|
+
3624381080,
|
|
109
|
+
310598401,
|
|
110
|
+
607225278,
|
|
111
|
+
1426881987,
|
|
112
|
+
1925078388,
|
|
113
|
+
2162078206,
|
|
114
|
+
2614888103,
|
|
115
|
+
3248222580,
|
|
116
|
+
3835390401,
|
|
117
|
+
4022224774,
|
|
118
|
+
264347078,
|
|
119
|
+
604807628,
|
|
120
|
+
770255983,
|
|
121
|
+
1249150122,
|
|
122
|
+
1555081692,
|
|
123
|
+
1996064986,
|
|
124
|
+
2554220882,
|
|
125
|
+
2821834349,
|
|
126
|
+
2952996808,
|
|
127
|
+
3210313671,
|
|
128
|
+
3336571891,
|
|
129
|
+
3584528711,
|
|
130
|
+
113926993,
|
|
131
|
+
338241895,
|
|
132
|
+
666307205,
|
|
133
|
+
773529912,
|
|
134
|
+
1294757372,
|
|
135
|
+
1396182291,
|
|
136
|
+
1695183700,
|
|
137
|
+
1986661051,
|
|
138
|
+
2177026350,
|
|
139
|
+
2456956037,
|
|
140
|
+
2730485921,
|
|
141
|
+
2820302411,
|
|
142
|
+
3259730800,
|
|
143
|
+
3345764771,
|
|
144
|
+
3516065817,
|
|
145
|
+
3600352804,
|
|
146
|
+
4094571909,
|
|
147
|
+
275423344,
|
|
148
|
+
430227734,
|
|
149
|
+
506948616,
|
|
150
|
+
659060556,
|
|
151
|
+
883997877,
|
|
152
|
+
958139571,
|
|
153
|
+
1322822218,
|
|
154
|
+
1537002063,
|
|
155
|
+
1747873779,
|
|
156
|
+
1955562222,
|
|
157
|
+
2024104815,
|
|
158
|
+
2227730452,
|
|
159
|
+
2361852424,
|
|
160
|
+
2428436474,
|
|
161
|
+
2756734187,
|
|
162
|
+
3204031479,
|
|
163
|
+
3329325298
|
|
164
|
+
]);
|
|
165
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
166
|
+
const utf8 = new TextEncoder();
|
|
167
|
+
/** SHA-256 over the UTF-8 bytes of `input`, as 64 hex chars. */
|
|
168
|
+
function sha256Hex(input) {
|
|
169
|
+
const data = utf8.encode(input);
|
|
170
|
+
const bitLenLo = data.length << 3 >>> 0;
|
|
171
|
+
const bitLenHi = Math.floor(data.length / 536870912);
|
|
172
|
+
const padded = new Uint8Array((data.length + 8 >> 6) + 1 << 6);
|
|
173
|
+
padded.set(data);
|
|
174
|
+
padded[data.length] = 128;
|
|
175
|
+
const view = new DataView(padded.buffer);
|
|
176
|
+
view.setUint32(padded.length - 8, bitLenHi);
|
|
177
|
+
view.setUint32(padded.length - 4, bitLenLo);
|
|
178
|
+
const state = new Uint32Array([
|
|
179
|
+
1779033703,
|
|
180
|
+
3144134277,
|
|
181
|
+
1013904242,
|
|
182
|
+
2773480762,
|
|
183
|
+
1359893119,
|
|
184
|
+
2600822924,
|
|
185
|
+
528734635,
|
|
186
|
+
1541459225
|
|
187
|
+
]);
|
|
188
|
+
const w = /* @__PURE__ */ new Uint32Array(64);
|
|
189
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
190
|
+
for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4);
|
|
191
|
+
for (let i = 16; i < 64; i++) {
|
|
192
|
+
const w15 = w[i - 15];
|
|
193
|
+
const w2 = w[i - 2];
|
|
194
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
|
|
195
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
|
|
196
|
+
w[i] = w[i - 16] + s0 + w[i - 7] + s1 >>> 0;
|
|
197
|
+
}
|
|
198
|
+
let [a, b, c, d, e, f, g, h] = state;
|
|
199
|
+
for (let i = 0; i < 64; i++) {
|
|
200
|
+
const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
201
|
+
const ch = e & f ^ ~e & g;
|
|
202
|
+
const t1 = h + s1 + ch + K[i] + w[i] >>> 0;
|
|
203
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) >>> 0;
|
|
204
|
+
h = g;
|
|
205
|
+
g = f;
|
|
206
|
+
f = e;
|
|
207
|
+
e = d + t1 >>> 0;
|
|
208
|
+
d = c;
|
|
209
|
+
c = b;
|
|
210
|
+
b = a;
|
|
211
|
+
a = t1 + t2 >>> 0;
|
|
212
|
+
}
|
|
213
|
+
state[0] = state[0] + a >>> 0;
|
|
214
|
+
state[1] = state[1] + b >>> 0;
|
|
215
|
+
state[2] = state[2] + c >>> 0;
|
|
216
|
+
state[3] = state[3] + d >>> 0;
|
|
217
|
+
state[4] = state[4] + e >>> 0;
|
|
218
|
+
state[5] = state[5] + f >>> 0;
|
|
219
|
+
state[6] = state[6] + g >>> 0;
|
|
220
|
+
state[7] = state[7] + h >>> 0;
|
|
221
|
+
}
|
|
222
|
+
return [...state].map((word) => word.toString(16).padStart(8, "0")).join("");
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Truncate to 128 bits (32 hex chars): collision-safe for content addressing at
|
|
226
|
+
* any realistic definition count, and short enough for KV keys and instance ids.
|
|
227
|
+
*/
|
|
228
|
+
const contentHashOf = (input) => sha256Hex(input).slice(0, 32);
|
|
229
|
+
/** Content hash of execution semantics (strip metadata → canonical JSON → truncated SHA-256). */
|
|
230
|
+
function semanticHash(value) {
|
|
231
|
+
return contentHashOf(canonicalJSON(stripMeta(value)));
|
|
232
|
+
}
|
|
233
|
+
/** Canonical hash of the full content — used to tell "semantics unchanged, metadata edited" apart. */
|
|
234
|
+
function fullHash(value) {
|
|
235
|
+
return contentHashOf(canonicalJSON(value));
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
exports.canonicalJSON = canonicalJSON;
|
|
239
|
+
exports.fullHash = fullHash;
|
|
240
|
+
exports.semanticHash = semanticHash;
|
|
241
|
+
exports.sha256Hex = sha256Hex;
|
|
242
|
+
exports.stripMeta = stripMeta;
|
|
243
|
+
exports.stripProvenance = stripProvenance;
|
|
244
|
+
|
|
245
|
+
//# sourceMappingURL=hash.cjs.map
|