@shware/workflow 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -29
- package/dist/apns.cjs +113 -26
- package/dist/apns.cjs.map +1 -1
- package/dist/apns.d.cts +52 -6
- package/dist/apns.d.cts.map +1 -1
- package/dist/apns.d.mts +52 -6
- package/dist/apns.d.mts.map +1 -1
- package/dist/apns.mjs +111 -27
- package/dist/apns.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +5 -5
- package/dist/aws/engine-step.cjs.map +1 -1
- package/dist/aws/engine-step.d.cts +8 -2
- package/dist/aws/engine-step.d.cts.map +1 -1
- package/dist/aws/engine-step.d.mts +8 -2
- package/dist/aws/engine-step.d.mts.map +1 -1
- package/dist/aws/engine-step.mjs +5 -5
- package/dist/aws/engine-step.mjs.map +1 -1
- package/dist/aws/index.cjs +47 -6
- package/dist/aws/index.cjs.map +1 -1
- package/dist/aws/index.d.cts +2 -2
- package/dist/aws/index.d.cts.map +1 -1
- package/dist/aws/index.d.mts +2 -2
- package/dist/aws/index.d.mts.map +1 -1
- package/dist/aws/index.mjs +47 -6
- package/dist/aws/index.mjs.map +1 -1
- package/dist/aws/instances.cjs +29 -6
- package/dist/aws/instances.cjs.map +1 -1
- package/dist/aws/instances.d.cts +11 -1
- package/dist/aws/instances.d.cts.map +1 -1
- package/dist/aws/instances.d.mts +11 -1
- package/dist/aws/instances.d.mts.map +1 -1
- package/dist/aws/instances.mjs +27 -4
- package/dist/aws/instances.mjs.map +1 -1
- package/dist/client.cjs +4 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -2
- package/dist/client.d.cts.map +1 -1
- package/dist/client.d.mts +2 -2
- package/dist/client.d.mts.map +1 -1
- package/dist/client.mjs +4 -1
- package/dist/client.mjs.map +1 -1
- package/dist/cloudflare/bindings.cjs +28 -25
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +17 -129
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +17 -129
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +28 -23
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/d1-env.cjs +6 -6
- package/dist/cloudflare/d1-env.cjs.map +1 -1
- package/dist/cloudflare/d1-env.d.cts +5 -5
- package/dist/cloudflare/d1-env.d.mts +5 -5
- package/dist/cloudflare/d1-env.mjs +6 -6
- package/dist/cloudflare/d1-env.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +28 -18
- package/dist/cloudflare/index.d.cts +8 -4
- package/dist/cloudflare/index.d.mts +8 -4
- package/dist/cloudflare/index.mjs +9 -5
- package/dist/cloudflare/runner.cjs +29 -14
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +9 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +9 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +28 -13
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/schema.sql +27 -6
- package/dist/cloudflare/worker.cjs +19 -9
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +2 -2
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +2 -2
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +19 -9
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +21 -5
- package/dist/config.cjs.map +1 -1
- package/dist/config.mjs +21 -5
- package/dist/config.mjs.map +1 -1
- package/dist/dsl/action.cjs +3 -6
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +12 -11
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +12 -11
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs +3 -6
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs +35 -31
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts.map +1 -1
- package/dist/dsl/bundle.d.mts.map +1 -1
- package/dist/dsl/bundle.mjs +35 -31
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/condition.cjs +13 -1
- package/dist/dsl/condition.cjs.map +1 -1
- package/dist/dsl/condition.d.cts +9 -1
- package/dist/dsl/condition.d.cts.map +1 -1
- package/dist/dsl/condition.d.mts +9 -1
- package/dist/dsl/condition.d.mts.map +1 -1
- package/dist/dsl/condition.mjs +13 -2
- package/dist/dsl/condition.mjs.map +1 -1
- package/dist/dsl/flow.cjs +33 -11
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +9 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +9 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs +34 -13
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/workflow.cjs +2 -3
- package/dist/dsl/workflow.cjs.map +1 -1
- package/dist/dsl/workflow.d.cts.map +1 -1
- package/dist/dsl/workflow.d.mts.map +1 -1
- package/dist/dsl/workflow.mjs +3 -4
- package/dist/dsl/workflow.mjs.map +1 -1
- package/dist/email-sender.cjs +24 -0
- package/dist/email-sender.cjs.map +1 -1
- package/dist/email-sender.d.cts +18 -0
- package/dist/email-sender.d.cts.map +1 -1
- package/dist/email-sender.d.mts +18 -0
- package/dist/email-sender.d.mts.map +1 -1
- package/dist/email-sender.mjs +24 -1
- package/dist/email-sender.mjs.map +1 -1
- package/dist/engine/actions.cjs +1 -8
- package/dist/engine/actions.cjs.map +1 -1
- package/dist/engine/actions.d.cts +1 -12
- package/dist/engine/actions.d.cts.map +1 -1
- package/dist/engine/actions.d.mts +1 -12
- package/dist/engine/actions.d.mts.map +1 -1
- package/dist/engine/actions.mjs +1 -8
- package/dist/engine/actions.mjs.map +1 -1
- package/dist/engine/condition.cjs +25 -5
- package/dist/engine/condition.cjs.map +1 -1
- package/dist/engine/condition.d.cts +12 -3
- package/dist/engine/condition.d.cts.map +1 -1
- package/dist/engine/condition.d.mts +12 -3
- package/dist/engine/condition.d.mts.map +1 -1
- package/dist/engine/condition.mjs +25 -6
- package/dist/engine/condition.mjs.map +1 -1
- package/dist/engine/index.cjs +3 -0
- package/dist/engine/index.d.cts +5 -5
- package/dist/engine/index.d.mts +5 -5
- package/dist/engine/index.mjs +4 -4
- package/dist/engine/interpreter.cjs +27 -20
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +27 -20
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +21 -6
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +21 -6
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/engine/retry.cjs +13 -0
- package/dist/engine/retry.cjs.map +1 -1
- package/dist/engine/retry.d.cts +12 -0
- package/dist/engine/retry.d.cts.map +1 -1
- package/dist/engine/retry.d.mts +12 -0
- package/dist/engine/retry.d.mts.map +1 -1
- package/dist/engine/retry.mjs +13 -1
- package/dist/engine/retry.mjs.map +1 -1
- package/dist/engine/time-scale.cjs +90 -15
- package/dist/engine/time-scale.cjs.map +1 -1
- package/dist/engine/time-scale.d.cts +9 -6
- package/dist/engine/time-scale.d.cts.map +1 -1
- package/dist/engine/time-scale.d.mts +9 -6
- package/dist/engine/time-scale.d.mts.map +1 -1
- package/dist/engine/time-scale.mjs +90 -16
- package/dist/engine/time-scale.mjs.map +1 -1
- package/dist/fcm.cjs +111 -38
- package/dist/fcm.cjs.map +1 -1
- package/dist/fcm.d.cts +45 -5
- package/dist/fcm.d.cts.map +1 -1
- package/dist/fcm.d.mts +45 -5
- package/dist/fcm.d.mts.map +1 -1
- package/dist/fcm.mjs +110 -39
- package/dist/fcm.mjs.map +1 -1
- package/dist/hash.cjs +6 -6
- package/dist/hash.cjs.map +1 -1
- package/dist/hash.mjs +6 -6
- package/dist/hash.mjs.map +1 -1
- package/dist/host/auth.cjs +49 -0
- package/dist/host/auth.cjs.map +1 -0
- package/dist/host/auth.d.cts +21 -0
- package/dist/host/auth.d.cts.map +1 -0
- package/dist/host/auth.d.mts +21 -0
- package/dist/host/auth.d.mts.map +1 -0
- package/dist/host/auth.mjs +47 -0
- package/dist/host/auth.mjs.map +1 -0
- package/dist/host/channels.cjs +118 -40
- package/dist/host/channels.cjs.map +1 -1
- package/dist/host/channels.d.cts +27 -1
- package/dist/host/channels.d.cts.map +1 -1
- package/dist/host/channels.d.mts +27 -1
- package/dist/host/channels.d.mts.map +1 -1
- package/dist/host/channels.mjs +121 -44
- package/dist/host/channels.mjs.map +1 -1
- package/dist/host/execute.cjs +14 -2
- package/dist/host/execute.cjs.map +1 -1
- package/dist/host/execute.d.cts +4 -4
- package/dist/host/execute.d.cts.map +1 -1
- package/dist/host/execute.d.mts +4 -4
- package/dist/host/execute.d.mts.map +1 -1
- package/dist/host/execute.mjs +13 -1
- package/dist/host/execute.mjs.map +1 -1
- package/dist/host/fresh-read.cjs +30 -0
- package/dist/host/fresh-read.cjs.map +1 -0
- package/dist/host/fresh-read.d.cts +18 -0
- package/dist/host/fresh-read.d.cts.map +1 -0
- package/dist/host/fresh-read.d.mts +18 -0
- package/dist/host/fresh-read.d.mts.map +1 -0
- package/dist/host/fresh-read.mjs +29 -0
- package/dist/host/fresh-read.mjs.map +1 -0
- package/dist/host/http.cjs +321 -0
- package/dist/host/http.cjs.map +1 -0
- package/dist/host/http.d.cts +54 -0
- package/dist/host/http.d.cts.map +1 -0
- package/dist/host/http.d.mts +54 -0
- package/dist/host/http.d.mts.map +1 -0
- package/dist/host/http.mjs +317 -0
- package/dist/host/http.mjs.map +1 -0
- package/dist/host/{router.cjs → ingest.cjs} +147 -175
- package/dist/host/ingest.cjs.map +1 -0
- package/dist/host/{router.d.cts → ingest.d.cts} +48 -30
- package/dist/host/ingest.d.cts.map +1 -0
- package/dist/host/{router.d.mts → ingest.d.mts} +48 -30
- package/dist/host/ingest.d.mts.map +1 -0
- package/dist/host/{router.mjs → ingest.mjs} +144 -173
- package/dist/host/ingest.mjs.map +1 -0
- package/dist/host/journey-host.cjs +65 -93
- package/dist/host/journey-host.cjs.map +1 -1
- package/dist/host/journey-host.d.cts +19 -26
- package/dist/host/journey-host.d.cts.map +1 -1
- package/dist/host/journey-host.d.mts +19 -26
- package/dist/host/journey-host.d.mts.map +1 -1
- package/dist/host/journey-host.mjs +65 -93
- package/dist/host/journey-host.mjs.map +1 -1
- package/dist/host/platform.cjs +21 -0
- package/dist/host/platform.cjs.map +1 -0
- package/dist/host/platform.d.cts +126 -0
- package/dist/host/platform.d.cts.map +1 -0
- package/dist/host/platform.d.mts +126 -0
- package/dist/host/platform.d.mts.map +1 -0
- package/dist/host/platform.mjs +19 -0
- package/dist/host/platform.mjs.map +1 -0
- package/dist/host/senders.cjs +18 -9
- package/dist/host/senders.cjs.map +1 -1
- package/dist/host/senders.d.cts +9 -4
- package/dist/host/senders.d.cts.map +1 -1
- package/dist/host/senders.d.mts +9 -4
- package/dist/host/senders.d.mts.map +1 -1
- package/dist/host/senders.mjs +18 -9
- package/dist/host/senders.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/ir.cjs +5 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -12
- package/dist/ir.d.cts.map +1 -1
- package/dist/ir.d.mts +3 -12
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +5 -7
- package/dist/ir.mjs.map +1 -1
- package/dist/plan.cjs +27 -33
- package/dist/plan.cjs.map +1 -1
- package/dist/plan.d.cts +1 -1
- package/dist/plan.d.cts.map +1 -1
- package/dist/plan.d.mts +1 -1
- package/dist/plan.d.mts.map +1 -1
- package/dist/plan.mjs +27 -33
- package/dist/plan.mjs.map +1 -1
- package/dist/reporting/observer.cjs +1 -0
- package/dist/reporting/observer.cjs.map +1 -1
- package/dist/reporting/observer.mjs +1 -0
- package/dist/reporting/observer.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +19 -4
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts +13 -11
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts +13 -11
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +19 -4
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +88 -29
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts +29 -11
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts +29 -11
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +88 -29
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/erasure.cjs +22 -0
- package/dist/store/erasure.cjs.map +1 -0
- package/dist/store/erasure.d.cts +18 -0
- package/dist/store/erasure.d.cts.map +1 -0
- package/dist/store/erasure.d.mts +18 -0
- package/dist/store/erasure.d.mts.map +1 -0
- package/dist/store/erasure.mjs +21 -0
- package/dist/store/erasure.mjs.map +1 -0
- package/dist/store/index.cjs +2 -0
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +67 -13
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +67 -13
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs +2 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +125 -62
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.d.cts +30 -2
- package/dist/store/postgres-reporting.d.cts.map +1 -1
- package/dist/store/postgres-reporting.d.mts +30 -2
- package/dist/store/postgres-reporting.d.mts.map +1 -1
- package/dist/store/postgres-reporting.mjs +125 -62
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +179 -60
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +44 -7
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +44 -7
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +179 -60
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +7 -5
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +7 -5
- package/dist/store/tables.mjs.map +1 -1
- package/dist/store/user-lock.cjs +43 -0
- package/dist/store/user-lock.cjs.map +1 -0
- package/dist/store/user-lock.d.cts +32 -0
- package/dist/store/user-lock.d.cts.map +1 -0
- package/dist/store/user-lock.d.mts +32 -0
- package/dist/store/user-lock.d.mts.map +1 -0
- package/dist/store/user-lock.mjs +40 -0
- package/dist/store/user-lock.mjs.map +1 -0
- package/dist/transports/cloudflare-email.cjs +3 -2
- package/dist/transports/cloudflare-email.cjs.map +1 -1
- package/dist/transports/cloudflare-email.mjs +3 -2
- package/dist/transports/cloudflare-email.mjs.map +1 -1
- package/dist/transports/ses.cjs +52 -7
- package/dist/transports/ses.cjs.map +1 -1
- package/dist/transports/ses.d.cts +9 -1
- package/dist/transports/ses.d.cts.map +1 -1
- package/dist/transports/ses.d.mts +9 -1
- package/dist/transports/ses.d.mts.map +1 -1
- package/dist/transports/ses.mjs +52 -8
- package/dist/transports/ses.mjs.map +1 -1
- package/dist/webpush.cjs +133 -47
- package/dist/webpush.cjs.map +1 -1
- package/dist/webpush.d.cts +49 -8
- package/dist/webpush.d.cts.map +1 -1
- package/dist/webpush.d.mts +49 -8
- package/dist/webpush.d.mts.map +1 -1
- package/dist/webpush.mjs +130 -47
- package/dist/webpush.mjs.map +1 -1
- package/package.json +1 -1
- package/src/cloudflare/schema.sql +27 -6
- package/dist/host/router.cjs.map +0 -1
- package/dist/host/router.d.cts.map +0 -1
- package/dist/host/router.d.mts.map +0 -1
- package/dist/host/router.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ses.d.mts","names":[],"sources":["../../src/transports/ses.ts"],"mappings":";;;;;;;iBAWiB;EACf;EACA;;EAEA;;;YAIU,8BAA8B,iBAAiB,QAAQ;iBAElD;EACf;;;;;;EAMA,cAAc,iBAAiB;;EAE/B;;EAEA;EACA,eAAe;;;
|
|
1
|
+
{"version":3,"file":"ses.d.mts","names":[],"sources":["../../src/transports/ses.ts"],"mappings":";;;;;;;iBAWiB;EACf;EACA;;EAEA;;;YAIU,8BAA8B,iBAAiB,QAAQ;iBAElD;EACf;;;;;;EAMA,cAAc,iBAAiB;;EAE/B;;EAEA;EACA,eAAe;;;qBA6BJ,qBAAqB;WAQrB;WACA;WARO;;;;;WAKT;EACT,YACW,gBACA,0BACT;;;;;;wBAWY,aAAa,SAAS,SAAS;wBAyB/B,kBAAkB,SAAS,2BAA2B"}
|
package/dist/transports/ses.mjs
CHANGED
|
@@ -1,21 +1,61 @@
|
|
|
1
|
-
import { mimeAddress } from "../email.mjs";
|
|
2
1
|
import { isRetryableStatus } from "../engine/retry.mjs";
|
|
2
|
+
import { mimeAddress } from "../email.mjs";
|
|
3
3
|
import { signRequest } from "./sigv4.mjs";
|
|
4
4
|
//#region src/transports/ses.ts
|
|
5
|
+
/**
|
|
6
|
+
* Error codes that mean "later", whatever the status line says. SES v2 answers its sending-rate
|
|
7
|
+
* limit with 429 `TooManyRequestsException`
|
|
8
|
+
* (https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html#API_SendEmail_Errors),
|
|
9
|
+
* but the AWS JSON protocols also carry throttling as a 400 (`Throttling`, `ThrottlingException`,
|
|
10
|
+
* `LimitExceededException`, …) — the set the AWS SDK itself retries on
|
|
11
|
+
* (https://github.com/aws/aws-sdk-js-v3/blob/main/packages/service-error-classification/src/constants.ts).
|
|
12
|
+
* `SendingPausedException`, `AccountSuspendedException`, `MessageRejected` and
|
|
13
|
+
* `MailFromDomainNotVerifiedException` are the account or the message: not here.
|
|
14
|
+
*/
|
|
15
|
+
const RETRYABLE_TYPES = /* @__PURE__ */ new Set([
|
|
16
|
+
"Throttling",
|
|
17
|
+
"ThrottlingException",
|
|
18
|
+
"ThrottledException",
|
|
19
|
+
"RequestThrottledException",
|
|
20
|
+
"RequestThrottled",
|
|
21
|
+
"TooManyRequestsException",
|
|
22
|
+
"LimitExceededException",
|
|
23
|
+
"RequestLimitExceeded",
|
|
24
|
+
"SlowDown",
|
|
25
|
+
"ServiceUnavailableException",
|
|
26
|
+
"InternalFailure",
|
|
27
|
+
"InternalServerError"
|
|
28
|
+
]);
|
|
5
29
|
/** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */
|
|
6
30
|
var SesSendError = class extends Error {
|
|
7
31
|
status;
|
|
8
32
|
type;
|
|
9
33
|
name = "SesSendError";
|
|
10
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A rejected request (an unverified sender, a bad address) fails the same way every time; a
|
|
36
|
+
* throttled one is worth another attempt even when it came as a 400.
|
|
37
|
+
*/
|
|
11
38
|
retryable;
|
|
12
39
|
constructor(status, type, message) {
|
|
13
40
|
super(`SES SendEmail failed (${status}${type === void 0 ? "" : ` ${type}`}): ${message}`);
|
|
14
41
|
this.status = status;
|
|
15
42
|
this.type = type;
|
|
16
|
-
this.retryable = isRetryableStatus(status);
|
|
43
|
+
this.retryable = isRetryableStatus(status) || type !== void 0 && RETRYABLE_TYPES.has(type);
|
|
17
44
|
}
|
|
18
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* The error code, from the `x-amzn-ErrorType` header (`Code:http://…`) or, when the header is
|
|
48
|
+
* missing, the body's `__type` (`prefix#Code` under the JSON protocols) or `code`.
|
|
49
|
+
*/
|
|
50
|
+
function sesErrorType(headers, body) {
|
|
51
|
+
const header = headers.get("x-amzn-errortype")?.split(":")[0]?.trim();
|
|
52
|
+
if (header) return header;
|
|
53
|
+
if (typeof body !== "object" || body === null) return void 0;
|
|
54
|
+
const typed = body;
|
|
55
|
+
const raw = typed.__type ?? typed.code ?? typed.Code;
|
|
56
|
+
if (typeof raw !== "string" || raw === "") return void 0;
|
|
57
|
+
return raw.slice(raw.lastIndexOf("#") + 1).split(":")[0];
|
|
58
|
+
}
|
|
19
59
|
function environmentCredentials() {
|
|
20
60
|
const env = globalThis.process?.env;
|
|
21
61
|
const accessKeyId = env?.AWS_ACCESS_KEY_ID;
|
|
@@ -48,7 +88,11 @@ function sesEmailTransport(options) {
|
|
|
48
88
|
Body: { Html: {
|
|
49
89
|
Data: message.html,
|
|
50
90
|
Charset: "UTF-8"
|
|
51
|
-
} }
|
|
91
|
+
} },
|
|
92
|
+
...message.headers === void 0 ? {} : { Headers: Object.entries(message.headers).map(([Name, Value]) => ({
|
|
93
|
+
Name,
|
|
94
|
+
Value
|
|
95
|
+
})) }
|
|
52
96
|
} },
|
|
53
97
|
...options.configurationSetName === void 0 ? {} : { ConfigurationSetName: options.configurationSetName }
|
|
54
98
|
});
|
|
@@ -70,13 +114,13 @@ function sesEmailTransport(options) {
|
|
|
70
114
|
const text = await response.text();
|
|
71
115
|
if (!response.ok) {
|
|
72
116
|
let detail = text;
|
|
117
|
+
let parsed;
|
|
73
118
|
try {
|
|
74
|
-
|
|
119
|
+
parsed = JSON.parse(text);
|
|
75
120
|
const said = parsed.message ?? parsed.Message;
|
|
76
121
|
if (typeof said === "string") detail = said;
|
|
77
122
|
} catch {}
|
|
78
|
-
|
|
79
|
-
throw new SesSendError(response.status, type, detail);
|
|
123
|
+
throw new SesSendError(response.status, sesErrorType(response.headers, parsed), detail);
|
|
80
124
|
}
|
|
81
125
|
let messageId;
|
|
82
126
|
try {
|
|
@@ -88,6 +132,6 @@ function sesEmailTransport(options) {
|
|
|
88
132
|
} };
|
|
89
133
|
}
|
|
90
134
|
//#endregion
|
|
91
|
-
export { SesSendError, sesEmailTransport };
|
|
135
|
+
export { SesSendError, sesEmailTransport, sesErrorType };
|
|
92
136
|
|
|
93
137
|
//# sourceMappingURL=ses.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ses.mjs","names":[],"sources":["../../src/transports/ses.ts"],"sourcesContent":["import { mimeAddress } from '../email';\nimport type { EmailMessage, EmailTransport } from '../email-sender';\nimport { isRetryableStatus } from '../engine/retry';\nimport { signRequest } from './sigv4';\n\n/**\n * `@shware/workflow/transports/ses` — email through Amazon SES (the v2 `SendEmail` API) over\n * plain `fetch` with a SigV4 signature, so the same transport runs in a Lambda and in a Worker.\n * Nothing here loads the AWS SDK or a Node-only credential chain.\n */\n\nexport interface AwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n /** Present for temporary credentials (a role's): signed with the request. */\n sessionToken?: string | undefined;\n}\n\n/** Called on every send, so rotated temporary credentials are picked up rather than frozen. */\nexport type AwsCredentialProvider = () => AwsCredentials | Promise<AwsCredentials>;\n\nexport interface SesEmailTransportOptions {\n region: string;\n /**\n * Fixed credentials, or a provider read on every send. Defaults to the process environment's\n * `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, read at send time —\n * a Lambda's execution role. A Worker passes them explicitly from its validated env.\n */\n credentials?: AwsCredentials | AwsCredentialProvider;\n /** An SES configuration set (event publishing, a dedicated IP pool). */\n configurationSetName?: string;\n /** Defaults to `https://email.<region>.amazonaws.com`. */\n endpoint?: string;\n fetch?: typeof fetch;\n}\n\n/** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */\nexport class SesSendError extends Error {\n override readonly name = 'SesSendError';\n /** A rejected request (an unverified sender, a bad address) fails the same way every time. */\n readonly retryable: boolean;\n constructor(\n readonly status: number,\n readonly type: string | undefined,\n message: string\n ) {\n super(`SES SendEmail failed (${status}${type === undefined ? '' : ` ${type}`}): ${message}`);\n this.retryable = isRetryableStatus(status);\n }\n}\n\nfunction environmentCredentials(): AwsCredentials {\n // Read through globalThis: a Worker has no `process`, and must pass credentials instead\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n const accessKeyId = env?.AWS_ACCESS_KEY_ID;\n const secretAccessKey = env?.AWS_SECRET_ACCESS_KEY;\n if (accessKeyId === undefined || secretAccessKey === undefined) {\n throw new Error(\n 'sesEmailTransport: no credentials — pass `credentials`, or run where AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set (a Lambda execution role)'\n );\n }\n return { accessKeyId, secretAccessKey, sessionToken: env?.AWS_SESSION_TOKEN };\n}\n\nexport function sesEmailTransport(options: SesEmailTransportOptions): EmailTransport {\n const endpoint = (options.endpoint ?? `https://email.${options.region}.amazonaws.com`).replace(\n /\\/$/,\n ''\n );\n const credentials = async (): Promise<AwsCredentials> => {\n const configured = options.credentials;\n if (configured === undefined) return environmentCredentials();\n return typeof configured === 'function' ? configured() : configured;\n };\n const fetchImpl = options.fetch ?? fetch;\n\n return {\n async send(message: EmailMessage) {\n const body = JSON.stringify({\n FromEmailAddress: mimeAddress(message.from),\n Destination: { ToAddresses: [message.to] },\n ...(message.replyTo === undefined\n ? {}\n : { ReplyToAddresses: [mimeAddress(message.replyTo)] }),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: 'UTF-8' },\n Body: { Html: { Data: message.html, Charset: 'UTF-8' } },\n },\n },\n ...(options.configurationSetName === undefined\n ? {}\n : { ConfigurationSetName: options.configurationSetName }),\n });\n const url = `${endpoint}/v2/email/outbound-emails`;\n const headers = await signRequest({\n method: 'POST',\n url,\n headers: { 'content-type': 'application/json' },\n body,\n credentials: await credentials(),\n region: options.region,\n service: 'ses',\n });\n // No retries here: the durable step owns them, and counts them — SES cannot de-duplicate,\n // so every extra attempt of an accepted send is another chance to deliver twice\n const response = await fetchImpl(url, { method: 'POST', headers, body });\n const text = await response.text();\n if (!response.ok) {\n let detail = text;\n try {\n const parsed = JSON.parse(text) as { message?: unknown; Message?: unknown };\n const said = parsed.message ?? parsed.Message;\n if (typeof said === 'string') detail = said;\n } catch {\n // Not JSON: the raw body says what went wrong\n }\n const type = response.headers.get('x-amzn-errortype')?.split(':')[0];\n throw new SesSendError(response.status, type, detail);\n }\n let messageId: unknown;\n try {\n messageId = (JSON.parse(text) as { MessageId?: unknown }).MessageId;\n } catch {\n messageId = undefined;\n }\n return typeof messageId === 'string' ? { providerMessageId: messageId } : {};\n },\n };\n}\n"],"mappings":";;;;;AAqCA,IAAa,eAAb,cAAkC,MAAM;CAK3B;CACA;CALX,OAAyB;;CAEzB;CACA,YACE,QACA,MACA,SACA;EACA,MAAM,yBAAyB,SAAS,SAAS,KAAA,IAAY,KAAK,IAAI,OAAO,KAAK,SAAS;EAJlF,KAAA,SAAA;EACA,KAAA,OAAA;EAIT,KAAK,YAAY,kBAAkB,MAAM;CAC3C;AACF;AAEA,SAAS,yBAAyC;CAEhD,MAAM,MAAO,WAA0E,SACnF;CACJ,MAAM,cAAc,KAAK;CACzB,MAAM,kBAAkB,KAAK;CAC7B,IAAI,gBAAgB,KAAA,KAAa,oBAAoB,KAAA,GACnD,MAAM,IAAI,MACR,oJACF;CAEF,OAAO;EAAE;EAAa;EAAiB,cAAc,KAAK;CAAkB;AAC9E;AAEA,SAAgB,kBAAkB,SAAmD;CACnF,MAAM,YAAY,QAAQ,YAAY,iBAAiB,QAAQ,OAAO,gBAAA,CAAiB,QACrF,OACA,EACF;CACA,MAAM,cAAc,YAAqC;EACvD,MAAM,aAAa,QAAQ;EAC3B,IAAI,eAAe,KAAA,GAAW,OAAO,uBAAuB;EAC5D,OAAO,OAAO,eAAe,aAAa,WAAW,IAAI;CAC3D;CACA,MAAM,YAAY,QAAQ,SAAS;CAEnC,OAAO,EACL,MAAM,KAAK,SAAuB;EAChC,MAAM,OAAO,KAAK,UAAU;GAC1B,kBAAkB,YAAY,QAAQ,IAAI;GAC1C,aAAa,EAAE,aAAa,CAAC,QAAQ,EAAE,EAAE;GACzC,GAAI,QAAQ,YAAY,KAAA,IACpB,CAAC,IACD,EAAE,kBAAkB,CAAC,YAAY,QAAQ,OAAO,CAAC,EAAE;GACvD,SAAS,EACP,QAAQ;IACN,SAAS;KAAE,MAAM,QAAQ;KAAS,SAAS;IAAQ;IACnD,MAAM,EAAE,MAAM;KAAE,MAAM,QAAQ;KAAM,SAAS;IAAQ,EAAE;GACzD,EACF;GACA,GAAI,QAAQ,yBAAyB,KAAA,IACjC,CAAC,IACD,EAAE,sBAAsB,QAAQ,qBAAqB;EAC3D,CAAC;EACD,MAAM,MAAM,GAAG,SAAS;EACxB,MAAM,UAAU,MAAM,YAAY;GAChC,QAAQ;GACR;GACA,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C;GACA,aAAa,MAAM,YAAY;GAC/B,QAAQ,QAAQ;GAChB,SAAS;EACX,CAAC;EAGD,MAAM,WAAW,MAAM,UAAU,KAAK;GAAE,QAAQ;GAAQ;GAAS;EAAK,CAAC;EACvE,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,IAAI,CAAC,SAAS,IAAI;GAChB,IAAI,SAAS;GACb,IAAI;IACF,MAAM,SAAS,KAAK,MAAM,IAAI;IAC9B,MAAM,OAAO,OAAO,WAAW,OAAO;IACtC,IAAI,OAAO,SAAS,UAAU,SAAS;GACzC,QAAQ,CAER;GACA,MAAM,OAAO,SAAS,QAAQ,IAAI,kBAAkB,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;GAClE,MAAM,IAAI,aAAa,SAAS,QAAQ,MAAM,MAAM;EACtD;EACA,IAAI;EACJ,IAAI;GACF,YAAa,KAAK,MAAM,IAAI,CAAC,CAA6B;EAC5D,QAAQ;GACN,YAAY,KAAA;EACd;EACA,OAAO,OAAO,cAAc,WAAW,EAAE,mBAAmB,UAAU,IAAI,CAAC;CAC7E,EACF;AACF"}
|
|
1
|
+
{"version":3,"file":"ses.mjs","names":[],"sources":["../../src/transports/ses.ts"],"sourcesContent":["import { mimeAddress } from '../email';\nimport type { EmailMessage, EmailTransport } from '../email-sender';\nimport { isRetryableStatus } from '../engine/retry';\nimport { signRequest } from './sigv4';\n\n/**\n * `@shware/workflow/transports/ses` — email through Amazon SES (the v2 `SendEmail` API) over\n * plain `fetch` with a SigV4 signature, so the same transport runs in a Lambda and in a Worker.\n * Nothing here loads the AWS SDK or a Node-only credential chain.\n */\n\nexport interface AwsCredentials {\n accessKeyId: string;\n secretAccessKey: string;\n /** Present for temporary credentials (a role's): signed with the request. */\n sessionToken?: string | undefined;\n}\n\n/** Called on every send, so rotated temporary credentials are picked up rather than frozen. */\nexport type AwsCredentialProvider = () => AwsCredentials | Promise<AwsCredentials>;\n\nexport interface SesEmailTransportOptions {\n region: string;\n /**\n * Fixed credentials, or a provider read on every send. Defaults to the process environment's\n * `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN`, read at send time —\n * a Lambda's execution role. A Worker passes them explicitly from its validated env.\n */\n credentials?: AwsCredentials | AwsCredentialProvider;\n /** An SES configuration set (event publishing, a dedicated IP pool). */\n configurationSetName?: string;\n /** Defaults to `https://email.<region>.amazonaws.com`. */\n endpoint?: string;\n fetch?: typeof fetch;\n}\n\n/**\n * Error codes that mean \"later\", whatever the status line says. SES v2 answers its sending-rate\n * limit with 429 `TooManyRequestsException`\n * (https://docs.aws.amazon.com/ses/latest/APIReference-V2/API_SendEmail.html#API_SendEmail_Errors),\n * but the AWS JSON protocols also carry throttling as a 400 (`Throttling`, `ThrottlingException`,\n * `LimitExceededException`, …) — the set the AWS SDK itself retries on\n * (https://github.com/aws/aws-sdk-js-v3/blob/main/packages/service-error-classification/src/constants.ts).\n * `SendingPausedException`, `AccountSuspendedException`, `MessageRejected` and\n * `MailFromDomainNotVerifiedException` are the account or the message: not here.\n */\nconst RETRYABLE_TYPES: ReadonlySet<string> = new Set([\n 'Throttling',\n 'ThrottlingException',\n 'ThrottledException',\n 'RequestThrottledException',\n 'RequestThrottled',\n 'TooManyRequestsException',\n 'LimitExceededException',\n 'RequestLimitExceeded',\n 'SlowDown',\n 'ServiceUnavailableException',\n 'InternalFailure',\n 'InternalServerError',\n]);\n\n/** An SES error response: `type` is SES's error code (`MessageRejected`, `TooManyRequestsException`, ...). */\nexport class SesSendError extends Error {\n override readonly name = 'SesSendError';\n /**\n * A rejected request (an unverified sender, a bad address) fails the same way every time; a\n * throttled one is worth another attempt even when it came as a 400.\n */\n readonly retryable: boolean;\n constructor(\n readonly status: number,\n readonly type: string | undefined,\n message: string\n ) {\n super(`SES SendEmail failed (${status}${type === undefined ? '' : ` ${type}`}): ${message}`);\n this.retryable = isRetryableStatus(status) || (type !== undefined && RETRYABLE_TYPES.has(type));\n }\n}\n\n/**\n * The error code, from the `x-amzn-ErrorType` header (`Code:http://…`) or, when the header is\n * missing, the body's `__type` (`prefix#Code` under the JSON protocols) or `code`.\n */\nexport function sesErrorType(headers: Headers, body: unknown): string | undefined {\n const header = headers.get('x-amzn-errortype')?.split(':')[0]?.trim();\n if (header) return header;\n if (typeof body !== 'object' || body === null) return undefined;\n const typed = body as { __type?: unknown; code?: unknown; Code?: unknown };\n const raw = typed.__type ?? typed.code ?? typed.Code;\n if (typeof raw !== 'string' || raw === '') return undefined;\n // `com.amazonaws.ses#TooManyRequestsException` → `TooManyRequestsException`\n return raw.slice(raw.lastIndexOf('#') + 1).split(':')[0];\n}\n\nfunction environmentCredentials(): AwsCredentials {\n // Read through globalThis: a Worker has no `process`, and must pass credentials instead\n const env = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process\n ?.env;\n const accessKeyId = env?.AWS_ACCESS_KEY_ID;\n const secretAccessKey = env?.AWS_SECRET_ACCESS_KEY;\n if (accessKeyId === undefined || secretAccessKey === undefined) {\n throw new Error(\n 'sesEmailTransport: no credentials — pass `credentials`, or run where AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set (a Lambda execution role)'\n );\n }\n return { accessKeyId, secretAccessKey, sessionToken: env?.AWS_SESSION_TOKEN };\n}\n\nexport function sesEmailTransport(options: SesEmailTransportOptions): EmailTransport {\n const endpoint = (options.endpoint ?? `https://email.${options.region}.amazonaws.com`).replace(\n /\\/$/,\n ''\n );\n const credentials = async (): Promise<AwsCredentials> => {\n const configured = options.credentials;\n if (configured === undefined) return environmentCredentials();\n return typeof configured === 'function' ? configured() : configured;\n };\n const fetchImpl = options.fetch ?? fetch;\n\n return {\n async send(message: EmailMessage) {\n const body = JSON.stringify({\n FromEmailAddress: mimeAddress(message.from),\n Destination: { ToAddresses: [message.to] },\n ...(message.replyTo === undefined\n ? {}\n : { ReplyToAddresses: [mimeAddress(message.replyTo)] }),\n Content: {\n Simple: {\n Subject: { Data: message.subject, Charset: 'UTF-8' },\n Body: { Html: { Data: message.html, Charset: 'UTF-8' } },\n ...(message.headers === undefined\n ? {}\n : {\n Headers: Object.entries(message.headers).map(([Name, Value]) => ({\n Name,\n Value,\n })),\n }),\n },\n },\n ...(options.configurationSetName === undefined\n ? {}\n : { ConfigurationSetName: options.configurationSetName }),\n });\n const url = `${endpoint}/v2/email/outbound-emails`;\n const headers = await signRequest({\n method: 'POST',\n url,\n headers: { 'content-type': 'application/json' },\n body,\n credentials: await credentials(),\n region: options.region,\n service: 'ses',\n });\n // No retries here: the durable step owns them, and counts them — SES cannot de-duplicate,\n // so every extra attempt of an accepted send is another chance to deliver twice\n const response = await fetchImpl(url, { method: 'POST', headers, body });\n const text = await response.text();\n if (!response.ok) {\n let detail = text;\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n const said =\n (parsed as { message?: unknown; Message?: unknown }).message ??\n (parsed as { Message?: unknown }).Message;\n if (typeof said === 'string') detail = said;\n } catch {\n // Not JSON: the raw body says what went wrong\n }\n throw new SesSendError(response.status, sesErrorType(response.headers, parsed), detail);\n }\n let messageId: unknown;\n try {\n messageId = (JSON.parse(text) as { MessageId?: unknown }).MessageId;\n } catch {\n messageId = undefined;\n }\n return typeof messageId === 'string' ? { providerMessageId: messageId } : {};\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/webpush.cjs
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_engine_retry = require("./engine/retry.cjs");
|
|
2
3
|
const require_jwt = require("./jwt.cjs");
|
|
3
4
|
//#region src/webpush.ts
|
|
5
|
+
/**
|
|
6
|
+
* How a push service's answer is classified (RFC 8030 §7 and §8.1, with the browsers' services
|
|
7
|
+
* answering as documented there):
|
|
8
|
+
*
|
|
9
|
+
* - dead subscription: 404 and 410 — the browser unsubscribed or the subscription expired
|
|
10
|
+
* (RFC 8030 §7.3: 410 Gone for an expired subscription) — and a subscription the sender
|
|
11
|
+
* cannot parse (`InvalidSubscription`). The copy settles undelivered and the host forgets it.
|
|
12
|
+
* - transient: 429 Too Many Requests (§8.4) and 5xx.
|
|
13
|
+
* - everything else is this request or this sender: 400 (a bad header or body), 401 / 403 (the
|
|
14
|
+
* VAPID key pair or subject, §8.2 of RFC 8292), 413 (the payload, RFC 8030 §7.2) and any other
|
|
15
|
+
* 4xx are a `PermanentError`.
|
|
16
|
+
*/
|
|
17
|
+
const DEAD_TOKEN_REASONS = /* @__PURE__ */ new Set([
|
|
18
|
+
"Gone",
|
|
19
|
+
"NotFound",
|
|
20
|
+
"InvalidSubscription"
|
|
21
|
+
]);
|
|
4
22
|
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
5
23
|
async function generateVapidKeys() {
|
|
6
24
|
const pair = await crypto.subtle.generateKey({
|
|
@@ -14,8 +32,15 @@ async function generateVapidKeys() {
|
|
|
14
32
|
privateKey: jwk.d ?? ""
|
|
15
33
|
};
|
|
16
34
|
}
|
|
17
|
-
/**
|
|
35
|
+
/** The record size the header announces: RFC 8291 §4 fixes it at 4096 and one record per push. */
|
|
18
36
|
const RECORD_SIZE = 4096;
|
|
37
|
+
/**
|
|
38
|
+
* The largest plaintext one record carries. A push message body is at most 4096 octets (RFC 8030
|
|
39
|
+
* §7.2); the aes128gcm header takes 86 of them (16 salt + 4 rs + 1 idlen + 65 key), the record its
|
|
40
|
+
* 16-octet tag and the 1-octet delimiter — 3993 octets of plaintext (RFC 8291 §4). Anything past
|
|
41
|
+
* that is either an invalid record or a 413 from the push service.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_PLAINTEXT_BYTES = 3993;
|
|
19
44
|
const encoder = new TextEncoder();
|
|
20
45
|
const utf8 = (text) => encoder.encode(text);
|
|
21
46
|
function fromBase64url(text) {
|
|
@@ -74,35 +99,83 @@ async function encryptPayload(payload, p256dh, authSecret, salt = crypto.getRand
|
|
|
74
99
|
header[20] = senderPublic.length;
|
|
75
100
|
return concat(header, senderPublic, sealed);
|
|
76
101
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
/** How long a VAPID token is good for: RFC 8292 §2 allows at most 24 hours. */
|
|
103
|
+
const VAPID_EXP_S = 86400;
|
|
104
|
+
/** How long a signed token is reused: half its life, so it is never sent near expiry. */
|
|
105
|
+
const VAPID_TTL_MS = 432e5;
|
|
106
|
+
/**
|
|
107
|
+
* The VAPID `Authorization` header (RFC 8292 §3) for one key pair. The private key is imported
|
|
108
|
+
* once; the JWT is signed per push service (its `aud` is the service's origin) and reused for
|
|
109
|
+
* `VAPID_TTL_MS`. Concurrent callers for one origin share the one signing in flight, and a signing
|
|
110
|
+
* that failed is not kept. One instance serves every sender built on the pair — a host keeps it
|
|
111
|
+
* across runs, and builds the senders per run.
|
|
112
|
+
*/
|
|
113
|
+
var VapidSigner = class {
|
|
114
|
+
options;
|
|
115
|
+
key;
|
|
116
|
+
signed = /* @__PURE__ */ new Map();
|
|
117
|
+
signing = /* @__PURE__ */ new Map();
|
|
118
|
+
constructor(options) {
|
|
119
|
+
this.options = options;
|
|
120
|
+
}
|
|
121
|
+
/** The header value for the push service at `audience` — its origin, or any URL on it. */
|
|
122
|
+
async authorization(audience, now = Date.now()) {
|
|
123
|
+
const origin = new URL(audience).origin;
|
|
124
|
+
const current = this.signed.get(origin);
|
|
125
|
+
if (current !== void 0 && now - current.issuedAt < VAPID_TTL_MS) return current.value;
|
|
126
|
+
let pending = this.signing.get(origin);
|
|
127
|
+
if (pending === void 0) {
|
|
128
|
+
pending = this.sign(origin, now).then((value) => {
|
|
129
|
+
this.signed.set(origin, {
|
|
130
|
+
value,
|
|
131
|
+
issuedAt: now
|
|
132
|
+
});
|
|
133
|
+
return value;
|
|
134
|
+
}).finally(() => {
|
|
135
|
+
this.signing.delete(origin);
|
|
136
|
+
});
|
|
137
|
+
this.signing.set(origin, pending);
|
|
138
|
+
}
|
|
139
|
+
return pending;
|
|
140
|
+
}
|
|
141
|
+
privateKey() {
|
|
142
|
+
if (this.key === void 0) {
|
|
143
|
+
const publicKey = fromBase64url(this.options.publicKey);
|
|
144
|
+
this.key = crypto.subtle.importKey("jwk", {
|
|
145
|
+
kty: "EC",
|
|
146
|
+
crv: "P-256",
|
|
147
|
+
d: this.options.privateKey,
|
|
148
|
+
x: require_jwt.base64url(publicKey.slice(1, 33)),
|
|
149
|
+
y: require_jwt.base64url(publicKey.slice(33, 65)),
|
|
150
|
+
ext: true
|
|
151
|
+
}, {
|
|
152
|
+
name: "ECDSA",
|
|
153
|
+
namedCurve: "P-256"
|
|
154
|
+
}, false, ["sign"]).catch((error) => {
|
|
155
|
+
this.key = void 0;
|
|
156
|
+
throw error;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return this.key;
|
|
160
|
+
}
|
|
161
|
+
async sign(origin, now) {
|
|
162
|
+
const key = await this.privateKey();
|
|
163
|
+
const header = require_jwt.base64url(JSON.stringify({
|
|
164
|
+
typ: "JWT",
|
|
165
|
+
alg: "ES256"
|
|
166
|
+
}));
|
|
167
|
+
const claims = require_jwt.base64url(JSON.stringify({
|
|
168
|
+
aud: origin,
|
|
169
|
+
exp: Math.floor(now / 1e3) + VAPID_EXP_S,
|
|
170
|
+
sub: this.options.subject
|
|
171
|
+
}));
|
|
172
|
+
const signature = new Uint8Array(await crypto.subtle.sign({
|
|
173
|
+
name: "ECDSA",
|
|
174
|
+
hash: "SHA-256"
|
|
175
|
+
}, key, utf8(`${header}.${claims}`)));
|
|
176
|
+
return `vapid t=${header}.${claims}.${require_jwt.base64url(signature)}, k=${this.options.publicKey}`;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
106
179
|
/**
|
|
107
180
|
* A browser subscription as a sender can use it: an https endpoint, a 65-byte uncompressed P-256
|
|
108
181
|
* key and a 16-byte auth secret. Anything else — not JSON, missing keys, a key of the wrong size,
|
|
@@ -133,48 +206,61 @@ var WebPushSender = class {
|
|
|
133
206
|
if (message.channel !== "push") throw new Error(`WebPushSender: unsupported channel '${message.channel}'`);
|
|
134
207
|
if (message.recipient === void 0) throw new Error(`WebPushSender: no subscription for user '${message.userId}'`);
|
|
135
208
|
const subscription = parseSubscription(message.recipient);
|
|
136
|
-
if (subscription === void 0) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
reason: "InvalidSubscription"
|
|
141
|
-
};
|
|
142
|
-
}
|
|
209
|
+
if (subscription === void 0) return {
|
|
210
|
+
delivered: false,
|
|
211
|
+
reason: "InvalidSubscription"
|
|
212
|
+
};
|
|
143
213
|
const content = await this.options.render(message.template, message.props);
|
|
144
|
-
const
|
|
214
|
+
const payload = utf8(JSON.stringify({
|
|
145
215
|
...content,
|
|
146
216
|
template: message.template,
|
|
147
217
|
idempotencyKey: message.idempotencyKey,
|
|
148
218
|
...message.tracking?.data
|
|
149
|
-
}))
|
|
219
|
+
}));
|
|
220
|
+
if (payload.length > 3993) throw new require_engine_retry.PermanentError(`[webpush] payload for template '${message.template}' is ${payload.length} bytes; a push message carries at most ${MAX_PLAINTEXT_BYTES}`);
|
|
221
|
+
const body = await encryptPayload(payload, subscription.p256dh, subscription.auth);
|
|
150
222
|
const response = await (this.options.fetch ?? fetch)(subscription.endpoint, {
|
|
151
223
|
method: "POST",
|
|
152
224
|
headers: {
|
|
153
|
-
authorization: await
|
|
225
|
+
authorization: await this.options.credentials.authorization(subscription.endpoint),
|
|
154
226
|
"content-encoding": "aes128gcm",
|
|
155
227
|
"content-type": "application/octet-stream",
|
|
156
228
|
ttl: String(this.options.ttl ?? 86400),
|
|
157
|
-
urgency: "normal"
|
|
229
|
+
urgency: "normal",
|
|
230
|
+
topic: await webPushTopic(message.idempotencyKey)
|
|
158
231
|
},
|
|
159
232
|
body
|
|
160
233
|
});
|
|
161
234
|
if (response.ok) return;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
const { status } = response;
|
|
236
|
+
if (status === 404 || status === 410) {
|
|
237
|
+
console.warn(`[webpush] subscription gone for user ${message.userId}: ${status}`);
|
|
165
238
|
return {
|
|
166
239
|
delivered: false,
|
|
167
|
-
reason:
|
|
240
|
+
reason: status === 410 ? "Gone" : "NotFound"
|
|
168
241
|
};
|
|
169
242
|
}
|
|
170
|
-
|
|
243
|
+
const text = await response.text();
|
|
244
|
+
if (require_engine_retry.isRetryableStatus(status)) throw new Error(`[webpush] ${status}: ${text}`);
|
|
245
|
+
throw new require_engine_retry.PermanentError(`[webpush] push service refused the request: ${status} ${text}`);
|
|
171
246
|
}
|
|
172
247
|
};
|
|
248
|
+
/**
|
|
249
|
+
* The `Topic` header for one copy: RFC 8030 §5.4 allows at most 32 characters of the URL-safe
|
|
250
|
+
* base64 alphabet, so the idempotency key goes through SHA-256 and is cut to size.
|
|
251
|
+
*/
|
|
252
|
+
async function webPushTopic(idempotencyKey) {
|
|
253
|
+
const digest = await crypto.subtle.digest("SHA-256", utf8(idempotencyKey));
|
|
254
|
+
return require_jwt.base64url(new Uint8Array(digest)).slice(0, 32);
|
|
255
|
+
}
|
|
173
256
|
//#endregion
|
|
257
|
+
exports.DEAD_TOKEN_REASONS = DEAD_TOKEN_REASONS;
|
|
258
|
+
exports.MAX_PLAINTEXT_BYTES = MAX_PLAINTEXT_BYTES;
|
|
259
|
+
exports.VapidSigner = VapidSigner;
|
|
174
260
|
exports.WebPushSender = WebPushSender;
|
|
175
261
|
exports.encryptPayload = encryptPayload;
|
|
176
262
|
exports.fromBase64url = fromBase64url;
|
|
177
263
|
exports.generateVapidKeys = generateVapidKeys;
|
|
178
|
-
exports.
|
|
264
|
+
exports.webPushTopic = webPushTopic;
|
|
179
265
|
|
|
180
266
|
//# sourceMappingURL=webpush.cjs.map
|
package/dist/webpush.cjs.map
CHANGED
|
@@ -1 +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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"}
|
|
1
|
+
{"version":3,"file":"webpush.cjs","names":["base64url","PermanentError","isRetryableStatus"],"sources":["../src/webpush.ts"],"sourcesContent":["import type { MessageSender, OutboundMessage, SendResult } from './engine';\nimport { PermanentError, isRetryableStatus } from './engine/retry';\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 VapidSignerOptions extends VapidKeys {\n /** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */\n subject: string;\n}\n\nexport interface WebPushOptions {\n /** The VAPID signer the requests are authorized with — shared by every sender on the same key pair. */\n credentials: VapidSigner;\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}\n\n/**\n * How a push service's answer is classified (RFC 8030 §7 and §8.1, with the browsers' services\n * answering as documented there):\n *\n * - dead subscription: 404 and 410 — the browser unsubscribed or the subscription expired\n * (RFC 8030 §7.3: 410 Gone for an expired subscription) — and a subscription the sender\n * cannot parse (`InvalidSubscription`). The copy settles undelivered and the host forgets it.\n * - transient: 429 Too Many Requests (§8.4) and 5xx.\n * - everything else is this request or this sender: 400 (a bad header or body), 401 / 403 (the\n * VAPID key pair or subject, §8.2 of RFC 8292), 413 (the payload, RFC 8030 §7.2) and any other\n * 4xx are a `PermanentError`.\n */\nexport const DEAD_TOKEN_REASONS: ReadonlySet<string> = new Set([\n 'Gone',\n 'NotFound',\n 'InvalidSubscription',\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/** The record size the header announces: RFC 8291 §4 fixes it at 4096 and one record per push. */\nconst RECORD_SIZE = 4096;\n/**\n * The largest plaintext one record carries. A push message body is at most 4096 octets (RFC 8030\n * §7.2); the aes128gcm header takes 86 of them (16 salt + 4 rs + 1 idlen + 65 key), the record its\n * 16-octet tag and the 1-octet delimiter — 3993 octets of plaintext (RFC 8291 §4). Anything past\n * that is either an invalid record or a 413 from the push service.\n */\nexport const MAX_PLAINTEXT_BYTES = 3993;\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/** How long a VAPID token is good for: RFC 8292 §2 allows at most 24 hours. */\nconst VAPID_EXP_S = 24 * 3600;\n/** How long a signed token is reused: half its life, so it is never sent near expiry. */\nconst VAPID_TTL_MS = 12 * 3600_000;\n\n/**\n * The VAPID `Authorization` header (RFC 8292 §3) for one key pair. The private key is imported\n * once; the JWT is signed per push service (its `aud` is the service's origin) and reused for\n * `VAPID_TTL_MS`. Concurrent callers for one origin share the one signing in flight, and a signing\n * that failed is not kept. One instance serves every sender built on the pair — a host keeps it\n * across runs, and builds the senders per run.\n */\nexport class VapidSigner {\n private key: Promise<CryptoKey> | undefined;\n private readonly signed = new Map<string, { value: string; issuedAt: number }>();\n private readonly signing = new Map<string, Promise<string>>();\n\n constructor(private readonly options: VapidSignerOptions) {}\n\n /** The header value for the push service at `audience` — its origin, or any URL on it. */\n async authorization(audience: string, now = Date.now()): Promise<string> {\n const origin = new URL(audience).origin;\n const current = this.signed.get(origin);\n if (current !== undefined && now - current.issuedAt < VAPID_TTL_MS) return current.value;\n let pending = this.signing.get(origin);\n if (pending === undefined) {\n pending = this.sign(origin, now)\n .then((value) => {\n this.signed.set(origin, { value, issuedAt: now });\n return value;\n })\n .finally(() => {\n this.signing.delete(origin);\n });\n this.signing.set(origin, pending);\n }\n return pending;\n }\n\n private privateKey(): Promise<CryptoKey> {\n if (this.key === undefined) {\n const publicKey = fromBase64url(this.options.publicKey);\n this.key = crypto.subtle\n .importKey(\n 'jwk',\n {\n kty: 'EC',\n crv: 'P-256',\n d: this.options.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 .catch((error: unknown) => {\n this.key = undefined;\n throw error;\n });\n }\n return this.key;\n }\n\n private async sign(origin: string, now: number): Promise<string> {\n const key = await this.privateKey();\n const header = base64url(JSON.stringify({ typ: 'JWT', alg: 'ES256' }));\n const claims = base64url(\n JSON.stringify({\n aud: origin,\n exp: Math.floor(now / 1000) + VAPID_EXP_S,\n sub: this.options.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=${this.options.publicKey}`;\n }\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 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 if (payload.length > MAX_PLAINTEXT_BYTES) {\n // The same template renders the same size on every attempt: settle it before encrypting\n throw new PermanentError(\n `[webpush] payload for template '${message.template}' is ${payload.length} bytes; a push message carries at most ${MAX_PLAINTEXT_BYTES}`\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 this.options.credentials.authorization(subscription.endpoint),\n 'content-encoding': 'aes128gcm',\n 'content-type': 'application/octet-stream',\n ttl: String(this.options.ttl ?? 86_400),\n urgency: 'normal',\n // A resend of the same copy replaces the first one while it is still queued (RFC 8030\n // §5.4), so an at-least-once retry does not show the notification twice\n topic: await webPushTopic(message.idempotencyKey),\n },\n body,\n });\n if (response.ok) return;\n const { status } = response;\n if (status === 404 || 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}: ${status}`);\n return { delivered: false, reason: status === 410 ? 'Gone' : 'NotFound' };\n }\n const text = await response.text();\n if (isRetryableStatus(status)) throw new Error(`[webpush] ${status}: ${text}`);\n throw new PermanentError(`[webpush] push service refused the request: ${status} ${text}`);\n }\n}\n\n/**\n * The `Topic` header for one copy: RFC 8030 §5.4 allows at most 32 characters of the URL-safe\n * base64 alphabet, so the idempotency key goes through SHA-256 and is cut to size.\n */\nexport async function webPushTopic(idempotencyKey: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', utf8(idempotencyKey));\n return base64url(new Uint8Array(digest)).slice(0, 32);\n}\n"],"mappings":"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"}
|
package/dist/webpush.d.cts
CHANGED
|
@@ -18,21 +18,41 @@ export interface VapidKeys {
|
|
|
18
18
|
publicKey: string;
|
|
19
19
|
privateKey: string;
|
|
20
20
|
}
|
|
21
|
+
export interface VapidSignerOptions extends VapidKeys {
|
|
22
|
+
/** Who to contact about this sender: `mailto:` or an `https:` URL (RFC 8292). */
|
|
23
|
+
subject: string;
|
|
24
|
+
}
|
|
21
25
|
export interface WebPushOptions {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
subject: string;
|
|
25
|
-
};
|
|
26
|
+
/** The VAPID signer the requests are authorized with — shared by every sender on the same key pair. */
|
|
27
|
+
credentials: VapidSigner;
|
|
26
28
|
render: PushRenderer;
|
|
27
29
|
/** Seconds the push service keeps an undelivered message; a day by default. */
|
|
28
30
|
ttl?: number;
|
|
29
31
|
/** Fetch implementation (defaults to the global one). */
|
|
30
32
|
fetch?: typeof fetch;
|
|
31
|
-
/** Called for a subscription the push service reports as gone (404 / 410). */
|
|
32
|
-
onUnregistered?: (message: OutboundMessage) => Promise<void> | void;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* How a push service's answer is classified (RFC 8030 §7 and §8.1, with the browsers' services
|
|
36
|
+
* answering as documented there):
|
|
37
|
+
*
|
|
38
|
+
* - dead subscription: 404 and 410 — the browser unsubscribed or the subscription expired
|
|
39
|
+
* (RFC 8030 §7.3: 410 Gone for an expired subscription) — and a subscription the sender
|
|
40
|
+
* cannot parse (`InvalidSubscription`). The copy settles undelivered and the host forgets it.
|
|
41
|
+
* - transient: 429 Too Many Requests (§8.4) and 5xx.
|
|
42
|
+
* - everything else is this request or this sender: 400 (a bad header or body), 401 / 403 (the
|
|
43
|
+
* VAPID key pair or subject, §8.2 of RFC 8292), 413 (the payload, RFC 8030 §7.2) and any other
|
|
44
|
+
* 4xx are a `PermanentError`.
|
|
45
|
+
*/
|
|
46
|
+
export declare const DEAD_TOKEN_REASONS: ReadonlySet<string>;
|
|
34
47
|
/** A fresh VAPID key pair (once per project; the private key is a secret). */
|
|
35
48
|
export declare function generateVapidKeys(): Promise<VapidKeys>;
|
|
49
|
+
/**
|
|
50
|
+
* The largest plaintext one record carries. A push message body is at most 4096 octets (RFC 8030
|
|
51
|
+
* §7.2); the aes128gcm header takes 86 of them (16 salt + 4 rs + 1 idlen + 65 key), the record its
|
|
52
|
+
* 16-octet tag and the 1-octet delimiter — 3993 octets of plaintext (RFC 8291 §4). Anything past
|
|
53
|
+
* that is either an invalid record or a 413 from the push service.
|
|
54
|
+
*/
|
|
55
|
+
export declare const MAX_PLAINTEXT_BYTES = 3993;
|
|
36
56
|
/** Bytes WebCrypto accepts (backed by a plain ArrayBuffer). */
|
|
37
57
|
type Bytes = Uint8Array<ArrayBuffer>;
|
|
38
58
|
export declare function fromBase64url(text: string): Bytes;
|
|
@@ -42,13 +62,34 @@ export declare function fromBase64url(text: string): Bytes;
|
|
|
42
62
|
* subscription's auth secret.
|
|
43
63
|
*/
|
|
44
64
|
export declare function encryptPayload(payload: Bytes, p256dh: Bytes, authSecret: Bytes, salt?: Bytes): Promise<Bytes>;
|
|
45
|
-
/**
|
|
46
|
-
|
|
65
|
+
/**
|
|
66
|
+
* The VAPID `Authorization` header (RFC 8292 §3) for one key pair. The private key is imported
|
|
67
|
+
* once; the JWT is signed per push service (its `aud` is the service's origin) and reused for
|
|
68
|
+
* `VAPID_TTL_MS`. Concurrent callers for one origin share the one signing in flight, and a signing
|
|
69
|
+
* that failed is not kept. One instance serves every sender built on the pair — a host keeps it
|
|
70
|
+
* across runs, and builds the senders per run.
|
|
71
|
+
*/
|
|
72
|
+
export declare class VapidSigner {
|
|
73
|
+
private readonly options;
|
|
74
|
+
private key;
|
|
75
|
+
private readonly signed;
|
|
76
|
+
private readonly signing;
|
|
77
|
+
constructor(options: VapidSignerOptions);
|
|
78
|
+
/** The header value for the push service at `audience` — its origin, or any URL on it. */
|
|
79
|
+
authorization(audience: string, now?: number): Promise<string>;
|
|
80
|
+
private privateKey;
|
|
81
|
+
private sign;
|
|
82
|
+
}
|
|
47
83
|
export declare class WebPushSender implements MessageSender {
|
|
48
84
|
private readonly options;
|
|
49
85
|
constructor(options: WebPushOptions);
|
|
50
86
|
send(message: OutboundMessage): Promise<void | SendResult>;
|
|
51
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The `Topic` header for one copy: RFC 8030 §5.4 allows at most 32 characters of the URL-safe
|
|
90
|
+
* base64 alphabet, so the idempotency key goes through SHA-256 and is cut to size.
|
|
91
|
+
*/
|
|
92
|
+
export declare function webPushTopic(idempotencyKey: string): Promise<string>;
|
|
52
93
|
//#endregion
|
|
53
94
|
export {};
|
|
54
95
|
//# sourceMappingURL=webpush.d.cts.map
|
package/dist/webpush.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webpush.d.cts","names":[],"sources":["../src/webpush.ts"],"mappings":";;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"webpush.d.cts","names":[],"sources":["../src/webpush.ts"],"mappings":";;;;;;;;;;;;;;;;iBAkBiB;EACf;EACA;;iBAGe,2BAA2B;;EAE1C;;iBAGe;;EAEf,aAAa;EACb,QAAQ;;EAER;;EAEA,eAAe;;;;;;;;;;;;;;qBAeJ,oBAAoB;;wBAYX,qBAAqB,QAAQ;;;;;;;qBAkBtC;;KAER,QAAQ,WAAW;wBAIR,cAAc,eAAe;;;;;;wBAqCvB,eACpB,SAAS,OACT,QAAQ,OACR,YAAY,OACZ,OAAM,QACL,QAAQ;;;;;;;;qBAmDE;mBAKkB;UAJrB;mBACS;mBACA;EAEjB,YAA6B,SAAS;;EAGhC,cAAc,kBAAkB,eAAmB;UAmBjD;UA0BM;;qBAuCH,yBAAyB;mBACP;EAA7B,YAA6B,SAAS;EAEhC,KAAK,SAAS,kBAAkB,eAAe;;;;;;wBA4DjC,aAAa,yBAAyB"}
|