@shware/workflow 4.1.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/apns.cjs +3 -2
  2. package/dist/apns.cjs.map +1 -1
  3. package/dist/apns.d.cts.map +1 -1
  4. package/dist/apns.d.mts.map +1 -1
  5. package/dist/apns.mjs +3 -2
  6. package/dist/apns.mjs.map +1 -1
  7. package/dist/aws/function-url.cjs +3 -11
  8. package/dist/aws/function-url.cjs.map +1 -1
  9. package/dist/aws/function-url.d.cts.map +1 -1
  10. package/dist/aws/function-url.d.mts.map +1 -1
  11. package/dist/aws/function-url.mjs +3 -11
  12. package/dist/aws/function-url.mjs.map +1 -1
  13. package/dist/fcm.cjs +2 -1
  14. package/dist/fcm.cjs.map +1 -1
  15. package/dist/fcm.d.cts.map +1 -1
  16. package/dist/fcm.d.mts.map +1 -1
  17. package/dist/fcm.mjs +2 -1
  18. package/dist/fcm.mjs.map +1 -1
  19. package/dist/host/channels.cjs +2 -1
  20. package/dist/host/channels.cjs.map +1 -1
  21. package/dist/host/channels.d.cts.map +1 -1
  22. package/dist/host/channels.d.mts.map +1 -1
  23. package/dist/host/channels.mjs +2 -1
  24. package/dist/host/channels.mjs.map +1 -1
  25. package/dist/jwt.cjs +5 -9
  26. package/dist/jwt.cjs.map +1 -1
  27. package/dist/jwt.d.cts +1 -1
  28. package/dist/jwt.d.cts.map +1 -1
  29. package/dist/jwt.d.mts +1 -1
  30. package/dist/jwt.d.mts.map +1 -1
  31. package/dist/jwt.mjs +5 -9
  32. package/dist/jwt.mjs.map +1 -1
  33. package/dist/store/sql.cjs +2 -1
  34. package/dist/store/sql.cjs.map +1 -1
  35. package/dist/store/sql.d.cts.map +1 -1
  36. package/dist/store/sql.d.mts.map +1 -1
  37. package/dist/store/sql.mjs +2 -1
  38. package/dist/store/sql.mjs.map +1 -1
  39. package/dist/transports/sigv4.cjs +2 -3
  40. package/dist/transports/sigv4.cjs.map +1 -1
  41. package/dist/transports/sigv4.d.cts.map +1 -1
  42. package/dist/transports/sigv4.d.mts.map +1 -1
  43. package/dist/transports/sigv4.mjs +2 -3
  44. package/dist/transports/sigv4.mjs.map +1 -1
  45. package/dist/webpush.cjs +11 -20
  46. package/dist/webpush.cjs.map +1 -1
  47. package/dist/webpush.d.cts.map +1 -1
  48. package/dist/webpush.d.mts.map +1 -1
  49. package/dist/webpush.mjs +11 -20
  50. package/dist/webpush.mjs.map +1 -1
  51. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"channels.cjs","names":["resolveRuntime","PermanentError","EmailSender","registryRenderer","registryPushRenderer","removeDevice","apnsEnvironment","ApnsProviderToken","apnsKeyFromBase64","ApnsPushSender","APNS_DEAD_TOKEN_REASONS","serviceAccountFromBase64","FcmAccessToken","FcmPushSender","FCM_DEAD_TOKEN_REASONS","VapidSigner","WebPushSender","WEB_PUSH_DEAD_TOKEN_REASONS","routeByPlatform","indexTemplates","DiscordWebhookSender","lookupTemplate","renderDiscordContent","SlackWebhookSender","renderSlackContent","routeByChannel"],"sources":["../../src/host/channels.ts"],"sourcesContent":["import {\n DEAD_TOKEN_REASONS as APNS_DEAD_TOKEN_REASONS,\n ApnsProviderToken,\n ApnsPushSender,\n apnsKeyFromBase64,\n} from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport {\n DEAD_TOKEN_REASONS as FCM_DEAD_TOKEN_REASONS,\n FcmAccessToken,\n FcmPushSender,\n serviceAccountFromBase64,\n} from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport {\n VapidSigner,\n DEAD_TOKEN_REASONS as WEB_PUSH_DEAD_TOKEN_REASONS,\n WebPushSender,\n} from '../webpush';\nimport { removeDevice } from './ingest';\nimport type { JourneyEnv } from './platform';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/**\n * A sender that fails every message with the reason the channel cannot deliver — for good: the\n * configuration is the same on every attempt, so the step does not retry into it.\n */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new PermanentError(\n `cannot send '${message.template}' on ${message.channel}: ${reason}`\n );\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n // Missing on this attempt, missing on the next: the deployment has to change, not the step\n throw new PermanentError(\n `${name} is not set: the environment must hold it for the configured channel`\n );\n }\n return value;\n}\n\n/**\n * The provider credentials — `ApnsProviderToken`, `FcmAccessToken`, `VapidSigner` — for as long\n * as the host lives. `channelsFromConfig` runs once per workflow run (every wake of every\n * instance) and builds the senders anew each time; what must outlive the run is the credential\n * state they hold: the APNs provider JWT must be reused for at least 20 minutes (Apple answers a\n * sender that re-signs on every push with 429 `TooManyProviderTokenUpdates`), the FCM access\n * token is good for an hour, the VAPID key import is wasted work twice.\n *\n * The cache is the host's (`createJourneyHost` makes one and hands it to every run), not the\n * run's `env`: a Lambda invocation builds a fresh env object for each run (its instance port is\n * in it), so a cache keyed on env would miss on every wake. Inside, the key names the provider,\n * the ids that identify the key and a digest of the secret — so a rotated secret or a reloaded\n * host mints a new credential instead of reusing the old, and the secret itself is in no key and\n * no log. Nothing that shapes the sender (topic, environment, relay, registry, store) is in it:\n * the sender is built per call around the shared credential.\n */\nexport interface ProviderCache {\n /** The credential under `key`, built on the first call and returned as is after that. */\n get<T>(key: string, build: () => T): T;\n}\n\nexport function createProviderCache(): ProviderCache {\n const credentials = new Map<string, unknown>();\n return {\n get<T>(key: string, build: () => T): T {\n if (!credentials.has(key)) credentials.set(key, build());\n return credentials.get(key) as T;\n },\n };\n}\n\n/** A number for each fetch implementation, so a credential minted through one is not reused with another. */\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nfunction identity(value: object | undefined): string {\n if (value === undefined) return 'global';\n let id = identities.get(value);\n if (id === undefined) {\n id = nextIdentity++;\n identities.set(value, id);\n }\n return String(id);\n}\n\n/** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */\nasync function secretDigest(secret: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));\n return Array.from(new Uint8Array(digest).slice(0, 4), (b) =>\n b.toString(16).padStart(2, '0')\n ).join('');\n}\n\n/**\n * A sender built on its first message, then kept for this call. `build` reads the secrets (and\n * so may throw a `PermanentError` naming the missing variable): a platform's secret is only\n * required when something is actually sent there, so a journey that only emails must not fail\n * because the APNs key is missing from a local `.dev.vars`. A build that failed is not kept.\n */\nfunction lazy(build: () => Promise<MessageSender>): MessageSender {\n let sender: Promise<MessageSender> | undefined;\n return {\n async send(message) {\n sender ??= build().catch((error: unknown) => {\n sender = undefined;\n throw error;\n });\n return (await sender).send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n /**\n * Where the provider credentials live across runs (`createProviderCache`): the host's, made\n * once with it. Without one, this call's credentials are its own — nothing is reused.\n */\n providers?: ProviderCache | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n const providers = options.providers ?? createProviderCache();\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n /**\n * A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n * waits that read devices, as a removal through the API does. Done here, per run, on the\n * settled result, so the credential cached across runs (see `ProviderCache`) never holds a\n * run's store.\n */\n const dropDeadTokens = (\n provider: MessageSender,\n deadTokenReasons: ReadonlySet<string>\n ): MessageSender => ({\n async send(message) {\n const result = await provider.send(message);\n if (\n result !== undefined &&\n deadTokenReasons.has(result.reason) &&\n message.device !== undefined &&\n options.store !== undefined\n ) {\n await removeDevice(env, message.device.token, options.store);\n }\n return result;\n },\n });\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n const apnsFetch = options.apnsFetch ?? options.fetch;\n const environment = apnsEnvironment(apns, mode);\n platforms.ios = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'APNS_PRIVATE_KEY_BASE64');\n const credentials = providers.get(\n JSON.stringify(['apns', apns.keyId, apns.teamId, await secretDigest(secret)]),\n () =>\n new ApnsProviderToken({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(secret),\n })\n );\n return new ApnsPushSender({\n credentials,\n topic: apns.topic,\n environment,\n render,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(apnsFetch === undefined ? {} : { fetch: apnsFetch }),\n });\n }),\n APNS_DEAD_TOKEN_REASONS\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64');\n const serviceAccount = serviceAccountFromBase64(secret);\n // The token endpoint is called through the fetch: another fetch is another credential\n const credentials = providers.get(\n JSON.stringify([\n 'fcm',\n serviceAccount.client_email,\n serviceAccount.project_id,\n identity(options.fetch),\n await secretDigest(secret),\n ]),\n () =>\n new FcmAccessToken({\n serviceAccount,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n return new FcmPushSender({\n credentials,\n render,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n FCM_DEAD_TOKEN_REASONS\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = dropDeadTokens(\n lazy(async () => {\n const publicKey = requireSecret(env, 'VAPID_PUBLIC_KEY');\n const privateKey = requireSecret(env, 'VAPID_PRIVATE_KEY');\n const credentials = providers.get(\n JSON.stringify(['vapid', webPush.subject, publicKey, await secretDigest(privateKey)]),\n () => new VapidSigner({ subject: webPush.subject, publicKey, privateKey })\n );\n return new WebPushSender({\n credentials,\n render,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n WEB_PUSH_DEAD_TOKEN_REASONS\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new PermanentError(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"channels.cjs","names":["resolveRuntime","PermanentError","Buffer","EmailSender","registryRenderer","registryPushRenderer","removeDevice","apnsEnvironment","ApnsProviderToken","apnsKeyFromBase64","ApnsPushSender","APNS_DEAD_TOKEN_REASONS","serviceAccountFromBase64","FcmAccessToken","FcmPushSender","FCM_DEAD_TOKEN_REASONS","VapidSigner","WebPushSender","WEB_PUSH_DEAD_TOKEN_REASONS","routeByPlatform","indexTemplates","DiscordWebhookSender","lookupTemplate","renderDiscordContent","SlackWebhookSender","renderSlackContent","routeByChannel"],"sources":["../../src/host/channels.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport {\n DEAD_TOKEN_REASONS as APNS_DEAD_TOKEN_REASONS,\n ApnsProviderToken,\n ApnsPushSender,\n apnsKeyFromBase64,\n} from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport {\n DEAD_TOKEN_REASONS as FCM_DEAD_TOKEN_REASONS,\n FcmAccessToken,\n FcmPushSender,\n serviceAccountFromBase64,\n} from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport {\n VapidSigner,\n DEAD_TOKEN_REASONS as WEB_PUSH_DEAD_TOKEN_REASONS,\n WebPushSender,\n} from '../webpush';\nimport { removeDevice } from './ingest';\nimport type { JourneyEnv } from './platform';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/**\n * A sender that fails every message with the reason the channel cannot deliver — for good: the\n * configuration is the same on every attempt, so the step does not retry into it.\n */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new PermanentError(\n `cannot send '${message.template}' on ${message.channel}: ${reason}`\n );\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n // Missing on this attempt, missing on the next: the deployment has to change, not the step\n throw new PermanentError(\n `${name} is not set: the environment must hold it for the configured channel`\n );\n }\n return value;\n}\n\n/**\n * The provider credentials — `ApnsProviderToken`, `FcmAccessToken`, `VapidSigner` — for as long\n * as the host lives. `channelsFromConfig` runs once per workflow run (every wake of every\n * instance) and builds the senders anew each time; what must outlive the run is the credential\n * state they hold: the APNs provider JWT must be reused for at least 20 minutes (Apple answers a\n * sender that re-signs on every push with 429 `TooManyProviderTokenUpdates`), the FCM access\n * token is good for an hour, the VAPID key import is wasted work twice.\n *\n * The cache is the host's (`createJourneyHost` makes one and hands it to every run), not the\n * run's `env`: a Lambda invocation builds a fresh env object for each run (its instance port is\n * in it), so a cache keyed on env would miss on every wake. Inside, the key names the provider,\n * the ids that identify the key and a digest of the secret — so a rotated secret or a reloaded\n * host mints a new credential instead of reusing the old, and the secret itself is in no key and\n * no log. Nothing that shapes the sender (topic, environment, relay, registry, store) is in it:\n * the sender is built per call around the shared credential.\n */\nexport interface ProviderCache {\n /** The credential under `key`, built on the first call and returned as is after that. */\n get<T>(key: string, build: () => T): T;\n}\n\nexport function createProviderCache(): ProviderCache {\n const credentials = new Map<string, unknown>();\n return {\n get<T>(key: string, build: () => T): T {\n if (!credentials.has(key)) credentials.set(key, build());\n return credentials.get(key) as T;\n },\n };\n}\n\n/** A number for each fetch implementation, so a credential minted through one is not reused with another. */\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nfunction identity(value: object | undefined): string {\n if (value === undefined) return 'global';\n let id = identities.get(value);\n if (id === undefined) {\n id = nextIdentity++;\n identities.set(value, id);\n }\n return String(id);\n}\n\n/** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */\nasync function secretDigest(secret: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));\n return Buffer.from(digest.slice(0, 4)).toString('hex');\n}\n\n/**\n * A sender built on its first message, then kept for this call. `build` reads the secrets (and\n * so may throw a `PermanentError` naming the missing variable): a platform's secret is only\n * required when something is actually sent there, so a journey that only emails must not fail\n * because the APNs key is missing from a local `.dev.vars`. A build that failed is not kept.\n */\nfunction lazy(build: () => Promise<MessageSender>): MessageSender {\n let sender: Promise<MessageSender> | undefined;\n return {\n async send(message) {\n sender ??= build().catch((error: unknown) => {\n sender = undefined;\n throw error;\n });\n return (await sender).send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n /**\n * Where the provider credentials live across runs (`createProviderCache`): the host's, made\n * once with it. Without one, this call's credentials are its own — nothing is reused.\n */\n providers?: ProviderCache | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n const providers = options.providers ?? createProviderCache();\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n /**\n * A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n * waits that read devices, as a removal through the API does. Done here, per run, on the\n * settled result, so the credential cached across runs (see `ProviderCache`) never holds a\n * run's store.\n */\n const dropDeadTokens = (\n provider: MessageSender,\n deadTokenReasons: ReadonlySet<string>\n ): MessageSender => ({\n async send(message) {\n const result = await provider.send(message);\n if (\n result !== undefined &&\n deadTokenReasons.has(result.reason) &&\n message.device !== undefined &&\n options.store !== undefined\n ) {\n await removeDevice(env, message.device.token, options.store);\n }\n return result;\n },\n });\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n const apnsFetch = options.apnsFetch ?? options.fetch;\n const environment = apnsEnvironment(apns, mode);\n platforms.ios = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'APNS_PRIVATE_KEY_BASE64');\n const credentials = providers.get(\n JSON.stringify(['apns', apns.keyId, apns.teamId, await secretDigest(secret)]),\n () =>\n new ApnsProviderToken({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(secret),\n })\n );\n return new ApnsPushSender({\n credentials,\n topic: apns.topic,\n environment,\n render,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(apnsFetch === undefined ? {} : { fetch: apnsFetch }),\n });\n }),\n APNS_DEAD_TOKEN_REASONS\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64');\n const serviceAccount = serviceAccountFromBase64(secret);\n // The token endpoint is called through the fetch: another fetch is another credential\n const credentials = providers.get(\n JSON.stringify([\n 'fcm',\n serviceAccount.client_email,\n serviceAccount.project_id,\n identity(options.fetch),\n await secretDigest(secret),\n ]),\n () =>\n new FcmAccessToken({\n serviceAccount,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n return new FcmPushSender({\n credentials,\n render,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n FCM_DEAD_TOKEN_REASONS\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = dropDeadTokens(\n lazy(async () => {\n const publicKey = requireSecret(env, 'VAPID_PUBLIC_KEY');\n const privateKey = requireSecret(env, 'VAPID_PRIVATE_KEY');\n const credentials = providers.get(\n JSON.stringify(['vapid', webPush.subject, publicKey, await secretDigest(privateKey)]),\n () => new VapidSigner({ subject: webPush.subject, publicKey, privateKey })\n );\n return new WebPushSender({\n credentials,\n render,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n WEB_PUSH_DEAD_TOKEN_REASONS\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new PermanentError(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"channels.d.cts","names":[],"sources":["../../src/host/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;;wBAkDgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;;;;;;;;;;;;;;;;iBAoDK;;EAEf,IAAI,GAAG,aAAa,aAAa,IAAI;;wBAGvB,uBAAuB;;YAmD3B,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;;EAER,QAAQ;EACR,eAAe;;EAEf,mBAAmB;;;;;EAKnB,YAAY;IAEb"}
1
+ {"version":3,"file":"channels.d.cts","names":[],"sources":["../../src/host/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;;wBAmDgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;;;;;;;;;;;;;;;;iBAoDK;;EAEf,IAAI,GAAG,aAAa,aAAa,IAAI;;wBAGvB,uBAAuB;;YAiD3B,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;;EAER,QAAQ;EACR,eAAe;;EAEf,mBAAmB;;;;;EAKnB,YAAY;IAEb"}
@@ -1 +1 @@
1
- {"version":3,"file":"channels.d.mts","names":[],"sources":["../../src/host/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;;wBAkDgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;;;;;;;;;;;;;;;;iBAoDK;;EAEf,IAAI,GAAG,aAAa,aAAa,IAAI;;wBAGvB,uBAAuB;;YAmD3B,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;;EAER,QAAQ;EACR,eAAe;;EAEf,mBAAmB;;;;;EAKnB,YAAY;IAEb"}
1
+ {"version":3,"file":"channels.d.mts","names":[],"sources":["../../src/host/channels.ts"],"mappings":";;;;;;;;;;;;;;;;;;wBAmDgB,YAAY,KAAK,aAAa;;wBAK9B,kBACd,QAAQ,KAAK,uCACb,KAAK,aACJ,SAAS;;;;;;;;;;;;;;;;;iBAoDK;;EAEf,IAAI,GAAG,aAAa,aAAa,IAAI;;wBAGvB,uBAAuB;;YAiD3B,iBAAiB,KAC3B;wBAIc,mBACd,QAAQ,gBACR,KAAK,YACL;;EAEE,QAAQ;;EAER,QAAQ;EACR,eAAe;;EAEf,mBAAmB;;;;;EAKnB,YAAY;IAEb"}
@@ -11,6 +11,7 @@ import { DiscordWebhookSender, SlackWebhookSender } from "../webhooks.mjs";
11
11
  import { DEAD_TOKEN_REASONS as DEAD_TOKEN_REASONS$2, VapidSigner, WebPushSender } from "../webpush.mjs";
12
12
  import { removeDevice } from "./ingest.mjs";
13
13
  import { routeByChannel, routeByPlatform } from "./senders.mjs";
14
+ import { Buffer } from "node:buffer";
14
15
  //#region src/host/channels.ts
15
16
  /**
16
17
  * The outlets a `JourneyConfig` describes, assembled against the environment:
@@ -66,7 +67,7 @@ function identity(value) {
66
67
  /** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */
67
68
  async function secretDigest(secret) {
68
69
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(secret));
69
- return Array.from(new Uint8Array(digest).slice(0, 4), (b) => b.toString(16).padStart(2, "0")).join("");
70
+ return Buffer.from(digest.slice(0, 4)).toString("hex");
70
71
  }
71
72
  /**
72
73
  * A sender built on its first message, then kept for this call. `build` reads the secrets (and
@@ -1 +1 @@
1
- {"version":3,"file":"channels.mjs","names":["APNS_DEAD_TOKEN_REASONS","FCM_DEAD_TOKEN_REASONS","WEB_PUSH_DEAD_TOKEN_REASONS"],"sources":["../../src/host/channels.ts"],"sourcesContent":["import {\n DEAD_TOKEN_REASONS as APNS_DEAD_TOKEN_REASONS,\n ApnsProviderToken,\n ApnsPushSender,\n apnsKeyFromBase64,\n} from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport {\n DEAD_TOKEN_REASONS as FCM_DEAD_TOKEN_REASONS,\n FcmAccessToken,\n FcmPushSender,\n serviceAccountFromBase64,\n} from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport {\n VapidSigner,\n DEAD_TOKEN_REASONS as WEB_PUSH_DEAD_TOKEN_REASONS,\n WebPushSender,\n} from '../webpush';\nimport { removeDevice } from './ingest';\nimport type { JourneyEnv } from './platform';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/**\n * A sender that fails every message with the reason the channel cannot deliver — for good: the\n * configuration is the same on every attempt, so the step does not retry into it.\n */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new PermanentError(\n `cannot send '${message.template}' on ${message.channel}: ${reason}`\n );\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n // Missing on this attempt, missing on the next: the deployment has to change, not the step\n throw new PermanentError(\n `${name} is not set: the environment must hold it for the configured channel`\n );\n }\n return value;\n}\n\n/**\n * The provider credentials — `ApnsProviderToken`, `FcmAccessToken`, `VapidSigner` — for as long\n * as the host lives. `channelsFromConfig` runs once per workflow run (every wake of every\n * instance) and builds the senders anew each time; what must outlive the run is the credential\n * state they hold: the APNs provider JWT must be reused for at least 20 minutes (Apple answers a\n * sender that re-signs on every push with 429 `TooManyProviderTokenUpdates`), the FCM access\n * token is good for an hour, the VAPID key import is wasted work twice.\n *\n * The cache is the host's (`createJourneyHost` makes one and hands it to every run), not the\n * run's `env`: a Lambda invocation builds a fresh env object for each run (its instance port is\n * in it), so a cache keyed on env would miss on every wake. Inside, the key names the provider,\n * the ids that identify the key and a digest of the secret — so a rotated secret or a reloaded\n * host mints a new credential instead of reusing the old, and the secret itself is in no key and\n * no log. Nothing that shapes the sender (topic, environment, relay, registry, store) is in it:\n * the sender is built per call around the shared credential.\n */\nexport interface ProviderCache {\n /** The credential under `key`, built on the first call and returned as is after that. */\n get<T>(key: string, build: () => T): T;\n}\n\nexport function createProviderCache(): ProviderCache {\n const credentials = new Map<string, unknown>();\n return {\n get<T>(key: string, build: () => T): T {\n if (!credentials.has(key)) credentials.set(key, build());\n return credentials.get(key) as T;\n },\n };\n}\n\n/** A number for each fetch implementation, so a credential minted through one is not reused with another. */\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nfunction identity(value: object | undefined): string {\n if (value === undefined) return 'global';\n let id = identities.get(value);\n if (id === undefined) {\n id = nextIdentity++;\n identities.set(value, id);\n }\n return String(id);\n}\n\n/** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */\nasync function secretDigest(secret: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));\n return Array.from(new Uint8Array(digest).slice(0, 4), (b) =>\n b.toString(16).padStart(2, '0')\n ).join('');\n}\n\n/**\n * A sender built on its first message, then kept for this call. `build` reads the secrets (and\n * so may throw a `PermanentError` naming the missing variable): a platform's secret is only\n * required when something is actually sent there, so a journey that only emails must not fail\n * because the APNs key is missing from a local `.dev.vars`. A build that failed is not kept.\n */\nfunction lazy(build: () => Promise<MessageSender>): MessageSender {\n let sender: Promise<MessageSender> | undefined;\n return {\n async send(message) {\n sender ??= build().catch((error: unknown) => {\n sender = undefined;\n throw error;\n });\n return (await sender).send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n /**\n * Where the provider credentials live across runs (`createProviderCache`): the host's, made\n * once with it. Without one, this call's credentials are its own — nothing is reused.\n */\n providers?: ProviderCache | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n const providers = options.providers ?? createProviderCache();\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n /**\n * A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n * waits that read devices, as a removal through the API does. Done here, per run, on the\n * settled result, so the credential cached across runs (see `ProviderCache`) never holds a\n * run's store.\n */\n const dropDeadTokens = (\n provider: MessageSender,\n deadTokenReasons: ReadonlySet<string>\n ): MessageSender => ({\n async send(message) {\n const result = await provider.send(message);\n if (\n result !== undefined &&\n deadTokenReasons.has(result.reason) &&\n message.device !== undefined &&\n options.store !== undefined\n ) {\n await removeDevice(env, message.device.token, options.store);\n }\n return result;\n },\n });\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n const apnsFetch = options.apnsFetch ?? options.fetch;\n const environment = apnsEnvironment(apns, mode);\n platforms.ios = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'APNS_PRIVATE_KEY_BASE64');\n const credentials = providers.get(\n JSON.stringify(['apns', apns.keyId, apns.teamId, await secretDigest(secret)]),\n () =>\n new ApnsProviderToken({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(secret),\n })\n );\n return new ApnsPushSender({\n credentials,\n topic: apns.topic,\n environment,\n render,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(apnsFetch === undefined ? {} : { fetch: apnsFetch }),\n });\n }),\n APNS_DEAD_TOKEN_REASONS\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64');\n const serviceAccount = serviceAccountFromBase64(secret);\n // The token endpoint is called through the fetch: another fetch is another credential\n const credentials = providers.get(\n JSON.stringify([\n 'fcm',\n serviceAccount.client_email,\n serviceAccount.project_id,\n identity(options.fetch),\n await secretDigest(secret),\n ]),\n () =>\n new FcmAccessToken({\n serviceAccount,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n return new FcmPushSender({\n credentials,\n render,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n FCM_DEAD_TOKEN_REASONS\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = dropDeadTokens(\n lazy(async () => {\n const publicKey = requireSecret(env, 'VAPID_PUBLIC_KEY');\n const privateKey = requireSecret(env, 'VAPID_PRIVATE_KEY');\n const credentials = providers.get(\n JSON.stringify(['vapid', webPush.subject, publicKey, await secretDigest(privateKey)]),\n () => new VapidSigner({ subject: webPush.subject, publicKey, privateKey })\n );\n return new WebPushSender({\n credentials,\n render,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n WEB_PUSH_DEAD_TOKEN_REASONS\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new PermanentError(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"channels.mjs","names":["APNS_DEAD_TOKEN_REASONS","FCM_DEAD_TOKEN_REASONS","WEB_PUSH_DEAD_TOKEN_REASONS"],"sources":["../../src/host/channels.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport {\n DEAD_TOKEN_REASONS as APNS_DEAD_TOKEN_REASONS,\n ApnsProviderToken,\n ApnsPushSender,\n apnsKeyFromBase64,\n} from '../apns';\nimport { type JourneyConfig, apnsEnvironment } from '../config';\nimport { EmailSender, type EmailTransport } from '../email-sender';\nimport type { MessageSender } from '../engine/ports';\nimport { PermanentError } from '../engine/retry';\nimport {\n DEAD_TOKEN_REASONS as FCM_DEAD_TOKEN_REASONS,\n FcmAccessToken,\n FcmPushSender,\n serviceAccountFromBase64,\n} from '../fcm';\nimport { registryPushRenderer } from '../push';\nimport { registryRenderer } from '../react-email';\nimport { type JourneyMode, type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport type { JourneyStore } from '../store/index';\nimport {\n type DiscordTemplate,\n type SlackTemplate,\n indexTemplates,\n lookupTemplate,\n renderDiscordContent,\n renderSlackContent,\n} from '../templates';\nimport { DiscordWebhookSender, SlackWebhookSender } from '../webhooks';\nimport {\n VapidSigner,\n DEAD_TOKEN_REASONS as WEB_PUSH_DEAD_TOKEN_REASONS,\n WebPushSender,\n} from '../webpush';\nimport { removeDevice } from './ingest';\nimport type { JourneyEnv } from './platform';\nimport { routeByChannel, routeByPlatform } from './senders';\n\n/**\n * The outlets a `JourneyConfig` describes, assembled against the environment:\n * email through the host's transport (SES, Cloudflare Email, ...) with each template's addresses, iOS pushes\n * straight to APNs with the configured key (the `.p8` from\n * `APNS_PRIVATE_KEY_BASE64`), Android pushes through FCM with the service\n * account from `GOOGLE_APPLICATION_CREDENTIALS_BASE64`. A channel the config\n * leaves out is simply absent, and a journey that sends on it fails that step\n * with a message saying so — rather than the host having to spell out every\n * sender by hand.\n */\n\n/** Which config entry applies: wrangler's `ENVIRONMENT` var, production when unset. */\nexport function journeyMode(env: JourneyEnv): JourneyMode {\n return env.ENVIRONMENT ?? 'production';\n}\n\n/** The runtime options for this environment's mode. */\nexport function runtimeFromConfig(\n config: Pick<JourneyConfig, 'runtime'> | undefined,\n env: JourneyEnv\n): Required<JourneyRuntimeOptions> {\n return resolveRuntime(config?.runtime, journeyMode(env));\n}\n\n/**\n * A sender that fails every message with the reason the channel cannot deliver — for good: the\n * configuration is the same on every attempt, so the step does not retry into it.\n */\nfunction unavailable(reason: string): MessageSender {\n return {\n async send(message) {\n throw new PermanentError(\n `cannot send '${message.template}' on ${message.channel}: ${reason}`\n );\n },\n };\n}\n\nfunction requireSecret(\n env: JourneyEnv,\n name:\n | 'APNS_PRIVATE_KEY_BASE64'\n | 'GOOGLE_APPLICATION_CREDENTIALS_BASE64'\n | 'VAPID_PUBLIC_KEY'\n | 'VAPID_PRIVATE_KEY'\n): string {\n const value = env[name];\n if (typeof value !== 'string' || value.length === 0) {\n // Missing on this attempt, missing on the next: the deployment has to change, not the step\n throw new PermanentError(\n `${name} is not set: the environment must hold it for the configured channel`\n );\n }\n return value;\n}\n\n/**\n * The provider credentials — `ApnsProviderToken`, `FcmAccessToken`, `VapidSigner` — for as long\n * as the host lives. `channelsFromConfig` runs once per workflow run (every wake of every\n * instance) and builds the senders anew each time; what must outlive the run is the credential\n * state they hold: the APNs provider JWT must be reused for at least 20 minutes (Apple answers a\n * sender that re-signs on every push with 429 `TooManyProviderTokenUpdates`), the FCM access\n * token is good for an hour, the VAPID key import is wasted work twice.\n *\n * The cache is the host's (`createJourneyHost` makes one and hands it to every run), not the\n * run's `env`: a Lambda invocation builds a fresh env object for each run (its instance port is\n * in it), so a cache keyed on env would miss on every wake. Inside, the key names the provider,\n * the ids that identify the key and a digest of the secret — so a rotated secret or a reloaded\n * host mints a new credential instead of reusing the old, and the secret itself is in no key and\n * no log. Nothing that shapes the sender (topic, environment, relay, registry, store) is in it:\n * the sender is built per call around the shared credential.\n */\nexport interface ProviderCache {\n /** The credential under `key`, built on the first call and returned as is after that. */\n get<T>(key: string, build: () => T): T;\n}\n\nexport function createProviderCache(): ProviderCache {\n const credentials = new Map<string, unknown>();\n return {\n get<T>(key: string, build: () => T): T {\n if (!credentials.has(key)) credentials.set(key, build());\n return credentials.get(key) as T;\n },\n };\n}\n\n/** A number for each fetch implementation, so a credential minted through one is not reused with another. */\nconst identities = new WeakMap<object, number>();\nlet nextIdentity = 1;\nfunction identity(value: object | undefined): string {\n if (value === undefined) return 'global';\n let id = identities.get(value);\n if (id === undefined) {\n id = nextIdentity++;\n identities.set(value, id);\n }\n return String(id);\n}\n\n/** The first 8 hex of the secret's SHA-256: enough to tell a rotation apart, nothing of the secret. */\nasync function secretDigest(secret: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(secret));\n return Buffer.from(digest.slice(0, 4)).toString('hex');\n}\n\n/**\n * A sender built on its first message, then kept for this call. `build` reads the secrets (and\n * so may throw a `PermanentError` naming the missing variable): a platform's secret is only\n * required when something is actually sent there, so a journey that only emails must not fail\n * because the APNs key is missing from a local `.dev.vars`. A build that failed is not kept.\n */\nfunction lazy(build: () => Promise<MessageSender>): MessageSender {\n let sender: Promise<MessageSender> | undefined;\n return {\n async send(message) {\n sender ??= build().catch((error: unknown) => {\n sender = undefined;\n throw error;\n });\n return (await sender).send(message);\n },\n };\n}\n\n/** The channels slice of the config — what `channelsFromConfig` reads. */\nexport type ChannelsConfig = Pick<\n JourneyConfig,\n 'templates' | 'email' | 'apns' | 'fcm' | 'webPush' | 'runtime'\n>;\n\nexport function channelsFromConfig(\n config: ChannelsConfig,\n env: JourneyEnv,\n options: {\n /** The store a token APNs / FCM / a push service reports as gone is removed from. */\n store?: JourneyStore | undefined;\n /** Where finished emails go — the host's choice, independent of where the workflow runs. */\n email?: EmailTransport | undefined;\n fetch?: typeof fetch;\n /** APNs is HTTP/2 only: a Node host passes an HTTP/2 fetch here (its own fetch cannot). */\n apnsFetch?: typeof fetch | undefined;\n /**\n * Where the provider credentials live across runs (`createProviderCache`): the host's, made\n * once with it. Without one, this call's credentials are its own — nothing is reused.\n */\n providers?: ProviderCache | undefined;\n } = {}\n): MessageSender {\n const mode = journeyMode(env);\n const providers = options.providers ?? createProviderCache();\n\n const email = (): MessageSender => {\n const registry = config.templates?.email;\n if (registry === undefined) return unavailable('no email templates are registered');\n const transport = options.email;\n if (transport === undefined) {\n return unavailable(\"no email transport is configured (the host's `email` option)\");\n }\n // Each template names its own sender: there is no global one to require\n return new EmailSender({ transport, render: registryRenderer(registry, config.email) });\n };\n\n const push = (): MessageSender => {\n const registry = config.templates?.push;\n if (registry === undefined) return unavailable('no push templates are registered');\n const render = registryPushRenderer(registry);\n /**\n * A token the provider reports as gone is forgotten (nothing left to deliver to), waking the\n * waits that read devices, as a removal through the API does. Done here, per run, on the\n * settled result, so the credential cached across runs (see `ProviderCache`) never holds a\n * run's store.\n */\n const dropDeadTokens = (\n provider: MessageSender,\n deadTokenReasons: ReadonlySet<string>\n ): MessageSender => ({\n async send(message) {\n const result = await provider.send(message);\n if (\n result !== undefined &&\n deadTokenReasons.has(result.reason) &&\n message.device !== undefined &&\n options.store !== undefined\n ) {\n await removeDevice(env, message.device.token, options.store);\n }\n return result;\n },\n });\n const platforms: Record<string, MessageSender> = {};\n const apns = config.apns;\n if (apns !== undefined) {\n const apnsFetch = options.apnsFetch ?? options.fetch;\n const environment = apnsEnvironment(apns, mode);\n platforms.ios = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'APNS_PRIVATE_KEY_BASE64');\n const credentials = providers.get(\n JSON.stringify(['apns', apns.keyId, apns.teamId, await secretDigest(secret)]),\n () =>\n new ApnsProviderToken({\n keyId: apns.keyId,\n teamId: apns.teamId,\n privateKey: apnsKeyFromBase64(secret),\n })\n );\n return new ApnsPushSender({\n credentials,\n topic: apns.topic,\n environment,\n render,\n ...(env.APNS_RELAY_ORIGIN === undefined ? {} : { origin: env.APNS_RELAY_ORIGIN }),\n ...(apnsFetch === undefined ? {} : { fetch: apnsFetch }),\n });\n }),\n APNS_DEAD_TOKEN_REASONS\n );\n }\n const fcm = config.fcm;\n if (fcm !== undefined) {\n platforms.android = dropDeadTokens(\n lazy(async () => {\n const secret = requireSecret(env, 'GOOGLE_APPLICATION_CREDENTIALS_BASE64');\n const serviceAccount = serviceAccountFromBase64(secret);\n // The token endpoint is called through the fetch: another fetch is another credential\n const credentials = providers.get(\n JSON.stringify([\n 'fcm',\n serviceAccount.client_email,\n serviceAccount.project_id,\n identity(options.fetch),\n await secretDigest(secret),\n ]),\n () =>\n new FcmAccessToken({\n serviceAccount,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n })\n );\n return new FcmPushSender({\n credentials,\n render,\n ...(fcm.androidChannelId === undefined\n ? {}\n : { android: { channel_id: fcm.androidChannelId } }),\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n FCM_DEAD_TOKEN_REASONS\n );\n }\n const webPush = config.webPush;\n if (webPush !== undefined) {\n platforms.web = dropDeadTokens(\n lazy(async () => {\n const publicKey = requireSecret(env, 'VAPID_PUBLIC_KEY');\n const privateKey = requireSecret(env, 'VAPID_PRIVATE_KEY');\n const credentials = providers.get(\n JSON.stringify(['vapid', webPush.subject, publicKey, await secretDigest(privateKey)]),\n () => new VapidSigner({ subject: webPush.subject, publicKey, privateKey })\n );\n return new WebPushSender({\n credentials,\n render,\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n }),\n WEB_PUSH_DEAD_TOKEN_REASONS\n );\n }\n if (Object.keys(platforms).length === 0)\n return unavailable('none of apns, fcm or webPush is configured');\n return routeByPlatform(platforms);\n };\n\n /**\n * A webhook template's URL, read from the environment the Worker validated at startup (its\n * `env` schema): a template whose variable is unset fails the step, naming the template.\n */\n const webhookUrl = (channel: 'discord' | 'slack', template: DiscordTemplate | SlackTemplate) => {\n const url = template.webhook(env as unknown as Record<string, unknown>);\n if (typeof url !== 'string' || url === '') {\n throw new PermanentError(\n `${channel} template '${template.id}': its webhook URL is not set in the environment`\n );\n }\n return url;\n };\n\n const discord = (): MessageSender => {\n const registry = config.templates?.discord;\n if (registry === undefined) return unavailable('no discord templates are registered');\n const index = indexTemplates('discord', registry);\n return new DiscordWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'discord', id);\n return {\n url: webhookUrl('discord', template),\n content: renderDiscordContent(template, props),\n };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n const slack = (): MessageSender => {\n const registry = config.templates?.slack;\n if (registry === undefined) return unavailable('no slack templates are registered');\n const index = indexTemplates('slack', registry);\n return new SlackWebhookSender({\n render: (id, props) => {\n const template = lookupTemplate(index, 'slack', id);\n return { url: webhookUrl('slack', template), content: renderSlackContent(template, props) };\n },\n ...(options.fetch === undefined ? {} : { fetch: options.fetch }),\n });\n };\n\n return routeByChannel({ email: email(), push: push(), discord: discord(), slack: slack() });\n}\n"],"mappings":"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"}
package/dist/jwt.cjs CHANGED
@@ -1,19 +1,15 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_buffer = require("node:buffer");
2
3
  //#region src/jwt.ts
3
4
  /**
4
- * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT
5
+ * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT
5
6
  * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.
6
7
  */
7
- const base64url = (bytes) => {
8
- const raw = typeof bytes === "string" ? bytes : String.fromCharCode(...bytes);
9
- return btoa(raw).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
10
- };
8
+ const base64url = (bytes) => node_buffer.Buffer.from(bytes).toString("base64url");
11
9
  function pemToDer(pem) {
12
10
  const body = pem.replace(/-----[A-Z ]+-----/g, "").replace(/\s+/g, "");
13
- const binary = atob(body);
14
- const bytes = new Uint8Array(binary.length);
15
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
16
- return bytes.buffer;
11
+ const der = node_buffer.Buffer.from(body, "base64");
12
+ return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);
17
13
  }
18
14
  //#endregion
19
15
  exports.base64url = base64url;
package/dist/jwt.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.cjs","names":[],"sources":["../src/jwt.ts"],"sourcesContent":["/**\n * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT\n * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.\n */\n\nexport const base64url = (bytes: Uint8Array | string): string => {\n const raw = typeof bytes === 'string' ? bytes : String.fromCharCode(...bytes);\n return btoa(raw).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n};\n\nexport function pemToDer(pem: string): ArrayBuffer {\n const body = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s+/g, '');\n const binary = atob(body);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes.buffer;\n}\n"],"mappings":";;;;;;AAKA,MAAa,aAAa,UAAuC;CAC/D,MAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,OAAO,aAAa,GAAG,KAAK;CAC5E,OAAO,KAAK,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;AAC5E;AAEA,SAAgB,SAAS,KAA0B;CACjD,MAAM,OAAO,IAAI,QAAQ,sBAAsB,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACrE,MAAM,SAAS,KAAK,IAAI;CACxB,MAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;CAC1C,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,MAAM,KAAK,OAAO,WAAW,CAAC;CACtE,OAAO,MAAM;AACf"}
1
+ {"version":3,"file":"jwt.cjs","names":["Buffer"],"sources":["../src/jwt.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n\n/**\n * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT\n * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.\n */\n\nexport const base64url = (bytes: Uint8Array | string): string =>\n Buffer.from(bytes).toString('base64url');\n\nexport function pemToDer(pem: string): ArrayBuffer {\n const body = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s+/g, '');\n const der = Buffer.from(body, 'base64');\n return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);\n}\n"],"mappings":";;;;;;;AAOA,MAAa,aAAa,UACxBA,YAAAA,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,WAAW;AAEzC,SAAgB,SAAS,KAA0B;CACjD,MAAM,OAAO,IAAI,QAAQ,sBAAsB,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACrE,MAAM,MAAMA,YAAAA,OAAO,KAAK,MAAM,QAAQ;CACtC,OAAO,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU;AACzE"}
package/dist/jwt.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/jwt.d.ts
2
2
  /**
3
- * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT
3
+ * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT
4
4
  * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.
5
5
  */
