@shware/workflow 4.1.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +16 -38
  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 +2 -3
  15. package/dist/aws/index.cjs.map +1 -1
  16. package/dist/aws/index.d.cts +1 -1
  17. package/dist/aws/index.d.mts +1 -1
  18. package/dist/aws/index.mjs +2 -3
  19. package/dist/aws/index.mjs.map +1 -1
  20. package/dist/aws/instances.cjs +1 -1
  21. package/dist/aws/instances.cjs.map +1 -1
  22. package/dist/aws/instances.d.cts +1 -1
  23. package/dist/aws/instances.d.mts +1 -1
  24. package/dist/aws/instances.mjs +1 -1
  25. package/dist/aws/instances.mjs.map +1 -1
  26. package/dist/cloudflare/bindings.cjs.map +1 -1
  27. package/dist/cloudflare/bindings.d.cts +0 -19
  28. package/dist/cloudflare/bindings.d.cts.map +1 -1
  29. package/dist/cloudflare/bindings.d.mts +0 -19
  30. package/dist/cloudflare/bindings.d.mts.map +1 -1
  31. package/dist/cloudflare/bindings.mjs.map +1 -1
  32. package/dist/cloudflare/index.cjs +0 -5
  33. package/dist/cloudflare/index.d.cts +4 -6
  34. package/dist/cloudflare/index.d.mts +4 -6
  35. package/dist/cloudflare/index.mjs +2 -4
  36. package/dist/cloudflare/runner.cjs +3 -4
  37. package/dist/cloudflare/runner.cjs.map +1 -1
  38. package/dist/cloudflare/runner.d.cts +3 -3
  39. package/dist/cloudflare/runner.d.cts.map +1 -1
  40. package/dist/cloudflare/runner.d.mts +3 -3
  41. package/dist/cloudflare/runner.d.mts.map +1 -1
  42. package/dist/cloudflare/runner.mjs +3 -4
  43. package/dist/cloudflare/runner.mjs.map +1 -1
  44. package/dist/config.cjs.map +1 -1
  45. package/dist/config.d.cts +1 -1
  46. package/dist/config.d.mts +1 -1
  47. package/dist/config.mjs.map +1 -1
  48. package/dist/engine/condition.cjs +2 -2
  49. package/dist/engine/condition.cjs.map +1 -1
  50. package/dist/engine/condition.d.cts +1 -1
  51. package/dist/engine/condition.d.mts +1 -1
  52. package/dist/engine/condition.mjs +2 -2
  53. package/dist/engine/condition.mjs.map +1 -1
  54. package/dist/engine/ports.d.cts +1 -1
  55. package/dist/engine/ports.d.mts +1 -1
  56. package/dist/fcm.cjs +2 -1
  57. package/dist/fcm.cjs.map +1 -1
  58. package/dist/fcm.d.cts.map +1 -1
  59. package/dist/fcm.d.mts.map +1 -1
  60. package/dist/fcm.mjs +2 -1
  61. package/dist/fcm.mjs.map +1 -1
  62. package/dist/host/channels.cjs +2 -1
  63. package/dist/host/channels.cjs.map +1 -1
  64. package/dist/host/channels.d.cts +1 -1
  65. package/dist/host/channels.d.cts.map +1 -1
  66. package/dist/host/channels.d.mts +1 -1
  67. package/dist/host/channels.d.mts.map +1 -1
  68. package/dist/host/channels.mjs +2 -1
  69. package/dist/host/channels.mjs.map +1 -1
  70. package/dist/host/execute.d.cts +1 -1
  71. package/dist/host/execute.d.mts +1 -1
  72. package/dist/host/http.cjs +3 -4
  73. package/dist/host/http.cjs.map +1 -1
  74. package/dist/host/http.d.cts +2 -2
  75. package/dist/host/http.d.cts.map +1 -1
  76. package/dist/host/http.d.mts +2 -2
  77. package/dist/host/http.d.mts.map +1 -1
  78. package/dist/host/http.mjs +3 -4
  79. package/dist/host/http.mjs.map +1 -1
  80. package/dist/host/ingest.cjs +11 -22
  81. package/dist/host/ingest.cjs.map +1 -1
  82. package/dist/host/ingest.d.cts +8 -13
  83. package/dist/host/ingest.d.cts.map +1 -1
  84. package/dist/host/ingest.d.mts +8 -13
  85. package/dist/host/ingest.d.mts.map +1 -1
  86. package/dist/host/ingest.mjs +12 -22
  87. package/dist/host/ingest.mjs.map +1 -1
  88. package/dist/host/journey-host.cjs +2 -4
  89. package/dist/host/journey-host.cjs.map +1 -1
  90. package/dist/host/journey-host.d.cts +6 -6
  91. package/dist/host/journey-host.d.cts.map +1 -1
  92. package/dist/host/journey-host.d.mts +6 -6
  93. package/dist/host/journey-host.d.mts.map +1 -1
  94. package/dist/host/journey-host.mjs +2 -4
  95. package/dist/host/journey-host.mjs.map +1 -1
  96. package/dist/host/platform.cjs.map +1 -1
  97. package/dist/host/platform.d.cts +1 -7
  98. package/dist/host/platform.d.cts.map +1 -1
  99. package/dist/host/platform.d.mts +1 -7
  100. package/dist/host/platform.d.mts.map +1 -1
  101. package/dist/host/platform.mjs.map +1 -1
  102. package/dist/jwt.cjs +5 -9
  103. package/dist/jwt.cjs.map +1 -1
  104. package/dist/jwt.d.cts +1 -1
  105. package/dist/jwt.d.cts.map +1 -1
  106. package/dist/jwt.d.mts +1 -1
  107. package/dist/jwt.d.mts.map +1 -1
  108. package/dist/jwt.mjs +5 -9
  109. package/dist/jwt.mjs.map +1 -1
  110. package/dist/store/facts.cjs +1 -1
  111. package/dist/store/facts.cjs.map +1 -1
  112. package/dist/store/facts.mjs +1 -1
  113. package/dist/store/facts.mjs.map +1 -1
  114. package/dist/store/index.cjs +0 -2
  115. package/dist/store/index.cjs.map +1 -1
  116. package/dist/store/index.d.cts +21 -26
  117. package/dist/store/index.d.cts.map +1 -1
  118. package/dist/store/index.d.mts +21 -26
  119. package/dist/store/index.d.mts.map +1 -1
  120. package/dist/store/index.mjs +1 -2
  121. package/dist/store/index.mjs.map +1 -1
  122. package/dist/store/postgres.cjs +2 -3
  123. package/dist/store/postgres.cjs.map +1 -1
  124. package/dist/store/postgres.d.cts +2 -3
  125. package/dist/store/postgres.d.cts.map +1 -1
  126. package/dist/store/postgres.d.mts +2 -3
  127. package/dist/store/postgres.d.mts.map +1 -1
  128. package/dist/store/postgres.mjs +2 -3
  129. package/dist/store/postgres.mjs.map +1 -1
  130. package/dist/store/sql.cjs +2 -1
  131. package/dist/store/sql.cjs.map +1 -1
  132. package/dist/store/sql.d.cts.map +1 -1
  133. package/dist/store/sql.d.mts.map +1 -1
  134. package/dist/store/sql.mjs +2 -1
  135. package/dist/store/sql.mjs.map +1 -1
  136. package/dist/transports/cloudflare-email.d.cts +1 -1
  137. package/dist/transports/cloudflare-email.d.mts +1 -1
  138. package/dist/transports/sigv4.cjs +2 -3
  139. package/dist/transports/sigv4.cjs.map +1 -1
  140. package/dist/transports/sigv4.d.cts.map +1 -1
  141. package/dist/transports/sigv4.d.mts.map +1 -1
  142. package/dist/transports/sigv4.mjs +2 -3
  143. package/dist/transports/sigv4.mjs.map +1 -1
  144. package/dist/webpush.cjs +11 -20
  145. package/dist/webpush.cjs.map +1 -1
  146. package/dist/webpush.d.cts.map +1 -1
  147. package/dist/webpush.d.mts.map +1 -1
  148. package/dist/webpush.mjs +11 -20
  149. package/dist/webpush.mjs.map +1 -1
  150. package/package.json +1 -3
  151. package/dist/cloudflare/d1-env.cjs +0 -17
  152. package/dist/cloudflare/d1-env.cjs.map +0 -1
  153. package/dist/cloudflare/d1-env.d.cts +0 -12
  154. package/dist/cloudflare/d1-env.d.cts.map +0 -1
  155. package/dist/cloudflare/d1-env.d.mts +0 -12
  156. package/dist/cloudflare/d1-env.d.mts.map +0 -1
  157. package/dist/cloudflare/d1-env.mjs +0 -16
  158. package/dist/cloudflare/d1-env.mjs.map +0 -1
  159. package/dist/cloudflare/schema.sql +0 -148
  160. package/dist/store/d1.cjs +0 -245
  161. package/dist/store/d1.cjs.map +0 -1
  162. package/dist/store/d1.d.cts +0 -73
  163. package/dist/store/d1.d.cts.map +0 -1
  164. package/dist/store/d1.d.mts +0 -73
  165. package/dist/store/d1.d.mts.map +0 -1
  166. package/dist/store/d1.mjs +0 -244
  167. package/dist/store/d1.mjs.map +0 -1
  168. package/src/cloudflare/schema.sql +0 -148
