@shware/workflow 7.0.0 → 8.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 (154) hide show
  1. package/README.md +121 -34
  2. package/dist/aws/engine-step.cjs +4 -1
  3. package/dist/aws/engine-step.cjs.map +1 -1
  4. package/dist/aws/engine-step.d.cts.map +1 -1
  5. package/dist/aws/engine-step.d.mts.map +1 -1
  6. package/dist/aws/engine-step.mjs +4 -1
  7. package/dist/aws/engine-step.mjs.map +1 -1
  8. package/dist/client.cjs +66 -41
  9. package/dist/client.cjs.map +1 -1
  10. package/dist/client.d.cts +72 -22
  11. package/dist/client.d.cts.map +1 -1
  12. package/dist/client.d.mts +72 -22
  13. package/dist/client.d.mts.map +1 -1
  14. package/dist/client.mjs +66 -42
  15. package/dist/client.mjs.map +1 -1
  16. package/dist/cloudflare/index.d.cts +4 -3
  17. package/dist/cloudflare/index.d.mts +4 -3
  18. package/dist/cloudflare/runner.cjs +3 -1
  19. package/dist/cloudflare/runner.cjs.map +1 -1
  20. package/dist/cloudflare/runner.d.cts.map +1 -1
  21. package/dist/cloudflare/runner.d.mts.map +1 -1
  22. package/dist/cloudflare/runner.mjs +3 -1
  23. package/dist/cloudflare/runner.mjs.map +1 -1
  24. package/dist/cloudflare/worker.cjs +12 -1
  25. package/dist/cloudflare/worker.cjs.map +1 -1
  26. package/dist/cloudflare/worker.d.cts +9 -1
  27. package/dist/cloudflare/worker.d.cts.map +1 -1
  28. package/dist/cloudflare/worker.d.mts +9 -1
  29. package/dist/cloudflare/worker.d.mts.map +1 -1
  30. package/dist/cloudflare/worker.mjs +12 -1
  31. package/dist/cloudflare/worker.mjs.map +1 -1
  32. package/dist/contract.cjs.map +1 -1
  33. package/dist/contract.d.cts +4 -4
  34. package/dist/contract.d.mts +4 -4
  35. package/dist/contract.mjs.map +1 -1
  36. package/dist/dsl/trigger.cjs +2 -1
  37. package/dist/dsl/trigger.cjs.map +1 -1
  38. package/dist/dsl/trigger.d.cts +6 -0
  39. package/dist/dsl/trigger.d.cts.map +1 -1
  40. package/dist/dsl/trigger.d.mts +6 -0
  41. package/dist/dsl/trigger.d.mts.map +1 -1
  42. package/dist/dsl/trigger.mjs +2 -1
  43. package/dist/dsl/trigger.mjs.map +1 -1
  44. package/dist/engine/condition.cjs +3 -3
  45. package/dist/engine/condition.cjs.map +1 -1
  46. package/dist/engine/condition.d.cts +3 -3
  47. package/dist/engine/condition.d.mts +3 -3
  48. package/dist/engine/condition.mjs +3 -3
  49. package/dist/engine/condition.mjs.map +1 -1
  50. package/dist/engine/interpreter.cjs +9 -5
  51. package/dist/engine/interpreter.cjs.map +1 -1
  52. package/dist/engine/interpreter.d.cts.map +1 -1
  53. package/dist/engine/interpreter.d.mts.map +1 -1
  54. package/dist/engine/interpreter.mjs +9 -5
  55. package/dist/engine/interpreter.mjs.map +1 -1
  56. package/dist/engine/ports.d.cts +2 -0
  57. package/dist/engine/ports.d.cts.map +1 -1
  58. package/dist/engine/ports.d.mts +2 -0
  59. package/dist/engine/ports.d.mts.map +1 -1
  60. package/dist/host/batch.cjs +172 -0
  61. package/dist/host/batch.cjs.map +1 -0
  62. package/dist/host/batch.d.cts +88 -0
  63. package/dist/host/batch.d.cts.map +1 -0
  64. package/dist/host/batch.d.mts +88 -0
  65. package/dist/host/batch.d.mts.map +1 -0
  66. package/dist/host/batch.mjs +163 -0
  67. package/dist/host/batch.mjs.map +1 -0
  68. package/dist/host/execute.cjs +10 -6
  69. package/dist/host/execute.cjs.map +1 -1
  70. package/dist/host/execute.mjs +10 -6
  71. package/dist/host/execute.mjs.map +1 -1
  72. package/dist/host/http.cjs +30 -89
  73. package/dist/host/http.cjs.map +1 -1
  74. package/dist/host/http.d.cts +1 -1
  75. package/dist/host/http.d.cts.map +1 -1
  76. package/dist/host/http.d.mts +1 -1
  77. package/dist/host/http.d.mts.map +1 -1
  78. package/dist/host/http.mjs +24 -83
  79. package/dist/host/http.mjs.map +1 -1
  80. package/dist/host/ingest.cjs +56 -13
  81. package/dist/host/ingest.cjs.map +1 -1
  82. package/dist/host/ingest.d.cts +17 -6
  83. package/dist/host/ingest.d.cts.map +1 -1
  84. package/dist/host/ingest.d.mts +17 -6
  85. package/dist/host/ingest.d.mts.map +1 -1
  86. package/dist/host/ingest.mjs +55 -14
  87. package/dist/host/ingest.mjs.map +1 -1
  88. package/dist/host/journey-host.cjs +6 -0
  89. package/dist/host/journey-host.cjs.map +1 -1
  90. package/dist/host/journey-host.d.cts +5 -2
  91. package/dist/host/journey-host.d.cts.map +1 -1
  92. package/dist/host/journey-host.d.mts +5 -2
  93. package/dist/host/journey-host.d.mts.map +1 -1
  94. package/dist/host/journey-host.mjs +6 -0
  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 +2 -0
  98. package/dist/host/platform.d.cts.map +1 -1
  99. package/dist/host/platform.d.mts +2 -0
  100. package/dist/host/platform.d.mts.map +1 -1
  101. package/dist/host/platform.mjs.map +1 -1
  102. package/dist/ir.cjs +2 -1
  103. package/dist/ir.cjs.map +1 -1
  104. package/dist/ir.d.cts +3 -0
  105. package/dist/ir.d.cts.map +1 -1
  106. package/dist/ir.d.mts +3 -0
  107. package/dist/ir.d.mts.map +1 -1
  108. package/dist/ir.mjs +2 -1
  109. package/dist/ir.mjs.map +1 -1
  110. package/dist/store/facts.cjs +13 -9
  111. package/dist/store/facts.cjs.map +1 -1
  112. package/dist/store/facts.d.cts +6 -2
  113. package/dist/store/facts.d.cts.map +1 -1
  114. package/dist/store/facts.d.mts +6 -2
  115. package/dist/store/facts.d.mts.map +1 -1
  116. package/dist/store/facts.mjs +13 -9
  117. package/dist/store/facts.mjs.map +1 -1
  118. package/dist/store/identity.cjs +34 -0
  119. package/dist/store/identity.cjs.map +1 -0
  120. package/dist/store/identity.d.cts +13 -0
  121. package/dist/store/identity.d.cts.map +1 -0
  122. package/dist/store/identity.d.mts +13 -0
  123. package/dist/store/identity.d.mts.map +1 -0
  124. package/dist/store/identity.mjs +30 -0
  125. package/dist/store/identity.mjs.map +1 -0
  126. package/dist/store/index.cjs +5 -0
  127. package/dist/store/index.cjs.map +1 -1
  128. package/dist/store/index.d.cts +49 -15
  129. package/dist/store/index.d.cts.map +1 -1
  130. package/dist/store/index.d.mts +49 -15
  131. package/dist/store/index.d.mts.map +1 -1
  132. package/dist/store/index.mjs +2 -1
  133. package/dist/store/index.mjs.map +1 -1
  134. package/dist/store/postgres.cjs +206 -44
  135. package/dist/store/postgres.cjs.map +1 -1
  136. package/dist/store/postgres.d.cts +17 -4
  137. package/dist/store/postgres.d.cts.map +1 -1
  138. package/dist/store/postgres.d.mts +17 -4
  139. package/dist/store/postgres.d.mts.map +1 -1
  140. package/dist/store/postgres.mjs +207 -45
  141. package/dist/store/postgres.mjs.map +1 -1
  142. package/dist/store/tables.cjs +24 -6
  143. package/dist/store/tables.cjs.map +1 -1
  144. package/dist/store/tables.d.cts +1 -0
  145. package/dist/store/tables.d.cts.map +1 -1
  146. package/dist/store/tables.d.mts +1 -0
  147. package/dist/store/tables.d.mts.map +1 -1
  148. package/dist/store/tables.mjs +24 -6
  149. package/dist/store/tables.mjs.map +1 -1
  150. package/dist/webpush.cjs.map +1 -1
  151. package/dist/webpush.d.cts +1 -1
  152. package/dist/webpush.d.mts +1 -1
  153. package/dist/webpush.mjs.map +1 -1
  154. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"execute.cjs","names":["resolveRuntime","checkEvent","ContractValidationError","ingestEvent","readWithRetry","scaleDurations","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(() => store.admitInstance(instanceId, userId), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n enteredAtMs: input.enteredAtMs,\n step,\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAUA,gBAAAA,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,YAAY,QAA6C,gBAC7D,UAAU,WAAW;EACnB,UAAU;EACV;EACA;EACA;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAUC,iBAAAA,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAIC,iBAAAA,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAMC,oBAAAA,YAAY,KAAK;GAAE;GAAI;GAAQ,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC9E,EACF;CAuBA,IAAI,CAAC,MAJkBC,wBAAAA,oBAAoB,MAAM,cAAc,YAAY,MAAM,GAAG;EAClF,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAKC,0BAAAA,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAMC,2BAAAA,WAAW,IAAI;GAC7B;GACA;GACA,aAAa,MAAM;GACnB;GACA,OAAO,IAAIC,oBAAAA,kBAAkB,OAAO,MAAM;GAC1C;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"execute.cjs","names":["resolveRuntime","instanceOwner","checkEvent","ContractValidationError","ingestEvent","readWithRetry","scaleDurations","runJourney","JourneyFactSource"],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport { type JourneyStore, instanceOwner } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n const owner = () => instanceOwner(store, instanceId, userId);\n\n const finished = async (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId: await owner(),\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId: await owner(), event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(async () => store.admitInstance(instanceId, await owner()), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n currentUserId: owner,\n // A delivery that arrives late still enters when the trigger happened: a purchase made\n // while the begin_checkout was being retried is the goal, not history from before entry\n enteredAtMs: Math.min(input.enteredAtMs, input.params.trigger.ts ?? input.enteredAtMs),\n step,\n facts: new JourneyFactSource(store, userId, owner),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAUA,gBAAAA,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CACpE,MAAM,cAAcC,uBAAAA,cAAc,OAAO,YAAY,MAAM;CAE3D,MAAM,WAAW,OAAO,QAA6C,gBACnE,UAAU,WAAW;EACnB,UAAU;EACV;EACA,QAAQ,MAAM,MAAM;EACpB;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAUC,iBAAAA,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAIC,iBAAAA,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAMC,oBAAAA,YAAY,KAAK;GAAE;GAAI,QAAQ,MAAM,MAAM;GAAG,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC7F,EACF;CAuBA,IAAI,CAAC,MAJkBC,wBAAAA,cAAc,YAAY,MAAM,cAAc,YAAY,MAAM,MAAM,CAAC,GAAG;EAC/F,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAKC,0BAAAA,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAMC,2BAAAA,WAAW,IAAI;GAC7B;GACA;GACA,eAAe;GAGf,aAAa,KAAK,IAAI,MAAM,aAAa,MAAM,OAAO,QAAQ,MAAM,MAAM,WAAW;GACrF;GACA,OAAO,IAAIC,oBAAAA,kBAAkB,OAAO,QAAQ,KAAK;GACjD;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
@@ -1,4 +1,6 @@
1
1
  import { JourneyFactSource } from "../store/facts.mjs";
2
+ import { instanceOwner } from "../store/identity.mjs";
3
+ import "../store/index.mjs";
2
4
  import { ingestEvent } from "./ingest.mjs";
3
5
  import { ContractValidationError, checkEvent } from "../contract.mjs";
4
6
  import { resolveRuntime } from "../runtime.mjs";
@@ -18,10 +20,11 @@ async function executeJourney(input, parts) {
18
20
  const { workflowId, contentHash, userId } = params;
19
21
  const { store, env, observer, contract } = parts;
20
22
  const runtime = resolveRuntime({ current: parts.runtime }, "current");
21
- const finished = (status, convertedAt) => observer?.finished?.({
23
+ const owner = () => instanceOwner(store, instanceId, userId);
24
+ const finished = async (status, convertedAt) => observer?.finished?.({
22
25
  workflow: workflowId,
23
26
  instanceId,
24
- userId,
27
+ userId: await owner(),
25
28
  status,
26
29
  at: Date.now(),
27
30
  ...convertedAt === void 0 ? {} : { convertedAt }
@@ -36,12 +39,12 @@ async function executeJourney(input, parts) {
36
39
  }
37
40
  await ingestEvent(env, {
38
41
  id,
39
- userId,
42
+ userId: await owner(),
40
43
  event: name,
41
44
  payload: accepted
42
45
  }, store);
43
46
  } };
44
- if (!await readWithRetry(() => store.admitInstance(instanceId, userId), {
47
+ if (!await readWithRetry(async () => store.admitInstance(instanceId, await owner()), {
45
48
  what: "admission",
46
49
  instanceId
47
50
  })) {
@@ -62,9 +65,10 @@ async function executeJourney(input, parts) {
62
65
  outcome = await runJourney(ir, {
63
66
  userId,
64
67
  instanceId,
65
- enteredAtMs: input.enteredAtMs,
68
+ currentUserId: owner,
69
+ enteredAtMs: Math.min(input.enteredAtMs, input.params.trigger.ts ?? input.enteredAtMs),
66
70
  step,
67
- facts: new JourneyFactSource(store, userId),
71
+ facts: new JourneyFactSource(store, userId, owner),
68
72
  messages,
69
73
  events,
70
74
  actions: parts.actions,
@@ -1 +1 @@
1
- {"version":3,"file":"execute.mjs","names":[],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport type { JourneyStore } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n\n const finished = (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId,\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId, event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(() => store.admitInstance(instanceId, userId), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n enteredAtMs: input.enteredAtMs,\n step,\n facts: new JourneyFactSource(store, userId),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAU,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CAEpE,MAAM,YAAY,QAA6C,gBAC7D,UAAU,WAAW;EACnB,UAAU;EACV;EACA;EACA;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAU,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAI,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAM,YAAY,KAAK;GAAE;GAAI;GAAQ,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC9E,EACF;CAuBA,IAAI,CAAC,MAJkB,oBAAoB,MAAM,cAAc,YAAY,MAAM,GAAG;EAClF,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAK,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAM,WAAW,IAAI;GAC7B;GACA;GACA,aAAa,MAAM;GACnB;GACA,OAAO,IAAI,kBAAkB,OAAO,MAAM;GAC1C;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
1
+ {"version":3,"file":"execute.mjs","names":[],"sources":["../../src/host/execute.ts"],"sourcesContent":["import { type Contract, ContractValidationError, checkEvent } from '../contract';\nimport { runJourney } from '../engine/interpreter';\nimport type {\n ActionInvoker,\n EngineStep,\n EventSink,\n JourneyObserver,\n JourneyOutcome,\n MessageSender,\n MessageValidator,\n} from '../engine/ports';\nimport { scaleDurations } from '../engine/time-scale';\nimport { type JourneyRuntimeOptions, resolveRuntime } from '../runtime';\nimport { JourneyFactSource } from '../store/facts';\nimport { type JourneyStore, instanceOwner } from '../store/index';\nimport { readWithRetry } from './fresh-read';\nimport { ingestEvent } from './ingest';\nimport type { JourneyEnv, JourneyParams } from './platform';\n\n/**\n * One journey instance run to its end, on whatever durable runtime supplies the `EngineStep`:\n * the runner admitted against the user's erasure tombstone, the IR loaded by the hash pinned at\n * entry, the outlets wired, the interpreter run, and the ledger finalized. Cloudflare's runner\n * and the Lambda handler both come through here; only their EngineStep differs.\n */\n\n/** Everything a run is wired to, assembled by the host. */\nexport interface JourneyRunParts {\n store: JourneyStore;\n /**\n * The env the emitted events are ingested with: its `JOURNEY` starts and wakes instances on\n * this runtime.\n */\n env: JourneyEnv;\n runtime: JourneyRuntimeOptions;\n messages: MessageSender;\n actions: ActionInvoker;\n contract?: Contract | undefined;\n validator?: MessageValidator | undefined;\n observer?: JourneyObserver | undefined;\n}\n\nexport interface JourneyRunInput {\n instanceId: string;\n params: JourneyParams;\n /** When the trigger happened: replay-stable, never Date.now() at run time. */\n enteredAtMs: number;\n step: EngineStep;\n}\n\n/** Development aid: echo each outbound message before handing it to the real sender. */\nfunction logging(sender: MessageSender): MessageSender {\n return {\n async send(message) {\n console.log(\n `[journey] ${message.channel} ${message.template} → ${message.recipient ?? message.userId} ${JSON.stringify(message.props)}`\n );\n return sender.send(message);\n },\n };\n}\n\nexport async function executeJourney(\n input: JourneyRunInput,\n parts: JourneyRunParts\n): Promise<JourneyOutcome> {\n const { instanceId, params, step } = input;\n const { workflowId, contentHash, userId } = params;\n const { store, env, observer, contract } = parts;\n\n const runtime = resolveRuntime({ current: parts.runtime }, 'current');\n const owner = () => instanceOwner(store, instanceId, userId);\n\n const finished = async (status: JourneyOutcome['status'] | 'failed', convertedAt?: number) =>\n observer?.finished?.({\n workflow: workflowId,\n instanceId,\n userId: await owner(),\n status,\n at: Date.now(),\n ...(convertedAt === undefined ? {} : { convertedAt }),\n });\n\n const messages = runtime.logMessages ? logging(parts.messages) : parts.messages;\n\n // send_event feeds straight back into the router logic — an in-process call\n // forming the event edge between workflows. The step's identity travels as\n // the event id, so a retried emit records the occurrence once.\n // An emitted event passes the same contract check as one arriving over HTTP.\n const events: EventSink = {\n emit: async (name, payload, id) => {\n let accepted: Record<string, unknown> = payload;\n if (contract !== undefined) {\n const verdict = checkEvent(contract, name, payload);\n if (!verdict.ok) throw new ContractValidationError(name, verdict.issues);\n accepted = verdict.data;\n }\n await ingestEvent(env, { id, userId: await owner(), event: name, payload: accepted }, store);\n },\n };\n\n /*\n * Admission: the ledger entry was committed before the runtime was asked to create this\n * instance, so an erasure can land in between (the entry is not found by `get`, the erase\n * goes through, and then the create lands). The runner is the only one who can refuse that\n * late run, and it must do so before its first observable effect — `observer.started`, the\n * IR read, any action or send.\n *\n * Outside any durable step, on purpose: a step would checkpoint the first invocation's\n * \"admitted\" and replay it on every wake and every rebuild after an eviction, which is\n * exactly when the tombstone may have arrived since. `admitInstance` is idempotent — the\n * entry's `admitted_at` keeps its first value — so a replay re-admits the same instance, and\n * the store makes the admission mutually exclusive with the erasure: either the entry is\n * admitted first and the erasure finds it and stops it, or the tombstone commits first and\n * this read answers false. Only the read itself is retried, briefly, when the database\n * stumbled; one that keeps failing throws, so the instance errors without running anything,\n * its entry stays, and the erasure protocol (or a later wake) decides what becomes of it.\n */\n const admitted = await readWithRetry(async () => store.admitInstance(instanceId, await owner()), {\n what: 'admission',\n instanceId,\n });\n if (!admitted) {\n /*\n * No finalize either way: the entry is gone (erased, or never ours to run) or is being\n * erased, and the finalize steps would write it back — a status on an entry the erasure\n * deletes, a `finished` the reporting records as a new row of an erased user. The\n * subscriptions, if any, go with the user's data.\n */\n console.warn(`[workflow] instance ${instanceId} of ${userId} refused admission: not running`);\n return { status: 'exited', reason: 'erased' };\n }\n\n let outcome: JourneyOutcome;\n try {\n // In a step: a read that fails is retried like any other, and one that keeps failing ends\n // the instance through the failure path below rather than leaving its entry 'running'.\n // Content addressing makes the IR immutable, so the checkpointed copy is the one to replay\n const stored = await step.do('$workflow:ir', async () => {\n const found = await store.getWorkflowIR(contentHash);\n if (found === undefined)\n throw new Error(`WorkflowIR not found: ${workflowId}@${contentHash}`);\n return found;\n });\n // Development runs the journey in fast-forward; the pinned hash still names the version.\n const ir = scaleDurations(stored, runtime.timeScale);\n outcome = await runJourney(ir, {\n userId,\n instanceId,\n currentUserId: owner,\n // A delivery that arrives late still enters when the trigger happened: a purchase made\n // while the begin_checkout was being retried is the goal, not history from before entry\n enteredAtMs: Math.min(input.enteredAtMs, input.params.trigger.ts ?? input.enteredAtMs),\n step,\n facts: new JourneyFactSource(store, userId, owner),\n messages,\n events,\n actions: parts.actions,\n validator: parts.validator,\n observer,\n });\n } catch (error) {\n /*\n * An error escaping run() ends the instance, and the ledger is the only\n * place anyone looks afterwards: leaving it at 'running' meant a dead\n * journey stayed indistinguishable from a waiting one, in the dashboard\n * and in the once-policy, forever.\n *\n * Subscriptions go too, as on every ordinary ending: left behind, the\n * rows would only make every later event of the user poke a dead\n * instance (one wake call to the runtime each, refused every time).\n *\n * A paused instance never gets here: a durable wait suspends the run\n * without throwing through it.\n */\n await step.do('finalize:failed', async () => {\n await store.setEntryStatus(instanceId, 'failed');\n await store.unsubscribe(instanceId);\n await finished('failed');\n });\n throw error;\n }\n\n await step.do('finalize', async () => {\n await store.setEntryStatus(instanceId, outcome.status);\n await store.unsubscribe(instanceId);\n await finished(outcome.status, outcome.convertedAt);\n });\n\n return outcome;\n}\n"],"mappings":";;;;;;;;;;;AAmDA,SAAS,QAAQ,QAAsC;CACrD,OAAO,EACL,MAAM,KAAK,SAAS;EAClB,QAAQ,IACN,aAAa,QAAQ,QAAQ,GAAG,QAAQ,SAAS,KAAK,QAAQ,aAAa,QAAQ,OAAO,GAAG,KAAK,UAAU,QAAQ,KAAK,GAC3H;EACA,OAAO,OAAO,KAAK,OAAO;CAC5B,EACF;AACF;AAEA,eAAsB,eACpB,OACA,OACyB;CACzB,MAAM,EAAE,YAAY,QAAQ,SAAS;CACrC,MAAM,EAAE,YAAY,aAAa,WAAW;CAC5C,MAAM,EAAE,OAAO,KAAK,UAAU,aAAa;CAE3C,MAAM,UAAU,eAAe,EAAE,SAAS,MAAM,QAAQ,GAAG,SAAS;CACpE,MAAM,cAAc,cAAc,OAAO,YAAY,MAAM;CAE3D,MAAM,WAAW,OAAO,QAA6C,gBACnE,UAAU,WAAW;EACnB,UAAU;EACV;EACA,QAAQ,MAAM,MAAM;EACpB;EACA,IAAI,KAAK,IAAI;EACb,GAAI,gBAAgB,KAAA,IAAY,CAAC,IAAI,EAAE,YAAY;CACrD,CAAC;CAEH,MAAM,WAAW,QAAQ,cAAc,QAAQ,MAAM,QAAQ,IAAI,MAAM;CAMvE,MAAM,SAAoB,EACxB,MAAM,OAAO,MAAM,SAAS,OAAO;EACjC,IAAI,WAAoC;EACxC,IAAI,aAAa,KAAA,GAAW;GAC1B,MAAM,UAAU,WAAW,UAAU,MAAM,OAAO;GAClD,IAAI,CAAC,QAAQ,IAAI,MAAM,IAAI,wBAAwB,MAAM,QAAQ,MAAM;GACvE,WAAW,QAAQ;EACrB;EACA,MAAM,YAAY,KAAK;GAAE;GAAI,QAAQ,MAAM,MAAM;GAAG,OAAO;GAAM,SAAS;EAAS,GAAG,KAAK;CAC7F,EACF;CAuBA,IAAI,CAAC,MAJkB,cAAc,YAAY,MAAM,cAAc,YAAY,MAAM,MAAM,CAAC,GAAG;EAC/F,MAAM;EACN;CACF,CAAC,GACc;EAOb,QAAQ,KAAK,uBAAuB,WAAW,MAAM,OAAO,gCAAgC;EAC5F,OAAO;GAAE,QAAQ;GAAU,QAAQ;EAAS;CAC9C;CAEA,IAAI;CACJ,IAAI;EAIF,MAAM,SAAS,MAAM,KAAK,GAAG,gBAAgB,YAAY;GACvD,MAAM,QAAQ,MAAM,MAAM,cAAc,WAAW;GACnD,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,yBAAyB,WAAW,GAAG,aAAa;GACtE,OAAO;EACT,CAAC;EAED,MAAM,KAAK,eAAe,QAAQ,QAAQ,SAAS;EACnD,UAAU,MAAM,WAAW,IAAI;GAC7B;GACA;GACA,eAAe;GAGf,aAAa,KAAK,IAAI,MAAM,aAAa,MAAM,OAAO,QAAQ,MAAM,MAAM,WAAW;GACrF;GACA,OAAO,IAAI,kBAAkB,OAAO,QAAQ,KAAK;GACjD;GACA;GACA,SAAS,MAAM;GACf,WAAW,MAAM;GACjB;EACF,CAAC;CACH,SAAS,OAAO;EAcd,MAAM,KAAK,GAAG,mBAAmB,YAAY;GAC3C,MAAM,MAAM,eAAe,YAAY,QAAQ;GAC/C,MAAM,MAAM,YAAY,UAAU;GAClC,MAAM,SAAS,QAAQ;EACzB,CAAC;EACD,MAAM;CACR;CAEA,MAAM,KAAK,GAAG,YAAY,YAAY;EACpC,MAAM,MAAM,eAAe,YAAY,QAAQ,MAAM;EACrD,MAAM,MAAM,YAAY,UAAU;EAClC,MAAM,SAAS,QAAQ,QAAQ,QAAQ,WAAW;CACpD,CAAC;CAED,OAAO;AACT"}
@@ -2,11 +2,12 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
3
3
  const require_reporting_tracking = require("../reporting/tracking.cjs");
4
4
  const require_store_erasure = require("../store/erasure.cjs");
5
+ const require_store_identity = require("../store/identity.cjs");
5
6
  const require_store_index = require("../store/index.cjs");
6
7
  const require_host_ingest = require("./ingest.cjs");
7
8
  const require_email = require("../email.cjs");
8
9
  const require_templates = require("../templates.cjs");
9
- const require_contract = require("../contract.cjs");
10
+ const require_host_batch = require("./batch.cjs");
10
11
  const require_host_channels = require("./channels.cjs");
11
12
  const require_host_auth = require("./auth.cjs");
12
13
  let zod_mini = require("zod/mini");
@@ -35,41 +36,7 @@ async function readJson(request) {
35
36
  */
36
37
  async function readBody(request) {
37
38
  const body = await readJson(request);
38
- return isPlainObject(body) ? body : {};
39
- }
40
- function isPlainObject(value) {
41
- return typeof value === "object" && value !== null && !Array.isArray(value);
42
- }
43
- function isNonEmptyString(value) {
44
- return typeof value === "string" && value.length > 0;
45
- }
46
- /** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */
47
- function isScalarOrNull(value) {
48
- return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
49
- }
50
- /** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */
51
- const TS_FUTURE_TOLERANCE_MS = 3e5;
52
- const TS_ERROR = "ts must be a non-negative integer no later than 5 minutes in the future";
53
- /**
54
- * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.
55
- * A far-future ts would put the event ahead of everything a window condition reads, and a
56
- * fractional or unsafe one does not survive the store.
57
- */
58
- function isValidTs(ts) {
59
- return typeof ts === "number" && Number.isSafeInteger(ts) && ts >= 0 && ts <= Date.now() + TS_FUTURE_TOLERANCE_MS;
60
- }
61
- /** The contract's verdict: the value to use, or the response refusing the request. */
62
- function checked(validation, result, original, what) {
63
- if (result.ok) return { value: result.data };
64
- const issues = result.issues.map((i) => `${i.path || "(root)"}: ${i.message}`);
65
- if (validation.mode === "report") {
66
- console.warn(`[workflow http] contract would refuse ${what}: ${issues.join("; ")}`);
67
- return { value: original };
68
- }
69
- return { refused: json({
70
- error: `${what} refused by the contract`,
71
- issues: result.issues
72
- }, 400) };
39
+ return require_host_batch.isPlainObject(body) ? body : {};
73
40
  }
74
41
  /**
75
42
  * The topics the engine knows: the deployed bundle's list (compiled from the contract), or the
@@ -106,46 +73,18 @@ const routes = [
106
73
  },
107
74
  {
108
75
  method: "POST",
109
- path: "/events",
110
- auth: "api",
111
- async handle({ request, env, store, validation }) {
112
- const { id, userId, event, payload, ts } = await readBody(request);
113
- if (!isNonEmptyString(userId) || !isNonEmptyString(event)) return json({ error: "userId and event required" }, 400);
114
- if (event.startsWith("$")) return json({ error: "event names starting with $ are reserved" }, 400);
115
- if (id !== void 0 && !isNonEmptyString(id)) return json({ error: "id must be a non-empty string" }, 400);
116
- if (ts !== void 0 && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);
117
- if (payload !== void 0 && !isPlainObject(payload)) return json({ error: "payload must be an object" }, 400);
118
- let accepted = payload;
119
- if (validation.contract !== void 0) {
120
- const verdict = checked(validation, require_contract.checkEvent(validation.contract, event, payload ?? {}), accepted ?? {}, `event '${event}'${typeof id === "string" ? ` (${id})` : ""}`);
121
- if ("refused" in verdict) return verdict.refused;
122
- accepted = verdict.value;
123
- }
124
- return json(await require_host_ingest.ingestEvent(env, {
125
- id,
126
- userId,
127
- event,
128
- payload: accepted,
129
- ts
130
- }, store));
131
- }
132
- },
133
- {
134
- method: "POST",
135
- path: "/identify",
76
+ path: "/ingest",
136
77
  auth: "api",
137
78
  async handle({ request, env, store, validation }) {
138
- const { userId, props } = await readBody(request);
139
- if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
140
- if (!isPlainObject(props)) return json({ error: "props must be an object" }, 400);
141
- if (!Object.values(props).every(isScalarOrNull)) return json({ error: "props values must be scalars (string, number, boolean) or null" }, 400);
142
- let patch = props;
143
- if (validation.contract !== void 0) {
144
- const verdict = checked(validation, validation.contract.parseProfilePatch(props), patch, "identify");
145
- if ("refused" in verdict) return verdict.refused;
146
- patch = verdict.value;
79
+ const parsed = require_host_batch.parseBatch(await readJson(request), validation);
80
+ if (!parsed.ok) {
81
+ const { error, issues } = parsed;
82
+ return json({
83
+ error,
84
+ ...issues === void 0 ? {} : { issues }
85
+ }, 400);
147
86
  }
148
- return json(await require_host_ingest.identifyUser(env, userId, patch, store));
87
+ return json(await require_host_batch.ingestBatch(env, parsed.batch, store));
149
88
  }
150
89
  },
151
90
  {
@@ -153,18 +92,19 @@ const routes = [
153
92
  path: "/devices",
154
93
  auth: "api",
155
94
  async handle({ request, env, store }) {
156
- const { userId, token, platform, ts } = await readBody(request);
157
- if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
158
- if (!isNonEmptyString(token)) return json({ error: "token required" }, 400);
95
+ const { deviceId, userId, visitorId, token, platform } = await readBody(request);
96
+ const refused = require_host_batch.identityError(userId, visitorId);
97
+ if (refused !== void 0) return json({ error: refused }, 400);
98
+ if (deviceId !== void 0 && !require_host_batch.isNonEmptyString(deviceId)) return json({ error: "deviceId must be a non-empty string" }, 400);
99
+ if (!require_host_batch.isNonEmptyString(token)) return json({ error: "token required" }, 400);
159
100
  if (platform !== "ios" && platform !== "android" && platform !== "web") return json({ error: "platform must be 'ios', 'android' or 'web'" }, 400);
160
- if (ts !== void 0 && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);
161
- const device = {
101
+ return json(await require_host_ingest.registerDevice(env, {
102
+ deviceId,
162
103
  userId,
104
+ visitorId,
163
105
  token,
164
- platform,
165
- ...ts === void 0 ? {} : { ts }
166
- };
167
- return json(await require_host_ingest.registerDevice(env, device, store));
106
+ platform
107
+ }, store));
168
108
  }
169
109
  },
170
110
  {
@@ -173,7 +113,7 @@ const routes = [
173
113
  auth: "api",
174
114
  async handle({ request, env, store }) {
175
115
  const { token } = await readBody(request);
176
- if (!isNonEmptyString(token)) return json({ error: "token required" }, 400);
116
+ if (!require_host_batch.isNonEmptyString(token)) return json({ error: "token required" }, 400);
177
117
  return json(await require_host_ingest.unbindDevice(env, token, store));
178
118
  }
179
119
  },
@@ -183,7 +123,7 @@ const routes = [
183
123
  auth: "api",
184
124
  async handle({ request, env, store }) {
185
125
  const { token } = await readBody(request);
186
- if (!isNonEmptyString(token)) return json({ error: "token required" }, 400);
126
+ if (!require_host_batch.isNonEmptyString(token)) return json({ error: "token required" }, 400);
187
127
  return json(await require_host_ingest.removeDevice(env, token, store));
188
128
  }
189
129
  },
@@ -193,8 +133,8 @@ const routes = [
193
133
  auth: "api",
194
134
  async handle({ request, store }) {
195
135
  const { userId, messageId } = await readBody(request);
196
- if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
197
- if (!isNonEmptyString(messageId)) return json({ error: "messageId required" }, 400);
136
+ if (!require_host_batch.isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
137
+ if (!require_host_batch.isNonEmptyString(messageId)) return json({ error: "messageId required" }, 400);
198
138
  const recorded = await require_reporting_tracking.recordPushOpen(store.reporting, userId, messageId);
199
139
  return json({
200
140
  ok: true,
@@ -259,8 +199,9 @@ const routes = [
259
199
  path: "/users/forget",
260
200
  auth: "api",
261
201
  async handle({ request, env, store }) {
262
- const { userId } = await readBody(request);
263
- if (!isNonEmptyString(userId)) return json({ error: "userId required" }, 400);
202
+ const { userId, visitorId } = await readBody(request);
203
+ if (require_host_batch.isNonEmptyString(visitorId)) return json(await require_host_ingest.forgetUser(env, require_store_identity.visitorSubject(visitorId), store));
204
+ if (!require_host_batch.isNonEmptyString(userId)) return json({ error: "userId or visitorId required" }, 400);
264
205
  return json(await require_host_ingest.forgetUser(env, userId, store));
265
206
  }
266
207
  },
@@ -312,7 +253,7 @@ const routes = [
312
253
  } catch {
313
254
  return json({ error: "props must be a JSON object" }, 400);
314
255
  }
315
- if (!isPlainObject(props)) return json({ error: "props must be a JSON object" }, 400);
256
+ if (!require_host_batch.isPlainObject(props)) return json({ error: "props must be a JSON object" }, 400);
316
257
  const { subject, html } = await preview.render(key, props, { messageId: require_email.PREVIEW_MESSAGE_ID });
317
258
  return new Response(html, { headers: {
318
259
  "content-type": "text/html; charset=utf-8",
@@ -1 +1 @@
1
- {"version":3,"file":"http.cjs","names":["handleTracking","checkEvent","ingestEvent","identifyUser","registerDevice","unbindDevice","removeDevice","recordPushOpen","getSubscriptions","parseSubscriptionsPatch","setSubscriptions","getSubscriptionsByMessage","setSubscriptionsByMessage","forgetUser","deployBundle","PREVIEW_MESSAGE_ID","journeyMode","authorized","z","BundleIntegrityError","TemplateValidationError","UserErasedError","SchemaMismatchError"],"sources":["../../src/host/http.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport { type Contract, type ContractResult, checkEvent } from '../contract';\nimport { PREVIEW_MESSAGE_ID } from '../email';\nimport type { EmailRenderer } from '../email-sender';\nimport type { BundleIR, ScalarIR } from '../ir';\nimport { type TrackingOptions, handleTracking, recordPushOpen } from '../reporting/tracking';\nimport {\n type BundleVersion,\n type JourneyStore,\n type ProfileProps,\n SchemaMismatchError,\n UserErasedError,\n} from '../store/index';\nimport { TemplateValidationError } from '../templates';\nimport { type Authorize, authorized } from './auth';\nimport { journeyMode } from './channels';\nimport {\n BundleIntegrityError,\n type SweepResult,\n deployBundle,\n forgetUser,\n getSubscriptions,\n getSubscriptionsByMessage,\n identifyUser,\n ingestEvent,\n parseSubscriptionsPatch,\n registerDevice,\n removeDevice,\n setSubscriptions,\n setSubscriptionsByMessage,\n unbindDevice,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes\n * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A\n * route whose part is absent answers 404, so the two share the table, the bearer check, the\n * body validation and the error mapping.\n */\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\n/** What the full host serves beyond the ingest API. A route whose part is absent answers 404. */\nexport interface HostParts {\n /** The bundle compiled into the code: `GET /bundle` shows it, `POST /bundle/deploy` makes it live. */\n bundle?: (() => BundleIR) | undefined;\n /** The code version serving this request, recorded as live by `POST /bundle/deploy`. */\n bundleVersion?: BundleVersion | undefined;\n /** `GET /preview/:id?props=<JSON>`: the email templates, rendered. */\n preview?: { has(id: string): boolean; render: EmailRenderer } | undefined;\n /** `/t/*`, what recipients open (no bearer): served on a store that keeps reporting. */\n tracking?: TrackingOptions | undefined;\n /** `POST /segments/sweep`. */\n sweep?: (() => Promise<SweepResult>) | undefined;\n}\n\nexport interface ServeContext {\n env: JourneyEnv;\n store: JourneyStore;\n validation?: IngestValidation | undefined;\n /**\n * The host's parts. Their presence is what makes this a deployed host rather than the bare\n * API: production then refuses to serve without `API_TOKEN` — unless the host authorizes\n * callers itself (`authorize`).\n */\n host?: HostParts | undefined;\n /** The host's own authorization for the API routes; the env's `API_TOKEN` bearer without one. */\n authorize?: Authorize<JourneyEnv> | undefined;\n}\n\ninterface RouteContext {\n request: Request;\n url: URL;\n env: JourneyEnv;\n store: JourneyStore;\n validation: IngestValidation;\n host: HostParts;\n}\n\ninterface Route {\n /** '*' takes every method (the tracking routes answer GET, HEAD and POST themselves). */\n method: 'GET' | 'POST' | 'PUT' | '*';\n path: string | RegExp | ((pathname: string) => boolean);\n /** `api`: the caller must be authorized (the host's `authorize`, or the bearer); `none`: public. */\n auth: 'none' | 'api';\n handle(ctx: RouteContext): Promise<Response>;\n}\n\nconst json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\nconst notFound = () => json({ error: 'not found' }, 404);\n\n/* --------------------------------- bodies ---------------------------------- */\n\n/** The request body is not JSON: the caller's to fix (a 400), not a server fault. */\nclass InvalidJsonError extends Error {\n override readonly name = 'InvalidJsonError';\n}\n\nasync function readJson(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch (error) {\n throw new InvalidJsonError('invalid JSON body', { cause: error });\n }\n}\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await readJson(request);\n return isPlainObject(body) ? body : {};\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === 'string' && value.length > 0;\n}\n\n/** A profile value: a scalar or null (which removes the property). Nested values are not profile data. */\nfunction isScalarOrNull(value: unknown): value is string | number | boolean | null {\n return (\n value === null ||\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n );\n}\n\n/** How far ahead of the server's clock an event or device `ts` may be: clock skew, not the future. */\nconst TS_FUTURE_TOLERANCE_MS = 5 * 60_000;\nconst TS_ERROR = 'ts must be a non-negative integer no later than 5 minutes in the future';\n\n/**\n * A caller's timestamp: epoch milliseconds, whole, not negative, and not past the skew tolerance.\n * A far-future ts would put the event ahead of everything a window condition reads, and a\n * fractional or unsafe one does not survive the store.\n */\nfunction isValidTs(ts: unknown): ts is number {\n return (\n typeof ts === 'number' &&\n Number.isSafeInteger(ts) &&\n ts >= 0 &&\n ts <= Date.now() + TS_FUTURE_TOLERANCE_MS\n );\n}\n\n/** The contract's verdict: the value to use, or the response refusing the request. */\nfunction checked<T>(\n validation: IngestValidation,\n result: ContractResult<T>,\n original: T,\n what: string\n): { value: T } | { refused: Response } {\n if (result.ok) return { value: result.data };\n const issues = result.issues.map((i) => `${i.path || '(root)'}: ${i.message}`);\n if (validation.mode === 'report') {\n console.warn(`[workflow http] contract would refuse ${what}: ${issues.join('; ')}`);\n return { value: original };\n }\n return {\n refused: json({ error: `${what} refused by the contract`, issues: result.issues }, 400),\n };\n}\n\n/**\n * The topics the engine knows: the deployed bundle's list (compiled from the contract), or the\n * contract's when the bare API runs without a host — never the caller's.\n */\nfunction declaredTopics(ctx: Pick<RouteContext, 'host' | 'validation'>): readonly string[] {\n return ctx.host.bundle?.().subscriptions?.topics ?? ctx.validation.contract?.topics ?? [];\n}\n\nconst SUBSCRIPTIONS = /^\\/subscriptions\\/[^/]+$/;\nconst subscriptionsUser = (url: URL) =>\n decodeURIComponent(url.pathname.slice('/subscriptions/'.length));\n/**\n * The same record, reached by one of the user's copies instead of their id: for the app's\n * preference page opened from an email's `preferencesUrl?m=<message>` without a sign-in. The\n * message id is the capability (an unguessable UUID, as `/t/u` trusts it); the bearer still\n * guards the route, so the page's own API proxies it, and rate-limits it as it sees fit.\n */\nconst SUBSCRIPTIONS_BY_MESSAGE = /^\\/subscriptions\\/by-message\\/[^/]+$/;\nconst subscriptionsMessage = (url: URL) =>\n decodeURIComponent(url.pathname.slice('/subscriptions/by-message/'.length));\n\n/* --------------------------------- routes ---------------------------------- */\n\nconst routes: readonly Route[] = [\n { method: 'GET', path: '/health', auth: 'none', handle: async () => json({ ok: true }) },\n {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n method: '*',\n path: /^\\/t\\//,\n auth: 'none',\n async handle({ request, store, host }) {\n if (host.tracking === undefined) return notFound();\n return (await handleTracking(request, store.reporting, host.tracking)) ?? notFound();\n },\n },\n {\n method: 'POST',\n path: '/events',\n auth: 'api',\n async handle({ request, env, store, validation }) {\n const { id, userId, event, payload, ts } = await readBody(request);\n if (!isNonEmptyString(userId) || !isNonEmptyString(event)) {\n return json({ error: 'userId and event required' }, 400);\n }\n if (event.startsWith('$')) {\n return json({ error: 'event names starting with $ are reserved' }, 400);\n }\n // An empty id is rejected rather than treated as absent: a caller that\n // sent one meant to identify the occurrence, and silently minting a\n // different one would turn their retry into a duplicate.\n if (id !== undefined && !isNonEmptyString(id)) {\n return json({ error: 'id must be a non-empty string' }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n if (payload !== undefined && !isPlainObject(payload)) {\n return json({ error: 'payload must be an object' }, 400);\n }\n let accepted = payload;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n checkEvent(validation.contract, event, payload ?? {}),\n accepted ?? {},\n `event '${event}'${typeof id === 'string' ? ` (${id})` : ''}`\n );\n if ('refused' in verdict) return verdict.refused;\n accepted = verdict.value;\n }\n return json(await ingestEvent(env, { id, userId, event, payload: accepted, ts }, store));\n },\n },\n {\n method: 'POST',\n path: '/identify',\n auth: 'api',\n async handle({ request, env, store, validation }) {\n const { userId, props } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isPlainObject(props)) return json({ error: 'props must be an object' }, 400);\n // Profiles hold scalars only (both stores merge at the top level; a nested object would\n // be merged differently by each): refused here, contract or not\n if (!Object.values(props).every(isScalarOrNull)) {\n return json(\n { error: 'props values must be scalars (string, number, boolean) or null' },\n 400\n );\n }\n let patch = props as ProfileProps;\n if (validation.contract !== undefined) {\n const verdict = checked(\n validation,\n validation.contract.parseProfilePatch(props),\n patch,\n 'identify'\n );\n if ('refused' in verdict) return verdict.refused;\n patch = verdict.value as ProfileProps;\n }\n return json(await identifyUser(env, userId, patch, store));\n },\n },\n {\n method: 'POST',\n path: '/devices',\n auth: 'api',\n async handle({ request, env, store }) {\n const { userId, token, platform, ts } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n if (ts !== undefined && !isValidTs(ts)) return json({ error: TS_ERROR }, 400);\n const device = { userId, token, platform, ...(ts === undefined ? {} : { ts }) };\n return json(await registerDevice(env, device, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/unbind',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/remove',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/messages/opened',\n auth: 'api',\n async handle({ request, store }) {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded = await recordPushOpen(store.reporting, userId, messageId);\n return json({ ok: true, recorded });\n },\n },\n {\n method: 'GET',\n path: SUBSCRIPTIONS,\n auth: 'api',\n async handle(ctx) {\n const userId = subscriptionsUser(ctx.url);\n if (userId === '') return json({ error: 'userId required' }, 400);\n return json(await getSubscriptions(ctx.store, userId, declaredTopics(ctx)));\n },\n },\n {\n method: 'PUT',\n path: SUBSCRIPTIONS,\n auth: 'api',\n async handle(ctx) {\n const userId = subscriptionsUser(ctx.url);\n if (userId === '') return json({ error: 'userId required' }, 400);\n const topics = declaredTopics(ctx);\n const parsed = parseSubscriptionsPatch(await readJson(ctx.request), topics);\n if (!parsed.ok) return json({ error: 'subscriptions refused', issues: parsed.issues }, 400);\n return json(await setSubscriptions(ctx.env, ctx.store, userId, parsed.patch, topics));\n },\n },\n {\n method: 'GET',\n path: SUBSCRIPTIONS_BY_MESSAGE,\n auth: 'api',\n async handle(ctx) {\n const messageId = subscriptionsMessage(ctx.url);\n const found = await getSubscriptionsByMessage(ctx.store, messageId, declaredTopics(ctx));\n return found === undefined ? json({ error: 'unknown message' }, 404) : json(found);\n },\n },\n {\n method: 'PUT',\n path: SUBSCRIPTIONS_BY_MESSAGE,\n auth: 'api',\n async handle(ctx) {\n const messageId = subscriptionsMessage(ctx.url);\n const topics = declaredTopics(ctx);\n const parsed = parseSubscriptionsPatch(await readJson(ctx.request), topics);\n if (!parsed.ok) return json({ error: 'subscriptions refused', issues: parsed.issues }, 400);\n const found = await setSubscriptionsByMessage(\n ctx.env,\n ctx.store,\n messageId,\n parsed.patch,\n topics\n );\n return found === undefined ? json({ error: 'unknown message' }, 404) : json(found);\n },\n },\n {\n method: 'POST',\n path: '/users/forget',\n auth: 'api',\n async handle({ request, env, store }) {\n const { userId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n return json(await forgetUser(env, userId, store));\n },\n },\n {\n method: 'POST',\n path: '/deploy',\n auth: 'api',\n async handle({ request, env, store }) {\n return json(await deployBundle(env, await readJson(request), store));\n },\n },\n {\n method: 'GET',\n path: '/bundle',\n auth: 'api',\n async handle({ host }) {\n return host.bundle === undefined ? notFound() : json(host.bundle());\n },\n },\n {\n method: 'POST',\n path: '/bundle/deploy',\n auth: 'api',\n async handle({ env, store, host }) {\n if (host.bundle === undefined) return notFound();\n // Forced, and recording the serving version: otherwise an older version still serving\n // during a rollout would put its routes live while the row kept naming the newer one —\n // which then never re-applies its own bundle\n return json(await deployBundle(env, host.bundle(), store, host.bundleVersion, true));\n },\n },\n {\n method: 'POST',\n path: '/segments/sweep',\n auth: 'api',\n async handle({ host }) {\n return host.sweep === undefined ? notFound() : json(await host.sweep());\n },\n },\n {\n method: 'GET',\n path: /^\\/preview\\//,\n auth: 'api',\n async handle({ url, host }) {\n const { preview } = host;\n if (preview === undefined) return notFound();\n const key = url.pathname.slice('/preview/'.length);\n if (!preview.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown;\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (!isPlainObject(props)) return json({ error: 'props must be a JSON object' }, 400);\n // A preview is no copy, but it shows the footer a sent one has: the preferences link\n // renders with a placeholder message id\n const { subject, html } = await preview.render(key, props as Record<string, ScalarIR>, {\n messageId: PREVIEW_MESSAGE_ID,\n });\n // A header holds bytes, not text: a subject with a non-Latin-1 character would throw\n // here, so it travels percent-encoded (the reader `decodeURIComponent`s it)\n return new Response(html, {\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'x-subject': encodeURIComponent(subject),\n },\n });\n },\n },\n];\n\nfunction matches(path: Route['path'], pathname: string): boolean {\n if (typeof path === 'string') return path === pathname;\n if (path instanceof RegExp) return path.test(pathname);\n return path(pathname);\n}\n\n/**\n * Production without a bearer token (and no authorizer of the host's own) is not an open\n * instance, it is a mistake: anyone could ingest, identify and deploy. Only what recipients open\n * (`/t/*`) and the health check stay up.\n */\nfunction refusesOpen(request: Request, ctx: ServeContext, pathname: string): boolean {\n return (\n ctx.authorize === undefined &&\n ctx.env.API_TOKEN === undefined &&\n journeyMode(ctx.env) === 'production' &&\n !pathname.startsWith('/t/') &&\n !(request.method === 'GET' && pathname === '/health')\n );\n}\n\n/**\n * Whether the caller may use an API route: the host's authorizer on a copy of the request (so a\n * body it verifies is still there for the route), or the env's `API_TOKEN` as a bearer. An\n * authorizer that throws is a failure, never an admission.\n */\nasync function allowed(request: Request, ctx: ServeContext): Promise<boolean> {\n if (ctx.authorize === undefined) return authorized(request, ctx.env);\n return ctx.authorize(request.clone(), ctx.env);\n}\n\n/**\n * What a failed request answers. Validation problems are the caller's to fix (a 400, saying\n * what); anything else stays out of the response body (internals are logged, not leaked).\n */\nfunction failure(error: unknown): Response {\n if (error instanceof InvalidJsonError) return json({ error: 'invalid JSON body' }, 400);\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) return json({ error: error.message }, 400);\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n // The user has an erasure tombstone: gone, and nothing about them is taken, now or later\n if (error instanceof UserErasedError) return json({ error: 'user is erased' }, 410);\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow http]', error);\n return json({ error: 'internal error' }, 500);\n}\n\n/** One request through the table: the production gate, the bearer, the route, the error mapping. */\nexport async function serve(request: Request, ctx: ServeContext): Promise<Response> {\n const url = new URL(request.url);\n if (refusesOpen(request, ctx, url.pathname)) {\n return json(\n {\n error:\n 'API_TOKEN is not set: production refuses to serve without it (set ENVIRONMENT=development to run open)',\n },\n 503\n );\n }\n const candidates = routes.filter((route) => matches(route.path, url.pathname));\n if (candidates.length === 0) return notFound();\n // Every API route asks the authorizer; the health check and the recipients' routes never do.\n // An unset token and no authorizer means an open dev instance (see JourneyEnv.API_TOKEN);\n // production refuses to run that way\n if (candidates.some((route) => route.auth === 'api')) {\n let ok: boolean;\n try {\n ok = await allowed(request, ctx);\n } catch (error) {\n console.error('[workflow http] authorize failed', error);\n return json({ error: 'internal error' }, 500);\n }\n if (!ok) return json({ error: 'unauthorized' }, 401);\n }\n const route = candidates.find((r) => r.method === '*' || r.method === request.method);\n if (route === undefined) return json({ error: 'method not allowed' }, 405);\n try {\n return await route.handle({\n request,\n url,\n env: ctx.env,\n store: ctx.store,\n validation: ctx.validation ?? {},\n host: ctx.host ?? {},\n });\n } catch (error) {\n return failure(error);\n }\n}\n\n/** The bare ingest API over one store: `/health`, `/events`, `/identify`, `/devices*`, `/messages/opened`, `/subscriptions/:userId`, `/subscriptions/by-message/:messageId`, `/users/forget`, `/deploy`. */\nexport function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore,\n validation: IngestValidation = {}\n): Promise<Response> {\n return serve(request, { env, store, validation });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"http.cjs","names":["isPlainObject","handleTracking","parseBatch","ingestBatch","identityError","isNonEmptyString","registerDevice","unbindDevice","removeDevice","recordPushOpen","getSubscriptions","parseSubscriptionsPatch","setSubscriptions","getSubscriptionsByMessage","setSubscriptionsByMessage","forgetUser","visitorSubject","deployBundle","PREVIEW_MESSAGE_ID","journeyMode","authorized","z","BundleIntegrityError","TemplateValidationError","UserErasedError","SchemaMismatchError"],"sources":["../../src/host/http.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type { Contract } from '../contract';\nimport { PREVIEW_MESSAGE_ID } from '../email';\nimport type { EmailRenderer } from '../email-sender';\nimport type { BundleIR, ScalarIR } from '../ir';\nimport { type TrackingOptions, handleTracking, recordPushOpen } from '../reporting/tracking';\nimport {\n type BundleVersion,\n type JourneyStore,\n SchemaMismatchError,\n UserErasedError,\n visitorSubject,\n} from '../store/index';\nimport { TemplateValidationError } from '../templates';\nimport { type Authorize, authorized } from './auth';\nimport { identityError, ingestBatch, isNonEmptyString, isPlainObject, parseBatch } from './batch';\nimport { journeyMode } from './channels';\nimport {\n BundleIntegrityError,\n type SweepResult,\n deployBundle,\n forgetUser,\n getSubscriptions,\n getSubscriptionsByMessage,\n parseSubscriptionsPatch,\n registerDevice,\n removeDevice,\n setSubscriptions,\n setSubscriptionsByMessage,\n unbindDevice,\n} from './ingest';\nimport type { JourneyEnv } from './platform';\n\n/**\n * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes\n * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A\n * route whose part is absent answers 404, so the two share the table, the bearer check, the\n * body validation and the error mapping.\n */\n\n/**\n * How the ingest API applies a contract: 'enforce' refuses an invalid event or\n * patch with a 400 before anything is stored, woken or started; 'report' logs\n * the issues (event id, name, field paths — never the payload) and accepts it,\n * for checking real producers before turning enforcement on.\n */\nexport interface IngestValidation {\n contract?: Contract | undefined;\n mode?: 'enforce' | 'report' | undefined;\n}\n\n/** What the full host serves beyond the ingest API. A route whose part is absent answers 404. */\nexport interface HostParts {\n /** The bundle compiled into the code: `GET /bundle` shows it, `POST /bundle/deploy` makes it live. */\n bundle?: (() => BundleIR) | undefined;\n /** The code version serving this request, recorded as live by `POST /bundle/deploy`. */\n bundleVersion?: BundleVersion | undefined;\n /** `GET /preview/:id?props=<JSON>`: the email templates, rendered. */\n preview?: { has(id: string): boolean; render: EmailRenderer } | undefined;\n /** `/t/*`, what recipients open (no bearer): served on a store that keeps reporting. */\n tracking?: TrackingOptions | undefined;\n /** `POST /segments/sweep`. */\n sweep?: (() => Promise<SweepResult>) | undefined;\n}\n\nexport interface ServeContext {\n env: JourneyEnv;\n store: JourneyStore;\n validation?: IngestValidation | undefined;\n /**\n * The host's parts. Their presence is what makes this a deployed host rather than the bare\n * API: production then refuses to serve without `API_TOKEN` — unless the host authorizes\n * callers itself (`authorize`).\n */\n host?: HostParts | undefined;\n /** The host's own authorization for the API routes; the env's `API_TOKEN` bearer without one. */\n authorize?: Authorize<JourneyEnv> | undefined;\n}\n\ninterface RouteContext {\n request: Request;\n url: URL;\n env: JourneyEnv;\n store: JourneyStore;\n validation: IngestValidation;\n host: HostParts;\n}\n\ninterface Route {\n /** '*' takes every method (the tracking routes answer GET, HEAD and POST themselves). */\n method: 'GET' | 'POST' | 'PUT' | '*';\n path: string | RegExp | ((pathname: string) => boolean);\n /** `api`: the caller must be authorized (the host's `authorize`, or the bearer); `none`: public. */\n auth: 'none' | 'api';\n handle(ctx: RouteContext): Promise<Response>;\n}\n\nconst json = (body: unknown, status = 200): Response =>\n new Response(JSON.stringify(body), {\n status,\n headers: { 'content-type': 'application/json' },\n });\n\nconst notFound = () => json({ error: 'not found' }, 404);\n\n/* --------------------------------- bodies ---------------------------------- */\n\n/** The request body is not JSON: the caller's to fix (a 400), not a server fault. */\nclass InvalidJsonError extends Error {\n override readonly name = 'InvalidJsonError';\n}\n\nasync function readJson(request: Request): Promise<unknown> {\n try {\n return await request.json();\n } catch (error) {\n throw new InvalidJsonError('invalid JSON body', { cause: error });\n }\n}\n\n/**\n * Request bodies are untrusted JSON. They are read as unknown fields and\n * checked, never cast into the shape they are supposed to have: a cast only\n * silences the compiler, and what arrives here reaches SQL parameters.\n */\nasync function readBody(request: Request): Promise<Record<string, unknown>> {\n const body = await readJson(request);\n return isPlainObject(body) ? body : {};\n}\n\n/**\n * The topics the engine knows: the deployed bundle's list (compiled from the contract), or the\n * contract's when the bare API runs without a host — never the caller's.\n */\nfunction declaredTopics(ctx: Pick<RouteContext, 'host' | 'validation'>): readonly string[] {\n return ctx.host.bundle?.().subscriptions?.topics ?? ctx.validation.contract?.topics ?? [];\n}\n\nconst SUBSCRIPTIONS = /^\\/subscriptions\\/[^/]+$/;\nconst subscriptionsUser = (url: URL) =>\n decodeURIComponent(url.pathname.slice('/subscriptions/'.length));\n/**\n * The same record, reached by one of the user's copies instead of their id: for the app's\n * preference page opened from an email's `preferencesUrl?m=<message>` without a sign-in. The\n * message id is the capability (an unguessable UUID, as `/t/u` trusts it); the bearer still\n * guards the route, so the page's own API proxies it, and rate-limits it as it sees fit.\n */\nconst SUBSCRIPTIONS_BY_MESSAGE = /^\\/subscriptions\\/by-message\\/[^/]+$/;\nconst subscriptionsMessage = (url: URL) =>\n decodeURIComponent(url.pathname.slice('/subscriptions/by-message/'.length));\n\n/* --------------------------------- routes ---------------------------------- */\n\nconst routes: readonly Route[] = [\n { method: 'GET', path: '/health', auth: 'none', handle: async () => json({ ok: true }) },\n {\n // Opened by recipients, so no bearer: the routes only record, and redirect to recorded URLs\n method: '*',\n path: /^\\/t\\//,\n auth: 'none',\n async handle({ request, store, host }) {\n if (host.tracking === undefined) return notFound();\n return (await handleTracking(request, store.reporting, host.tracking)) ?? notFound();\n },\n },\n {\n method: 'POST',\n path: '/ingest',\n auth: 'api',\n async handle({ request, env, store, validation }) {\n const parsed = parseBatch(await readJson(request), validation);\n if (!parsed.ok) {\n const { error, issues } = parsed;\n return json({ error, ...(issues === undefined ? {} : { issues }) }, 400);\n }\n return json(await ingestBatch(env, parsed.batch, store));\n },\n },\n {\n method: 'POST',\n path: '/devices',\n auth: 'api',\n async handle({ request, env, store }) {\n const { deviceId, userId, visitorId, token, platform } = await readBody(request);\n const refused = identityError(userId, visitorId);\n if (refused !== undefined) return json({ error: refused }, 400);\n if (deviceId !== undefined && !isNonEmptyString(deviceId)) {\n return json({ error: 'deviceId must be a non-empty string' }, 400);\n }\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n if (platform !== 'ios' && platform !== 'android' && platform !== 'web') {\n return json({ error: \"platform must be 'ios', 'android' or 'web'\" }, 400);\n }\n const device = {\n deviceId,\n userId: userId as string | undefined,\n visitorId: visitorId as string | undefined,\n token,\n platform,\n };\n return json(await registerDevice(env, device, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/unbind',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await unbindDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/devices/remove',\n auth: 'api',\n async handle({ request, env, store }) {\n const { token } = await readBody(request);\n if (!isNonEmptyString(token)) return json({ error: 'token required' }, 400);\n return json(await removeDevice(env, token, store));\n },\n },\n {\n method: 'POST',\n path: '/messages/opened',\n auth: 'api',\n async handle({ request, store }) {\n const { userId, messageId } = await readBody(request);\n if (!isNonEmptyString(userId)) return json({ error: 'userId required' }, 400);\n if (!isNonEmptyString(messageId)) return json({ error: 'messageId required' }, 400);\n const recorded = await recordPushOpen(store.reporting, userId, messageId);\n return json({ ok: true, recorded });\n },\n },\n {\n method: 'GET',\n path: SUBSCRIPTIONS,\n auth: 'api',\n async handle(ctx) {\n const userId = subscriptionsUser(ctx.url);\n if (userId === '') return json({ error: 'userId required' }, 400);\n return json(await getSubscriptions(ctx.store, userId, declaredTopics(ctx)));\n },\n },\n {\n method: 'PUT',\n path: SUBSCRIPTIONS,\n auth: 'api',\n async handle(ctx) {\n const userId = subscriptionsUser(ctx.url);\n if (userId === '') return json({ error: 'userId required' }, 400);\n const topics = declaredTopics(ctx);\n const parsed = parseSubscriptionsPatch(await readJson(ctx.request), topics);\n if (!parsed.ok) return json({ error: 'subscriptions refused', issues: parsed.issues }, 400);\n return json(await setSubscriptions(ctx.env, ctx.store, userId, parsed.patch, topics));\n },\n },\n {\n method: 'GET',\n path: SUBSCRIPTIONS_BY_MESSAGE,\n auth: 'api',\n async handle(ctx) {\n const messageId = subscriptionsMessage(ctx.url);\n const found = await getSubscriptionsByMessage(ctx.store, messageId, declaredTopics(ctx));\n return found === undefined ? json({ error: 'unknown message' }, 404) : json(found);\n },\n },\n {\n method: 'PUT',\n path: SUBSCRIPTIONS_BY_MESSAGE,\n auth: 'api',\n async handle(ctx) {\n const messageId = subscriptionsMessage(ctx.url);\n const topics = declaredTopics(ctx);\n const parsed = parseSubscriptionsPatch(await readJson(ctx.request), topics);\n if (!parsed.ok) return json({ error: 'subscriptions refused', issues: parsed.issues }, 400);\n const found = await setSubscriptionsByMessage(\n ctx.env,\n ctx.store,\n messageId,\n parsed.patch,\n topics\n );\n return found === undefined ? json({ error: 'unknown message' }, 404) : json(found);\n },\n },\n {\n method: 'POST',\n path: '/users/forget',\n auth: 'api',\n async handle({ request, env, store }) {\n const { userId, visitorId } = await readBody(request);\n if (isNonEmptyString(visitorId)) {\n return json(await forgetUser(env, visitorSubject(visitorId), store));\n }\n if (!isNonEmptyString(userId)) return json({ error: 'userId or visitorId required' }, 400);\n return json(await forgetUser(env, userId, store));\n },\n },\n {\n method: 'POST',\n path: '/deploy',\n auth: 'api',\n async handle({ request, env, store }) {\n return json(await deployBundle(env, await readJson(request), store));\n },\n },\n {\n method: 'GET',\n path: '/bundle',\n auth: 'api',\n async handle({ host }) {\n return host.bundle === undefined ? notFound() : json(host.bundle());\n },\n },\n {\n method: 'POST',\n path: '/bundle/deploy',\n auth: 'api',\n async handle({ env, store, host }) {\n if (host.bundle === undefined) return notFound();\n // Forced, and recording the serving version: otherwise an older version still serving\n // during a rollout would put its routes live while the row kept naming the newer one —\n // which then never re-applies its own bundle\n return json(await deployBundle(env, host.bundle(), store, host.bundleVersion, true));\n },\n },\n {\n method: 'POST',\n path: '/segments/sweep',\n auth: 'api',\n async handle({ host }) {\n return host.sweep === undefined ? notFound() : json(await host.sweep());\n },\n },\n {\n method: 'GET',\n path: /^\\/preview\\//,\n auth: 'api',\n async handle({ url, host }) {\n const { preview } = host;\n if (preview === undefined) return notFound();\n const key = url.pathname.slice('/preview/'.length);\n if (!preview.has(key)) return json({ error: `unknown email template: ${key}` }, 404);\n // `?props=<JSON object>`: JSON keeps numbers and booleans typed, which query strings cannot\n let props: unknown;\n try {\n props = JSON.parse(url.searchParams.get('props') ?? '{}');\n } catch {\n return json({ error: 'props must be a JSON object' }, 400);\n }\n if (!isPlainObject(props)) return json({ error: 'props must be a JSON object' }, 400);\n // A preview is no copy, but it shows the footer a sent one has: the preferences link\n // renders with a placeholder message id\n const { subject, html } = await preview.render(key, props as Record<string, ScalarIR>, {\n messageId: PREVIEW_MESSAGE_ID,\n });\n // A header holds bytes, not text: a subject with a non-Latin-1 character would throw\n // here, so it travels percent-encoded (the reader `decodeURIComponent`s it)\n return new Response(html, {\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'x-subject': encodeURIComponent(subject),\n },\n });\n },\n },\n];\n\nfunction matches(path: Route['path'], pathname: string): boolean {\n if (typeof path === 'string') return path === pathname;\n if (path instanceof RegExp) return path.test(pathname);\n return path(pathname);\n}\n\n/**\n * Production without a bearer token (and no authorizer of the host's own) is not an open\n * instance, it is a mistake: anyone could ingest, identify and deploy. Only what recipients open\n * (`/t/*`) and the health check stay up.\n */\nfunction refusesOpen(request: Request, ctx: ServeContext, pathname: string): boolean {\n return (\n ctx.authorize === undefined &&\n ctx.env.API_TOKEN === undefined &&\n journeyMode(ctx.env) === 'production' &&\n !pathname.startsWith('/t/') &&\n !(request.method === 'GET' && pathname === '/health')\n );\n}\n\n/**\n * Whether the caller may use an API route: the host's authorizer on a copy of the request (so a\n * body it verifies is still there for the route), or the env's `API_TOKEN` as a bearer. An\n * authorizer that throws is a failure, never an admission.\n */\nasync function allowed(request: Request, ctx: ServeContext): Promise<boolean> {\n if (ctx.authorize === undefined) return authorized(request, ctx.env);\n return ctx.authorize(request.clone(), ctx.env);\n}\n\n/**\n * What a failed request answers. Validation problems are the caller's to fix (a 400, saying\n * what); anything else stays out of the response body (internals are logged, not leaked).\n */\nfunction failure(error: unknown): Response {\n if (error instanceof InvalidJsonError) return json({ error: 'invalid JSON body' }, 400);\n if (error instanceof z.core.$ZodError) {\n return json({ error: `invalid payload: ${error.message}` }, 400);\n }\n // The caller sent a self-inconsistent bundle: their problem to fix, so say what is wrong\n if (error instanceof BundleIntegrityError) return json({ error: error.message }, 400);\n // The query string is the props: a refusal is the caller's to fix, with the field paths\n if (error instanceof TemplateValidationError) {\n return json({ error: error.message, issues: error.issues }, 400);\n }\n // The user has an erasure tombstone: gone, and nothing about them is taken, now or later\n if (error instanceof UserErasedError) return json({ error: 'user is erased' }, 410);\n // The operator's to fix (run the migrations), so the response says what is missing\n if (error instanceof SchemaMismatchError) {\n return json({ error: error.message, problems: error.problems }, 503);\n }\n console.error('[workflow http]', error);\n return json({ error: 'internal error' }, 500);\n}\n\n/** One request through the table: the production gate, the bearer, the route, the error mapping. */\nexport async function serve(request: Request, ctx: ServeContext): Promise<Response> {\n const url = new URL(request.url);\n if (refusesOpen(request, ctx, url.pathname)) {\n return json(\n {\n error:\n 'API_TOKEN is not set: production refuses to serve without it (set ENVIRONMENT=development to run open)',\n },\n 503\n );\n }\n const candidates = routes.filter((route) => matches(route.path, url.pathname));\n if (candidates.length === 0) return notFound();\n // Every API route asks the authorizer; the health check and the recipients' routes never do.\n // An unset token and no authorizer means an open dev instance (see JourneyEnv.API_TOKEN);\n // production refuses to run that way\n if (candidates.some((route) => route.auth === 'api')) {\n let ok: boolean;\n try {\n ok = await allowed(request, ctx);\n } catch (error) {\n console.error('[workflow http] authorize failed', error);\n return json({ error: 'internal error' }, 500);\n }\n if (!ok) return json({ error: 'unauthorized' }, 401);\n }\n const route = candidates.find((r) => r.method === '*' || r.method === request.method);\n if (route === undefined) return json({ error: 'method not allowed' }, 405);\n try {\n return await route.handle({\n request,\n url,\n env: ctx.env,\n store: ctx.store,\n validation: ctx.validation ?? {},\n host: ctx.host ?? {},\n });\n } catch (error) {\n return failure(error);\n }\n}\n\n/** The bare ingest API over one store: `/health`, `/events`, `/identify`, `/devices*`, `/messages/opened`, `/subscriptions/:userId`, `/subscriptions/by-message/:messageId`, `/users/forget`, `/deploy`. */\nexport function handleRequest(\n request: Request,\n env: JourneyEnv,\n store: JourneyStore,\n validation: IngestValidation = {}\n): Promise<Response> {\n return serve(request, { env, store, validation });\n}\n"],"mappings":"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"}
@@ -3,9 +3,9 @@ import { Contract } from "../contract.cjs";
3
3
  import { BundleVersion, JourneyStore } from "../store/index.cjs";
4
4
  import { EmailRenderer } from "../email-sender.cjs";
5
5
  import { Authorize } from "./auth.cjs";
6
+ import { TrackingOptions } from "../reporting/tracking.cjs";
6
7
  import { JourneyEnv } from "./platform.cjs";
7
8
  import { SweepResult } from "./ingest.cjs";
8
- import { TrackingOptions } from "../reporting/tracking.cjs";
9
9
  /**
10
10
  * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes
11
11
  * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.cts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA+CiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA8bF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,OAAO,cACP,aAAY,mBACX,QAAQ"}
1
+ {"version":3,"file":"http.d.cts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA8CiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA+VF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,OAAO,cACP,aAAY,mBACX,QAAQ"}
@@ -3,9 +3,9 @@ import { Contract } from "../contract.mjs";
3
3
  import { BundleVersion, JourneyStore } from "../store/index.mjs";
4
4
  import { EmailRenderer } from "../email-sender.mjs";
5
5
  import { Authorize } from "./auth.mjs";
6
+ import { TrackingOptions } from "../reporting/tracking.mjs";
6
7
  import { JourneyEnv } from "./platform.mjs";
7
8
  import { SweepResult } from "./ingest.mjs";
8
- import { TrackingOptions } from "../reporting/tracking.mjs";
9
9
  /**
10
10
  * The HTTP surface, as one route table: the bare ingest API (`handleRequest`) and the routes
11
11
  * the full host adds (`serve` with its parts — the bundle, previews, tracking, the sweep). A
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA+CiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA8bF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,OAAO,cACP,aAAY,mBACX,QAAQ"}
1
+ {"version":3,"file":"http.d.mts","names":[],"sources":["../../src/host/http.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;iBA8CiB;EACf,WAAW;EACX;;;iBAIe;;EAEf,gBAAgB;;EAEhB,gBAAgB;;EAEhB;IAAY,IAAI;IAAsB,QAAQ;;;EAE9C,WAAW;;EAEX,eAAe,QAAQ;;iBAGR;EACf,KAAK;EACL,OAAO;EACP,aAAa;;;;;;EAMb,OAAO;;EAEP,YAAY,UAAU;;;wBA+VF,MAAM,SAAS,SAAS,KAAK,eAAe,QAAQ;;wBA2C1D,cACd,SAAS,SACT,KAAK,YACL,OAAO,cACP,aAAY,mBACX,QAAQ"}