6
6
  export declare const base64url: (bytes: Uint8Array | string) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.cts","names":[],"sources":["../src/jwt.ts"],"mappings":";;;;;qBAKa,YAAS,OAAW;wBAKjB,SAAS,cAAc"}
1
+ {"version":3,"file":"jwt.d.cts","names":[],"sources":["../src/jwt.ts"],"mappings":";;;;;qBAOa,YAAS,OAAW;wBAGjB,SAAS,cAAc"}
package/dist/jwt.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  //#region src/jwt.d.ts
2
2
  /**
3
- * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT
3
+ * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT
4
4
  * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.
5
5
  */
6
6
  export declare const base64url: (bytes: Uint8Array | string) => string;
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.mts","names":[],"sources":["../src/jwt.ts"],"mappings":";;;;;qBAKa,YAAS,OAAW;wBAKjB,SAAS,cAAc"}
1
+ {"version":3,"file":"jwt.d.mts","names":[],"sources":["../src/jwt.ts"],"mappings":";;;;;qBAOa,YAAS,OAAW;wBAGjB,SAAS,cAAc"}
package/dist/jwt.mjs CHANGED
@@ -1,18 +1,14 @@
1
+ import { Buffer } from "node:buffer";
1
2
  //#region src/jwt.ts
2
3
  /**
3
- * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT
4
+ * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT
4
5
  * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.
5
6
  */