@@ -20,9 +20,8 @@ function asJson(value) {
20
20
  return typeof value === "string" ? JSON.parse(value) : value;
21
21
  }
22
22
  /**
23
- * Journey data plane on Postgres. Same contract as D1JourneyStore, native
24
- * types where Postgres has them (jsonb, timestamptz), and the concurrency
25
- * guarantees come from the database: ON CONFLICT for the once policy and
23
+ * The journey data plane on Postgres: native types where Postgres has them
24
+ * (jsonb, timestamptz), and the concurrency guarantees come from the database: ON CONFLICT for the once policy and
26
25
  * idempotent subscriptions, a transaction for the route swap, a jsonb merge
27
26
  * for /identify. The tables are declared in tables.ts.
28
27
  *
@@ -1 +1 @@
1
- {"version":3,"file":"postgres.mjs","names":[],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport type {\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport { type ColumnType, DEFAULT_SCHEMA, TABLES, type TableKey, columnName } from './tables';\nimport { lockUserTransaction } from './user-lock';\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * Journey data plane on Postgres. Same contract as D1JourneyStore, native\n * types where Postgres has them (jsonb, timestamptz), and the concurrency\n * guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n *\n * Every write about a user is conditional on their erasure tombstone not existing\n * (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that\n * holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a\n * profile merge, a device, a subscription, a segment membership with its starts, an entry, an\n * admission, and the reporting's instance and message rows (postgres-reporting.ts). The\n * erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ\n * COMMITTED a conditional write sees only what was committed when its statement began, and an\n * erasure sees only rows committed when it looked — so without the lock a write's transaction\n * could commit its rows after the erase deleted everything (the user comes back), and a runner\n * could admit itself after the tombstone (the instance runs on). With it, every write and the\n * erasure of one user run one after another: a write either commits before the tombstone,\n * where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is\n * refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The\n * conditional statement stays the in-transaction check; a write that wrote nothing reads the\n * tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).\n * Suppressions are the one exception: an opt-out is written whatever the user's state.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n readonly executions: ExecutionRegistry;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t, this.schema);\n this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets\n * — without one the first ingest fails, not the deploy). Read-only — the engine never migrates\n * the host's database; a deploy fails with these problems listed, and the fix is to regenerate\n * the drizzle file and run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const { columns, indexes } = await this.sql.transaction(async (tx) => ({\n columns: await tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n ),\n indexes: await tx.query<{ tablename: string; indexname: string; indexdef: string }>(\n `SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`,\n [this.schema]\n ),\n }));\n const live = new Map(columns.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(columns.map((row) => row.table_name));\n const liveIndexes = new Map(indexes.map((row) => [`${row.tablename}.${row.indexname}`, row]));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n // The names drizzle-kit and renderPostgresDdl both give an unnamed unique index\n for (const keys of spec.uniqueIndexes ?? []) {\n const name = `${spec.name}_${keys.map(columnName).join('_')}_index`;\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`missing unique index \"${this.schema}\".\"${name}\"`);\n else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef))\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, unique per user, which is\n // what makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, input.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n SELECT $1::uuid, $2, $3, $4, ${TS('$5')}, $6::text::jsonb ${this.notErased('$3')}\n ON CONFLICT (user_id, event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n if (rows.length > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(tx, input.userId);\n return false;\n });\n }\n\n /** The condition every write about a user carries: no erasure tombstone for `$user`. */\n private notErased(user: string): string {\n return `WHERE NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = ${user})`;\n }\n\n /**\n * A conditional write wrote nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(sql: SqlClientLike, userId: string): Promise<void> {\n const rows = await sql.query(`SELECT 1 AS x FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (rows.length > 0) throw new UserErasedError(userId);\n }\n\n /**\n * The first two statements of every transaction that writes about the user (user-lock.ts):\n * READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read\n * about the user comes after, on a snapshot taken once the lock is held.\n */\n private lockUser(tx: SqlClientLike, userId: string): Promise<void> {\n return lockUserTransaction(tx, this.schema, userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ c: number | string }>(\n `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ payload: unknown }>(\n `SELECT payload FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return rows.map((row) => asJson<Record<string, unknown>>(row.payload));\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // RFC 7386 merge-patch in one statement: jsonb `||` overwrites key by key\n // and jsonb_strip_nulls then drops the keys the patch set to null (the\n // API's only way to unset a property). Atomic in the database, so two\n // concurrent identifies cannot lose each other's keys.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const rows = await tx.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props)\n SELECT $1, jsonb_strip_nulls($2::text::jsonb) ${this.notErased('$1')}\n ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(props), JSON.stringify(props)]\n );\n // The upsert always yields its row — unless the tombstone refused it\n if (rows.length === 0) throw new UserErasedError(userId);\n return asJson<ProfileProps>(rows[0].props);\n });\n }\n\n async listUserIds(page?: {\n after?: string | undefined;\n limit?: number | undefined;\n }): Promise<string[]> {\n // Ascending user_id with a keyset bound, so a sweep can resume past the last id it saw\n const params: unknown[] = [];\n let where = '';\n if (page?.after !== undefined) {\n params.push(page.after);\n where = ` WHERE user_id > $${params.length}`;\n }\n let limit = '';\n if (page?.limit !== undefined) {\n params.push(page.limit);\n limit = ` LIMIT $${params.length}`;\n }\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM (SELECT user_id FROM ${this.t.profile} UNION SELECT user_id FROM ${this.t.event}) u${where} ORDER BY user_id${limit}`,\n params\n );\n return rows.map((row) => row.user_id);\n }\n\n listDevices(userId: string): Promise<PushDevice[]> {\n return this.sql.query<PushDevice>(\n `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`,\n [userId]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, device.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.device} (token, user_id, platform, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$2')}\n ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts\n RETURNING token`,\n [device.token, device.userId, device.platform, device.ts]\n );\n if (rows.length === 0) throw new UserErasedError(device.userId);\n });\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n // Under the user's lock: a write in flight commits before the tombstone (and is found by\n // the erase) or reads it after (and is refused)\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n await tx.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n });\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} WHERE erased_at IS NULL ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const rows = await this.sql.query<{ erased: boolean }>(\n `SELECT erased_at IS NOT NULL AS erased FROM ${this.t.erasure} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n if (row === undefined) return 'none';\n return row.erased ? 'erased' : 'erasing';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const rows = await this.sql.query<{ instance_id: string; admitted: boolean }>(\n `SELECT instance_id, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => ({ instanceId: row.instance_id, admitted: row.admitted }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n // A first admission needs a running entry; an instance once admitted may replay its\n // finalisation after the entry reached a terminal status, under its own instance id (a\n // reclaim resets `admitted_at`, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both.\n const rows = await tx.query(\n `UPDATE ${this.t.entry} SET admitted_at = COALESCE(admitted_at, ${TS('$3')})\n WHERE instance_id = $1 AND user_id = $2\n AND (status = 'running' OR admitted_at IS NOT NULL)\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $2)\n RETURNING instance_id`,\n [instanceId, userId, Date.now()]\n );\n return rows.length > 0;\n });\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Under the user's lock, every row committed before the tombstone is visible from here,\n // and no write about the user can commit until this transaction ends\n await this.lockUser(tx, userId);\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [userId]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n // Not suppression: an opt-out is a compliance record keyed by an opaque id, and it\n // outlives the erasure\n this.t.device,\n this.t.event,\n this.t.profile,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);\n }\n // The tombstone stays, now marked erased; the first erase's time is kept by a repeat\n await tx.query(\n `UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS('$2')}) WHERE user_id = $1`,\n [userId, at]\n );\n });\n }\n\n async isSuppressed(userId: string, channel: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2`,\n [userId, channel]\n );\n return rows.length > 0;\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the null\n const rows = await this.sql.query<{ user_id: string | null }>(\n `WITH old AS (SELECT token, user_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token\n RETURNING old.user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n version: string | null;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n version: row.version ?? undefined,\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n segment: string;\n version: string | null;\n }>(`SELECT workflow, hash, segment, version FROM ${this.t.segmentTrigger}`);\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n segment: row.segment,\n version: row.version ?? undefined,\n }));\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR): Promise<void> {\n return this.sql.transaction((tx) => this.swapRoutes(tx, bundle));\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle, version.id);\n return true;\n });\n }\n\n /** `version`: the code version applying the bundle, kept on every route it writes. */\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR, version?: string): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n version ?? null,\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment, version ?? null]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n // One locked transaction: the membership and the starts it owes commit together\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const member = await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts)\n SELECT $1, $2, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT DO NOTHING RETURNING segment`,\n [segment, userId, ts]\n );\n // Already a member, or refused: either way the starts below write nothing new\n if (member.length === 0) await this.refuseIfErased(tx, userId);\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$3')}\n ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const { execution } = entry;\n // One transaction under the user's lock: the claim (or reclaim) and the start it owes commit\n // together — a crash between them would leave a running entry no retry could ever start —\n // and before or after an erasure of the user, never astride it\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, entry.userId);\n const outcome = await this.claimEntry(tx, entry);\n if (outcome !== null && execution !== undefined) {\n await this.recordStart(tx, entry, execution);\n }\n return outcome;\n });\n }\n\n private async claimEntry(sql: SqlClientLike, entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const inserted = await sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts)\n SELECT $1, $2, $3, $4, 'running', ${TS('$5')} ${this.notErased('$2')}\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back. The reclaim is a new attempt: it\n // starts unadmitted, even under the same instance id, so the previous\n // attempt's admission is not inherited (`runningInstances` would report a\n // runner that has not been created yet).\n const claimed = await sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')},\n admitted_at = NULL\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed'\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)\n RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId]\n );\n if (claimed.length > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(sql, entry.userId);\n return null;\n }\n\n /**\n * Only an entry this call claimed records a start, so a row already there is a previous\n * attempt's — a failed entry reclaimed under the same instance id (an event replayed with its\n * ts). It is replaced whole: the new attempt's name and payload, nothing submitted yet.\n */\n private async recordStart(\n tx: SqlClientLike,\n entry: EntryInput,\n execution: ExecutionStart\n ): Promise<void> {\n const rows = await tx.query(\n `INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)\n SELECT $1, $2, $3, $4, $5, $6, ${TS('$7')}, 0 ${this.notErased('$3')}\n ON CONFLICT (instance_id) DO UPDATE SET\n name = EXCLUDED.name, target = EXCLUDED.target, input = EXCLUDED.input,\n created_at = EXCLUDED.created_at, generation = 0, submitted_at = NULL, arn = NULL,\n unresolved_at = NULL, callback_id = NULL, deadline_at = NULL, checked_at = NULL,\n stopped_at = NULL\n RETURNING instance_id`,\n [\n entry.instanceId,\n entry.workflow,\n entry.userId,\n execution.name,\n execution.target,\n execution.input,\n // The start's own age (the retry budget), not the event's ts, which the producer chose\n Date.now(),\n ]\n );\n // Refused between the claim and this: the throw rolls the claimed entry back with it\n if (rows.length === 0) throw new UserErasedError(entry.userId);\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n // One locked transaction for the call's registrations: all of them, or none\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n let skipped = false;\n for (const event of events) {\n const rows = await tx.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$1')}\n ON CONFLICT DO NOTHING RETURNING event`,\n [userId, event, handle, ts]\n );\n if (rows.length === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(tx, userId);\n });\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n\ninterface ExecutionRow {\n instance_id: string;\n workflow: string;\n user_id: string;\n name: string;\n target: string;\n input: string;\n // float8 comes back as a number from most drivers, as a string from some\n created_at: number | string;\n submitted_at: number | string | null;\n arn: string | null;\n unresolved_at: number | string | null;\n callback_id: string | null;\n generation: number | string;\n deadline_at: number | string | null;\n checked_at: number | string | null;\n stopped_at: number | string | null;\n}\n\nconst PLAIN_COLUMNS = ['instance_id', 'workflow', 'user_id', 'name', 'target', 'input', 'arn'];\nconst MORE_PLAIN = ['callback_id', 'generation'];\nconst TIME_COLUMNS = [\n 'created_at',\n 'submitted_at',\n 'unresolved_at',\n 'deadline_at',\n 'checked_at',\n 'stopped_at',\n];\n\n/** The execution columns, timestamps as epoch ms; `alias` qualifies them in a join. */\nfunction executionColumns(alias = ''): string {\n const q = alias === '' ? '' : `${alias}.`;\n return [\n ...[...PLAIN_COLUMNS, ...MORE_PLAIN].map((column) => `${q}${column}`),\n ...TIME_COLUMNS.map(\n (column) => `(extract(epoch from ${q}${column}) * 1000)::double precision AS ${column}`\n ),\n ].join(', ');\n}\n\nconst optional = <T>(value: T | null): T | undefined => value ?? undefined;\nconst ms = (value: number | string | null): number | undefined =>\n value === null ? undefined : Number(value);\n\nfunction toRecord(row: ExecutionRow): ExecutionRecord {\n return {\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n name: row.name,\n target: row.target,\n input: row.input,\n createdAt: Number(row.created_at),\n submittedAt: ms(row.submitted_at),\n arn: optional(row.arn),\n unresolvedAt: ms(row.unresolved_at),\n callbackId: optional(row.callback_id),\n generation: Number(row.generation),\n deadlineAt: ms(row.deadline_at),\n checkedAt: ms(row.checked_at),\n stoppedAt: ms(row.stopped_at),\n };\n}\n\n/** The execution table (tables.ts): starts to confirm, and the callback each wait parks on. */\nclass PostgresExecutions implements ExecutionRegistry {\n constructor(\n private readonly sql: SqlClientLike,\n private readonly table: string,\n private readonly entry: string\n ) {}\n\n async get(instanceId: string): Promise<ExecutionRecord | undefined> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns()} FROM ${this.table} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows.length === 0 ? undefined : toRecord(rows[0]);\n }\n\n async pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]> {\n const where = ['x.submitted_at IS NULL', 'x.unresolved_at IS NULL', \"e.status = 'running'\"];\n const params: unknown[] = [];\n if (filter.workflow !== undefined) {\n params.push(filter.workflow);\n where.push(`x.workflow = $${params.length}`);\n }\n if (filter.userId !== undefined) {\n params.push(filter.userId);\n where.push(`x.user_id = $${params.length}`);\n }\n if (filter.createdBefore !== undefined) {\n params.push(filter.createdBefore);\n where.push(`x.created_at < ${TS(`$${params.length}`)}`);\n }\n params.push(filter.limit);\n // Only the start the entry still names: a reclaimed entry's old start is history\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE ${where.join(' AND ')} ORDER BY x.created_at LIMIT $${params.length}`,\n params\n );\n return rows.map(toRecord);\n }\n\n async markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET submitted_at = COALESCE(submitted_at, ${TS('$2')}), arn = COALESCE($3, arn) WHERE instance_id = $1`,\n [instanceId, at, arn ?? null]\n );\n }\n\n async markUnresolved(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET unresolved_at = ${TS('$2')} WHERE instance_id = $1 AND submitted_at IS NULL`,\n [instanceId, at]\n );\n }\n\n async setArn(instanceId: string, arn: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.table} SET arn = $2 WHERE instance_id = $1`, [\n instanceId,\n arn,\n ]);\n }\n\n async armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean> {\n // The generation test is inside the UPDATE: a retried arm of an older wait cannot overwrite\n // the callback a newer one parks on\n const rows = await this.sql.query(\n `UPDATE ${this.table} SET callback_id = $2, generation = $3,\n deadline_at = CASE WHEN $4::double precision IS NULL THEN NULL ELSE ${TS('$4')} END\n WHERE instance_id = $1 AND generation <= $3 RETURNING instance_id`,\n [instanceId, callback.id, callback.generation, callback.deadlineAt ?? null]\n );\n return rows.length > 0;\n }\n\n async clearCallback(instanceId: string, generation: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET callback_id = NULL, deadline_at = NULL WHERE instance_id = $1 AND generation = $2`,\n [instanceId, generation]\n );\n }\n\n async toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE e.status = 'running' AND x.submitted_at IS NOT NULL\n AND (x.checked_at IS NULL OR x.checked_at < ${TS('$1')})\n ORDER BY x.checked_at NULLS FIRST LIMIT $2`,\n [checkedBefore, limit]\n );\n return rows.map(toRecord);\n }\n\n async markStopped(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET stopped_at = COALESCE(stopped_at, ${TS('$2')}) WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n\n async markChecked(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET checked_at = ${TS('$2')} WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"postgres.mjs","names":[],"sources":["../../src/store/postgres.ts"],"sourcesContent":["import type { PushDevice } from '../engine/ports';\nimport { type BundleIR, ConditionIR, WorkflowIR } from '../ir';\nimport { type ErasureState, UserErasedError } from './erasure';\nimport type {\n BundleVersion,\n DeviceInput,\n EntryInput,\n EntryOutcome,\n EventInput,\n EventWindow,\n ExecutionRecord,\n ExecutionRegistry,\n ExecutionStart,\n JourneyReporting,\n JourneyStore,\n ProfileProps,\n RunningInstance,\n SegmentTriggerRoute,\n TriggerRoute,\n} from './index';\nimport { PostgresReporting } from './postgres-reporting';\nimport { type SqlClientLike, uuidv7 } from './sql';\nimport { type ColumnType, DEFAULT_SCHEMA, TABLES, type TableKey, columnName } from './tables';\nimport { lockUserTransaction } from './user-lock';\n\nexport { DEFAULT_SCHEMA } from './tables';\nexport { type SqlClientLike, uuidv7 } from './sql';\n\nexport interface PostgresJourneyStoreOptions {\n /**\n * The Postgres schema the engine's tables live in — `workflow` by default, the config's\n * `database.schema` under journeyWorker. A schema of its own keeps the tables apart from the\n * app's. The host creates them with its own migrations, from the drizzle file\n * `renderDrizzleSchema` generates (or the plain `renderPostgresDdl`).\n */\n schema?: string;\n}\n\n/** information_schema's udt_name per column type. */\nconst UDT: Record<ColumnType, string> = {\n uuid: 'uuid',\n text: 'text',\n jsonb: 'jsonb',\n timestamptz: 'timestamptz',\n integer: 'int4',\n boolean: 'bool',\n};\n\n/** Epoch milliseconds → timestamptz, so the tables keep the database's native type. */\nconst TS = (param: string) => `to_timestamp(${param}::double precision / 1000)`;\n\nfunction asJson<T>(value: unknown): T {\n return (typeof value === 'string' ? JSON.parse(value) : value) as T;\n}\n\n/**\n * The journey data plane on Postgres: native types where Postgres has them\n * (jsonb, timestamptz), and the concurrency guarantees come from the database: ON CONFLICT for the once policy and\n * idempotent subscriptions, a transaction for the route swap, a jsonb merge\n * for /identify. The tables are declared in tables.ts.\n *\n * Every write about a user is conditional on their erasure tombstone not existing\n * (`INSERT ... SELECT ... WHERE NOT EXISTS`, see `notErased`), and runs in a transaction that\n * holds the user's advisory lock (`lockUser`, the protocol in user-lock.ts): an event, a\n * profile merge, a device, a subscription, a segment membership with its starts, an entry, an\n * admission, and the reporting's instance and message rows (postgres-reporting.ts). The\n * erasure's two steps, `requestErasure` and `forgetUser`, take the same lock. Under READ\n * COMMITTED a conditional write sees only what was committed when its statement began, and an\n * erasure sees only rows committed when it looked — so without the lock a write's transaction\n * could commit its rows after the erase deleted everything (the user comes back), and a runner\n * could admit itself after the tombstone (the instance runs on). With it, every write and the\n * erasure of one user run one after another: a write either commits before the tombstone,\n * where the erasure finds and deletes it, or waits for the lock, reads the tombstone and is\n * refused (`UserErasedError`). Nothing lands after the erase, so there is no second pass. The\n * conditional statement stays the in-transaction check; a write that wrote nothing reads the\n * tombstone on that path only, to tell a refusal from a plain no-op (`refuseIfErased`).\n * Suppressions are the one exception: an opt-out is written whatever the user's state.\n */\nexport class PostgresJourneyStore implements JourneyStore {\n private readonly t: Record<TableKey, string>;\n private readonly schema: string;\n readonly reporting: JourneyReporting;\n readonly executions: ExecutionRegistry;\n\n constructor(\n private readonly sql: SqlClientLike,\n options: PostgresJourneyStoreOptions = {}\n ) {\n this.schema = options.schema ?? DEFAULT_SCHEMA;\n const qualify = (name: string) => `\"${this.schema}\".\"${name}\"`;\n this.t = Object.fromEntries(\n (Object.keys(TABLES) as TableKey[]).map((key) => [key, qualify(TABLES[key].name)])\n ) as Record<TableKey, string>;\n this.reporting = new PostgresReporting(sql, this.t, this.schema);\n this.executions = new PostgresExecutions(sql, this.t.execution, this.t.entry);\n }\n\n /**\n * What the live database lacks against tables.ts: missing tables, missing columns, a column\n * of another type or nullability, and missing unique indexes (the store's ON CONFLICT targets\n * — without one the first ingest fails, not the deploy). Read-only — the engine never migrates\n * the host's database; a deploy fails with these problems listed, and the fix is to regenerate\n * the drizzle file and run the host's migrations.\n */\n async checkSchema(): Promise<string[]> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache — a\n // deploy right after the migration must see the new columns\n const { columns, indexes } = await this.sql.transaction(async (tx) => ({\n columns: await tx.query<{\n table_name: string;\n column_name: string;\n udt_name: string;\n is_nullable: string;\n }>(\n `SELECT table_name, column_name, udt_name, is_nullable FROM information_schema.columns WHERE table_schema = $1`,\n [this.schema]\n ),\n indexes: await tx.query<{ tablename: string; indexname: string; indexdef: string }>(\n `SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = $1`,\n [this.schema]\n ),\n }));\n const live = new Map(columns.map((row) => [`${row.table_name}.${row.column_name}`, row]));\n const tables = new Set(columns.map((row) => row.table_name));\n const liveIndexes = new Map(indexes.map((row) => [`${row.tablename}.${row.indexname}`, row]));\n const problems: string[] = [];\n for (const spec of Object.values(TABLES)) {\n const where = `\"${this.schema}\".\"${spec.name}\"`;\n if (!tables.has(spec.name)) {\n problems.push(`table ${where} is missing`);\n continue;\n }\n for (const [key, column] of Object.entries(spec.columns)) {\n const name = columnName(key);\n const row = live.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`column ${where}.\"${name}\" is missing`);\n else if (row.udt_name !== UDT[column.type])\n problems.push(\n `column ${where}.\"${name}\" is ${row.udt_name}, expected ${UDT[column.type]}`\n );\n else if ((row.is_nullable === 'YES') !== (column.nullable === true))\n problems.push(\n `column ${where}.\"${name}\" should be ${column.nullable ? 'nullable' : 'NOT NULL'}`\n );\n }\n // The names drizzle-kit and renderPostgresDdl both give an unnamed unique index\n for (const keys of spec.uniqueIndexes ?? []) {\n const name = `${spec.name}_${keys.map(columnName).join('_')}_index`;\n const row = liveIndexes.get(`${spec.name}.${name}`);\n if (row === undefined) problems.push(`missing unique index \"${this.schema}\".\"${name}\"`);\n else if (!/^CREATE UNIQUE INDEX/i.test(row.indexdef))\n problems.push(`index \"${this.schema}\".\"${name}\" is not unique`);\n }\n }\n return problems;\n }\n\n async insertEvent(input: EventInput): Promise<boolean> {\n // The row key stays a UUID v7 (the house rule for every table); the\n // sender's occurrence id lives in event_id, unique per user, which is\n // what makes a second delivery a no-op. ON CONFLICT DO NOTHING + RETURNING\n // reports which of the two happened without a second round trip.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, input.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.event} (id, event_id, user_id, name, ts, payload)\n SELECT $1::uuid, $2, $3, $4, ${TS('$5')}, $6::text::jsonb ${this.notErased('$3')}\n ON CONFLICT (user_id, event_id) DO NOTHING RETURNING id`,\n [\n uuidv7(input.ts),\n input.id,\n input.userId,\n input.event,\n input.ts,\n JSON.stringify(input.payload),\n ]\n );\n if (rows.length > 0) return true;\n // Nothing written: a duplicate, unless the tombstone refused it\n await this.refuseIfErased(tx, input.userId);\n return false;\n });\n }\n\n /** The condition every write about a user carries: no erasure tombstone for `$user`. */\n private notErased(user: string): string {\n return `WHERE NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = ${user})`;\n }\n\n /**\n * A conditional write wrote nothing: on that path only, read why. Throws when the tombstone\n * refused it; returns when the statement had another reason (a duplicate, an entry taken).\n */\n private async refuseIfErased(sql: SqlClientLike, userId: string): Promise<void> {\n const rows = await sql.query(`SELECT 1 AS x FROM ${this.t.erasure} WHERE user_id = $1`, [\n userId,\n ]);\n if (rows.length > 0) throw new UserErasedError(userId);\n }\n\n /**\n * The first two statements of every transaction that writes about the user (user-lock.ts):\n * READ COMMITTED, then the user's advisory lock, held until the transaction ends. Any read\n * about the user comes after, on a snapshot taken once the lock is held.\n */\n private lockUser(tx: SqlClientLike, userId: string): Promise<void> {\n return lockUserTransaction(tx, this.schema, userId);\n }\n\n /** The event rows of one user and name, within the window's bounds (both inclusive). */\n private eventWindow(userId: string, event: string, window?: EventWindow) {\n const params: unknown[] = [userId, event];\n let where = 'user_id = $1 AND name = $2';\n if (window?.sinceMs !== undefined) {\n params.push(window.sinceMs);\n where += ` AND ts >= ${TS(`$${params.length}`)}`;\n }\n if (window?.untilMs !== undefined) {\n params.push(window.untilMs);\n where += ` AND ts <= ${TS(`$${params.length}`)}`;\n }\n return { where, params };\n }\n\n async countEvents(userId: string, event: string, window?: EventWindow): Promise<number> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ c: number | string }>(\n `SELECT count(*)::int AS c FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return Number(rows.at(0)?.c ?? 0);\n }\n\n async listEventPayloads(\n userId: string,\n event: string,\n window?: EventWindow\n ): Promise<Record<string, unknown>[]> {\n const { where, params } = this.eventWindow(userId, event, window);\n const rows = await this.sql.query<{ payload: unknown }>(\n `SELECT payload FROM ${this.t.event} WHERE ${where}`,\n params\n );\n return rows.map((row) => asJson<Record<string, unknown>>(row.payload));\n }\n\n async getProfile(userId: string): Promise<ProfileProps | undefined> {\n const rows = await this.sql.query<{ props: unknown }>(\n `SELECT props FROM ${this.t.profile} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : asJson<ProfileProps>(row.props);\n }\n\n async mergeProfile(userId: string, props: ProfileProps): Promise<ProfileProps> {\n // RFC 7386 merge-patch in one statement: jsonb `||` overwrites key by key\n // and jsonb_strip_nulls then drops the keys the patch set to null (the\n // API's only way to unset a property). Atomic in the database, so two\n // concurrent identifies cannot lose each other's keys.\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const rows = await tx.query<{ props: unknown }>(\n `INSERT INTO ${this.t.profile} (user_id, props)\n SELECT $1, jsonb_strip_nulls($2::text::jsonb) ${this.notErased('$1')}\n ON CONFLICT (user_id) DO UPDATE SET props = jsonb_strip_nulls(${this.t.profile}.props || $3::text::jsonb)\n RETURNING props`,\n // EXCLUDED.props would be the stripped insert value, so the update merges the raw patch again\n [userId, JSON.stringify(props), JSON.stringify(props)]\n );\n // The upsert always yields its row — unless the tombstone refused it\n if (rows.length === 0) throw new UserErasedError(userId);\n return asJson<ProfileProps>(rows[0].props);\n });\n }\n\n async listUserIds(page?: {\n after?: string | undefined;\n limit?: number | undefined;\n }): Promise<string[]> {\n // Ascending user_id with a keyset bound, so a sweep can resume past the last id it saw\n const params: unknown[] = [];\n let where = '';\n if (page?.after !== undefined) {\n params.push(page.after);\n where = ` WHERE user_id > $${params.length}`;\n }\n let limit = '';\n if (page?.limit !== undefined) {\n params.push(page.limit);\n limit = ` LIMIT $${params.length}`;\n }\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM (SELECT user_id FROM ${this.t.profile} UNION SELECT user_id FROM ${this.t.event}) u${where} ORDER BY user_id${limit}`,\n params\n );\n return rows.map((row) => row.user_id);\n }\n\n listDevices(userId: string): Promise<PushDevice[]> {\n return this.sql.query<PushDevice>(\n `SELECT token, platform FROM ${this.t.device} WHERE user_id = $1 ORDER BY ts DESC`,\n [userId]\n );\n }\n\n async upsertDevice(device: DeviceInput): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, device.userId);\n const rows = await tx.query(\n `INSERT INTO ${this.t.device} (token, user_id, platform, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$2')}\n ON CONFLICT (token) DO UPDATE SET user_id = EXCLUDED.user_id, platform = EXCLUDED.platform, ts = EXCLUDED.ts\n RETURNING token`,\n [device.token, device.userId, device.platform, device.ts]\n );\n if (rows.length === 0) throw new UserErasedError(device.userId);\n });\n }\n\n async requestErasure(userId: string, at: number): Promise<void> {\n // Under the user's lock: a write in flight commits before the tombstone (and is found by\n // the erase) or reads it after (and is refused)\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n await tx.query(\n `INSERT INTO ${this.t.erasure} (user_id, requested_at) VALUES ($1, ${TS('$2')})\n ON CONFLICT DO NOTHING`,\n [userId, at]\n );\n });\n }\n\n async pendingErasures(): Promise<string[]> {\n const rows = await this.sql.query<{ user_id: string }>(\n `SELECT user_id FROM ${this.t.erasure} WHERE erased_at IS NULL ORDER BY requested_at`\n );\n return rows.map((row) => row.user_id);\n }\n\n async erasureState(userId: string): Promise<ErasureState> {\n const rows = await this.sql.query<{ erased: boolean }>(\n `SELECT erased_at IS NOT NULL AS erased FROM ${this.t.erasure} WHERE user_id = $1`,\n [userId]\n );\n const row = rows.at(0);\n if (row === undefined) return 'none';\n return row.erased ? 'erased' : 'erasing';\n }\n\n async runningInstances(userId: string): Promise<RunningInstance[]> {\n const rows = await this.sql.query<{ instance_id: string; admitted: boolean }>(\n `SELECT instance_id, admitted_at IS NOT NULL AS admitted\n FROM ${this.t.entry} WHERE user_id = $1 AND status = 'running'`,\n [userId]\n );\n return rows.map((row) => ({ instanceId: row.instance_id, admitted: row.admitted }));\n }\n\n async admitInstance(instanceId: string, userId: string): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n // A first admission needs a running entry; an instance once admitted may replay its\n // finalisation after the entry reached a terminal status, under its own instance id (a\n // reclaim resets `admitted_at`, so a superseded attempt cannot). The first admission's\n // time is kept, the status never touched, and the tombstone refuses both.\n const rows = await tx.query(\n `UPDATE ${this.t.entry} SET admitted_at = COALESCE(admitted_at, ${TS('$3')})\n WHERE instance_id = $1 AND user_id = $2\n AND (status = 'running' OR admitted_at IS NOT NULL)\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $2)\n RETURNING instance_id`,\n [instanceId, userId, Date.now()]\n );\n return rows.length > 0;\n });\n }\n\n async forgetUser(userId: string, at: number): Promise<void> {\n await this.sql.transaction(async (tx) => {\n // Under the user's lock, every row committed before the tombstone is visible from here,\n // and no write about the user can commit until this transaction ends\n await this.lockUser(tx, userId);\n // Children before the copies they belong to\n for (const child of [this.t.messageOpen, this.t.messageClick, this.t.messageLink]) {\n await tx.query(\n `DELETE FROM ${child} WHERE message_id IN (SELECT id FROM ${this.t.message} WHERE user_id = $1)`,\n [userId]\n );\n }\n for (const table of [\n this.t.message,\n this.t.instance,\n this.t.entry,\n this.t.execution,\n this.t.subscription,\n this.t.segmentStart,\n this.t.segmentMember,\n // Not suppression: an opt-out is a compliance record keyed by an opaque id, and it\n // outlives the erasure\n this.t.device,\n this.t.event,\n this.t.profile,\n ]) {\n await tx.query(`DELETE FROM ${table} WHERE user_id = $1`, [userId]);\n }\n // The tombstone stays, now marked erased; the first erase's time is kept by a repeat\n await tx.query(\n `UPDATE ${this.t.erasure} SET erased_at = COALESCE(erased_at, ${TS('$2')}) WHERE user_id = $1`,\n [userId, at]\n );\n });\n }\n\n async isSuppressed(userId: string, channel: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.suppression} WHERE user_id = $1 AND channel = $2`,\n [userId, channel]\n );\n return rows.length > 0;\n }\n\n async removeDevice(token: string): Promise<string | undefined> {\n const rows = await this.sql.query<{ user_id: string | null }>(\n `DELETE FROM ${this.t.device} WHERE token = $1 RETURNING user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async unbindDevice(token: string): Promise<string | undefined> {\n // The owner as it was before the update: RETURNING alone would only show the null\n const rows = await this.sql.query<{ user_id: string | null }>(\n `WITH old AS (SELECT token, user_id FROM ${this.t.device} WHERE token = $1 FOR UPDATE)\n UPDATE ${this.t.device} d SET user_id = NULL FROM old WHERE d.token = old.token\n RETURNING old.user_id`,\n [token]\n );\n return rows[0]?.user_id ?? undefined;\n }\n\n async listSegments(): Promise<{ id: string; condition: ConditionIR }[]> {\n const rows = await this.sql.query<{ id: string; condition: unknown }>(\n `SELECT id, condition FROM ${this.t.segment} ORDER BY id`\n );\n return rows.map((row) => ({\n id: row.id,\n condition: ConditionIR.parse(asJson(row.condition)),\n }));\n }\n\n async getSegmentCondition(id: string): Promise<ConditionIR | undefined> {\n const rows = await this.sql.query<{ condition: unknown }>(\n `SELECT condition FROM ${this.t.segment} WHERE id = $1`,\n [id]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : ConditionIR.parse(asJson(row.condition));\n }\n\n async findTriggers(event: string): Promise<TriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n where_clause: unknown;\n filter: unknown;\n version: string | null;\n }>(\n `SELECT workflow, hash, \"where\" AS where_clause, filter, version FROM ${this.t.trigger} WHERE event = $1`,\n [event]\n );\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n where: row.where_clause === null ? null : ConditionIR.parse(asJson(row.where_clause)),\n filter: row.filter === null ? null : ConditionIR.parse(asJson(row.filter)),\n version: row.version ?? undefined,\n }));\n }\n\n async listSegmentTriggers(): Promise<SegmentTriggerRoute[]> {\n const rows = await this.sql.query<{\n workflow: string;\n hash: string;\n segment: string;\n version: string | null;\n }>(`SELECT workflow, hash, segment, version FROM ${this.t.segmentTrigger}`);\n return rows.map((row) => ({\n workflow: row.workflow,\n hash: row.hash,\n segment: row.segment,\n version: row.version ?? undefined,\n }));\n }\n\n async putWorkflowIR(workflow: WorkflowIR): Promise<void> {\n await this.sql.query(\n `INSERT INTO ${this.t.ir} (content_hash, ir) VALUES ($1, $2::text::jsonb)\n ON CONFLICT (content_hash) DO UPDATE SET ir = EXCLUDED.ir`,\n [workflow.contentHash, JSON.stringify(workflow)]\n );\n }\n\n async getWorkflowIR(contentHash: string): Promise<WorkflowIR | undefined> {\n const rows = await this.sql.query<{ ir: unknown }>(\n `SELECT ir FROM ${this.t.ir} WHERE content_hash = $1`,\n [contentHash]\n );\n const row = rows.at(0);\n return row === undefined ? undefined : WorkflowIR.parse(asJson(row.ir));\n }\n\n replaceRoutes(bundle: BundleIR): Promise<void> {\n return this.sql.transaction((tx) => this.swapRoutes(tx, bundle));\n }\n\n async liveBundleVersion(): Promise<BundleVersion | undefined> {\n // In a transaction, so Hyperdrive answers from the database, not from its read cache\n const rows = await this.sql.transaction((tx) =>\n tx.query<{ version_id: string; created_at: number | string }>(\n `SELECT version_id, (extract(epoch FROM version_created_at) * 1000)::double precision AS created_at\n FROM ${this.t.deployment} WHERE scope = 'routes'`\n )\n );\n const row = rows.at(0);\n return row === undefined\n ? undefined\n : { id: row.version_id, createdAt: Number(row.created_at) };\n }\n\n replaceRoutesIfNewer(bundle: BundleIR, version: BundleVersion): Promise<boolean> {\n return this.sql.transaction(async (tx) => {\n // The row lock decides between versions applying at once: the loser's WHERE re-reads the\n // winner's committed row and updates nothing\n const claimed = await tx.query(\n `INSERT INTO ${this.t.deployment} AS live (scope, version_id, version_created_at, applied_at)\n VALUES ('routes', $1, ${TS('$2')}, ${TS('$3')})\n ON CONFLICT (scope) DO UPDATE SET version_id = EXCLUDED.version_id,\n version_created_at = EXCLUDED.version_created_at, applied_at = EXCLUDED.applied_at\n WHERE live.version_created_at < EXCLUDED.version_created_at\n RETURNING scope`,\n [version.id, version.createdAt, Date.now()]\n );\n if (claimed.length === 0) return false;\n await this.swapRoutes(tx, bundle, version.id);\n return true;\n });\n }\n\n /** `version`: the code version applying the bundle, kept on every route it writes. */\n private async swapRoutes(tx: SqlClientLike, bundle: BundleIR, version?: string): Promise<void> {\n await tx.query(`DELETE FROM ${this.t.trigger}`);\n await tx.query(`DELETE FROM ${this.t.segmentTrigger}`);\n await tx.query(`DELETE FROM ${this.t.segment}`);\n for (const segment of bundle.segments) {\n await tx.query(\n `INSERT INTO ${this.t.segment} (id, condition, hash) VALUES ($1, $2::text::jsonb, $3)`,\n [segment.id, JSON.stringify(segment.condition), segment.contentHash]\n );\n }\n for (const workflow of bundle.workflows) {\n if (workflow.trigger.type === 'event') {\n await tx.query(\n `INSERT INTO ${this.t.trigger} (workflow, hash, event, \"where\", filter, version) VALUES ($1, $2, $3, $4::text::jsonb, $5::text::jsonb, $6)`,\n [\n workflow.id,\n workflow.contentHash,\n workflow.trigger.event,\n workflow.trigger.where === undefined ? null : JSON.stringify(workflow.trigger.where),\n workflow.trigger.filter === undefined ? null : JSON.stringify(workflow.trigger.filter),\n version ?? null,\n ]\n );\n } else if (workflow.trigger.type === 'segment') {\n await tx.query(\n `INSERT INTO ${this.t.segmentTrigger} (workflow, hash, segment, version) VALUES ($1, $2, $3, $4)`,\n [workflow.id, workflow.contentHash, workflow.trigger.segment, version ?? null]\n );\n }\n }\n }\n\n async isSegmentMember(segment: string, userId: string): Promise<boolean> {\n const rows = await this.sql.query(\n `SELECT 1 AS x FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.length > 0;\n }\n\n async addSegmentMember(\n segment: string,\n userId: string,\n ts: number,\n workflows: readonly string[]\n ): Promise<void> {\n // One locked transaction: the membership and the starts it owes commit together\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n const member = await tx.query(\n `INSERT INTO ${this.t.segmentMember} (segment, user_id, ts)\n SELECT $1, $2, ${TS('$3')} ${this.notErased('$2')}\n ON CONFLICT DO NOTHING RETURNING segment`,\n [segment, userId, ts]\n );\n // Already a member, or refused: either way the starts below write nothing new\n if (member.length === 0) await this.refuseIfErased(tx, userId);\n for (const workflow of workflows) {\n await tx.query(\n `INSERT INTO ${this.t.segmentStart} (segment, workflow, user_id, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$3')}\n ON CONFLICT DO NOTHING`,\n [segment, workflow, userId, ts]\n );\n }\n });\n }\n\n async pendingSegmentStarts(segment: string, userId: string): Promise<string[]> {\n const rows = await this.sql.query<{ workflow: string }>(\n `SELECT workflow FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`,\n [segment, userId]\n );\n return rows.map((row) => row.workflow);\n }\n\n async completeSegmentStart(segment: string, userId: string, workflow: string): Promise<void> {\n await this.sql.query(\n `DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2 AND workflow = $3`,\n [segment, userId, workflow]\n );\n }\n\n async removeSegmentMember(segment: string, userId: string): Promise<void> {\n await this.sql.transaction(async (tx) => {\n await tx.query(`DELETE FROM ${this.t.segmentStart} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n await tx.query(`DELETE FROM ${this.t.segmentMember} WHERE segment = $1 AND user_id = $2`, [\n segment,\n userId,\n ]);\n });\n }\n\n async enterJourney(entry: EntryInput): Promise<EntryOutcome> {\n const { execution } = entry;\n // One transaction under the user's lock: the claim (or reclaim) and the start it owes commit\n // together — a crash between them would leave a running entry no retry could ever start —\n // and before or after an erasure of the user, never astride it\n return this.sql.transaction(async (tx) => {\n await this.lockUser(tx, entry.userId);\n const outcome = await this.claimEntry(tx, entry);\n if (outcome !== null && execution !== undefined) {\n await this.recordStart(tx, entry, execution);\n }\n return outcome;\n });\n }\n\n private async claimEntry(sql: SqlClientLike, entry: EntryInput): Promise<EntryOutcome> {\n // The (workflow, user_id) primary key makes the once policy atomic:\n // concurrent ingests race on the insert and exactly one wins.\n const inserted = await sql.query(\n `INSERT INTO ${this.t.entry} (workflow, user_id, instance_id, hash, status, ts)\n SELECT $1, $2, $3, $4, 'running', ${TS('$5')} ${this.notErased('$2')}\n ON CONFLICT (workflow, user_id) DO NOTHING RETURNING instance_id`,\n [entry.workflow, entry.userId, entry.instanceId, entry.hash, entry.ts]\n );\n if (inserted.length > 0) return 'entered';\n\n // An existing row blocks the entry unless it records an instance that died:\n // 'failed' is reclaimed rather than barring the user forever over one\n // outage. The status test is inside the UPDATE, so of two concurrent\n // retries exactly one gets a row back. The reclaim is a new attempt: it\n // starts unadmitted, even under the same instance id, so the previous\n // attempt's admission is not inherited (`runningInstances` would report a\n // runner that has not been created yet).\n const claimed = await sql.query(\n `UPDATE ${this.t.entry} SET instance_id = $1, hash = $2, status = 'running', ts = ${TS('$3')},\n admitted_at = NULL\n WHERE workflow = $4 AND user_id = $5 AND status = 'failed'\n AND NOT EXISTS (SELECT 1 FROM ${this.t.erasure} WHERE user_id = $5)\n RETURNING instance_id`,\n [entry.instanceId, entry.hash, entry.ts, entry.workflow, entry.userId]\n );\n if (claimed.length > 0) return 'reclaimed';\n // Taken by a live instance, or refused by the tombstone: tell them apart\n await this.refuseIfErased(sql, entry.userId);\n return null;\n }\n\n /**\n * Only an entry this call claimed records a start, so a row already there is a previous\n * attempt's — a failed entry reclaimed under the same instance id (an event replayed with its\n * ts). It is replaced whole: the new attempt's name and payload, nothing submitted yet.\n */\n private async recordStart(\n tx: SqlClientLike,\n entry: EntryInput,\n execution: ExecutionStart\n ): Promise<void> {\n const rows = await tx.query(\n `INSERT INTO ${this.t.execution} (instance_id, workflow, user_id, name, target, input, created_at, generation)\n SELECT $1, $2, $3, $4, $5, $6, ${TS('$7')}, 0 ${this.notErased('$3')}\n ON CONFLICT (instance_id) DO UPDATE SET\n name = EXCLUDED.name, target = EXCLUDED.target, input = EXCLUDED.input,\n created_at = EXCLUDED.created_at, generation = 0, submitted_at = NULL, arn = NULL,\n unresolved_at = NULL, callback_id = NULL, deadline_at = NULL, checked_at = NULL,\n stopped_at = NULL\n RETURNING instance_id`,\n [\n entry.instanceId,\n entry.workflow,\n entry.userId,\n execution.name,\n execution.target,\n execution.input,\n // The start's own age (the retry budget), not the event's ts, which the producer chose\n Date.now(),\n ]\n );\n // Refused between the claim and this: the throw rolls the claimed entry back with it\n if (rows.length === 0) throw new UserErasedError(entry.userId);\n }\n\n async removeEntry(instanceId: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.entry} WHERE instance_id = $1`, [instanceId]);\n }\n\n async setEntryStatus(instanceId: string, status: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.t.entry} SET status = $1 WHERE instance_id = $2`, [\n status,\n instanceId,\n ]);\n }\n\n async findWakeHandles(userId: string, event: string): Promise<string[]> {\n const rows = await this.sql.query<{ handle: string }>(\n `SELECT DISTINCT wake_handle AS handle FROM ${this.t.subscription} WHERE user_id = $1 AND event = $2`,\n [userId, event]\n );\n return rows.map((row) => row.handle);\n }\n\n async subscribe(\n handle: string,\n userId: string,\n events: readonly string[],\n ts: number\n ): Promise<void> {\n // One locked transaction for the call's registrations: all of them, or none\n await this.sql.transaction(async (tx) => {\n await this.lockUser(tx, userId);\n let skipped = false;\n for (const event of events) {\n const rows = await tx.query(\n `INSERT INTO ${this.t.subscription} (user_id, event, wake_handle, ts)\n SELECT $1, $2, $3, ${TS('$4')} ${this.notErased('$1')}\n ON CONFLICT DO NOTHING RETURNING event`,\n [userId, event, handle, ts]\n );\n if (rows.length === 0) skipped = true;\n }\n // A registration already there, or refused: one read tells which\n if (skipped) await this.refuseIfErased(tx, userId);\n });\n }\n\n async unsubscribe(handle: string): Promise<void> {\n await this.sql.query(`DELETE FROM ${this.t.subscription} WHERE wake_handle = $1`, [handle]);\n }\n}\n\ninterface ExecutionRow {\n instance_id: string;\n workflow: string;\n user_id: string;\n name: string;\n target: string;\n input: string;\n // float8 comes back as a number from most drivers, as a string from some\n created_at: number | string;\n submitted_at: number | string | null;\n arn: string | null;\n unresolved_at: number | string | null;\n callback_id: string | null;\n generation: number | string;\n deadline_at: number | string | null;\n checked_at: number | string | null;\n stopped_at: number | string | null;\n}\n\nconst PLAIN_COLUMNS = ['instance_id', 'workflow', 'user_id', 'name', 'target', 'input', 'arn'];\nconst MORE_PLAIN = ['callback_id', 'generation'];\nconst TIME_COLUMNS = [\n 'created_at',\n 'submitted_at',\n 'unresolved_at',\n 'deadline_at',\n 'checked_at',\n 'stopped_at',\n];\n\n/** The execution columns, timestamps as epoch ms; `alias` qualifies them in a join. */\nfunction executionColumns(alias = ''): string {\n const q = alias === '' ? '' : `${alias}.`;\n return [\n ...[...PLAIN_COLUMNS, ...MORE_PLAIN].map((column) => `${q}${column}`),\n ...TIME_COLUMNS.map(\n (column) => `(extract(epoch from ${q}${column}) * 1000)::double precision AS ${column}`\n ),\n ].join(', ');\n}\n\nconst optional = <T>(value: T | null): T | undefined => value ?? undefined;\nconst ms = (value: number | string | null): number | undefined =>\n value === null ? undefined : Number(value);\n\nfunction toRecord(row: ExecutionRow): ExecutionRecord {\n return {\n instanceId: row.instance_id,\n workflow: row.workflow,\n userId: row.user_id,\n name: row.name,\n target: row.target,\n input: row.input,\n createdAt: Number(row.created_at),\n submittedAt: ms(row.submitted_at),\n arn: optional(row.arn),\n unresolvedAt: ms(row.unresolved_at),\n callbackId: optional(row.callback_id),\n generation: Number(row.generation),\n deadlineAt: ms(row.deadline_at),\n checkedAt: ms(row.checked_at),\n stoppedAt: ms(row.stopped_at),\n };\n}\n\n/** The execution table (tables.ts): starts to confirm, and the callback each wait parks on. */\nclass PostgresExecutions implements ExecutionRegistry {\n constructor(\n private readonly sql: SqlClientLike,\n private readonly table: string,\n private readonly entry: string\n ) {}\n\n async get(instanceId: string): Promise<ExecutionRecord | undefined> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns()} FROM ${this.table} WHERE instance_id = $1`,\n [instanceId]\n );\n return rows.length === 0 ? undefined : toRecord(rows[0]);\n }\n\n async pendingStarts(filter: {\n workflow?: string;\n userId?: string;\n createdBefore?: number;\n limit: number;\n }): Promise<ExecutionRecord[]> {\n const where = ['x.submitted_at IS NULL', 'x.unresolved_at IS NULL', \"e.status = 'running'\"];\n const params: unknown[] = [];\n if (filter.workflow !== undefined) {\n params.push(filter.workflow);\n where.push(`x.workflow = $${params.length}`);\n }\n if (filter.userId !== undefined) {\n params.push(filter.userId);\n where.push(`x.user_id = $${params.length}`);\n }\n if (filter.createdBefore !== undefined) {\n params.push(filter.createdBefore);\n where.push(`x.created_at < ${TS(`$${params.length}`)}`);\n }\n params.push(filter.limit);\n // Only the start the entry still names: a reclaimed entry's old start is history\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE ${where.join(' AND ')} ORDER BY x.created_at LIMIT $${params.length}`,\n params\n );\n return rows.map(toRecord);\n }\n\n async markSubmitted(instanceId: string, arn: string | undefined, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET submitted_at = COALESCE(submitted_at, ${TS('$2')}), arn = COALESCE($3, arn) WHERE instance_id = $1`,\n [instanceId, at, arn ?? null]\n );\n }\n\n async markUnresolved(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET unresolved_at = ${TS('$2')} WHERE instance_id = $1 AND submitted_at IS NULL`,\n [instanceId, at]\n );\n }\n\n async setArn(instanceId: string, arn: string): Promise<void> {\n await this.sql.query(`UPDATE ${this.table} SET arn = $2 WHERE instance_id = $1`, [\n instanceId,\n arn,\n ]);\n }\n\n async armCallback(\n instanceId: string,\n callback: { id: string; generation: number; deadlineAt?: number | undefined }\n ): Promise<boolean> {\n // The generation test is inside the UPDATE: a retried arm of an older wait cannot overwrite\n // the callback a newer one parks on\n const rows = await this.sql.query(\n `UPDATE ${this.table} SET callback_id = $2, generation = $3,\n deadline_at = CASE WHEN $4::double precision IS NULL THEN NULL ELSE ${TS('$4')} END\n WHERE instance_id = $1 AND generation <= $3 RETURNING instance_id`,\n [instanceId, callback.id, callback.generation, callback.deadlineAt ?? null]\n );\n return rows.length > 0;\n }\n\n async clearCallback(instanceId: string, generation: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET callback_id = NULL, deadline_at = NULL WHERE instance_id = $1 AND generation = $2`,\n [instanceId, generation]\n );\n }\n\n async toReconcile(limit: number, checkedBefore: number): Promise<ExecutionRecord[]> {\n const rows = await this.sql.query<ExecutionRow>(\n `SELECT ${executionColumns('x')}\n FROM ${this.table} x JOIN ${this.entry} e ON e.instance_id = x.instance_id\n WHERE e.status = 'running' AND x.submitted_at IS NOT NULL\n AND (x.checked_at IS NULL OR x.checked_at < ${TS('$1')})\n ORDER BY x.checked_at NULLS FIRST LIMIT $2`,\n [checkedBefore, limit]\n );\n return rows.map(toRecord);\n }\n\n async markStopped(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET stopped_at = COALESCE(stopped_at, ${TS('$2')}) WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n\n async markChecked(instanceId: string, at: number): Promise<void> {\n await this.sql.query(\n `UPDATE ${this.table} SET checked_at = ${TS('$2')} WHERE instance_id = $1`,\n [instanceId, at]\n );\n }\n}\n"],"mappings":"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"}
@@ -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,5 +1,5 @@
1
- import { EmailBindingLike } from "../cloudflare/bindings.cjs";
2
1
  import { EmailTransport } from "../email-sender.cjs";
2
+ import { EmailBindingLike } from "../cloudflare/bindings.cjs";
3
3
  //#region src/transports/cloudflare-email.d.ts
4
4
  /**
5
5
  * `@shware/workflow/transports/cloudflare-email` — Cloudflare Email Service, through the
@@ -1,5 +1,5 @@
1
- import { EmailBindingLike } from "../cloudflare/bindings.mjs";
2
1
  import { EmailTransport } from "../email-sender.mjs";
2
+ import { EmailBindingLike } from "../cloudflare/bindings.mjs";
3
3
  //#region src/transports/cloudflare-email.d.ts
4
4
  /**
5
5
  * `@shware/workflow/transports/cloudflare-email` — Cloudflare Email Service, through the
@@ -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