@shware/workflow 4.0.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 (88) hide show
  1. package/README.md +141 -13
  2. package/dist/apns.cjs +3 -2
  3. package/dist/apns.cjs.map +1 -1
  4. package/dist/apns.d.cts.map +1 -1
  5. package/dist/apns.d.mts.map +1 -1
  6. package/dist/apns.mjs +3 -2
  7. package/dist/apns.mjs.map +1 -1
  8. package/dist/aws/function-url.cjs +3 -11
  9. package/dist/aws/function-url.cjs.map +1 -1
  10. package/dist/aws/function-url.d.cts.map +1 -1
  11. package/dist/aws/function-url.d.mts.map +1 -1
  12. package/dist/aws/function-url.mjs +3 -11
  13. package/dist/aws/function-url.mjs.map +1 -1
  14. package/dist/aws/index.cjs +3 -0
  15. package/dist/aws/index.cjs.map +1 -1
  16. package/dist/aws/index.d.cts +2 -1
  17. package/dist/aws/index.d.cts.map +1 -1
  18. package/dist/aws/index.d.mts +2 -1
  19. package/dist/aws/index.d.mts.map +1 -1
  20. package/dist/aws/index.mjs +2 -1
  21. package/dist/aws/index.mjs.map +1 -1
  22. package/dist/cloudflare/index.cjs +1 -0
  23. package/dist/cloudflare/index.d.cts +2 -2
  24. package/dist/cloudflare/index.d.mts +2 -2
  25. package/dist/cloudflare/index.mjs +2 -2
  26. package/dist/fcm.cjs +2 -1
  27. package/dist/fcm.cjs.map +1 -1
  28. package/dist/fcm.d.cts.map +1 -1
  29. package/dist/fcm.d.mts.map +1 -1
  30. package/dist/fcm.mjs +2 -1
  31. package/dist/fcm.mjs.map +1 -1
  32. package/dist/host/auth.cjs +15 -15
  33. package/dist/host/auth.cjs.map +1 -1
  34. package/dist/host/auth.d.cts +16 -2
  35. package/dist/host/auth.d.cts.map +1 -1
  36. package/dist/host/auth.d.mts +16 -2
  37. package/dist/host/auth.d.mts.map +1 -1
  38. package/dist/host/auth.mjs +15 -16
  39. package/dist/host/auth.mjs.map +1 -1
  40. package/dist/host/channels.cjs +2 -1
  41. package/dist/host/channels.cjs.map +1 -1
  42. package/dist/host/channels.d.cts.map +1 -1
  43. package/dist/host/channels.d.mts.map +1 -1
  44. package/dist/host/channels.mjs +2 -1
  45. package/dist/host/channels.mjs.map +1 -1
  46. package/dist/host/http.cjs +37 -18
  47. package/dist/host/http.cjs.map +1 -1
  48. package/dist/host/http.d.cts +5 -1
  49. package/dist/host/http.d.cts.map +1 -1
  50. package/dist/host/http.d.mts +5 -1
  51. package/dist/host/http.d.mts.map +1 -1
  52. package/dist/host/http.mjs +37 -18
  53. package/dist/host/http.mjs.map +1 -1
  54. package/dist/host/journey-host.cjs +1 -0
  55. package/dist/host/journey-host.cjs.map +1 -1
  56. package/dist/host/journey-host.d.cts +12 -0
  57. package/dist/host/journey-host.d.cts.map +1 -1
  58. package/dist/host/journey-host.d.mts +12 -0
  59. package/dist/host/journey-host.d.mts.map +1 -1
  60. package/dist/host/journey-host.mjs +1 -0
  61. package/dist/host/journey-host.mjs.map +1 -1
  62. package/dist/jwt.cjs +5 -9
  63. package/dist/jwt.cjs.map +1 -1
  64. package/dist/jwt.d.cts +1 -1
  65. package/dist/jwt.d.cts.map +1 -1
  66. package/dist/jwt.d.mts +1 -1
  67. package/dist/jwt.d.mts.map +1 -1
  68. package/dist/jwt.mjs +5 -9
  69. package/dist/jwt.mjs.map +1 -1
  70. package/dist/store/sql.cjs +2 -1
  71. package/dist/store/sql.cjs.map +1 -1
  72. package/dist/store/sql.d.cts.map +1 -1
  73. package/dist/store/sql.d.mts.map +1 -1
  74. package/dist/store/sql.mjs +2 -1
  75. package/dist/store/sql.mjs.map +1 -1
  76. package/dist/transports/sigv4.cjs +2 -3
  77. package/dist/transports/sigv4.cjs.map +1 -1
  78. package/dist/transports/sigv4.d.cts.map +1 -1
  79. package/dist/transports/sigv4.d.mts.map +1 -1
  80. package/dist/transports/sigv4.mjs +2 -3
  81. package/dist/transports/sigv4.mjs.map +1 -1
  82. package/dist/webpush.cjs +11 -20
  83. package/dist/webpush.cjs.map +1 -1
  84. package/dist/webpush.d.cts.map +1 -1
  85. package/dist/webpush.d.mts.map +1 -1
  86. package/dist/webpush.mjs +11 -20
  87. package/dist/webpush.mjs.map +1 -1
  88. package/package.json +1 -1