6
- const base64url = (bytes) => {
7
- const raw = typeof bytes === "string" ? bytes : String.fromCharCode(...bytes);
8
- return btoa(raw).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9
- };
7
+ const base64url = (bytes) => Buffer.from(bytes).toString("base64url");
10
8
  function pemToDer(pem) {
11
9
  const body = pem.replace(/-----[A-Z ]+-----/g, "").replace(/\s+/g, "");
12
- const binary = atob(body);
13
- const bytes = new Uint8Array(binary.length);
14
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
15
- return bytes.buffer;
10
+ const der = Buffer.from(body, "base64");
11
+ return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);
16
12
  }
17
13
  //#endregion
18
14
  export { base64url, pemToDer };
package/dist/jwt.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.mjs","names":[],"sources":["../src/jwt.ts"],"sourcesContent":["/**\n * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`): JWT\n * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.\n */\n\nexport const base64url = (bytes: Uint8Array | string): string => {\n const raw = typeof bytes === 'string' ? bytes : String.fromCharCode(...bytes);\n return btoa(raw).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n};\n\nexport function pemToDer(pem: string): ArrayBuffer {\n const body = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s+/g, '');\n const binary = atob(body);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);\n return bytes.buffer;\n}\n"],"mappings":";;;;;AAKA,MAAa,aAAa,UAAuC;CAC/D,MAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,OAAO,aAAa,GAAG,KAAK;CAC5E,OAAO,KAAK,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,GAAG,CAAC,CAAC,QAAQ,OAAO,EAAE;AAC5E;AAEA,SAAgB,SAAS,KAA0B;CACjD,MAAM,OAAO,IAAI,QAAQ,sBAAsB,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACrE,MAAM,SAAS,KAAK,IAAI;CACxB,MAAM,QAAQ,IAAI,WAAW,OAAO,MAAM;CAC1C,KAAK,IAAI,IAAI,GAAG,IAAI,OAAO,QAAQ,KAAK,MAAM,KAAK,OAAO,WAAW,CAAC;CACtE,OAAO,MAAM;AACf"}
1
+ {"version":3,"file":"jwt.mjs","names":[],"sources":["../src/jwt.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n\n/**\n * The byte-level helpers the token signers share (`fcm.ts`, `apns.ts`, `webpush.ts`): JWT\n * base64url and PEM → DER for WebCrypto's `importKey('pkcs8', …)`.\n */\n\nexport const base64url = (bytes: Uint8Array | string): string =>\n Buffer.from(bytes).toString('base64url');\n\nexport function pemToDer(pem: string): ArrayBuffer {\n const body = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\\s+/g, '');\n const der = Buffer.from(body, 'base64');\n return der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength);\n}\n"],"mappings":";;;;;;AAOA,MAAa,aAAa,UACxB,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,WAAW;AAEzC,SAAgB,SAAS,KAA0B;CACjD,MAAM,OAAO,IAAI,QAAQ,sBAAsB,EAAE,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACrE,MAAM,MAAM,OAAO,KAAK,MAAM,QAAQ;CACtC,OAAO,IAAI,OAAO,MAAM,IAAI,YAAY,IAAI,aAAa,IAAI,UAAU;AACzE"}
@@ -1,4 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_buffer = require("node:buffer");
2
3
  //#region src/store/sql.ts