@@ -5,6 +5,7 @@ import { BundleVersion, JourneyStore, StoreLease } from "../store/index.cjs";
5
5
  import { JourneyRuntimeOptions } from "../runtime.cjs";
6
6
  import { JourneyConfig } from "../config.cjs";
7
7
  import { EmailTransport } from "../email-sender.cjs";
8
+ import { Authorize } from "./auth.cjs";
8
9
  import { JourneyRunParts } from "./execute.cjs";
9
10
  import { SweepResult } from "./ingest.cjs";
10
11
  import * as core from "zod/v4/core";
@@ -60,6 +61,17 @@ export interface JourneyHostOptions<Env extends JourneyEnv> {
60
61
  env?: core.$ZodType<object>;
61
62
  /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
62
63
  contractMode?: 'enforce' | 'report';
64
+ /**
65
+ * Who may call the API routes, in whatever form the host chooses: an HMAC over the body, a
66
+ * JWT, a header its proxy adds. Receives a copy of the request and the env; true lets it
67
+ * through, false answers 401, a throw answers 500. The tracking routes and `/health` stay
68
+ * public. Without it, the env's `API_TOKEN` as a bearer (`bearerMatches`), which production
69
+ * then requires.
70
+ *
71
+ * authorize: async (request, env) =>
72
+ * timingSafeEqual(request.headers.get('x-signature') ?? '', await hmac(env.SECRET, await request.text()))
73
+ */
74
+ authorize?: Authorize<Env>;
63
75
  }
64
76
  /** The host's environment failed its schema (`env` option). */
65
77
  export declare class EnvValidationError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"journey-host.d.cts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAuCiB,mBAAmB,YAAY;;;;;;;;EAQ9C,QAAQ;;;;;;;;EAQR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;EAMnE,SAAS,KAAK,QAAQ;;;;;;;EAOtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAqCL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