3
4
  /**
4
5
  * UUID v7 (time-ordered) for event rows, generated here so the store has no
@@ -15,7 +16,7 @@ function uuidv7(now = Date.now()) {
15
16
  bytes[5] = now & 255;
16
17
  bytes[6] = bytes[6] & 15 | 112;
17
18
  bytes[8] = bytes[8] & 63 | 128;
18
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
19
+ const hex = node_buffer.Buffer.from(bytes).toString("hex");
19
20
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
20
21
  }
21
22
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"sql.cjs","names":[],"sources":["../../src/store/sql.ts"],"sourcesContent":["/**\n * The smallest SQL client this store needs: parameterized queries (`$1`, `$2`,\n * …) and a transaction scope. It is deliberately not postgres.js, pg or\n * drizzle — the app wraps whichever it already uses in a few lines:\n *\n * const client: SqlClientLike = {\n * query: (text, params) => sql.unsafe(text, params), // postgres.js\n * transaction: (fn) => sql.begin((tx) => fn(wrap(tx))),\n * };\n *\n * jsonb columns may come back parsed (postgres.js, pg) or as strings (some\n * drivers); both are accepted.\n */\nexport interface SqlClientLike {\n query<T = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<T[]>;\n transaction<T>(fn: (tx: SqlClientLike) => Promise<T>): Promise<T>;\n}\n\n/**\n * UUID v7 (time-ordered) for event rows, generated here so the store has no\n * dependency: 48-bit ms timestamp, version 7, RFC 4122 variant, 74 random bits.\n */\nexport function uuidv7(now = Date.now()): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[0] = (now / 2 ** 40) & 0xff;\n bytes[1] = (now / 2 ** 32) & 0xff;\n bytes[2] = (now / 2 ** 24) & 0xff;\n bytes[3] = (now / 2 ** 16) & 0xff;\n bytes[4] = (now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n"],"mappings":";;;;;;AAsBA,SAAgB,OAAO,MAAM,KAAK,IAAI,GAAW;CAC/C,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAC/B,OAAO,gBAAgB,KAAK;CAC5B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,MAAU;CAC5B,MAAM,KAAK,MAAM;CACjB,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;CAC1E,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE;AACzG"}
1
+ {"version":3,"file":"sql.cjs","names":["Buffer"],"sources":["../../src/store/sql.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n\n/**\n * The smallest SQL client this store needs: parameterized queries (`$1`, `$2`,\n * …) and a transaction scope. It is deliberately not postgres.js, pg or\n * drizzle — the app wraps whichever it already uses in a few lines:\n *\n * const client: SqlClientLike = {\n * query: (text, params) => sql.unsafe(text, params), // postgres.js\n * transaction: (fn) => sql.begin((tx) => fn(wrap(tx))),\n * };\n *\n * jsonb columns may come back parsed (postgres.js, pg) or as strings (some\n * drivers); both are accepted.\n */\nexport interface SqlClientLike {\n query<T = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<T[]>;\n transaction<T>(fn: (tx: SqlClientLike) => Promise<T>): Promise<T>;\n}\n\n/**\n * UUID v7 (time-ordered) for event rows, generated here so the store has no\n * dependency: 48-bit ms timestamp, version 7, RFC 4122 variant, 74 random bits.\n */\nexport function uuidv7(now = Date.now()): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[0] = (now / 2 ** 40) & 0xff;\n bytes[1] = (now / 2 ** 32) & 0xff;\n bytes[2] = (now / 2 ** 24) & 0xff;\n bytes[3] = (now / 2 ** 16) & 0xff;\n bytes[4] = (now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Buffer.from(bytes).toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n"],"mappings":";;;;;;;AAwBA,SAAgB,OAAO,MAAM,KAAK,IAAI,GAAW;CAC/C,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAC/B,OAAO,gBAAgB,KAAK;CAC5B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,MAAU;CAC5B,MAAM,KAAK,MAAM;CACjB,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,MAAMA,YAAAA,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,KAAK;CAC7C,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE;AACzG"}
@@ -1 +1 @@
1
- {"version":3,"file":"sql.d.cts","names":[],"sources":["../../src/store/sql.ts"],"mappings":";;;;;;;;;;;;;;iBAaiB;EACf,MAAM,IAAI,yBAAyB,cAAc,8BAA8B,QAAQ;EACvF,YAAY,GAAG,KAAK,IAAI,kBAAkB,QAAQ,KAAK,QAAQ;;;;;;wBAOjD,OAAO"}
1
+ {"version":3,"file":"sql.d.cts","names":[],"sources":["../../src/store/sql.ts"],"mappings":";;;;;;;;;;;;;;iBAeiB;EACf,MAAM,IAAI,yBAAyB,cAAc,8BAA8B,QAAQ;EACvF,YAAY,GAAG,KAAK,IAAI,kBAAkB,QAAQ,KAAK,QAAQ;;;;;;wBAOjD,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"sql.d.mts","names":[],"sources":["../../src/store/sql.ts"],"mappings":";;;;;;;;;;;;;;iBAaiB;EACf,MAAM,IAAI,yBAAyB,cAAc,8BAA8B,QAAQ;EACvF,YAAY,GAAG,KAAK,IAAI,kBAAkB,QAAQ,KAAK,QAAQ;;;;;;wBAOjD,OAAO"}
1
+ {"version":3,"file":"sql.d.mts","names":[],"sources":["../../src/store/sql.ts"],"mappings":";;;;;;;;;;;;;;iBAeiB;EACf,MAAM,IAAI,yBAAyB,cAAc,8BAA8B,QAAQ;EACvF,YAAY,GAAG,KAAK,IAAI,kBAAkB,QAAQ,KAAK,QAAQ;;;;;;wBAOjD,OAAO"}
@@ -1,3 +1,4 @@
1
+ import { Buffer } from "node:buffer";
1
2
  //#region src/store/sql.ts
2
3
  /**
3
4
  * UUID v7 (time-ordered) for event rows, generated here so the store has no
@@ -14,7 +15,7 @@ function uuidv7(now = Date.now()) {
14
15
  bytes[5] = now & 255;
15
16
  bytes[6] = bytes[6] & 15 | 112;
16
17
  bytes[8] = bytes[8] & 63 | 128;
17
- const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
18
+ const hex = Buffer.from(bytes).toString("hex");
18
19
  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
19
20
  }
20
21
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"sql.mjs","names":[],"sources":["../../src/store/sql.ts"],"sourcesContent":["/**\n * The smallest SQL client this store needs: parameterized queries (`$1`, `$2`,\n * …) and a transaction scope. It is deliberately not postgres.js, pg or\n * drizzle — the app wraps whichever it already uses in a few lines:\n *\n * const client: SqlClientLike = {\n * query: (text, params) => sql.unsafe(text, params), // postgres.js\n * transaction: (fn) => sql.begin((tx) => fn(wrap(tx))),\n * };\n *\n * jsonb columns may come back parsed (postgres.js, pg) or as strings (some\n * drivers); both are accepted.\n */\nexport interface SqlClientLike {\n query<T = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<T[]>;\n transaction<T>(fn: (tx: SqlClientLike) => Promise<T>): Promise<T>;\n}\n\n/**\n * UUID v7 (time-ordered) for event rows, generated here so the store has no\n * dependency: 48-bit ms timestamp, version 7, RFC 4122 variant, 74 random bits.\n */\nexport function uuidv7(now = Date.now()): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[0] = (now / 2 ** 40) & 0xff;\n bytes[1] = (now / 2 ** 32) & 0xff;\n bytes[2] = (now / 2 ** 24) & 0xff;\n bytes[3] = (now / 2 ** 16) & 0xff;\n bytes[4] = (now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = [...bytes].map((b) => b.toString(16).padStart(2, '0')).join('');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n"],"mappings":";;;;;AAsBA,SAAgB,OAAO,MAAM,KAAK,IAAI,GAAW;CAC/C,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAC/B,OAAO,gBAAgB,KAAK;CAC5B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,MAAU;CAC5B,MAAM,KAAK,MAAM;CACjB,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,SAAS,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE;CAC1E,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE;AACzG"}
1
+ {"version":3,"file":"sql.mjs","names":[],"sources":["../../src/store/sql.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n\n/**\n * The smallest SQL client this store needs: parameterized queries (`$1`, `$2`,\n * …) and a transaction scope. It is deliberately not postgres.js, pg or\n * drizzle — the app wraps whichever it already uses in a few lines:\n *\n * const client: SqlClientLike = {\n * query: (text, params) => sql.unsafe(text, params), // postgres.js\n * transaction: (fn) => sql.begin((tx) => fn(wrap(tx))),\n * };\n *\n * jsonb columns may come back parsed (postgres.js, pg) or as strings (some\n * drivers); both are accepted.\n */\nexport interface SqlClientLike {\n query<T = Record<string, unknown>>(text: string, params?: readonly unknown[]): Promise<T[]>;\n transaction<T>(fn: (tx: SqlClientLike) => Promise<T>): Promise<T>;\n}\n\n/**\n * UUID v7 (time-ordered) for event rows, generated here so the store has no\n * dependency: 48-bit ms timestamp, version 7, RFC 4122 variant, 74 random bits.\n */\nexport function uuidv7(now = Date.now()): string {\n const bytes = new Uint8Array(16);\n crypto.getRandomValues(bytes);\n bytes[0] = (now / 2 ** 40) & 0xff;\n bytes[1] = (now / 2 ** 32) & 0xff;\n bytes[2] = (now / 2 ** 24) & 0xff;\n bytes[3] = (now / 2 ** 16) & 0xff;\n bytes[4] = (now / 2 ** 8) & 0xff;\n bytes[5] = now & 0xff;\n bytes[6] = (bytes[6] & 0x0f) | 0x70;\n bytes[8] = (bytes[8] & 0x3f) | 0x80;\n const hex = Buffer.from(bytes).toString('hex');\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n"],"mappings":";;;;;;AAwBA,SAAgB,OAAO,MAAM,KAAK,IAAI,GAAW;CAC/C,MAAM,wBAAQ,IAAI,WAAW,EAAE;CAC/B,OAAO,gBAAgB,KAAK;CAC5B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,KAAK,KAAM;CAC7B,MAAM,KAAM,MAAM,MAAU;CAC5B,MAAM,KAAK,MAAM;CACjB,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,KAAM,MAAM,KAAK,KAAQ;CAC/B,MAAM,MAAM,OAAO,KAAK,KAAK,CAAC,CAAC,SAAS,KAAK;CAC7C,OAAO,GAAG,IAAI,MAAM,GAAG,CAAC,EAAE,GAAG,IAAI,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,IAAI,EAAE,EAAE,GAAG,IAAI,MAAM,EAAE;AACzG"}
@@ -1,9 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let node_buffer = require("node:buffer");
2
3
  //#region src/transports/sigv4.ts
3
4
  const utf8 = new TextEncoder();
4
- function hex(bytes) {
5
- return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
6
- }
5
+ const hex = (bytes) => node_buffer.Buffer.from(bytes).toString("hex");
7
6
  async function sha256(data) {
8
7
  return hex(await crypto.subtle.digest("SHA-256", utf8.encode(data)));
9
8
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sigv4.cjs","names":[],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/**\n * Each path segment encoded once (the URL already decoded it). SigV4 asks non-S3 services to\n * encode the path twice; for the fixed, plain paths signed here (SES's\n * `/v2/email/outbound-emails`) the two agree. A path with reserved characters would need the\n * second pass before this signer could be used for it.\n */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sigv4.cjs","names":["Buffer"],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nconst hex = (bytes: ArrayBuffer): string => Buffer.from(bytes).toString('hex');\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/**\n * Each path segment encoded once (the URL already decoded it). SigV4 asks non-S3 services to\n * encode the path twice; for the fixed, plain paths signed here (SES's\n * `/v2/email/outbound-emails`) the two agree. A path with reserved characters would need the\n * second pass before this signer could be used for it.\n */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"sigv4.d.cts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAOiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAsEoB,YAAY,SAAS,eAAe,QAAQ"}
1
+ {"version":3,"file":"sigv4.d.cts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAQiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAoEoB,YAAY,SAAS,eAAe,QAAQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"sigv4.d.mts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAOiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAsEoB,YAAY,SAAS,eAAe,QAAQ"}
1
+ {"version":3,"file":"sigv4.d.mts","names":[],"sources":["../../src/transports/sigv4.ts"],"mappings":";;;;;;;iBAQiB;EACf;EACA;EACA;;iBAGe;EACf;EACA;EACA,UAAU;EACV;EACA,aAAa;EACb;EACA;;EAEA;;;wBAoEoB,YAAY,SAAS,eAAe,QAAQ"}
@@ -1,8 +1,7 @@
1
+ import { Buffer } from "node:buffer";
1
2
  //#region src/transports/sigv4.ts
2
3
  const utf8 = new TextEncoder();
3
- function hex(bytes) {
4
- return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
5
- }
4
+ const hex = (bytes) => Buffer.from(bytes).toString("hex");
6
5
  async function sha256(data) {
7
6
  return hex(await crypto.subtle.digest("SHA-256", utf8.encode(data)));
8
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sigv4.mjs","names":[],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nfunction hex(bytes: ArrayBuffer): string {\n return [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, '0')).join('');\n}\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/**\n * Each path segment encoded once (the URL already decoded it). SigV4 asks non-S3 services to\n * encode the path twice; for the fixed, plain paths signed here (SES's\n * `/v2/email/outbound-emails`) the two agree. A path with reserved characters would need the\n * second pass before this signer could be used for it.\n */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sigv4.mjs","names":[],"sources":["../../src/transports/sigv4.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\n/**\n * AWS Signature Version 4 over WebCrypto, which a Worker and Node both have: enough to sign\n * the JSON API calls the transports make. Headers are signed as given (plus `host` and\n * `x-amz-date`); the payload hash is the body's SHA-256.\n * https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html\n */\n\nexport interface SigV4Credentials {\n accessKeyId: string;\n secretAccessKey: string;\n sessionToken?: string | undefined;\n}\n\nexport interface SigV4Request {\n method: string;\n url: string;\n headers?: Record<string, string>;\n body?: string;\n credentials: SigV4Credentials;\n region: string;\n service: string;\n /** `YYYYMMDD'T'HHMMSS'Z'`; now by default. */\n datetime?: string;\n}\n\nconst utf8 = new TextEncoder();\n\nconst hex = (bytes: ArrayBuffer): string => Buffer.from(bytes).toString('hex');\n\nasync function sha256(data: string): Promise<string> {\n return hex(await crypto.subtle.digest('SHA-256', utf8.encode(data)));\n}\n\nasync function hmac(\n key: ArrayBuffer | Uint8Array<ArrayBuffer>,\n data: string\n): Promise<ArrayBuffer> {\n const imported = await crypto.subtle.importKey(\n 'raw',\n key,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n );\n return crypto.subtle.sign('HMAC', imported, utf8.encode(data));\n}\n\n/** RFC 3986 encoding, as SigV4 wants it: only unreserved characters stay. */\nfunction encode(value: string): string {\n return encodeURIComponent(value).replace(\n /[!'()*]/g,\n (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`\n );\n}\n\n/**\n * Each path segment encoded once (the URL already decoded it). SigV4 asks non-S3 services to\n * encode the path twice; for the fixed, plain paths signed here (SES's\n * `/v2/email/outbound-emails`) the two agree. A path with reserved characters would need the\n * second pass before this signer could be used for it.\n */\nfunction canonicalPath(pathname: string): string {\n return pathname\n .split('/')\n .map((segment) => encode(decodeURIComponent(segment)))\n .join('/');\n}\n\n/**\n * The query as sent, each name and value re-encoded. Parsed by hand: URLSearchParams reads `+`\n * as a space, which would sign a value other than the one on the wire.\n */\nfunction canonicalQuery(search: string): string {\n if (search === '' || search === '?') return '';\n return search\n .slice(1)\n .split('&')\n .filter((pair) => pair !== '')\n .map((pair) => {\n const at = pair.indexOf('=');\n const name = at === -1 ? pair : pair.slice(0, at);\n const value = at === -1 ? '' : pair.slice(at + 1);\n return [encode(decodeURIComponent(name)), encode(decodeURIComponent(value))] as const;\n })\n .sort(([a, av], [b, bv]) => (a === b ? (av < bv ? -1 : av > bv ? 1 : 0) : a < b ? -1 : 1))\n .map(([name, value]) => `${name}=${value}`)\n .join('&');\n}\n\n/** The request's headers with `authorization` (and `x-amz-date`, the session token) added. */\nexport async function signRequest(request: SigV4Request): Promise<Record<string, string>> {\n const url = new URL(request.url);\n const datetime = request.datetime ?? new Date().toISOString().replace(/[:-]|\\.\\d{3}/g, '');\n const date = datetime.slice(0, 8);\n const { accessKeyId, secretAccessKey, sessionToken } = request.credentials;\n\n const headers: Record<string, string> = {};\n for (const [name, value] of Object.entries(request.headers ?? {})) {\n headers[name.toLowerCase()] = value.trim().replace(/\\s+/g, ' ');\n }\n headers.host = url.host;\n headers['x-amz-date'] = datetime;\n if (sessionToken !== undefined) headers['x-amz-security-token'] = sessionToken;\n\n const names = Object.keys(headers).sort();\n const signedHeaders = names.join(';');\n const canonical = [\n request.method.toUpperCase(),\n canonicalPath(url.pathname),\n canonicalQuery(url.search),\n ...names.map((name) => `${name}:${headers[name]}`),\n '',\n signedHeaders,\n await sha256(request.body ?? ''),\n ].join('\\n');\n\n const scope = `${date}/${request.region}/${request.service}/aws4_request`;\n const toSign = ['AWS4-HMAC-SHA256', datetime, scope, await sha256(canonical)].join('\\n');\n\n let key = await hmac(utf8.encode(`AWS4${secretAccessKey}`), date);\n for (const part of [request.region, request.service, 'aws4_request']) {\n key = await hmac(key, part);\n }\n const signature = hex(await hmac(key, toSign));\n\n const { host: _host, ...sent } = headers;\n return {\n ...sent,\n authorization: `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, SignedHeaders=${signedHeaders}, Signature=${signature}`,\n };\n}\n"],"mappings":"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"}
package/dist/webpush.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_engine_retry = require("./engine/retry.cjs");
3
3
  const require_jwt = require("./jwt.cjs");
4
+ let node_buffer = require("node:buffer");
4
5
  //#region src/webpush.ts
5
6
  /**
6
7
  * How a push service's answer is classified (RFC 8030 §7 and §8.1, with the browsers' services
@@ -44,28 +45,18 @@ const MAX_PLAINTEXT_BYTES = 3993;
44
45
  const encoder = new TextEncoder();
45
46
  const utf8 = (text) => encoder.encode(text);
46
47
  function fromBase64url(text) {
47
- const binary = atob(text.replace(/-/g, "+").replace(/_/g, "/"));
48
- return Uint8Array.from(binary, (char) => char.charCodeAt(0));
48
+ return new Uint8Array(node_buffer.Buffer.from(text, "base64url"));
49
49
  }
50
- function concat(...parts) {
51
- const out = new Uint8Array(parts.reduce((n, part) => n + part.length, 0));
52
- let offset = 0;
53
- for (const part of parts) {
54
- out.set(part, offset);
55
- offset += part.length;
56
- }
57
- return out;
58
- }
59
- async function hmac(key, data) {
60
- const imported = await crypto.subtle.importKey("raw", key, {
61
- name: "HMAC",
62
- hash: "SHA-256"
63
- }, false, ["sign"]);
64
- return new Uint8Array(await crypto.subtle.sign("HMAC", imported, data));
65
- }
66
- /** HKDF-SHA256 with a single output block (every length here is ≤ 32 bytes). */
50
+ const concat = (...parts) => new Uint8Array(node_buffer.Buffer.concat(parts));
51
+ /** HKDF-SHA256 (RFC 5869), as WebCrypto provides it. */
67
52
  async function hkdf(salt, ikm, info, length) {
68
- return (await hmac(await hmac(salt, ikm), concat(info, new Uint8Array([1])))).slice(0, length);
53
+ const key = await crypto.subtle.importKey("raw", ikm, "HKDF", false, ["deriveBits"]);
54
+ return new Uint8Array(await crypto.subtle.deriveBits({
55
+ name: "HKDF",
56
+ hash: "SHA-256",
57
+ salt,
58
+ info
59
+ }, key, length * 8));
69
60
  }
70
61
  /**
71
62
  * The `aes128gcm` body for one subscription (RFC 8291 §3.4): salt, record size, the sender's