1
+ {"version":3,"file":"journey-host.d.cts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAwCiB,mBAAmB,YAAY;;;;;;;;EAQ9C,QAAQ;;;;;;;;EAQR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;EAMnE,SAAS,KAAK,QAAQ;;;;;;;EAOtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;;;;;;;;;EAWA,YAAY,UAAU;;;qBAIX,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAqCL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
@@ -5,6 +5,7 @@ import { BundleVersion, JourneyStore, StoreLease } from "../store/index.mjs";
5
5
  import { JourneyRuntimeOptions } from "../runtime.mjs";
6
6
  import { JourneyConfig } from "../config.mjs";
7
7
  import { EmailTransport } from "../email-sender.mjs";
8
+ import { Authorize } from "./auth.mjs";
8
9
  import { JourneyRunParts } from "./execute.mjs";
9
10
  import { SweepResult } from "./ingest.mjs";
10
11
  import * as core from "zod/v4/core";
@@ -60,6 +61,17 @@ export interface JourneyHostOptions<Env extends JourneyEnv> {
60
61
  env?: core.$ZodType<object>;
61
62
  /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */
62
63
  contractMode?: 'enforce' | 'report';
64
+ /**
65
+ * Who may call the API routes, in whatever form the host chooses: an HMAC over the body, a
66
+ * JWT, a header its proxy adds. Receives a copy of the request and the env; true lets it
67
+ * through, false answers 401, a throw answers 500. The tracking routes and `/health` stay
68
+ * public. Without it, the env's `API_TOKEN` as a bearer (`bearerMatches`), which production
69
+ * then requires.
70
+ *
71
+ * authorize: async (request, env) =>
72
+ * timingSafeEqual(request.headers.get('x-signature') ?? '', await hmac(env.SECRET, await request.text()))
73
+ */
74
+ authorize?: Authorize<Env>;
63
75
  }
64
76
  /** The host's environment failed its schema (`env` option). */
65
77
  export declare class EnvValidationError extends Error {
@@ -1 +1 @@
1
- {"version":3,"file":"journey-host.d.mts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;iBAuCiB,mBAAmB,YAAY;;;;;;;;EAQ9C,QAAQ;;;;;;;;EAQR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;EAMnE,SAAS,KAAK,QAAQ;;;;;;;EAOtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;qBAIW,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAqCL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
1
+ {"version":3,"file":"journey-host.d.mts","names":[],"sources":["../../src/host/journey-host.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAwCiB,mBAAmB,YAAY;;;;;;;;EAQ9C,QAAQ;;;;;;;;EAQR,SAAS,KAAK,KAAK;IAAY;QAAqB,eAAe;;;;;;EAMnE,SAAS,KAAK,QAAQ;;;;;;;EAOtB,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,YAAY,KAAK,KAAK,OAAO,iBAAiB;;EAE9C,WAAW,KAAK,KAAK,QAAQ,kBAAkB;;;;;;;EAO/C,MAAM,KAAK;;EAEX;;;;;;;;;;;EAWA,YAAY,UAAU;;;qBAIX,2BAA2B;WAEjB;WADH;EAClB,YAAqB;;iBAKN,YAAY,YAAY;EACvC,QAAQ;;EAER,SAAS,KAAK,MAAM;;EAEpB,MAAM,KAAK,MAAM,SAAS;;EAE1B,UAAU;;EAEV,SAAS,KAAK,KAAK,OAAO,cAAc,QAAQ,gBAAgB;;EAEhE,OAAO,SAAS,SAAS,KAAK,KAAK,OAAO,eAAe,QAAQ;;EAEjE,MAAM,KAAK,MAAM,QAAQ;;;;;;EAMzB,eAAe,KAAK,KAAK,SAAS,4BAA4B;;;iBAI/C;;EAEf,mBAAmB;;wBAqCL,kBAAkB,YAAY,YAC5C,SAAS,mBAAmB,MAC5B,WAAU,kBACT,YAAY"}
@@ -140,6 +140,7 @@ function createJourneyHost(options, platform = {}) {
140
140
  contract: config.contract,
141
141
  mode: options.contractMode
142
142
  },
143
+ authorize: options.authorize,
143
144
  host: {
144
145
  bundle,
145
146
  preview: preview === void 0 ? void 0 : {
@@ -1 +1 @@
1
- {"version":3,"file":"journey-host.mjs","names":[],"sources":["../../src/host/journey-host.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { assertEmailConfig } from '../email';\nimport type { EmailTransport } from '../email-sender';\nimport { RegistryActionInvoker } from '../engine/actions';\nimport { profileUpdatedEvent } from '../engine/condition';\nimport type { JourneyObserver, MessageSender } from '../engine/ports';\nimport { RECIPIENT_PROPERTY } from '../engine/recipient';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { BundleIR, NodeIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport type { TrackingOptions } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport type { BundleVersion, JourneyStore, StoreLease } from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { indexTemplates, registryValidator } from '../templates';\nimport { channelsFromConfig, createProviderCache, runtimeFromConfig } from './channels';\nimport type { JourneyRunParts } from './execute';\nimport { serve } from './http';\nimport {\n type SweepResult,\n deployBundle,\n ingestEvent,\n sweepSegments,\n wakeSubscribers,\n} from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * What a journey host is, apart from the runtime it runs on: the validated environment, the\n * store lease, the outlets a run is wired to, the HTTP API (`http.ts`, with the host's parts\n * plugged in) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and\n * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.\n */\n\nexport interface JourneyHostOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the host deploys (`GET /bundle`,\n * `POST /bundle/deploy` serve `compileConfig`), the template registries the senders, the\n * validator and `/preview` render from, the contract every `/events` and `/identify` is\n * checked against, the email defaults, actions, and the runtime / APNs / FCM settings.\n * Secrets come from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a `StoreLease` when the\n * client must be released afterwards (a Postgres connection); defaults to D1 from the env's\n * binding. `database` is the config's (`schema` defaulting to `workflow`), for Postgres:\n *\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Where journey emails go: a transport built from the validated env — `sesEmailTransport(...)`,\n * `cloudflareEmailTransport(...)`, or your own. Which one is independent of where the workflow\n * runs. Without it the email channel fails its sends, saying so.\n */\n email?: (env: Env) => EmailTransport;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the host serves\n * the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /** Message outlet override (tests, a custom gateway): replaces every channel, `email` included. */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the host runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n}\n\n/** The host's environment failed its schema (`env` option). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the host's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport interface JourneyHost<Env extends JourneyEnv> {\n config: JourneyConfig;\n /** The env, parsed by the schema once per env object (throws EnvValidationError). */\n checkEnv(env: Env): Env;\n /** A store for one request or run, its segment windows scaled with the runtime. */\n lease(env: Env): Required<StoreLease>;\n /** The bundle compiled into this code. */\n bundle(): BundleIR;\n /** The outlets one run is wired to (the store is the run's). */\n runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;\n /** The HTTP API over one store: the ingest API plus tracking, the sweep, the bundle and previews. */\n handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;\n /** The scheduled sweep (`sweepSegments`) on a store of its own. */\n sweep(env: Env): Promise<SweepResult>;\n /**\n * Make the bundle compiled into `version` live, once per process, only over an older live\n * version (the store records it). Undefined when there is nothing to do; a failure is logged\n * and retried a minute later.\n */\n applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;\n}\n\n/** What the platform adapter itself contributes. */\nexport interface JourneyPlatform {\n /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */\n apnsFetch?: typeof fetch;\n}\n\nfunction sendsEmail(nodes: readonly NodeIR[]): boolean {\n return nodes.some((node) => {\n switch (node.type) {\n case 'message':\n return node.channel === 'email';\n case 'wait_until':\n return Array.isArray(node.onTimeout) && sendsEmail(node.onTimeout);\n case 'branch':\n return (\n node.cases.some((branch) => sendsEmail(branch.flow)) || sendsEmail(node.otherwise ?? [])\n );\n case 'cohort':\n return node.arms.some((arm) => sendsEmail(arm.flow));\n default:\n return false;\n }\n });\n}\n\nconst describeIssue = (issue: { path: PropertyKey[]; message: string }): string =>\n `${issue.path.join('.') || '(root)'}: ${issue.message}`;\n\n/** An event the host emits itself, checked against the contract like any ingest. */\nfunction checkedPayload(\n contract: Contract | undefined,\n event: string,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n if (contract === undefined) return payload;\n const verdict = checkEvent(contract, event, payload);\n if (!verdict.ok) throw new ContractValidationError(event, verdict.issues);\n return verdict.data;\n}\n\nexport function createJourneyHost<Env extends JourneyEnv>(\n options: JourneyHostOptions<Env>,\n platform: JourneyPlatform = {}\n): JourneyHost<Env> {\n const { config } = options;\n const emailIds = indexTemplates('email', config.templates?.email);\n // A config still on `emails` (its sender would be silently dropped) fails here, not at a send\n assertEmailConfig(config, emailIds.values());\n let compiled: BundleIR | undefined;\n const bundle = () => (compiled ??= compileConfig(config));\n // A journey that emails with nowhere to send fails here too, not at its first email\n const emailing = bundle().workflows.filter((workflow) => sendsEmail(workflow.flow));\n if (options.email === undefined && options.messages === undefined && emailing.length > 0) {\n throw new TypeError(\n `workflows ${emailing.map((w) => `'${w.id}'`).join(', ')} send email, but the host has no ` +\n '`email` transport: pass `email: (env) => cloudflareEmailTransport(...)` or `sesEmailTransport(...)`'\n );\n }\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n // Names and reasons only: the values are secrets\n if (!result.success) throw new EnvValidationError(result.error.issues.map(describeIssue));\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n // The provider clients (APNs JWT, FCM access token) live with the host, not with a run: a\n // run's env is a fresh object on every Lambda invocation, so nothing keyed on it would be reused\n const providers = createProviderCache();\n const messages = (env: Env, store: JourneyStore): MessageSender => {\n if (options.messages !== undefined) return options.messages(env, store);\n return channelsFromConfig(config, env, {\n store,\n email: options.email?.(env),\n apnsFetch: platform.apnsFetch,\n providers,\n });\n };\n\n const observer = (env: Env, store: JourneyStore): JourneyObserver | undefined => {\n if (options.observer !== undefined) return options.observer(env, store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);\n };\n\n const runParts = (env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts => ({\n store,\n env,\n runtime:\n options.runtime === undefined ? runtimeFromConfig(config, env) : options.runtime(env, params),\n messages: messages(env, store),\n // An action ref is a registered action structurally: the same object carries name, code and hash\n actions: new RegistryActionInvoker(config.actions ?? []),\n contract: config.contract,\n validator: registryValidator(config.templates ?? {}),\n observer: observer(env, store),\n });\n\n const preview = emails === undefined ? undefined : registryRenderer(emails, config.email);\n\n /**\n * The recipients' routes (`/t/*`): a push tapped open is recorded as the config's\n * `webPush.openedEvent` — keyed by the copy, so a second tap of the same notification is the\n * same event — and an unsubscribe wakes the waits reading email reachability (`reachable\n * ('email')` subscribes to the email property's channel), so an `exitWhen:\n * not(reachable('email'))` takes effect without waiting for a timeout.\n */\n const tracking = (env: Env, store: JourneyStore): TrackingOptions => {\n const opened = config.webPush?.openedEvent;\n return {\n product: config.title,\n quickMs: config.email?.tracking?.quickClickMs,\n pushOpened:\n opened === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload = checkedPayload(config.contract, opened, { message_id: messageId });\n const input = {\n id: `${opened}:${messageId}`,\n userId,\n event: opened,\n payload,\n ts: at,\n };\n await ingestEvent(env, input, store);\n },\n onUnsubscribed: async ({ userId }) => {\n const email = profileUpdatedEvent(RECIPIENT_PROPERTY.email ?? 'email');\n await wakeSubscribers(env, store, userId, [email]);\n },\n };\n };\n\n const handle = (request: Request, env: Env, store: JourneyStore): Promise<Response> =>\n serve(request, {\n env,\n store,\n validation: { contract: config.contract, mode: options.contractMode },\n host: {\n bundle,\n preview:\n preview === undefined ? undefined : { has: (id) => emailIds.has(id), render: preview },\n tracking: tracking(env, store),\n sweep: () => sweepSegments(env, store),\n },\n });\n\n const sweep = async (env: Env) => {\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n // The bundle compiled into this version goes live on its own, once per process: the first\n // request or scheduled run reads which version's bundle is live (one row) and applies this one\n // only over an older version, so one still serving during a rollout can never put its routes\n // back. A failure (say the migrations have not run) is logged and retried a minute later. Only\n // the request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env, version: BundleVersion | undefined) => {\n if (version === undefined || version.id === '' || Number.isNaN(version.createdAt)) {\n return undefined;\n }\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n return { config, checkEnv, lease, bundle, runParts, handle, sweep, applyOwnBundle };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"journey-host.mjs","names":[],"sources":["../../src/host/journey-host.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { d1Store } from '../cloudflare/d1-env';\nimport { type JourneyConfig, compileConfig } from '../config';\nimport { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { assertEmailConfig } from '../email';\nimport type { EmailTransport } from '../email-sender';\nimport { RegistryActionInvoker } from '../engine/actions';\nimport { profileUpdatedEvent } from '../engine/condition';\nimport type { JourneyObserver, MessageSender } from '../engine/ports';\nimport { RECIPIENT_PROPERTY } from '../engine/recipient';\nimport { scaleSegmentWindows } from '../engine/time-scale';\nimport type { BundleIR, NodeIR } from '../ir';\nimport { registryRenderer } from '../react-email';\nimport { reportingObserver } from '../reporting/observer';\nimport type { TrackingOptions } from '../reporting/tracking';\nimport type { JourneyRuntimeOptions } from '../runtime';\nimport type { BundleVersion, JourneyStore, StoreLease } from '../store/index';\nimport { DEFAULT_SCHEMA } from '../store/tables';\nimport { indexTemplates, registryValidator } from '../templates';\nimport type { Authorize } from './auth';\nimport { channelsFromConfig, createProviderCache, runtimeFromConfig } from './channels';\nimport type { JourneyRunParts } from './execute';\nimport { serve } from './http';\nimport {\n type SweepResult,\n deployBundle,\n ingestEvent,\n sweepSegments,\n wakeSubscribers,\n} from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * What a journey host is, apart from the runtime it runs on: the validated environment, the\n * store lease, the outlets a run is wired to, the HTTP API (`http.ts`, with the host's parts\n * plugged in) and the bundle that goes live with the code. `journeyWorker` (Cloudflare) and\n * `journeyLambda` (AWS) are this plus how their platform starts, wakes and stops instances.\n */\n\nexport interface JourneyHostOptions<Env extends JourneyEnv> {\n /**\n * The project's `workflow.config.ts` (`defineConfig`): what the host deploys (`GET /bundle`,\n * `POST /bundle/deploy` serve `compileConfig`), the template registries the senders, the\n * validator and `/preview` render from, the contract every `/events` and `/identify` is\n * checked against, the email defaults, actions, and the runtime / APNs / FCM settings.\n * Secrets come from the env under the names `JourneyEnv` documents.\n */\n config: JourneyConfig;\n /**\n * The data plane, opened per request and per workflow run. Return a `StoreLease` when the\n * client must be released afterwards (a Postgres connection); defaults to D1 from the env's\n * binding. `database` is the config's (`schema` defaulting to `workflow`), for Postgres:\n *\n * store: (env, database) => postgresJsStore(env.DATABASE_URL, database)\n */\n store?: (env: Env, database: { schema: string }) => JourneyStore | StoreLease;\n /**\n * Where journey emails go: a transport built from the validated env — `sesEmailTransport(...)`,\n * `cloudflareEmailTransport(...)`, or your own. Which one is independent of where the workflow\n * runs. Without it the email channel fails its sends, saying so.\n */\n email?: (env: Env) => EmailTransport;\n /**\n * Reporting hooks: node arrivals, every copy sent (with the tracking it should carry — click\n * links and an open pixel for email, payload data for a push), and how each instance ended.\n * By default a store that keeps reporting (Postgres) records them itself, and the host serves\n * the email tracking routes (`/t/...`, on `WORKFLOW_ORIGIN`); override to report elsewhere.\n */\n observer?: (env: Env, store: JourneyStore) => JourneyObserver;\n /** Message outlet override (tests, a custom gateway): replaces every channel, `email` included. */\n messages?: (env: Env, store: JourneyStore) => MessageSender;\n /** Per-instance runtime options override (a tester in fast-forward); by default `config.runtime` for `ENVIRONMENT`. */\n runtime?: (env: Env, params: JourneyParams) => JourneyRuntimeOptions;\n /**\n * The host's environment, as a zod schema over its vars and secrets (bindings pass through\n * untouched): checked the first time the host runs, so a missing or malformed variable fails\n * there — every problem listed — not at the send that needs it. Webhook templates read their\n * URL from the parsed values: `webhook: (env: Env) => env.WEBHOOK_DISCORD_ALERTS`.\n */\n env?: core.$ZodType<object>;\n /** 'enforce' (default) answers an event or patch the contract refuses with a 400; 'report' only logs it. */\n contractMode?: 'enforce' | 'report';\n /**\n * Who may call the API routes, in whatever form the host chooses: an HMAC over the body, a\n * JWT, a header its proxy adds. Receives a copy of the request and the env; true lets it\n * through, false answers 401, a throw answers 500. The tracking routes and `/health` stay\n * public. Without it, the env's `API_TOKEN` as a bearer (`bearerMatches`), which production\n * then requires.\n *\n * authorize: async (request, env) =>\n * timingSafeEqual(request.headers.get('x-signature') ?? '', await hmac(env.SECRET, await request.text()))\n */\n authorize?: Authorize<Env>;\n}\n\n/** The host's environment failed its schema (`env` option). */\nexport class EnvValidationError extends Error {\n override readonly name = 'EnvValidationError';\n constructor(readonly issues: readonly string[]) {\n super(`the host's environment is invalid:\\n ${issues.join('\\n ')}`);\n }\n}\n\nexport interface JourneyHost<Env extends JourneyEnv> {\n config: JourneyConfig;\n /** The env, parsed by the schema once per env object (throws EnvValidationError). */\n checkEnv(env: Env): Env;\n /** A store for one request or run, its segment windows scaled with the runtime. */\n lease(env: Env): Required<StoreLease>;\n /** The bundle compiled into this code. */\n bundle(): BundleIR;\n /** The outlets one run is wired to (the store is the run's). */\n runParts(env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts;\n /** The HTTP API over one store: the ingest API plus tracking, the sweep, the bundle and previews. */\n handle(request: Request, env: Env, store: JourneyStore): Promise<Response>;\n /** The scheduled sweep (`sweepSegments`) on a store of its own. */\n sweep(env: Env): Promise<SweepResult>;\n /**\n * Make the bundle compiled into `version` live, once per process, only over an older live\n * version (the store records it). Undefined when there is nothing to do; a failure is logged\n * and retried a minute later.\n */\n applyOwnBundle(env: Env, version: BundleVersion | undefined): Promise<void> | undefined;\n}\n\n/** What the platform adapter itself contributes. */\nexport interface JourneyPlatform {\n /** An HTTP/2 fetch for APNs, where the runtime's own fetch is HTTP/1.1 (Node). */\n apnsFetch?: typeof fetch;\n}\n\nfunction sendsEmail(nodes: readonly NodeIR[]): boolean {\n return nodes.some((node) => {\n switch (node.type) {\n case 'message':\n return node.channel === 'email';\n case 'wait_until':\n return Array.isArray(node.onTimeout) && sendsEmail(node.onTimeout);\n case 'branch':\n return (\n node.cases.some((branch) => sendsEmail(branch.flow)) || sendsEmail(node.otherwise ?? [])\n );\n case 'cohort':\n return node.arms.some((arm) => sendsEmail(arm.flow));\n default:\n return false;\n }\n });\n}\n\nconst describeIssue = (issue: { path: PropertyKey[]; message: string }): string =>\n `${issue.path.join('.') || '(root)'}: ${issue.message}`;\n\n/** An event the host emits itself, checked against the contract like any ingest. */\nfunction checkedPayload(\n contract: Contract | undefined,\n event: string,\n payload: Record<string, unknown>\n): Record<string, unknown> {\n if (contract === undefined) return payload;\n const verdict = checkEvent(contract, event, payload);\n if (!verdict.ok) throw new ContractValidationError(event, verdict.issues);\n return verdict.data;\n}\n\nexport function createJourneyHost<Env extends JourneyEnv>(\n options: JourneyHostOptions<Env>,\n platform: JourneyPlatform = {}\n): JourneyHost<Env> {\n const { config } = options;\n const emailIds = indexTemplates('email', config.templates?.email);\n // A config still on `emails` (its sender would be silently dropped) fails here, not at a send\n assertEmailConfig(config, emailIds.values());\n let compiled: BundleIR | undefined;\n const bundle = () => (compiled ??= compileConfig(config));\n // A journey that emails with nowhere to send fails here too, not at its first email\n const emailing = bundle().workflows.filter((workflow) => sendsEmail(workflow.flow));\n if (options.email === undefined && options.messages === undefined && emailing.length > 0) {\n throw new TypeError(\n `workflows ${emailing.map((w) => `'${w.id}'`).join(', ')} send email, but the host has no ` +\n '`email` transport: pass `email: (env) => cloudflareEmailTransport(...)` or `sesEmailTransport(...)`'\n );\n }\n // Parsed once per env object: the runtime hands the same one to every request of an isolate\n const checked = new WeakMap<object, Env>();\n const checkEnv = (env: Env): Env => {\n if (options.env === undefined) return env;\n const cached = checked.get(env);\n if (cached !== undefined) return cached;\n const result = z.safeParse(options.env, env);\n // Names and reasons only: the values are secrets\n if (!result.success) throw new EnvValidationError(result.error.issues.map(describeIssue));\n const merged: Env = { ...env, ...result.data };\n checked.set(env, merged);\n return merged;\n };\n const emails = config.templates?.email;\n const database = { schema: config.database?.schema ?? DEFAULT_SCHEMA };\n // In fast-forward, segment windows scale with the journeys reading them (time-scale.ts)\n const lease = (env: Env): Required<StoreLease> => {\n const opened = options.store === undefined ? d1Store(env) : options.store(env, database);\n const noop = async () => {};\n const factor = runtimeFromConfig(config, env).timeScale;\n return 'store' in opened\n ? { store: scaleSegmentWindows(opened.store, factor), close: opened.close ?? noop }\n : { store: scaleSegmentWindows(opened, factor), close: noop };\n };\n\n // The provider clients (APNs JWT, FCM access token) live with the host, not with a run: a\n // run's env is a fresh object on every Lambda invocation, so nothing keyed on it would be reused\n const providers = createProviderCache();\n const messages = (env: Env, store: JourneyStore): MessageSender => {\n if (options.messages !== undefined) return options.messages(env, store);\n return channelsFromConfig(config, env, {\n store,\n email: options.email?.(env),\n apnsFetch: platform.apnsFetch,\n providers,\n });\n };\n\n const observer = (env: Env, store: JourneyStore): JourneyObserver | undefined => {\n if (options.observer !== undefined) return options.observer(env, store);\n return store.reporting === undefined\n ? undefined\n : reportingObserver(store.reporting, env.WORKFLOW_ORIGIN);\n };\n\n const runParts = (env: Env, store: JourneyStore, params: JourneyParams): JourneyRunParts => ({\n store,\n env,\n runtime:\n options.runtime === undefined ? runtimeFromConfig(config, env) : options.runtime(env, params),\n messages: messages(env, store),\n // An action ref is a registered action structurally: the same object carries name, code and hash\n actions: new RegistryActionInvoker(config.actions ?? []),\n contract: config.contract,\n validator: registryValidator(config.templates ?? {}),\n observer: observer(env, store),\n });\n\n const preview = emails === undefined ? undefined : registryRenderer(emails, config.email);\n\n /**\n * The recipients' routes (`/t/*`): a push tapped open is recorded as the config's\n * `webPush.openedEvent` — keyed by the copy, so a second tap of the same notification is the\n * same event — and an unsubscribe wakes the waits reading email reachability (`reachable\n * ('email')` subscribes to the email property's channel), so an `exitWhen:\n * not(reachable('email'))` takes effect without waiting for a timeout.\n */\n const tracking = (env: Env, store: JourneyStore): TrackingOptions => {\n const opened = config.webPush?.openedEvent;\n return {\n product: config.title,\n quickMs: config.email?.tracking?.quickClickMs,\n pushOpened:\n opened === undefined\n ? undefined\n : async ({ messageId, userId, at }) => {\n const payload = checkedPayload(config.contract, opened, { message_id: messageId });\n const input = {\n id: `${opened}:${messageId}`,\n userId,\n event: opened,\n payload,\n ts: at,\n };\n await ingestEvent(env, input, store);\n },\n onUnsubscribed: async ({ userId }) => {\n const email = profileUpdatedEvent(RECIPIENT_PROPERTY.email ?? 'email');\n await wakeSubscribers(env, store, userId, [email]);\n },\n };\n };\n\n const handle = (request: Request, env: Env, store: JourneyStore): Promise<Response> =>\n serve(request, {\n env,\n store,\n validation: { contract: config.contract, mode: options.contractMode },\n authorize: options.authorize as Authorize<JourneyEnv> | undefined,\n host: {\n bundle,\n preview:\n preview === undefined ? undefined : { has: (id) => emailIds.has(id), render: preview },\n tracking: tracking(env, store),\n sweep: () => sweepSegments(env, store),\n },\n });\n\n const sweep = async (env: Env) => {\n const { store, close } = lease(env);\n try {\n return await sweepSegments(env, store);\n } finally {\n await close();\n }\n };\n\n // The bundle compiled into this version goes live on its own, once per process: the first\n // request or scheduled run reads which version's bundle is live (one row) and applies this one\n // only over an older version, so one still serving during a rollout can never put its routes\n // back. A failure (say the migrations have not run) is logged and retried a minute later. Only\n // the request that starts the check gets its promise back: a request context cannot wait on\n // another's I/O.\n let applying: Promise<void> | undefined;\n let retryAt = 0;\n const applyOwnBundle = (env: Env, version: BundleVersion | undefined) => {\n if (version === undefined || version.id === '' || Number.isNaN(version.createdAt)) {\n return undefined;\n }\n if (applying !== undefined || Date.now() < retryAt) return undefined;\n applying = (async () => {\n const { store, close } = lease(env);\n try {\n if (store.liveBundleVersion === undefined) return; // D1: POST /bundle/deploy\n const live = await store.liveBundleVersion();\n if (live !== undefined && (live.id === version.id || live.createdAt >= version.createdAt))\n return;\n const result = await deployBundle(env, bundle(), store, version);\n if (result.applied) console.log(`[workflow] version ${version.id} is live`);\n } finally {\n await close();\n }\n })().catch((error: unknown) => {\n applying = undefined;\n retryAt = Date.now() + 60_000;\n console.error(`[workflow] could not make version ${version.id} live`, error);\n });\n return applying;\n };\n\n return { config, checkEnv, lease, bundle, runParts, handle, sweep, applyOwnBundle };\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