@shware/workflow 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -2
- package/dist/apns-relay.cjs +4 -17
- package/dist/apns-relay.cjs.map +1 -1
- package/dist/apns-relay.mjs +4 -17
- package/dist/apns-relay.mjs.map +1 -1
- package/dist/aws/engine-step.cjs +104 -0
- package/dist/aws/engine-step.cjs.map +1 -0
- package/dist/aws/engine-step.d.cts +28 -0
- package/dist/aws/engine-step.d.cts.map +1 -0
- package/dist/aws/engine-step.d.mts +28 -0
- package/dist/aws/engine-step.d.mts.map +1 -0
- package/dist/aws/engine-step.mjs +102 -0
- package/dist/aws/engine-step.mjs.map +1 -0
- package/dist/aws/function-url.cjs +62 -0
- package/dist/aws/function-url.cjs.map +1 -0
- package/dist/aws/function-url.d.cts +38 -0
- package/dist/aws/function-url.d.cts.map +1 -0
- package/dist/aws/function-url.d.mts +38 -0
- package/dist/aws/function-url.d.mts.map +1 -0
- package/dist/aws/function-url.mjs +59 -0
- package/dist/aws/function-url.mjs.map +1 -0
- package/dist/aws/index.cjs +160 -0
- package/dist/aws/index.cjs.map +1 -0
- package/dist/aws/index.d.cts +62 -0
- package/dist/aws/index.d.cts.map +1 -0
- package/dist/aws/index.d.mts +62 -0
- package/dist/aws/index.d.mts.map +1 -0
- package/dist/aws/index.mjs +148 -0
- package/dist/aws/index.mjs.map +1 -0
- package/dist/aws/instances.cjs +176 -0
- package/dist/aws/instances.cjs.map +1 -0
- package/dist/aws/instances.d.cts +75 -0
- package/dist/aws/instances.d.cts.map +1 -0
- package/dist/aws/instances.d.mts +75 -0
- package/dist/aws/instances.d.mts.map +1 -0
- package/dist/aws/instances.mjs +172 -0
- package/dist/aws/instances.mjs.map +1 -0
- package/dist/aws/lambda-api.cjs +89 -0
- package/dist/aws/lambda-api.cjs.map +1 -0
- package/dist/aws/lambda-api.d.cts +43 -0
- package/dist/aws/lambda-api.d.cts.map +1 -0
- package/dist/aws/lambda-api.d.mts +43 -0
- package/dist/aws/lambda-api.d.mts.map +1 -0
- package/dist/aws/lambda-api.mjs +88 -0
- package/dist/aws/lambda-api.mjs.map +1 -0
- package/dist/client.d.cts +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/cloudflare/bindings.cjs +19 -0
- package/dist/cloudflare/bindings.cjs.map +1 -1
- package/dist/cloudflare/bindings.d.cts +37 -0
- package/dist/cloudflare/bindings.d.cts.map +1 -1
- package/dist/cloudflare/bindings.d.mts +37 -0
- package/dist/cloudflare/bindings.d.mts.map +1 -1
- package/dist/cloudflare/bindings.mjs +18 -1
- package/dist/cloudflare/bindings.mjs.map +1 -1
- package/dist/cloudflare/index.cjs +32 -28
- package/dist/cloudflare/index.d.cts +8 -6
- package/dist/cloudflare/index.d.mts +8 -6
- package/dist/cloudflare/index.mjs +12 -10
- package/dist/cloudflare/runner.cjs +27 -76
- package/dist/cloudflare/runner.cjs.map +1 -1
- package/dist/cloudflare/runner.d.cts +7 -2
- package/dist/cloudflare/runner.d.cts.map +1 -1
- package/dist/cloudflare/runner.d.mts +7 -2
- package/dist/cloudflare/runner.d.mts.map +1 -1
- package/dist/cloudflare/runner.mjs +26 -75
- package/dist/cloudflare/runner.mjs.map +1 -1
- package/dist/cloudflare/worker.cjs +22 -203
- package/dist/cloudflare/worker.cjs.map +1 -1
- package/dist/cloudflare/worker.d.cts +10 -56
- package/dist/cloudflare/worker.d.cts.map +1 -1
- package/dist/cloudflare/worker.d.mts +10 -56
- package/dist/cloudflare/worker.d.mts.map +1 -1
- package/dist/cloudflare/worker.mjs +20 -199
- package/dist/cloudflare/worker.mjs.map +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +1 -1
- package/dist/config.mjs +2 -2
- package/dist/config.mjs.map +1 -1
- package/dist/contract.cjs.map +1 -1
- package/dist/contract.d.cts +1 -1
- package/dist/contract.d.cts.map +1 -1
- package/dist/contract.d.mts +1 -1
- package/dist/contract.d.mts.map +1 -1
- package/dist/contract.mjs.map +1 -1
- package/dist/dsl/action.cjs.map +1 -1
- package/dist/dsl/action.d.cts +2 -2
- package/dist/dsl/action.d.cts.map +1 -1
- package/dist/dsl/action.d.mts +2 -2
- package/dist/dsl/action.d.mts.map +1 -1
- package/dist/dsl/action.mjs.map +1 -1
- package/dist/dsl/bundle.cjs.map +1 -1
- package/dist/dsl/bundle.d.cts +1 -1
- package/dist/dsl/bundle.d.mts +1 -1
- package/dist/dsl/bundle.mjs.map +1 -1
- package/dist/dsl/flow.cjs.map +1 -1
- package/dist/dsl/flow.d.cts +1 -1
- package/dist/dsl/flow.d.cts.map +1 -1
- package/dist/dsl/flow.d.mts +1 -1
- package/dist/dsl/flow.d.mts.map +1 -1
- package/dist/dsl/flow.mjs.map +1 -1
- package/dist/dsl/refs.cjs.map +1 -1
- package/dist/dsl/refs.d.cts +5 -4
- package/dist/dsl/refs.d.cts.map +1 -1
- package/dist/dsl/refs.d.mts +5 -4
- package/dist/dsl/refs.d.mts.map +1 -1
- package/dist/dsl/refs.mjs.map +1 -1
- package/dist/dsl/template.cjs.map +1 -1
- package/dist/dsl/template.d.cts +2 -2
- package/dist/dsl/template.d.cts.map +1 -1
- package/dist/dsl/template.d.mts +2 -2
- package/dist/dsl/template.d.mts.map +1 -1
- package/dist/dsl/template.mjs.map +1 -1
- package/dist/email-sender.cjs +62 -0
- package/dist/email-sender.cjs.map +1 -0
- package/dist/email-sender.d.cts +70 -0
- package/dist/email-sender.d.cts.map +1 -0
- package/dist/email-sender.d.mts +70 -0
- package/dist/email-sender.d.mts.map +1 -0
- package/dist/email-sender.mjs +60 -0
- package/dist/email-sender.mjs.map +1 -0
- package/dist/email.cjs +21 -0
- package/dist/email.cjs.map +1 -1
- package/dist/email.d.cts +6 -0
- package/dist/email.d.cts.map +1 -1
- package/dist/email.d.mts +6 -0
- package/dist/email.d.mts.map +1 -1
- package/dist/email.mjs +21 -1
- package/dist/email.mjs.map +1 -1
- package/dist/engine/index.cjs +1 -1
- package/dist/engine/index.mjs +1 -1
- package/dist/engine/interpreter.cjs +1 -1
- package/dist/engine/interpreter.cjs.map +1 -1
- package/dist/engine/interpreter.d.cts.map +1 -1
- package/dist/engine/interpreter.d.mts.map +1 -1
- package/dist/engine/interpreter.mjs +1 -1
- package/dist/engine/interpreter.mjs.map +1 -1
- package/dist/engine/ports.d.cts +6 -1
- package/dist/engine/ports.d.cts.map +1 -1
- package/dist/engine/ports.d.mts +6 -1
- package/dist/engine/ports.d.mts.map +1 -1
- package/dist/{cloudflare → host}/channels.cjs +13 -11
- package/dist/host/channels.cjs.map +1 -0
- package/dist/{cloudflare → host}/channels.d.cts +8 -3
- package/dist/host/channels.d.cts.map +1 -0
- package/dist/{cloudflare → host}/channels.d.mts +8 -3
- package/dist/host/channels.d.mts.map +1 -0
- package/dist/{cloudflare → host}/channels.mjs +9 -7
- package/dist/host/channels.mjs.map +1 -0
- package/dist/host/execute.cjs +82 -0
- package/dist/host/execute.cjs.map +1 -0
- package/dist/host/execute.d.cts +41 -0
- package/dist/host/execute.d.cts.map +1 -0
- package/dist/host/execute.d.mts +41 -0
- package/dist/host/execute.d.mts.map +1 -0
- package/dist/host/execute.mjs +81 -0
- package/dist/host/execute.mjs.map +1 -0
- package/dist/host/journey-host.cjs +214 -0
- package/dist/host/journey-host.cjs.map +1 -0
- package/dist/host/journey-host.d.cts +105 -0
- package/dist/host/journey-host.d.cts.map +1 -0
- package/dist/host/journey-host.d.mts +105 -0
- package/dist/host/journey-host.d.mts.map +1 -0
- package/dist/host/journey-host.mjs +210 -0
- package/dist/host/journey-host.mjs.map +1 -0
- package/dist/{cloudflare → host}/router.cjs +28 -9
- package/dist/host/router.cjs.map +1 -0
- package/dist/{cloudflare → host}/router.d.cts +2 -2
- package/dist/host/router.d.cts.map +1 -0
- package/dist/{cloudflare → host}/router.d.mts +2 -2
- package/dist/host/router.d.mts.map +1 -0
- package/dist/{cloudflare → host}/router.mjs +28 -9
- package/dist/host/router.mjs.map +1 -0
- package/dist/host/senders.cjs +63 -0
- package/dist/host/senders.cjs.map +1 -0
- package/dist/host/senders.d.cts +31 -0
- package/dist/host/senders.d.cts.map +1 -0
- package/dist/host/senders.d.mts +31 -0
- package/dist/host/senders.d.mts.map +1 -0
- package/dist/host/senders.mjs +59 -0
- package/dist/host/senders.mjs.map +1 -0
- package/dist/http2-client.cjs +88 -0
- package/dist/http2-client.cjs.map +1 -0
- package/dist/http2-client.d.cts +44 -0
- package/dist/http2-client.d.cts.map +1 -0
- package/dist/http2-client.d.mts +44 -0
- package/dist/http2-client.d.mts.map +1 -0
- package/dist/http2-client.mjs +86 -0
- package/dist/http2-client.mjs.map +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/react-email.cjs.map +1 -1
- package/dist/react-email.d.cts +1 -2
- package/dist/react-email.d.cts.map +1 -1
- package/dist/react-email.d.mts +1 -2
- package/dist/react-email.d.mts.map +1 -1
- package/dist/react-email.mjs.map +1 -1
- package/dist/reporting/tracking.cjs +5 -1
- package/dist/reporting/tracking.cjs.map +1 -1
- package/dist/reporting/tracking.d.cts.map +1 -1
- package/dist/reporting/tracking.d.mts.map +1 -1
- package/dist/reporting/tracking.mjs +5 -1
- package/dist/reporting/tracking.mjs.map +1 -1
- package/dist/store/d1.cjs +1 -0
- package/dist/store/d1.cjs.map +1 -1
- package/dist/store/d1.d.cts.map +1 -1
- package/dist/store/d1.d.mts.map +1 -1
- package/dist/store/d1.mjs +1 -0
- package/dist/store/d1.mjs.map +1 -1
- package/dist/store/index.cjs.map +1 -1
- package/dist/store/index.d.cts +63 -0
- package/dist/store/index.d.cts.map +1 -1
- package/dist/store/index.d.mts +63 -0
- package/dist/store/index.d.mts.map +1 -1
- package/dist/store/index.mjs.map +1 -1
- package/dist/store/postgres-reporting.cjs +5 -5
- package/dist/store/postgres-reporting.cjs.map +1 -1
- package/dist/store/postgres-reporting.mjs +5 -5
- package/dist/store/postgres-reporting.mjs.map +1 -1
- package/dist/store/postgres.cjs +143 -2
- package/dist/store/postgres.cjs.map +1 -1
- package/dist/store/postgres.d.cts +4 -1
- package/dist/store/postgres.d.cts.map +1 -1
- package/dist/store/postgres.d.mts +4 -1
- package/dist/store/postgres.d.mts.map +1 -1
- package/dist/store/postgres.mjs +143 -2
- package/dist/store/postgres.mjs.map +1 -1
- package/dist/store/tables.cjs +26 -0
- package/dist/store/tables.cjs.map +1 -1
- package/dist/store/tables.d.cts +1 -0
- package/dist/store/tables.d.cts.map +1 -1
- package/dist/store/tables.d.mts +1 -0
- package/dist/store/tables.d.mts.map +1 -1
- package/dist/store/tables.mjs +26 -0
- package/dist/store/tables.mjs.map +1 -1
- package/dist/templates.cjs.map +1 -1
- package/dist/templates.mjs.map +1 -1
- package/dist/transports/cloudflare-email.cjs +59 -0
- package/dist/transports/cloudflare-email.cjs.map +1 -0
- package/dist/transports/cloudflare-email.d.cts +38 -0
- package/dist/transports/cloudflare-email.d.cts.map +1 -0
- package/dist/transports/cloudflare-email.d.mts +38 -0
- package/dist/transports/cloudflare-email.d.mts.map +1 -0
- package/dist/transports/cloudflare-email.mjs +57 -0
- package/dist/transports/cloudflare-email.mjs.map +1 -0
- package/dist/transports/ses.cjs +91 -0
- package/dist/transports/ses.cjs.map +1 -0
- package/dist/transports/ses.d.cts +39 -0
- package/dist/transports/ses.d.cts.map +1 -0
- package/dist/transports/ses.d.mts +39 -0
- package/dist/transports/ses.d.mts.map +1 -0
- package/dist/transports/ses.mjs +89 -0
- package/dist/transports/ses.mjs.map +1 -0
- package/dist/transports/sigv4.cjs +83 -0
- package/dist/transports/sigv4.cjs.map +1 -0
- package/dist/transports/sigv4.d.cts +27 -0
- package/dist/transports/sigv4.d.cts.map +1 -0
- package/dist/transports/sigv4.d.mts +27 -0
- package/dist/transports/sigv4.d.mts.map +1 -0
- package/dist/transports/sigv4.mjs +82 -0
- package/dist/transports/sigv4.mjs.map +1 -0
- package/package.json +53 -1
- package/dist/cloudflare/channels.cjs.map +0 -1
- package/dist/cloudflare/channels.d.cts.map +0 -1
- package/dist/cloudflare/channels.d.mts.map +0 -1
- package/dist/cloudflare/channels.mjs.map +0 -1
- package/dist/cloudflare/router.cjs.map +0 -1
- package/dist/cloudflare/router.d.cts.map +0 -1
- package/dist/cloudflare/router.d.mts.map +0 -1
- package/dist/cloudflare/router.mjs.map +0 -1
- package/dist/cloudflare/senders.cjs +0 -121
- package/dist/cloudflare/senders.cjs.map +0 -1
- package/dist/cloudflare/senders.d.cts +0 -72
- package/dist/cloudflare/senders.d.cts.map +0 -1
- package/dist/cloudflare/senders.d.mts +0 -72
- package/dist/cloudflare/senders.d.mts.map +0 -1
- package/dist/cloudflare/senders.mjs +0 -115
- package/dist/cloudflare/senders.mjs.map +0 -1
package/dist/config.d.cts
CHANGED
|
@@ -85,7 +85,7 @@ export interface JourneyConfigDefinition {
|
|
|
85
85
|
contract?: Contract;
|
|
86
86
|
workflows: readonly WorkflowBuilder[];
|
|
87
87
|
segments?: readonly SegmentRef[];
|
|
88
|
-
actions?: readonly ActionRef
|
|
88
|
+
actions?: readonly ActionRef[];
|
|
89
89
|
/** The template definitions, per channel; each is identified by its own `id`. */
|
|
90
90
|
templates?: TemplateRegistry;
|
|
91
91
|
/** What every email template shares: the default unsubscribe link, how opens and clicks are judged. */
|
package/dist/config.d.mts
CHANGED
|
@@ -85,7 +85,7 @@ export interface JourneyConfigDefinition {
|
|
|
85
85
|
contract?: Contract;
|
|
86
86
|
workflows: readonly WorkflowBuilder[];
|
|
87
87
|
segments?: readonly SegmentRef[];
|
|
88
|
-
actions?: readonly ActionRef
|
|
88
|
+
actions?: readonly ActionRef[];
|
|
89
89
|
/** The template definitions, per channel; each is identified by its own `id`. */
|
|
90
90
|
templates?: TemplateRegistry;
|
|
91
91
|
/** What every email template shares: the default unsubscribe link, how opens and clicks are judged. */
|
package/dist/config.mjs
CHANGED
|
@@ -131,7 +131,7 @@ function checkMessage(workflow, node, index, config) {
|
|
|
131
131
|
problems.push(`'${field}' is not a prop of the template`);
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
|
-
if (typeof value === "object" &&
|
|
134
|
+
if (typeof value === "object" && "type" in value) {
|
|
135
135
|
const profile = config.contract?.schemas.profile._zod.def;
|
|
136
136
|
if (profile !== void 0 && value.path !== "userId" && !Object.hasOwn(profile.shape, value.path)) problems.push(`'${field}' reads u.${value.path}, which the contract's profile does not declare`);
|
|
137
137
|
continue;
|
|
@@ -155,7 +155,7 @@ function checkEmit(workflow, node, config) {
|
|
|
155
155
|
const shape = contract.schemas.events[node.event]._zod.def.shape;
|
|
156
156
|
const problems = [];
|
|
157
157
|
for (const [field, value] of Object.entries(node.payload)) {
|
|
158
|
-
if (typeof value === "object" &&
|
|
158
|
+
if (typeof value === "object" && "type" in value) continue;
|
|
159
159
|
if (!Object.hasOwn(shape, field)) continue;
|
|
160
160
|
if (!z.safeParse(shape[field], value).success) problems.push(`'${field}' = ${JSON.stringify(value)} is refused by the contract`);
|
|
161
161
|
}
|
package/dist/config.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { Contract } from './contract';\nimport { type ActionRef, type SegmentRef, type WorkflowBuilder, compileBundle } from './dsl';\nimport { templateUses } from './dsl/flow';\nimport { type EmailConfig, assertEmailConfig } from './email';\nimport type { BundleIR, ChannelIR, NodeIR, WorkflowIR } from './ir';\nimport type { JourneyMode, JourneyRuntimeConfig } from './runtime';\nimport {\n type AnyTemplate,\n CHANNEL_OF,\n type TemplateChannel,\n type TemplateRegistry,\n indexTemplates,\n} from './templates';\n\n/**\n * `workflow.config.ts` — the project's journey configuration, typed, flat and\n * committed with the code: what it deploys (workflows, segments, actions),\n * the template registries behind them, the contract every inbound event is\n * checked against, what the email templates share, and how the engine runs per\n * environment (time scale, APNs, FCM). The compiler (`compileConfig`), the\n * Worker (`journeyWorker({ config })`) and the Workflow Studio all read this\n * one object, so everything is registered exactly once:\n *\n * export default defineConfig({\n * title: 'Workflow Studio · Acme',\n * contract,\n * workflows, // [onboarding, notificationTest]\n * templates, // { email, push }: the definitions the workflows send\n * email: { unsubscribeUrl: 'https://acme.test/unsubscribe' },\n * runtime: { development: { timeScale: 600, logMessages: true } },\n * apns: { topic: 'io.acme.app', keyId: 'ABC123DEFG', teamId: 'TEAM123456' },\n * });\n *\n * Secrets are the one thing that stay out: a `.p8`, a service account, the\n * ingest bearer. The host reads them from its environment under fixed names\n * (see `JourneyEnv`), so the config says *which* key without carrying it. The\n * config is a source object (schemas, components, functions); only its\n * compiled bundle and preview-safe template data may cross to a browser.\n */\n\n/** Token-based APNs, the `.p8` auth key; the key itself is `APNS_PRIVATE_KEY_BASE64` in the environment. */\nexport interface ApnsConfig {\n /** The app's bundle id (`apns-topic`). */\n topic: string;\n /** The key's 10-character id. */\n keyId: string;\n /** The Apple Developer team id. */\n teamId: string;\n /**\n * Which APNs host each mode's device tokens belong to: `sandbox` for builds\n * signed with `aps-environment: development` (Xcode, the simulator),\n * `production` for App Store and TestFlight builds. Unlisted modes default\n * to `production` for the production mode and `sandbox` for every other.\n */\n environment?: Partial<Record<JourneyMode, 'sandbox' | 'production'>>;\n}\n\n/**\n * Standard Web Push for browsers (VAPID); the key pair is `VAPID_PUBLIC_KEY` /\n * `VAPID_PRIVATE_KEY` in the environment, and the site subscribes with the same public key.\n */\nexport interface WebPushConfig {\n /** Who push services may contact about this sender: `mailto:` or an `https:` URL. */\n subject: string;\n /**\n * The contract event a notification tap the browser reports (`opened_url`) is also recorded\n * as, with `{ message_id }`: the app reports its own taps as an event, and a service worker has\n * no session to do so — without this, browser opens reach the reports but not the conditions\n * (a `performed(e.push_opened)` segment). One event per copy, however often it is tapped.\n */\n openedEvent?: string;\n}\n\n/** FCM HTTP v1 for Android; the service account is `GOOGLE_APPLICATION_CREDENTIALS_BASE64` in the environment. */\nexport interface FcmConfig {\n /** Android notification channel the pushes land in (`android.notification.channel_id`). */\n androidChannelId?: string;\n}\n\n/**\n * The engine's tables live in a Postgres schema of their own, so their short names (`event`,\n * `profile`, `message`, …) never meet the app's. `workflow-ui generate` writes them as one drizzle\n * file on this schema; the host migrates it with its own tooling.\n */\nexport interface DatabaseConfig {\n /** The Postgres schema; `workflow` by default. */\n schema?: string;\n}\n\nexport interface JourneyConfigDefinition {\n /** Studio title (the browser tab names the project). */\n title?: string;\n /** The shared `defineContract(...)` the Worker validates ingest against. */\n contract?: Contract;\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n actions?: readonly ActionRef<object>[];\n /** The template definitions, per channel; each is identified by its own `id`. */\n templates?: TemplateRegistry;\n /** What every email template shares: the default unsubscribe link, how opens and clicks are judged. */\n email?: EmailConfig;\n /** How the engine behaves per mode (`development`, `production`, …): time scale, message logging. */\n runtime?: JourneyRuntimeConfig;\n apns?: ApnsConfig;\n fcm?: FcmConfig;\n webPush?: WebPushConfig;\n /** Where a Postgres store keeps the engine's tables (the D1 store ignores it). */\n database?: DatabaseConfig;\n}\n\nexport interface JourneyConfig extends JourneyConfigDefinition {\n readonly kind: 'journey-config';\n}\n\nexport function defineConfig(definition: JourneyConfigDefinition): JourneyConfig {\n return { kind: 'journey-config', ...definition };\n}\n\nexport function isConfig(value: unknown): value is JourneyConfig {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-config'\n );\n}\n\n/** The APNs environment for `mode`: the configured one, else production only for production. */\nexport function apnsEnvironment(config: ApnsConfig, mode: JourneyMode): 'sandbox' | 'production' {\n return config.environment?.[mode] ?? (mode === 'production' ? 'production' : 'sandbox');\n}\n\n/** A message node the config's registries cannot serve, or whose literal props its template refuses. */\nexport class ConfigCompileError extends Error {\n override readonly name = 'ConfigCompileError';\n}\n\n/**\n * Compile the config: the bundle `compileBundle` would produce, with the\n * template manifest generated from the registry (every registered template,\n * by id, not a hand-kept list). The registry is checked first, used or not:\n * every entry is a template of its channel with an id, an id is registered\n * once per channel, and every email template's metadata is valid, as is the\n * config's `email` (which replaced `emails`: a config still carrying it fails).\n * Then every message node is checked against it — an\n * unregistered or cross-channel template fails with the workflow, node and\n * channel named, and so does a literal prop the template's schema refuses (a\n * missing required prop, an undeclared one, a wrong literal) — and every\n * definition a workflow sends must be the very object registered under its id,\n * not merely one with the same id. User-property references cannot be parsed\n * here (their values exist only at send time) — they are checked by name only,\n * and parsed before sending.\n */\nexport function compileConfig(config: JourneyConfig): BundleIR {\n let index: RegistryIndex;\n try {\n const email = indexTemplates('email', config.templates?.email);\n assertEmailConfig(config, email.values());\n index = {\n email,\n push: indexTemplates('push', config.templates?.push),\n discord: indexTemplates('discord', config.templates?.discord),\n slack: indexTemplates('slack', config.templates?.slack),\n };\n } catch (error) {\n throw new ConfigCompileError((error as Error).message);\n }\n const bundle = compileBundle({\n workflows: config.workflows,\n ...(config.segments === undefined ? {} : { segments: config.segments }),\n ...(config.actions === undefined ? {} : { actions: config.actions }),\n templates: TEMPLATE_CHANNELS.flatMap((channel) => [...index[channel].values()]),\n });\n bundle.workflows.forEach((workflow, i) => {\n checkMessages(workflow, workflow.flow, index, config);\n checkUses(workflow, config.workflows[i], index, config);\n });\n return bundle;\n}\n\nconst TEMPLATE_CHANNELS = ['email', 'push', 'discord', 'slack'] as const;\n\n/** The registry by id, per channel. */\ntype RegistryIndex = Record<TemplateChannel, ReadonlyMap<string, AnyTemplate>>;\n\n/** The channel's templates by id; undefined for a channel templates cannot be declared for. */\nfunction indexOf(\n index: RegistryIndex,\n channel: ChannelIR\n): ReadonlyMap<string, AnyTemplate> | undefined {\n return (TEMPLATE_CHANNELS as readonly ChannelIR[]).includes(channel)\n ? index[channel as TemplateChannel]\n : undefined;\n}\n\n/**\n * The definitions a workflow sends against the registered ones: same id is not enough — a\n * workflow typed by one definition and a registry holding another under that id would render\n * with a schema the call was never checked against.\n */\nfunction checkUses(\n workflow: WorkflowIR,\n builder: WorkflowBuilder,\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n for (const { template, loc } of templateUses(builder)) {\n const channel = CHANNEL_OF[template.kind];\n const registered = index[channel].get(template.id);\n if (registered === template) continue;\n const at = loc === undefined ? '' : ` (${loc.file}:${loc.line})`;\n const where = `workflow '${workflow.id}'${at}: ${channel} template '${template.id}'`;\n if (registered === undefined) throw new ConfigCompileError(`${where} is not registered`);\n const name = Object.entries(config.templates?.[channel] ?? {}).find(\n ([, entry]) => entry === registered\n )?.[0];\n throw new ConfigCompileError(\n `${where} is not the definition registered under that id (templates.${channel}.${name}) — two definitions share the id`\n );\n }\n}\n\n/** A field that may be left out: optional, or with a default the schema fills in. */\nfunction canBeOmitted(schema: core.$ZodType): boolean {\n const type = schema._zod.def.type;\n return type === 'optional' || type === 'default' || type === 'prefault';\n}\n\nfunction checkMessages(\n workflow: WorkflowIR,\n nodes: readonly NodeIR[],\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'message':\n checkMessage(workflow, node, index, config);\n break;\n case 'send_event':\n checkEmit(workflow, node, config);\n break;\n case 'branch':\n for (const c of node.cases) checkMessages(workflow, c.flow, index, config);\n if (node.otherwise) checkMessages(workflow, node.otherwise, index, config);\n break;\n case 'cohort':\n for (const arm of node.arms) checkMessages(workflow, arm.flow, index, config);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) checkMessages(workflow, node.onTimeout, index, config);\n break;\n default:\n break;\n }\n }\n}\n\nfunction checkMessage(\n workflow: WorkflowIR,\n node: Extract<NodeIR, { type: 'message' }>,\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n const where = `workflow '${workflow.id}' node ${node.id}`;\n const registry = indexOf(index, node.channel);\n // Channels without template definitions (sms, …) are rendered by app-supplied senders\n if (registry === undefined) return;\n const entry = registry.get(node.template);\n if (entry === undefined) {\n const other = TEMPLATE_CHANNELS.find(\n (channel) => channel !== node.channel && index[channel].has(node.template)\n );\n const hint = other === undefined ? '' : ` (it is registered as a ${other} template)`;\n throw new ConfigCompileError(\n `${where}: ${node.channel} template '${node.template}' is not registered${hint}`\n );\n }\n const shape = (entry.props._zod.def as core.$ZodObjectDef).shape;\n const problems: string[] = [];\n for (const [field, value] of Object.entries(node.props)) {\n if (!Object.hasOwn(shape, field)) {\n problems.push(`'${field}' is not a prop of the template`);\n continue;\n }\n if (typeof value === 'object' && value !== null && 'type' in value) {\n // A user ref's value exists only at send time (parsed then); here its name can be checked\n const profile = config.contract?.schemas.profile._zod.def;\n if (\n profile !== undefined &&\n value.path !== 'userId' &&\n !Object.hasOwn(profile.shape, value.path)\n ) {\n problems.push(\n `'${field}' reads u.${value.path}, which the contract's profile does not declare`\n );\n }\n continue;\n }\n const result = z.safeParse(shape[field], value);\n if (!result.success)\n problems.push(`'${field}' = ${JSON.stringify(value)} is refused by the schema`);\n }\n for (const [field, schema] of Object.entries(shape)) {\n const optional = canBeOmitted(schema);\n if (!optional && !Object.hasOwn(node.props, field))\n problems.push(`required prop '${field}' is missing`);\n }\n if (problems.length > 0) {\n throw new ConfigCompileError(\n `${where}: ${node.channel} '${node.template}': ${problems.join('; ')}`\n );\n }\n}\n\n/**\n * An emitted event against the contract, as far as compile time can see: the\n * event is declared, literal payload values parse against their field, and no\n * required field is missing. Values read from the profile are checked when the\n * event is emitted, by the same contract.\n */\nfunction checkEmit(\n workflow: WorkflowIR,\n node: Extract<NodeIR, { type: 'send_event' }>,\n config: JourneyConfig\n): void {\n const contract = config.contract;\n if (contract === undefined) return;\n const where = `workflow '${workflow.id}' node ${node.id}`;\n if (!contract.isEvent(node.event)) {\n throw new ConfigCompileError(\n `${where}: emits '${String(node.event)}', which is not in the contract`\n );\n }\n const schema = contract.schemas.events[node.event];\n const shape = (schema._zod.def as core.$ZodObjectDef).shape;\n const problems: string[] = [];\n for (const [field, value] of Object.entries(node.payload)) {\n if (typeof value === 'object' && value !== null && 'type' in value) continue; // read at emit time\n if (!Object.hasOwn(shape, field)) continue; // a loose payload may carry extra fields\n const result = z.safeParse(shape[field], value);\n if (!result.success)\n problems.push(`'${field}' = ${JSON.stringify(value)} is refused by the contract`);\n }\n for (const [field, fieldSchema] of Object.entries(shape)) {\n const optional = canBeOmitted(fieldSchema);\n if (!optional && !Object.hasOwn(node.payload, field))\n problems.push(`required field '${field}' is missing`);\n }\n if (problems.length > 0) {\n throw new ConfigCompileError(`${where}: event '${node.event}': ${problems.join('; ')}`);\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.mjs","names":[],"sources":["../src/config.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport type { Contract } from './contract';\nimport { type ActionRef, type SegmentRef, type WorkflowBuilder, compileBundle } from './dsl';\nimport { templateUses } from './dsl/flow';\nimport { type EmailConfig, assertEmailConfig } from './email';\nimport type { BundleIR, ChannelIR, NodeIR, WorkflowIR } from './ir';\nimport type { JourneyMode, JourneyRuntimeConfig } from './runtime';\nimport {\n type AnyTemplate,\n CHANNEL_OF,\n type TemplateChannel,\n type TemplateRegistry,\n indexTemplates,\n} from './templates';\n\n/**\n * `workflow.config.ts` — the project's journey configuration, typed, flat and\n * committed with the code: what it deploys (workflows, segments, actions),\n * the template registries behind them, the contract every inbound event is\n * checked against, what the email templates share, and how the engine runs per\n * environment (time scale, APNs, FCM). The compiler (`compileConfig`), the\n * Worker (`journeyWorker({ config })`) and the Workflow Studio all read this\n * one object, so everything is registered exactly once:\n *\n * export default defineConfig({\n * title: 'Workflow Studio · Acme',\n * contract,\n * workflows, // [onboarding, notificationTest]\n * templates, // { email, push }: the definitions the workflows send\n * email: { unsubscribeUrl: 'https://acme.test/unsubscribe' },\n * runtime: { development: { timeScale: 600, logMessages: true } },\n * apns: { topic: 'io.acme.app', keyId: 'ABC123DEFG', teamId: 'TEAM123456' },\n * });\n *\n * Secrets are the one thing that stay out: a `.p8`, a service account, the\n * ingest bearer. The host reads them from its environment under fixed names\n * (see `JourneyEnv`), so the config says *which* key without carrying it. The\n * config is a source object (schemas, components, functions); only its\n * compiled bundle and preview-safe template data may cross to a browser.\n */\n\n/** Token-based APNs, the `.p8` auth key; the key itself is `APNS_PRIVATE_KEY_BASE64` in the environment. */\nexport interface ApnsConfig {\n /** The app's bundle id (`apns-topic`). */\n topic: string;\n /** The key's 10-character id. */\n keyId: string;\n /** The Apple Developer team id. */\n teamId: string;\n /**\n * Which APNs host each mode's device tokens belong to: `sandbox` for builds\n * signed with `aps-environment: development` (Xcode, the simulator),\n * `production` for App Store and TestFlight builds. Unlisted modes default\n * to `production` for the production mode and `sandbox` for every other.\n */\n environment?: Partial<Record<JourneyMode, 'sandbox' | 'production'>>;\n}\n\n/**\n * Standard Web Push for browsers (VAPID); the key pair is `VAPID_PUBLIC_KEY` /\n * `VAPID_PRIVATE_KEY` in the environment, and the site subscribes with the same public key.\n */\nexport interface WebPushConfig {\n /** Who push services may contact about this sender: `mailto:` or an `https:` URL. */\n subject: string;\n /**\n * The contract event a notification tap the browser reports (`opened_url`) is also recorded\n * as, with `{ message_id }`: the app reports its own taps as an event, and a service worker has\n * no session to do so — without this, browser opens reach the reports but not the conditions\n * (a `performed(e.push_opened)` segment). One event per copy, however often it is tapped.\n */\n openedEvent?: string;\n}\n\n/** FCM HTTP v1 for Android; the service account is `GOOGLE_APPLICATION_CREDENTIALS_BASE64` in the environment. */\nexport interface FcmConfig {\n /** Android notification channel the pushes land in (`android.notification.channel_id`). */\n androidChannelId?: string;\n}\n\n/**\n * The engine's tables live in a Postgres schema of their own, so their short names (`event`,\n * `profile`, `message`, …) never meet the app's. `workflow-ui generate` writes them as one drizzle\n * file on this schema; the host migrates it with its own tooling.\n */\nexport interface DatabaseConfig {\n /** The Postgres schema; `workflow` by default. */\n schema?: string;\n}\n\nexport interface JourneyConfigDefinition {\n /** Studio title (the browser tab names the project). */\n title?: string;\n /** The shared `defineContract(...)` the Worker validates ingest against. */\n contract?: Contract;\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n actions?: readonly ActionRef[];\n /** The template definitions, per channel; each is identified by its own `id`. */\n templates?: TemplateRegistry;\n /** What every email template shares: the default unsubscribe link, how opens and clicks are judged. */\n email?: EmailConfig;\n /** How the engine behaves per mode (`development`, `production`, …): time scale, message logging. */\n runtime?: JourneyRuntimeConfig;\n apns?: ApnsConfig;\n fcm?: FcmConfig;\n webPush?: WebPushConfig;\n /** Where a Postgres store keeps the engine's tables (the D1 store ignores it). */\n database?: DatabaseConfig;\n}\n\nexport interface JourneyConfig extends JourneyConfigDefinition {\n readonly kind: 'journey-config';\n}\n\nexport function defineConfig(definition: JourneyConfigDefinition): JourneyConfig {\n return { kind: 'journey-config', ...definition };\n}\n\nexport function isConfig(value: unknown): value is JourneyConfig {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-config'\n );\n}\n\n/** The APNs environment for `mode`: the configured one, else production only for production. */\nexport function apnsEnvironment(config: ApnsConfig, mode: JourneyMode): 'sandbox' | 'production' {\n return config.environment?.[mode] ?? (mode === 'production' ? 'production' : 'sandbox');\n}\n\n/** A message node the config's registries cannot serve, or whose literal props its template refuses. */\nexport class ConfigCompileError extends Error {\n override readonly name = 'ConfigCompileError';\n}\n\n/**\n * Compile the config: the bundle `compileBundle` would produce, with the\n * template manifest generated from the registry (every registered template,\n * by id, not a hand-kept list). The registry is checked first, used or not:\n * every entry is a template of its channel with an id, an id is registered\n * once per channel, and every email template's metadata is valid, as is the\n * config's `email` (which replaced `emails`: a config still carrying it fails).\n * Then every message node is checked against it — an\n * unregistered or cross-channel template fails with the workflow, node and\n * channel named, and so does a literal prop the template's schema refuses (a\n * missing required prop, an undeclared one, a wrong literal) — and every\n * definition a workflow sends must be the very object registered under its id,\n * not merely one with the same id. User-property references cannot be parsed\n * here (their values exist only at send time) — they are checked by name only,\n * and parsed before sending.\n */\nexport function compileConfig(config: JourneyConfig): BundleIR {\n let index: RegistryIndex;\n try {\n const email = indexTemplates('email', config.templates?.email);\n assertEmailConfig(config, email.values());\n index = {\n email,\n push: indexTemplates('push', config.templates?.push),\n discord: indexTemplates('discord', config.templates?.discord),\n slack: indexTemplates('slack', config.templates?.slack),\n };\n } catch (error) {\n throw new ConfigCompileError((error as Error).message);\n }\n const bundle = compileBundle({\n workflows: config.workflows,\n ...(config.segments === undefined ? {} : { segments: config.segments }),\n ...(config.actions === undefined ? {} : { actions: config.actions }),\n templates: TEMPLATE_CHANNELS.flatMap((channel) => [...index[channel].values()]),\n });\n bundle.workflows.forEach((workflow, i) => {\n checkMessages(workflow, workflow.flow, index, config);\n checkUses(workflow, config.workflows[i], index, config);\n });\n return bundle;\n}\n\nconst TEMPLATE_CHANNELS = ['email', 'push', 'discord', 'slack'] as const;\n\n/** The registry by id, per channel. */\ntype RegistryIndex = Record<TemplateChannel, ReadonlyMap<string, AnyTemplate>>;\n\n/** The channel's templates by id; undefined for a channel templates cannot be declared for. */\nfunction indexOf(\n index: RegistryIndex,\n channel: ChannelIR\n): ReadonlyMap<string, AnyTemplate> | undefined {\n return (TEMPLATE_CHANNELS as readonly ChannelIR[]).includes(channel)\n ? index[channel as TemplateChannel]\n : undefined;\n}\n\n/**\n * The definitions a workflow sends against the registered ones: same id is not enough — a\n * workflow typed by one definition and a registry holding another under that id would render\n * with a schema the call was never checked against.\n */\nfunction checkUses(\n workflow: WorkflowIR,\n builder: WorkflowBuilder,\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n for (const { template, loc } of templateUses(builder)) {\n const channel = CHANNEL_OF[template.kind];\n const registered = index[channel].get(template.id);\n if (registered === template) continue;\n const at = loc === undefined ? '' : ` (${loc.file}:${loc.line})`;\n const where = `workflow '${workflow.id}'${at}: ${channel} template '${template.id}'`;\n if (registered === undefined) throw new ConfigCompileError(`${where} is not registered`);\n const name = Object.entries(config.templates?.[channel] ?? {}).find(\n ([, entry]) => entry === registered\n )?.[0];\n throw new ConfigCompileError(\n `${where} is not the definition registered under that id (templates.${channel}.${name}) — two definitions share the id`\n );\n }\n}\n\n/** A field that may be left out: optional, or with a default the schema fills in. */\nfunction canBeOmitted(schema: core.$ZodType): boolean {\n const type = schema._zod.def.type;\n return type === 'optional' || type === 'default' || type === 'prefault';\n}\n\nfunction checkMessages(\n workflow: WorkflowIR,\n nodes: readonly NodeIR[],\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'message':\n checkMessage(workflow, node, index, config);\n break;\n case 'send_event':\n checkEmit(workflow, node, config);\n break;\n case 'branch':\n for (const c of node.cases) checkMessages(workflow, c.flow, index, config);\n if (node.otherwise) checkMessages(workflow, node.otherwise, index, config);\n break;\n case 'cohort':\n for (const arm of node.arms) checkMessages(workflow, arm.flow, index, config);\n break;\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) checkMessages(workflow, node.onTimeout, index, config);\n break;\n default:\n break;\n }\n }\n}\n\nfunction checkMessage(\n workflow: WorkflowIR,\n node: Extract<NodeIR, { type: 'message' }>,\n index: RegistryIndex,\n config: JourneyConfig\n): void {\n const where = `workflow '${workflow.id}' node ${node.id}`;\n const registry = indexOf(index, node.channel);\n // Channels without template definitions (sms, …) are rendered by app-supplied senders\n if (registry === undefined) return;\n const entry = registry.get(node.template);\n if (entry === undefined) {\n const other = TEMPLATE_CHANNELS.find(\n (channel) => channel !== node.channel && index[channel].has(node.template)\n );\n const hint = other === undefined ? '' : ` (it is registered as a ${other} template)`;\n throw new ConfigCompileError(\n `${where}: ${node.channel} template '${node.template}' is not registered${hint}`\n );\n }\n const shape = (entry.props._zod.def as core.$ZodObjectDef).shape;\n const problems: string[] = [];\n for (const [field, value] of Object.entries(node.props)) {\n if (!Object.hasOwn(shape, field)) {\n problems.push(`'${field}' is not a prop of the template`);\n continue;\n }\n if (typeof value === 'object' && 'type' in value) {\n // A user ref's value exists only at send time (parsed then); here its name can be checked\n const profile = config.contract?.schemas.profile._zod.def;\n if (\n profile !== undefined &&\n value.path !== 'userId' &&\n !Object.hasOwn(profile.shape, value.path)\n ) {\n problems.push(\n `'${field}' reads u.${value.path}, which the contract's profile does not declare`\n );\n }\n continue;\n }\n const result = z.safeParse(shape[field], value);\n if (!result.success)\n problems.push(`'${field}' = ${JSON.stringify(value)} is refused by the schema`);\n }\n for (const [field, schema] of Object.entries(shape)) {\n const optional = canBeOmitted(schema);\n if (!optional && !Object.hasOwn(node.props, field))\n problems.push(`required prop '${field}' is missing`);\n }\n if (problems.length > 0) {\n throw new ConfigCompileError(\n `${where}: ${node.channel} '${node.template}': ${problems.join('; ')}`\n );\n }\n}\n\n/**\n * An emitted event against the contract, as far as compile time can see: the\n * event is declared, literal payload values parse against their field, and no\n * required field is missing. Values read from the profile are checked when the\n * event is emitted, by the same contract.\n */\nfunction checkEmit(\n workflow: WorkflowIR,\n node: Extract<NodeIR, { type: 'send_event' }>,\n config: JourneyConfig\n): void {\n const contract = config.contract;\n if (contract === undefined) return;\n const where = `workflow '${workflow.id}' node ${node.id}`;\n if (!contract.isEvent(node.event)) {\n throw new ConfigCompileError(\n `${where}: emits '${String(node.event)}', which is not in the contract`\n );\n }\n const schema = contract.schemas.events[node.event];\n const shape = (schema._zod.def as core.$ZodObjectDef).shape;\n const problems: string[] = [];\n for (const [field, value] of Object.entries(node.payload)) {\n if (typeof value === 'object' && 'type' in value) continue; // read at emit time\n if (!Object.hasOwn(shape, field)) continue; // a loose payload may carry extra fields\n const result = z.safeParse(shape[field], value);\n if (!result.success)\n problems.push(`'${field}' = ${JSON.stringify(value)} is refused by the contract`);\n }\n for (const [field, fieldSchema] of Object.entries(shape)) {\n const optional = canBeOmitted(fieldSchema);\n if (!optional && !Object.hasOwn(node.payload, field))\n problems.push(`required field '${field}' is missing`);\n }\n if (problems.length > 0) {\n throw new ConfigCompileError(`${where}: event '${node.event}': ${problems.join('; ')}`);\n }\n}\n"],"mappings":"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"}
|
package/dist/contract.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.cjs","names":["event","user","z"],"sources":["../src/contract.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EventRefs, type UserRefs, event, user } from './dsl';\n\n/**\n * `defineContract({ events, profile })` — the producer/engine contract as Zod\n * schemas, written once in a package both sides import. From it:\n *\n * - `contract.events` / `contract.user` are the DSL's typed reference tables\n * (what `event<E>()` / `user<U>()` used to be, now derived from the schemas);\n * - `JourneyClient({ contract })` types `track` / `identify` by event name and\n * checks payloads before sending;\n * - `journeyWorker({ contract })` re-validates every inbound `/events` and\n * `/identify` — never trusting the caller used the SDK — and refuses a bad\n * one with a 4xx before anything is stored, woken or started.\n *\n * Supported schemas (v1), synchronous and JSON-shaped: event payloads are\n * object schemas (`z.looseObject` keeps a producer's extra analytics fields,\n * `z.strictObject` refuses them) and may nest objects and arrays; the profile\n * is a flat object of string / number / boolean / enum / literal fields. Input\n * and output must mean the same thing, so defaults, transforms, coercion,\n * custom refinements and async checks are refused when the contract is\n * defined. The Zod instances stay in the host; nothing here changes the IR.\n */\n\ntype ObjectSchema = core.$ZodObject;\n\n/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */\nexport type DeclaredOutput<S> =\n S extends core.$ZodObject<infer Shape>\n ? core.$InferObjectOutput<Shape, Record<never, never>>\n : never;\n\nexport interface ContractDefinition<\n E extends Record<string, ObjectSchema>,\n P extends ObjectSchema,\n> {\n events: E;\n profile: P;\n}\n\n/** One rejected field, with the dotted path the producer can fix. */\nexport interface ContractIssue {\n path: string;\n message: string;\n}\n\nexport type ContractResult<T> = { ok: true; data: T } | { ok: false; issues: ContractIssue[] };\n\nexport interface Contract<\n E extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,\n P extends ObjectSchema = ObjectSchema,\n> {\n readonly kind: 'journey-contract';\n /** The DSL's event reference table: `e.purchase` — payload refs from the declared fields only. */\n readonly events: EventRefs<{ [K in keyof E]: DeclaredOutput<E[K]> }>;\n /** The DSL's user-property reference table: `u.name`, plus the system `u.userId`. */\n readonly user: UserRefs<DeclaredOutput<P> & { userId: string }>;\n /** The raw schemas, for hosts that need them (validation, docs). */\n readonly schemas: { readonly events: E; readonly profile: P };\n /** Every event name the contract declares — what a producer forwards. */\n readonly eventNames: readonly (keyof E & string)[];\n isEvent(name: string): name is keyof E & string;\n /** Validate one event's payload; the parsed value is what may be stored. */\n parseEvent<K extends keyof E & string>(\n name: K,\n payload: unknown\n ): ContractResult<core.output<E[K]>>;\n /**\n * Validate an `/identify` merge patch: omitted fields stay as they are,\n * `null` removes a field, every provided value is checked against its field\n * schema, and an undeclared field is refused — a patch, never a full parse\n * that would fill defaults over fields the caller did not send.\n */\n parseProfilePatch(patch: unknown): ContractResult<ProfilePatch<P>>;\n}\n\n/** An `/identify` patch over the profile: any subset of fields, `null` to remove one. */\nexport type ProfilePatch<P> = { [K in keyof DeclaredOutput<P>]?: DeclaredOutput<P>[K] | null };\n\n/** Payload type of one event, for producers: `EventPayload<typeof contract, 'purchase'>`. */\nexport type EventPayload<C, K extends string> =\n C extends Contract<infer E> ? (K extends keyof E ? core.input<E[K]> : never) : never;\n\nexport function defineContract<E extends Record<string, ObjectSchema>, P extends ObjectSchema>(\n definition: ContractDefinition<E, P>\n): Contract<E, P> {\n for (const [name, schema] of Object.entries(definition.events)) {\n if (name.startsWith('$')) {\n throw new TypeError(`contract event '${name}': names starting with $ are reserved`);\n }\n assertJsonSchema(schema, `event '${name}'`, true);\n }\n assertJsonSchema(definition.profile, 'profile', true);\n const profileShape = (definition.profile._zod.def as core.$ZodObjectDef).shape;\n for (const [field, schema] of Object.entries(profileShape)) {\n assertFlatScalar(schema, `profile.${field}`);\n }\n const strictProfile = definition.profile._zod.def.catchall?._zod.def.type === 'never';\n\n const names = Object.keys(definition.events) as (keyof E & string)[];\n return {\n kind: 'journey-contract',\n events: event() as never,\n user: user() as never,\n schemas: { events: definition.events, profile: definition.profile },\n eventNames: names,\n isEvent: (name): name is keyof E & string => Object.hasOwn(definition.events, name),\n parseEvent(name, payload) {\n if (!Object.hasOwn(definition.events, name)) {\n return { ok: false, issues: [{ path: '', message: `unknown event '${name}'` }] };\n }\n return toResult(z.safeParse(definition.events[name], payload));\n },\n parseProfilePatch(patch) {\n if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {\n return { ok: false, issues: [{ path: '', message: 'props must be an object' }] };\n }\n const issues: ContractIssue[] = [];\n const data: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(patch)) {\n if (!Object.hasOwn(profileShape, field)) {\n // userId is the request's identity, not a profile field\n if (strictProfile || field === 'userId') {\n issues.push({ path: field, message: 'not a declared profile field' });\n } else {\n data[field] = value;\n }\n continue;\n }\n if (value === null) {\n data[field] = null;\n continue;\n }\n const result = z.safeParse(profileShape[field], value);\n if (result.success) data[field] = result.data;\n else issues.push(...prefixed(field, result.error));\n }\n return issues.length > 0 ? { ok: false, issues } : { ok: true, data: data as never };\n },\n };\n}\n\n/**\n * An event the contract refuses — raised for events a journey emits itself\n * (`.sendEvent()`), which never pass the HTTP ingest: they go through the same\n * check, and a refusal is deterministic, so it is not retried.\n */\nexport class ContractValidationError extends Error {\n override readonly name = 'ContractValidationError';\n constructor(\n readonly event: string,\n readonly issues: ContractIssue[]\n ) {\n super(\n `event '${event}' refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\n/** The one check every event passes, from HTTP or from a journey: declared, and its payload parses. */\nexport function checkEvent(\n contract: Contract,\n name: string,\n payload: unknown\n): ContractResult<Record<string, unknown>> {\n if (!contract.isEvent(name)) {\n return {\n ok: false,\n issues: [{ path: '', message: `'${String(name)}' is not in the contract` }],\n };\n }\n return contract.parseEvent(name, payload);\n}\n\nexport function isContract(value: unknown): value is Contract {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-contract'\n );\n}\n\nfunction toResult<T>(result: core.util.SafeParseResult<T>): ContractResult<T> {\n return result.success\n ? { ok: true, data: result.data }\n : { ok: false, issues: prefixed('', result.error) };\n}\n\nfunction prefixed(prefix: string, error: core.$ZodError): ContractIssue[] {\n return error.issues.map((issue) => ({\n path: [prefix, ...issue.path.map(String)].filter((p) => p !== '').join('.'),\n message: issue.message,\n }));\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\n/**\n * The schema types a contract may use — JSON shapes that parse synchronously\n * and mean the same before and after parsing. Anything not listed (default,\n * transform, pipe, lazy, promise, custom, date, bigint, …) is refused, and\n * every container is walked so nothing unsupported hides inside one.\n */\nconst ALLOWED = new Set([\n 'object',\n 'array',\n 'tuple',\n 'union',\n 'intersection',\n 'record',\n 'optional',\n 'nullable',\n 'readonly',\n 'nonoptional',\n 'string',\n 'number',\n 'boolean',\n 'null',\n 'enum',\n 'literal',\n 'unknown',\n 'any',\n 'never',\n]);\n\n/** Checks that change the value (`overwrite`: `.trim()`, `.toLowerCase()`, …) or run arbitrary code (`custom`: `.refine()`, async or not). */\nconst REFUSED_CHECKS = new Set(['custom', 'overwrite']);\n\nfunction assertJsonSchema(schema: unknown, where: string, top = false): void {\n const def = (schema as core.$ZodType | undefined)?._zod?.def;\n if (def === undefined) throw new TypeError(`contract ${where}: not a Zod schema`);\n if (top && def.type !== 'object') {\n throw new TypeError(\n `contract ${where}: must be an object schema (z.looseObject / z.strictObject)`\n );\n }\n if (!ALLOWED.has(def.type)) {\n throw new TypeError(\n `contract ${where}: ${def.type} schemas are not supported (input and output must mean the same JSON, parsed synchronously)`\n );\n }\n if ((def as { coerce?: boolean }).coerce === true) {\n throw new TypeError(`contract ${where}: coercion (z.coerce) is not supported`);\n }\n for (const check of def.checks ?? []) {\n if (REFUSED_CHECKS.has(check._zod.def.check)) {\n throw new TypeError(\n `contract ${where}: ${check._zod.def.check === 'overwrite' ? 'value-changing checks (.overwrite, .trim, …)' : 'custom refinements'} are not supported`\n );\n }\n }\n const children: [string, unknown][] = [];\n switch (def.type) {\n case 'object': {\n const object = def as core.$ZodObjectDef;\n for (const [k, v] of Object.entries(object.shape)) children.push([`${where}.${k}`, v]);\n if (object.catchall !== undefined) children.push([`${where}.*`, object.catchall]);\n break;\n }\n case 'optional':\n case 'nullable':\n case 'readonly':\n case 'nonoptional':\n children.push([where, (def as unknown as { innerType: unknown }).innerType]);\n break;\n case 'array':\n children.push([`${where}[]`, (def as core.$ZodArrayDef).element]);\n break;\n case 'tuple': {\n const tuple = def as core.$ZodTupleDef;\n tuple.items.forEach((item, index) => children.push([`${where}[${index}]`, item]));\n if (tuple.rest !== null) children.push([`${where}[…]`, tuple.rest]);\n break;\n }\n case 'union':\n for (const option of (def as core.$ZodUnionDef).options) children.push([where, option]);\n break;\n case 'intersection': {\n const intersection = def as core.$ZodIntersectionDef;\n children.push([where, intersection.left], [where, intersection.right]);\n break;\n }\n case 'record': {\n const record = def as core.$ZodRecordDef;\n children.push([`${where}{key}`, record.keyType], [`${where}{}`, record.valueType]);\n break;\n }\n default:\n break;\n }\n for (const [path, child] of children) assertJsonSchema(child, path);\n}\n\nconst SCALARS = new Set(['string', 'number', 'boolean', 'enum', 'literal']);\n\nfunction assertFlatScalar(schema: unknown, where: string): void {\n let def = (schema as core.$ZodType)._zod.def;\n if (def.type === 'optional') def = (def as core.$ZodOptionalDef).innerType._zod.def;\n if (!SCALARS.has(def.type)) {\n throw new TypeError(\n `contract ${where}: profile fields must be flat scalars (string, number, boolean, enum, literal), got ${def.type}`\n );\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contract.cjs","names":["event","user","z"],"sources":["../src/contract.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EventRefs, type UserRefs, event, user } from './dsl';\n\n/**\n * `defineContract({ events, profile })` — the producer/engine contract as Zod\n * schemas, written once in a package both sides import. From it:\n *\n * - `contract.events` / `contract.user` are the DSL's typed reference tables\n * (what `event<E>()` / `user<U>()` used to be, now derived from the schemas);\n * - `JourneyClient({ contract })` types `track` / `identify` by event name and\n * checks payloads before sending;\n * - `journeyWorker({ contract })` re-validates every inbound `/events` and\n * `/identify` — never trusting the caller used the SDK — and refuses a bad\n * one with a 4xx before anything is stored, woken or started.\n *\n * Supported schemas (v1), synchronous and JSON-shaped: event payloads are\n * object schemas (`z.looseObject` keeps a producer's extra analytics fields,\n * `z.strictObject` refuses them) and may nest objects and arrays; the profile\n * is a flat object of string / number / boolean / enum / literal fields. Input\n * and output must mean the same thing, so defaults, transforms, coercion,\n * custom refinements and async checks are refused when the contract is\n * defined. The Zod instances stay in the host; nothing here changes the IR.\n */\n\ntype ObjectSchema = core.$ZodObject;\n\n/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */\nexport type DeclaredOutput<S> =\n S extends core.$ZodObject<infer Shape>\n ? core.$InferObjectOutput<Shape, core.$strict['out']>\n : never;\n\nexport interface ContractDefinition<\n E extends Record<string, ObjectSchema>,\n P extends ObjectSchema,\n> {\n events: E;\n profile: P;\n}\n\n/** One rejected field, with the dotted path the producer can fix. */\nexport interface ContractIssue {\n path: string;\n message: string;\n}\n\nexport type ContractResult<T> = { ok: true; data: T } | { ok: false; issues: ContractIssue[] };\n\nexport interface Contract<\n E extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,\n P extends ObjectSchema = ObjectSchema,\n> {\n readonly kind: 'journey-contract';\n /** The DSL's event reference table: `e.purchase` — payload refs from the declared fields only. */\n readonly events: EventRefs<{ [K in keyof E]: DeclaredOutput<E[K]> }>;\n /** The DSL's user-property reference table: `u.name`, plus the system `u.userId`. */\n readonly user: UserRefs<DeclaredOutput<P> & { userId: string }>;\n /** The raw schemas, for hosts that need them (validation, docs). */\n readonly schemas: { readonly events: E; readonly profile: P };\n /** Every event name the contract declares — what a producer forwards. */\n readonly eventNames: readonly (keyof E & string)[];\n isEvent(name: string): name is keyof E & string;\n /** Validate one event's payload; the parsed value is what may be stored. */\n parseEvent<K extends keyof E & string>(\n name: K,\n payload: unknown\n ): ContractResult<core.output<E[K]>>;\n /**\n * Validate an `/identify` merge patch: omitted fields stay as they are,\n * `null` removes a field, every provided value is checked against its field\n * schema, and an undeclared field is refused — a patch, never a full parse\n * that would fill defaults over fields the caller did not send.\n */\n parseProfilePatch(patch: unknown): ContractResult<ProfilePatch<P>>;\n}\n\n/** An `/identify` patch over the profile: any subset of fields, `null` to remove one. */\nexport type ProfilePatch<P> = { [K in keyof DeclaredOutput<P>]?: DeclaredOutput<P>[K] | null };\n\n/** Payload type of one event, for producers: `EventPayload<typeof contract, 'purchase'>`. */\nexport type EventPayload<C, K extends string> =\n C extends Contract<infer E> ? (K extends keyof E ? core.input<E[K]> : never) : never;\n\nexport function defineContract<E extends Record<string, ObjectSchema>, P extends ObjectSchema>(\n definition: ContractDefinition<E, P>\n): Contract<E, P> {\n for (const [name, schema] of Object.entries(definition.events)) {\n if (name.startsWith('$')) {\n throw new TypeError(`contract event '${name}': names starting with $ are reserved`);\n }\n assertJsonSchema(schema, `event '${name}'`, true);\n }\n assertJsonSchema(definition.profile, 'profile', true);\n const profileShape = (definition.profile._zod.def as core.$ZodObjectDef).shape;\n for (const [field, schema] of Object.entries(profileShape)) {\n assertFlatScalar(schema, `profile.${field}`);\n }\n const strictProfile = definition.profile._zod.def.catchall?._zod.def.type === 'never';\n\n const names = Object.keys(definition.events) as (keyof E & string)[];\n return {\n kind: 'journey-contract',\n events: event() as never,\n user: user() as never,\n schemas: { events: definition.events, profile: definition.profile },\n eventNames: names,\n isEvent: (name): name is keyof E & string => Object.hasOwn(definition.events, name),\n parseEvent(name, payload) {\n if (!Object.hasOwn(definition.events, name)) {\n return { ok: false, issues: [{ path: '', message: `unknown event '${name}'` }] };\n }\n return toResult(z.safeParse(definition.events[name], payload));\n },\n parseProfilePatch(patch) {\n if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {\n return { ok: false, issues: [{ path: '', message: 'props must be an object' }] };\n }\n const issues: ContractIssue[] = [];\n const data: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(patch)) {\n if (!Object.hasOwn(profileShape, field)) {\n // userId is the request's identity, not a profile field\n if (strictProfile || field === 'userId') {\n issues.push({ path: field, message: 'not a declared profile field' });\n } else {\n data[field] = value;\n }\n continue;\n }\n if (value === null) {\n data[field] = null;\n continue;\n }\n const result = z.safeParse(profileShape[field], value);\n if (result.success) data[field] = result.data;\n else issues.push(...prefixed(field, result.error));\n }\n return issues.length > 0 ? { ok: false, issues } : { ok: true, data: data as never };\n },\n };\n}\n\n/**\n * An event the contract refuses — raised for events a journey emits itself\n * (`.sendEvent()`), which never pass the HTTP ingest: they go through the same\n * check, and a refusal is deterministic, so it is not retried.\n */\nexport class ContractValidationError extends Error {\n override readonly name = 'ContractValidationError';\n constructor(\n readonly event: string,\n readonly issues: ContractIssue[]\n ) {\n super(\n `event '${event}' refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\n/** The one check every event passes, from HTTP or from a journey: declared, and its payload parses. */\nexport function checkEvent(\n contract: Contract,\n name: string,\n payload: unknown\n): ContractResult<Record<string, unknown>> {\n if (!contract.isEvent(name)) {\n return {\n ok: false,\n issues: [{ path: '', message: `'${String(name)}' is not in the contract` }],\n };\n }\n return contract.parseEvent(name, payload);\n}\n\nexport function isContract(value: unknown): value is Contract {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-contract'\n );\n}\n\nfunction toResult<T>(result: core.util.SafeParseResult<T>): ContractResult<T> {\n return result.success\n ? { ok: true, data: result.data }\n : { ok: false, issues: prefixed('', result.error) };\n}\n\nfunction prefixed(prefix: string, error: core.$ZodError): ContractIssue[] {\n return error.issues.map((issue) => ({\n path: [prefix, ...issue.path.map(String)].filter((p) => p !== '').join('.'),\n message: issue.message,\n }));\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\n/**\n * The schema types a contract may use — JSON shapes that parse synchronously\n * and mean the same before and after parsing. Anything not listed (default,\n * transform, pipe, lazy, promise, custom, date, bigint, …) is refused, and\n * every container is walked so nothing unsupported hides inside one.\n */\nconst ALLOWED = new Set([\n 'object',\n 'array',\n 'tuple',\n 'union',\n 'intersection',\n 'record',\n 'optional',\n 'nullable',\n 'readonly',\n 'nonoptional',\n 'string',\n 'number',\n 'boolean',\n 'null',\n 'enum',\n 'literal',\n 'unknown',\n 'any',\n 'never',\n]);\n\n/** Checks that change the value (`overwrite`: `.trim()`, `.toLowerCase()`, …) or run arbitrary code (`custom`: `.refine()`, async or not). */\nconst REFUSED_CHECKS = new Set(['custom', 'overwrite']);\n\nfunction assertJsonSchema(schema: unknown, where: string, top = false): void {\n const def = (schema as { _zod?: { def?: core.$ZodTypeDef } } | null | undefined)?._zod?.def;\n if (def === undefined) throw new TypeError(`contract ${where}: not a Zod schema`);\n if (top && def.type !== 'object') {\n throw new TypeError(\n `contract ${where}: must be an object schema (z.looseObject / z.strictObject)`\n );\n }\n if (!ALLOWED.has(def.type)) {\n throw new TypeError(\n `contract ${where}: ${def.type} schemas are not supported (input and output must mean the same JSON, parsed synchronously)`\n );\n }\n if ((def as { coerce?: boolean }).coerce === true) {\n throw new TypeError(`contract ${where}: coercion (z.coerce) is not supported`);\n }\n for (const check of def.checks ?? []) {\n if (REFUSED_CHECKS.has(check._zod.def.check)) {\n throw new TypeError(\n `contract ${where}: ${check._zod.def.check === 'overwrite' ? 'value-changing checks (.overwrite, .trim, …)' : 'custom refinements'} are not supported`\n );\n }\n }\n const children: [string, unknown][] = [];\n switch (def.type) {\n case 'object': {\n const object = def as core.$ZodObjectDef;\n for (const [k, v] of Object.entries(object.shape)) children.push([`${where}.${k}`, v]);\n if (object.catchall !== undefined) children.push([`${where}.*`, object.catchall]);\n break;\n }\n case 'optional':\n case 'nullable':\n case 'readonly':\n case 'nonoptional':\n children.push([where, (def as unknown as { innerType: unknown }).innerType]);\n break;\n case 'array':\n children.push([`${where}[]`, (def as core.$ZodArrayDef).element]);\n break;\n case 'tuple': {\n const tuple = def as core.$ZodTupleDef;\n tuple.items.forEach((item, index) => children.push([`${where}[${index}]`, item]));\n if (tuple.rest !== null) children.push([`${where}[…]`, tuple.rest]);\n break;\n }\n case 'union':\n for (const option of (def as core.$ZodUnionDef).options) children.push([where, option]);\n break;\n case 'intersection': {\n const intersection = def as core.$ZodIntersectionDef;\n children.push([where, intersection.left], [where, intersection.right]);\n break;\n }\n case 'record': {\n const record = def as core.$ZodRecordDef;\n children.push([`${where}{key}`, record.keyType], [`${where}{}`, record.valueType]);\n break;\n }\n default:\n break;\n }\n for (const [path, child] of children) assertJsonSchema(child, path);\n}\n\nconst SCALARS = new Set(['string', 'number', 'boolean', 'enum', 'literal']);\n\nfunction assertFlatScalar(schema: unknown, where: string): void {\n let def = (schema as core.$ZodType)._zod.def;\n if (def.type === 'optional') def = (def as core.$ZodOptionalDef).innerType._zod.def;\n if (!SCALARS.has(def.type)) {\n throw new TypeError(\n `contract ${where}: profile fields must be flat scalars (string, number, boolean, enum, literal), got ${def.type}`\n );\n }\n}\n"],"mappings":"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"}
|
package/dist/contract.d.cts
CHANGED
|
@@ -24,7 +24,7 @@ import * as core from "zod/v4/core";
|
|
|
24
24
|
*/
|
|
25
25
|
type ObjectSchema = core.$ZodObject;
|
|
26
26
|
/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */
|
|
27
|
-
export type DeclaredOutput<S> = S extends core.$ZodObject<infer Shape> ? core.$InferObjectOutput<Shape,
|
|
27
|
+
export type DeclaredOutput<S> = S extends core.$ZodObject<infer Shape> ? core.$InferObjectOutput<Shape, core.$strict['out']> : never;
|
|
28
28
|
export interface ContractDefinition<E extends Record<string, ObjectSchema>, P extends ObjectSchema> {
|
|
29
29
|
events: E;
|
|
30
30
|
profile: P;
|
package/dist/contract.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.cts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBK,eAAe,KAAK;;YAGb,eAAe,KACzB,UAAU,KAAK,iBAAiB,SAC5B,KAAK,mBAAmB,OAAO;
|
|
1
|
+
{"version":3,"file":"contract.d.cts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBK,eAAe,KAAK;;YAGb,eAAe,KACzB,UAAU,KAAK,iBAAiB,SAC5B,KAAK,mBAAmB,OAAO,KAAK;iBAGzB,mBACf,UAAU,eAAe,eACzB,UAAU;EAEV,QAAQ;EACR,SAAS;;;iBAIM;EACf;EACA;;YAGU,eAAe;EAAO;EAAU,MAAM;;EAAQ;EAAW,QAAQ;;iBAE5D,SACf,UAAU,eAAe,gBAAgB,eAAe,eACxD,UAAU,eAAe;WAEhB;;WAEA,QAAQ,aAAa,WAAW,IAAI,eAAe,EAAE;;WAErD,MAAM,SAAS,eAAe;IAAO;;;WAErC;aAAoB,QAAQ;aAAY,SAAS;;;WAEjD,4BAA4B;EACrC,QAAQ,eAAe,cAAc;;EAErC,WAAW,gBAAgB,YACzB,MAAM,GACN,mBACC,eAAe,KAAK,OAAO,EAAE;;;;;;;EAOhC,kBAAkB,iBAAiB,eAAe,aAAa;;;YAIrD,aAAa,QAAQ,WAAW,eAAe,MAAM,eAAe,GAAG;;YAGvE,aAAa,GAAG,oBAC1B,UAAU,eAAe,MAAM,gBAAgB,IAAI,KAAK,MAAM,EAAE;wBAElD,eAAe,UAAU,eAAe,eAAe,UAAU,cAC/E,YAAY,mBAAmB,GAAG,KACjC,SAAS,GAAG;;;;;;qBA8DF,gCAAgC;WAGhC;WACA,QAAQ;WAHD;EAClB,YACW,eACA,QAAQ;;;wBAWL,WACd,UAAU,UACV,cACA,mBACC,eAAe;wBAUF,WAAW,iBAAiB,SAAS"}
|
package/dist/contract.d.mts
CHANGED
|
@@ -24,7 +24,7 @@ import * as core from "zod/v4/core";
|
|
|
24
24
|
*/
|
|
25
25
|
type ObjectSchema = core.$ZodObject;
|
|
26
26
|
/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */
|
|
27
|
-
export type DeclaredOutput<S> = S extends core.$ZodObject<infer Shape> ? core.$InferObjectOutput<Shape,
|
|
27
|
+
export type DeclaredOutput<S> = S extends core.$ZodObject<infer Shape> ? core.$InferObjectOutput<Shape, core.$strict['out']> : never;
|
|
28
28
|
export interface ContractDefinition<E extends Record<string, ObjectSchema>, P extends ObjectSchema> {
|
|
29
29
|
events: E;
|
|
30
30
|
profile: P;
|
package/dist/contract.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.mts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBK,eAAe,KAAK;;YAGb,eAAe,KACzB,UAAU,KAAK,iBAAiB,SAC5B,KAAK,mBAAmB,OAAO;
|
|
1
|
+
{"version":3,"file":"contract.d.mts","names":[],"sources":["../src/contract.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;KAyBK,eAAe,KAAK;;YAGb,eAAe,KACzB,UAAU,KAAK,iBAAiB,SAC5B,KAAK,mBAAmB,OAAO,KAAK;iBAGzB,mBACf,UAAU,eAAe,eACzB,UAAU;EAEV,QAAQ;EACR,SAAS;;;iBAIM;EACf;EACA;;YAGU,eAAe;EAAO;EAAU,MAAM;;EAAQ;EAAW,QAAQ;;iBAE5D,SACf,UAAU,eAAe,gBAAgB,eAAe,eACxD,UAAU,eAAe;WAEhB;;WAEA,QAAQ,aAAa,WAAW,IAAI,eAAe,EAAE;;WAErD,MAAM,SAAS,eAAe;IAAO;;;WAErC;aAAoB,QAAQ;aAAY,SAAS;;;WAEjD,4BAA4B;EACrC,QAAQ,eAAe,cAAc;;EAErC,WAAW,gBAAgB,YACzB,MAAM,GACN,mBACC,eAAe,KAAK,OAAO,EAAE;;;;;;;EAOhC,kBAAkB,iBAAiB,eAAe,aAAa;;;YAIrD,aAAa,QAAQ,WAAW,eAAe,MAAM,eAAe,GAAG;;YAGvE,aAAa,GAAG,oBAC1B,UAAU,eAAe,MAAM,gBAAgB,IAAI,KAAK,MAAM,EAAE;wBAElD,eAAe,UAAU,eAAe,eAAe,UAAU,cAC/E,YAAY,mBAAmB,GAAG,KACjC,SAAS,GAAG;;;;;;qBA8DF,gCAAgC;WAGhC;WACA,QAAQ;WAHD;EAClB,YACW,eACA,QAAQ;;;wBAWL,WACd,UAAU,UACV,cACA,mBACC,eAAe;wBAUF,WAAW,iBAAiB,SAAS"}
|
package/dist/contract.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.mjs","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EventRefs, type UserRefs, event, user } from './dsl';\n\n/**\n * `defineContract({ events, profile })` — the producer/engine contract as Zod\n * schemas, written once in a package both sides import. From it:\n *\n * - `contract.events` / `contract.user` are the DSL's typed reference tables\n * (what `event<E>()` / `user<U>()` used to be, now derived from the schemas);\n * - `JourneyClient({ contract })` types `track` / `identify` by event name and\n * checks payloads before sending;\n * - `journeyWorker({ contract })` re-validates every inbound `/events` and\n * `/identify` — never trusting the caller used the SDK — and refuses a bad\n * one with a 4xx before anything is stored, woken or started.\n *\n * Supported schemas (v1), synchronous and JSON-shaped: event payloads are\n * object schemas (`z.looseObject` keeps a producer's extra analytics fields,\n * `z.strictObject` refuses them) and may nest objects and arrays; the profile\n * is a flat object of string / number / boolean / enum / literal fields. Input\n * and output must mean the same thing, so defaults, transforms, coercion,\n * custom refinements and async checks are refused when the contract is\n * defined. The Zod instances stay in the host; nothing here changes the IR.\n */\n\ntype ObjectSchema = core.$ZodObject;\n\n/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */\nexport type DeclaredOutput<S> =\n S extends core.$ZodObject<infer Shape>\n ? core.$InferObjectOutput<Shape, Record<never, never>>\n : never;\n\nexport interface ContractDefinition<\n E extends Record<string, ObjectSchema>,\n P extends ObjectSchema,\n> {\n events: E;\n profile: P;\n}\n\n/** One rejected field, with the dotted path the producer can fix. */\nexport interface ContractIssue {\n path: string;\n message: string;\n}\n\nexport type ContractResult<T> = { ok: true; data: T } | { ok: false; issues: ContractIssue[] };\n\nexport interface Contract<\n E extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,\n P extends ObjectSchema = ObjectSchema,\n> {\n readonly kind: 'journey-contract';\n /** The DSL's event reference table: `e.purchase` — payload refs from the declared fields only. */\n readonly events: EventRefs<{ [K in keyof E]: DeclaredOutput<E[K]> }>;\n /** The DSL's user-property reference table: `u.name`, plus the system `u.userId`. */\n readonly user: UserRefs<DeclaredOutput<P> & { userId: string }>;\n /** The raw schemas, for hosts that need them (validation, docs). */\n readonly schemas: { readonly events: E; readonly profile: P };\n /** Every event name the contract declares — what a producer forwards. */\n readonly eventNames: readonly (keyof E & string)[];\n isEvent(name: string): name is keyof E & string;\n /** Validate one event's payload; the parsed value is what may be stored. */\n parseEvent<K extends keyof E & string>(\n name: K,\n payload: unknown\n ): ContractResult<core.output<E[K]>>;\n /**\n * Validate an `/identify` merge patch: omitted fields stay as they are,\n * `null` removes a field, every provided value is checked against its field\n * schema, and an undeclared field is refused — a patch, never a full parse\n * that would fill defaults over fields the caller did not send.\n */\n parseProfilePatch(patch: unknown): ContractResult<ProfilePatch<P>>;\n}\n\n/** An `/identify` patch over the profile: any subset of fields, `null` to remove one. */\nexport type ProfilePatch<P> = { [K in keyof DeclaredOutput<P>]?: DeclaredOutput<P>[K] | null };\n\n/** Payload type of one event, for producers: `EventPayload<typeof contract, 'purchase'>`. */\nexport type EventPayload<C, K extends string> =\n C extends Contract<infer E> ? (K extends keyof E ? core.input<E[K]> : never) : never;\n\nexport function defineContract<E extends Record<string, ObjectSchema>, P extends ObjectSchema>(\n definition: ContractDefinition<E, P>\n): Contract<E, P> {\n for (const [name, schema] of Object.entries(definition.events)) {\n if (name.startsWith('$')) {\n throw new TypeError(`contract event '${name}': names starting with $ are reserved`);\n }\n assertJsonSchema(schema, `event '${name}'`, true);\n }\n assertJsonSchema(definition.profile, 'profile', true);\n const profileShape = (definition.profile._zod.def as core.$ZodObjectDef).shape;\n for (const [field, schema] of Object.entries(profileShape)) {\n assertFlatScalar(schema, `profile.${field}`);\n }\n const strictProfile = definition.profile._zod.def.catchall?._zod.def.type === 'never';\n\n const names = Object.keys(definition.events) as (keyof E & string)[];\n return {\n kind: 'journey-contract',\n events: event() as never,\n user: user() as never,\n schemas: { events: definition.events, profile: definition.profile },\n eventNames: names,\n isEvent: (name): name is keyof E & string => Object.hasOwn(definition.events, name),\n parseEvent(name, payload) {\n if (!Object.hasOwn(definition.events, name)) {\n return { ok: false, issues: [{ path: '', message: `unknown event '${name}'` }] };\n }\n return toResult(z.safeParse(definition.events[name], payload));\n },\n parseProfilePatch(patch) {\n if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {\n return { ok: false, issues: [{ path: '', message: 'props must be an object' }] };\n }\n const issues: ContractIssue[] = [];\n const data: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(patch)) {\n if (!Object.hasOwn(profileShape, field)) {\n // userId is the request's identity, not a profile field\n if (strictProfile || field === 'userId') {\n issues.push({ path: field, message: 'not a declared profile field' });\n } else {\n data[field] = value;\n }\n continue;\n }\n if (value === null) {\n data[field] = null;\n continue;\n }\n const result = z.safeParse(profileShape[field], value);\n if (result.success) data[field] = result.data;\n else issues.push(...prefixed(field, result.error));\n }\n return issues.length > 0 ? { ok: false, issues } : { ok: true, data: data as never };\n },\n };\n}\n\n/**\n * An event the contract refuses — raised for events a journey emits itself\n * (`.sendEvent()`), which never pass the HTTP ingest: they go through the same\n * check, and a refusal is deterministic, so it is not retried.\n */\nexport class ContractValidationError extends Error {\n override readonly name = 'ContractValidationError';\n constructor(\n readonly event: string,\n readonly issues: ContractIssue[]\n ) {\n super(\n `event '${event}' refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\n/** The one check every event passes, from HTTP or from a journey: declared, and its payload parses. */\nexport function checkEvent(\n contract: Contract,\n name: string,\n payload: unknown\n): ContractResult<Record<string, unknown>> {\n if (!contract.isEvent(name)) {\n return {\n ok: false,\n issues: [{ path: '', message: `'${String(name)}' is not in the contract` }],\n };\n }\n return contract.parseEvent(name, payload);\n}\n\nexport function isContract(value: unknown): value is Contract {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-contract'\n );\n}\n\nfunction toResult<T>(result: core.util.SafeParseResult<T>): ContractResult<T> {\n return result.success\n ? { ok: true, data: result.data }\n : { ok: false, issues: prefixed('', result.error) };\n}\n\nfunction prefixed(prefix: string, error: core.$ZodError): ContractIssue[] {\n return error.issues.map((issue) => ({\n path: [prefix, ...issue.path.map(String)].filter((p) => p !== '').join('.'),\n message: issue.message,\n }));\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\n/**\n * The schema types a contract may use — JSON shapes that parse synchronously\n * and mean the same before and after parsing. Anything not listed (default,\n * transform, pipe, lazy, promise, custom, date, bigint, …) is refused, and\n * every container is walked so nothing unsupported hides inside one.\n */\nconst ALLOWED = new Set([\n 'object',\n 'array',\n 'tuple',\n 'union',\n 'intersection',\n 'record',\n 'optional',\n 'nullable',\n 'readonly',\n 'nonoptional',\n 'string',\n 'number',\n 'boolean',\n 'null',\n 'enum',\n 'literal',\n 'unknown',\n 'any',\n 'never',\n]);\n\n/** Checks that change the value (`overwrite`: `.trim()`, `.toLowerCase()`, …) or run arbitrary code (`custom`: `.refine()`, async or not). */\nconst REFUSED_CHECKS = new Set(['custom', 'overwrite']);\n\nfunction assertJsonSchema(schema: unknown, where: string, top = false): void {\n const def = (schema as core.$ZodType | undefined)?._zod?.def;\n if (def === undefined) throw new TypeError(`contract ${where}: not a Zod schema`);\n if (top && def.type !== 'object') {\n throw new TypeError(\n `contract ${where}: must be an object schema (z.looseObject / z.strictObject)`\n );\n }\n if (!ALLOWED.has(def.type)) {\n throw new TypeError(\n `contract ${where}: ${def.type} schemas are not supported (input and output must mean the same JSON, parsed synchronously)`\n );\n }\n if ((def as { coerce?: boolean }).coerce === true) {\n throw new TypeError(`contract ${where}: coercion (z.coerce) is not supported`);\n }\n for (const check of def.checks ?? []) {\n if (REFUSED_CHECKS.has(check._zod.def.check)) {\n throw new TypeError(\n `contract ${where}: ${check._zod.def.check === 'overwrite' ? 'value-changing checks (.overwrite, .trim, …)' : 'custom refinements'} are not supported`\n );\n }\n }\n const children: [string, unknown][] = [];\n switch (def.type) {\n case 'object': {\n const object = def as core.$ZodObjectDef;\n for (const [k, v] of Object.entries(object.shape)) children.push([`${where}.${k}`, v]);\n if (object.catchall !== undefined) children.push([`${where}.*`, object.catchall]);\n break;\n }\n case 'optional':\n case 'nullable':\n case 'readonly':\n case 'nonoptional':\n children.push([where, (def as unknown as { innerType: unknown }).innerType]);\n break;\n case 'array':\n children.push([`${where}[]`, (def as core.$ZodArrayDef).element]);\n break;\n case 'tuple': {\n const tuple = def as core.$ZodTupleDef;\n tuple.items.forEach((item, index) => children.push([`${where}[${index}]`, item]));\n if (tuple.rest !== null) children.push([`${where}[…]`, tuple.rest]);\n break;\n }\n case 'union':\n for (const option of (def as core.$ZodUnionDef).options) children.push([where, option]);\n break;\n case 'intersection': {\n const intersection = def as core.$ZodIntersectionDef;\n children.push([where, intersection.left], [where, intersection.right]);\n break;\n }\n case 'record': {\n const record = def as core.$ZodRecordDef;\n children.push([`${where}{key}`, record.keyType], [`${where}{}`, record.valueType]);\n break;\n }\n default:\n break;\n }\n for (const [path, child] of children) assertJsonSchema(child, path);\n}\n\nconst SCALARS = new Set(['string', 'number', 'boolean', 'enum', 'literal']);\n\nfunction assertFlatScalar(schema: unknown, where: string): void {\n let def = (schema as core.$ZodType)._zod.def;\n if (def.type === 'optional') def = (def as core.$ZodOptionalDef).innerType._zod.def;\n if (!SCALARS.has(def.type)) {\n throw new TypeError(\n `contract ${where}: profile fields must be flat scalars (string, number, boolean, enum, literal), got ${def.type}`\n );\n }\n}\n"],"mappings":";;;;AAoFA,SAAgB,eACd,YACgB;CAChB,KAAK,MAAM,CAAC,MAAM,WAAW,OAAO,QAAQ,WAAW,MAAM,GAAG;EAC9D,IAAI,KAAK,WAAW,GAAG,GACrB,MAAM,IAAI,UAAU,mBAAmB,KAAK,sCAAsC;EAEpF,iBAAiB,QAAQ,UAAU,KAAK,IAAI,IAAI;CAClD;CACA,iBAAiB,WAAW,SAAS,WAAW,IAAI;CACpD,MAAM,eAAgB,WAAW,QAAQ,KAAK,IAA2B;CACzE,KAAK,MAAM,CAAC,OAAO,WAAW,OAAO,QAAQ,YAAY,GACvD,iBAAiB,QAAQ,WAAW,OAAO;CAE7C,MAAM,gBAAgB,WAAW,QAAQ,KAAK,IAAI,UAAU,KAAK,IAAI,SAAS;CAE9E,MAAM,QAAQ,OAAO,KAAK,WAAW,MAAM;CAC3C,OAAO;EACL,MAAM;EACN,QAAQ,MAAM;EACd,MAAM,KAAK;EACX,SAAS;GAAE,QAAQ,WAAW;GAAQ,SAAS,WAAW;EAAQ;EAClE,YAAY;EACZ,UAAU,SAAmC,OAAO,OAAO,WAAW,QAAQ,IAAI;EAClF,WAAW,MAAM,SAAS;GACxB,IAAI,CAAC,OAAO,OAAO,WAAW,QAAQ,IAAI,GACxC,OAAO;IAAE,IAAI;IAAO,QAAQ,CAAC;KAAE,MAAM;KAAI,SAAS,kBAAkB,KAAK;IAAG,CAAC;GAAE;GAEjF,OAAO,SAAS,EAAE,UAAU,WAAW,OAAO,OAAO,OAAO,CAAC;EAC/D;EACA,kBAAkB,OAAO;GACvB,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO;IAAE,IAAI;IAAO,QAAQ,CAAC;KAAE,MAAM;KAAI,SAAS;IAA0B,CAAC;GAAE;GAEjF,MAAM,SAA0B,CAAC;GACjC,MAAM,OAAgC,CAAC;GACvC,KAAK,MAAM,CAAC,OAAO,UAAU,OAAO,QAAQ,KAAK,GAAG;IAClD,IAAI,CAAC,OAAO,OAAO,cAAc,KAAK,GAAG;KAEvC,IAAI,iBAAiB,UAAU,UAC7B,OAAO,KAAK;MAAE,MAAM;MAAO,SAAS;KAA+B,CAAC;UAEpE,KAAK,SAAS;KAEhB;IACF;IACA,IAAI,UAAU,MAAM;KAClB,KAAK,SAAS;KACd;IACF;IACA,MAAM,SAAS,EAAE,UAAU,aAAa,QAAQ,KAAK;IACrD,IAAI,OAAO,SAAS,KAAK,SAAS,OAAO;SACpC,OAAO,KAAK,GAAG,SAAS,OAAO,OAAO,KAAK,CAAC;GACnD;GACA,OAAO,OAAO,SAAS,IAAI;IAAE,IAAI;IAAO;GAAO,IAAI;IAAE,IAAI;IAAY;GAAc;EACrF;CACF;AACF;;;;;;AAOA,IAAa,0BAAb,cAA6C,MAAM;CAGtC;CACA;CAHX,OAAyB;CACzB,YACE,OACA,QACA;EACA,MACE,UAAU,MAAM,6BAA6B,OAC1C,KAAK,MAAM,GAAG,EAAE,QAAQ,SAAS,GAAG,EAAE,SAAS,CAAC,CAChD,KAAK,IAAI,GACd;EAPS,KAAA,QAAA;EACA,KAAA,SAAA;CAOX;AACF;;AAGA,SAAgB,WACd,UACA,MACA,SACyC;CACzC,IAAI,CAAC,SAAS,QAAQ,IAAI,GACxB,OAAO;EACL,IAAI;EACJ,QAAQ,CAAC;GAAE,MAAM;GAAI,SAAS,IAAI,OAAO,IAAI,EAAE;EAA0B,CAAC;CAC5E;CAEF,OAAO,SAAS,WAAW,MAAM,OAAO;AAC1C;AAEA,SAAgB,WAAW,OAAmC;CAC5D,OACE,OAAO,UAAU,YACjB,UAAU,QACT,MAA6B,SAAS;AAE3C;AAEA,SAAS,SAAY,QAAyD;CAC5E,OAAO,OAAO,UACV;EAAE,IAAI;EAAM,MAAM,OAAO;CAAK,IAC9B;EAAE,IAAI;EAAO,QAAQ,SAAS,IAAI,OAAO,KAAK;CAAE;AACtD;AAEA,SAAS,SAAS,QAAgB,OAAwC;CACxE,OAAO,MAAM,OAAO,KAAK,WAAW;EAClC,MAAM,CAAC,QAAQ,GAAG,MAAM,KAAK,IAAI,MAAM,CAAC,CAAC,CAAC,QAAQ,MAAM,MAAM,EAAE,CAAC,CAAC,KAAK,GAAG;EAC1E,SAAS,MAAM;CACjB,EAAE;AACJ;;;;;;;AAUA,MAAM,0BAAU,IAAI,IAAI;CACtB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,MAAM,iCAAiB,IAAI,IAAI,CAAC,UAAU,WAAW,CAAC;AAEtD,SAAS,iBAAiB,QAAiB,OAAe,MAAM,OAAa;CAC3E,MAAM,MAAO,QAAsC,MAAM;CACzD,IAAI,QAAQ,KAAA,GAAW,MAAM,IAAI,UAAU,YAAY,MAAM,mBAAmB;CAChF,IAAI,OAAO,IAAI,SAAS,UACtB,MAAM,IAAI,UACR,YAAY,MAAM,4DACpB;CAEF,IAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,GACvB,MAAM,IAAI,UACR,YAAY,MAAM,IAAI,IAAI,KAAK,4FACjC;CAEF,IAAK,IAA6B,WAAW,MAC3C,MAAM,IAAI,UAAU,YAAY,MAAM,uCAAuC;CAE/E,KAAK,MAAM,SAAS,IAAI,UAAU,CAAC,GACjC,IAAI,eAAe,IAAI,MAAM,KAAK,IAAI,KAAK,GACzC,MAAM,IAAI,UACR,YAAY,MAAM,IAAI,MAAM,KAAK,IAAI,UAAU,cAAc,iDAAiD,qBAAqB,mBACrI;CAGJ,MAAM,WAAgC,CAAC;CACvC,QAAQ,IAAI,MAAZ;EACE,KAAK,UAAU;GACb,MAAM,SAAS;GACf,KAAK,MAAM,CAAC,GAAG,MAAM,OAAO,QAAQ,OAAO,KAAK,GAAG,SAAS,KAAK,CAAC,GAAG,MAAM,GAAG,KAAK,CAAC,CAAC;GACrF,IAAI,OAAO,aAAa,KAAA,GAAW,SAAS,KAAK,CAAC,GAAG,MAAM,KAAK,OAAO,QAAQ,CAAC;GAChF;EACF;EACA,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;GACH,SAAS,KAAK,CAAC,OAAQ,IAA0C,SAAS,CAAC;GAC3E;EACF,KAAK;GACH,SAAS,KAAK,CAAC,GAAG,MAAM,KAAM,IAA0B,OAAO,CAAC;GAChE;EACF,KAAK,SAAS;GACZ,MAAM,QAAQ;GACd,MAAM,MAAM,SAAS,MAAM,UAAU,SAAS,KAAK,CAAC,GAAG,MAAM,GAAG,MAAM,IAAI,IAAI,CAAC,CAAC;GAChF,IAAI,MAAM,SAAS,MAAM,SAAS,KAAK,CAAC,GAAG,MAAM,MAAM,MAAM,IAAI,CAAC;GAClE;EACF;EACA,KAAK;GACH,KAAK,MAAM,UAAW,IAA0B,SAAS,SAAS,KAAK,CAAC,OAAO,MAAM,CAAC;GACtF;EACF,KAAK,gBAAgB;GACnB,MAAM,eAAe;GACrB,SAAS,KAAK,CAAC,OAAO,aAAa,IAAI,GAAG,CAAC,OAAO,aAAa,KAAK,CAAC;GACrE;EACF;EACA,KAAK,UAAU;GACb,MAAM,SAAS;GACf,SAAS,KAAK,CAAC,GAAG,MAAM,QAAQ,OAAO,OAAO,GAAG,CAAC,GAAG,MAAM,KAAK,OAAO,SAAS,CAAC;GACjF;EACF;CAGF;CACA,KAAK,MAAM,CAAC,MAAM,UAAU,UAAU,iBAAiB,OAAO,IAAI;AACpE;AAEA,MAAM,0BAAU,IAAI,IAAI;CAAC;CAAU;CAAU;CAAW;CAAQ;AAAS,CAAC;AAE1E,SAAS,iBAAiB,QAAiB,OAAqB;CAC9D,IAAI,MAAO,OAAyB,KAAK;CACzC,IAAI,IAAI,SAAS,YAAY,MAAO,IAA6B,UAAU,KAAK;CAChF,IAAI,CAAC,QAAQ,IAAI,IAAI,IAAI,GACvB,MAAM,IAAI,UACR,YAAY,MAAM,sFAAsF,IAAI,MAC9G;AAEJ"}
|
|
1
|
+
{"version":3,"file":"contract.mjs","names":[],"sources":["../src/contract.ts"],"sourcesContent":["import * as z from 'zod/mini';\nimport type * as core from 'zod/v4/core';\nimport { type EventRefs, type UserRefs, event, user } from './dsl';\n\n/**\n * `defineContract({ events, profile })` — the producer/engine contract as Zod\n * schemas, written once in a package both sides import. From it:\n *\n * - `contract.events` / `contract.user` are the DSL's typed reference tables\n * (what `event<E>()` / `user<U>()` used to be, now derived from the schemas);\n * - `JourneyClient({ contract })` types `track` / `identify` by event name and\n * checks payloads before sending;\n * - `journeyWorker({ contract })` re-validates every inbound `/events` and\n * `/identify` — never trusting the caller used the SDK — and refuses a bad\n * one with a 4xx before anything is stored, woken or started.\n *\n * Supported schemas (v1), synchronous and JSON-shaped: event payloads are\n * object schemas (`z.looseObject` keeps a producer's extra analytics fields,\n * `z.strictObject` refuses them) and may nest objects and arrays; the profile\n * is a flat object of string / number / boolean / enum / literal fields. Input\n * and output must mean the same thing, so defaults, transforms, coercion,\n * custom refinements and async checks are refused when the contract is\n * defined. The Zod instances stay in the host; nothing here changes the IR.\n */\n\ntype ObjectSchema = core.$ZodObject;\n\n/** The declared fields of an object schema — without a loose object's `[key: string]: unknown`, so the DSL only offers fields that were written down. */\nexport type DeclaredOutput<S> =\n S extends core.$ZodObject<infer Shape>\n ? core.$InferObjectOutput<Shape, core.$strict['out']>\n : never;\n\nexport interface ContractDefinition<\n E extends Record<string, ObjectSchema>,\n P extends ObjectSchema,\n> {\n events: E;\n profile: P;\n}\n\n/** One rejected field, with the dotted path the producer can fix. */\nexport interface ContractIssue {\n path: string;\n message: string;\n}\n\nexport type ContractResult<T> = { ok: true; data: T } | { ok: false; issues: ContractIssue[] };\n\nexport interface Contract<\n E extends Record<string, ObjectSchema> = Record<string, ObjectSchema>,\n P extends ObjectSchema = ObjectSchema,\n> {\n readonly kind: 'journey-contract';\n /** The DSL's event reference table: `e.purchase` — payload refs from the declared fields only. */\n readonly events: EventRefs<{ [K in keyof E]: DeclaredOutput<E[K]> }>;\n /** The DSL's user-property reference table: `u.name`, plus the system `u.userId`. */\n readonly user: UserRefs<DeclaredOutput<P> & { userId: string }>;\n /** The raw schemas, for hosts that need them (validation, docs). */\n readonly schemas: { readonly events: E; readonly profile: P };\n /** Every event name the contract declares — what a producer forwards. */\n readonly eventNames: readonly (keyof E & string)[];\n isEvent(name: string): name is keyof E & string;\n /** Validate one event's payload; the parsed value is what may be stored. */\n parseEvent<K extends keyof E & string>(\n name: K,\n payload: unknown\n ): ContractResult<core.output<E[K]>>;\n /**\n * Validate an `/identify` merge patch: omitted fields stay as they are,\n * `null` removes a field, every provided value is checked against its field\n * schema, and an undeclared field is refused — a patch, never a full parse\n * that would fill defaults over fields the caller did not send.\n */\n parseProfilePatch(patch: unknown): ContractResult<ProfilePatch<P>>;\n}\n\n/** An `/identify` patch over the profile: any subset of fields, `null` to remove one. */\nexport type ProfilePatch<P> = { [K in keyof DeclaredOutput<P>]?: DeclaredOutput<P>[K] | null };\n\n/** Payload type of one event, for producers: `EventPayload<typeof contract, 'purchase'>`. */\nexport type EventPayload<C, K extends string> =\n C extends Contract<infer E> ? (K extends keyof E ? core.input<E[K]> : never) : never;\n\nexport function defineContract<E extends Record<string, ObjectSchema>, P extends ObjectSchema>(\n definition: ContractDefinition<E, P>\n): Contract<E, P> {\n for (const [name, schema] of Object.entries(definition.events)) {\n if (name.startsWith('$')) {\n throw new TypeError(`contract event '${name}': names starting with $ are reserved`);\n }\n assertJsonSchema(schema, `event '${name}'`, true);\n }\n assertJsonSchema(definition.profile, 'profile', true);\n const profileShape = (definition.profile._zod.def as core.$ZodObjectDef).shape;\n for (const [field, schema] of Object.entries(profileShape)) {\n assertFlatScalar(schema, `profile.${field}`);\n }\n const strictProfile = definition.profile._zod.def.catchall?._zod.def.type === 'never';\n\n const names = Object.keys(definition.events) as (keyof E & string)[];\n return {\n kind: 'journey-contract',\n events: event() as never,\n user: user() as never,\n schemas: { events: definition.events, profile: definition.profile },\n eventNames: names,\n isEvent: (name): name is keyof E & string => Object.hasOwn(definition.events, name),\n parseEvent(name, payload) {\n if (!Object.hasOwn(definition.events, name)) {\n return { ok: false, issues: [{ path: '', message: `unknown event '${name}'` }] };\n }\n return toResult(z.safeParse(definition.events[name], payload));\n },\n parseProfilePatch(patch) {\n if (typeof patch !== 'object' || patch === null || Array.isArray(patch)) {\n return { ok: false, issues: [{ path: '', message: 'props must be an object' }] };\n }\n const issues: ContractIssue[] = [];\n const data: Record<string, unknown> = {};\n for (const [field, value] of Object.entries(patch)) {\n if (!Object.hasOwn(profileShape, field)) {\n // userId is the request's identity, not a profile field\n if (strictProfile || field === 'userId') {\n issues.push({ path: field, message: 'not a declared profile field' });\n } else {\n data[field] = value;\n }\n continue;\n }\n if (value === null) {\n data[field] = null;\n continue;\n }\n const result = z.safeParse(profileShape[field], value);\n if (result.success) data[field] = result.data;\n else issues.push(...prefixed(field, result.error));\n }\n return issues.length > 0 ? { ok: false, issues } : { ok: true, data: data as never };\n },\n };\n}\n\n/**\n * An event the contract refuses — raised for events a journey emits itself\n * (`.sendEvent()`), which never pass the HTTP ingest: they go through the same\n * check, and a refusal is deterministic, so it is not retried.\n */\nexport class ContractValidationError extends Error {\n override readonly name = 'ContractValidationError';\n constructor(\n readonly event: string,\n readonly issues: ContractIssue[]\n ) {\n super(\n `event '${event}' refused by the contract: ${issues\n .map((i) => `${i.path || '(root)'} ${i.message}`)\n .join('; ')}`\n );\n }\n}\n\n/** The one check every event passes, from HTTP or from a journey: declared, and its payload parses. */\nexport function checkEvent(\n contract: Contract,\n name: string,\n payload: unknown\n): ContractResult<Record<string, unknown>> {\n if (!contract.isEvent(name)) {\n return {\n ok: false,\n issues: [{ path: '', message: `'${String(name)}' is not in the contract` }],\n };\n }\n return contract.parseEvent(name, payload);\n}\n\nexport function isContract(value: unknown): value is Contract {\n return (\n typeof value === 'object' &&\n value !== null &&\n (value as { kind?: unknown }).kind === 'journey-contract'\n );\n}\n\nfunction toResult<T>(result: core.util.SafeParseResult<T>): ContractResult<T> {\n return result.success\n ? { ok: true, data: result.data }\n : { ok: false, issues: prefixed('', result.error) };\n}\n\nfunction prefixed(prefix: string, error: core.$ZodError): ContractIssue[] {\n return error.issues.map((issue) => ({\n path: [prefix, ...issue.path.map(String)].filter((p) => p !== '').join('.'),\n message: issue.message,\n }));\n}\n\n/* ---------------------------- supported schemas ---------------------------- */\n\n/**\n * The schema types a contract may use — JSON shapes that parse synchronously\n * and mean the same before and after parsing. Anything not listed (default,\n * transform, pipe, lazy, promise, custom, date, bigint, …) is refused, and\n * every container is walked so nothing unsupported hides inside one.\n */\nconst ALLOWED = new Set([\n 'object',\n 'array',\n 'tuple',\n 'union',\n 'intersection',\n 'record',\n 'optional',\n 'nullable',\n 'readonly',\n 'nonoptional',\n 'string',\n 'number',\n 'boolean',\n 'null',\n 'enum',\n 'literal',\n 'unknown',\n 'any',\n 'never',\n]);\n\n/** Checks that change the value (`overwrite`: `.trim()`, `.toLowerCase()`, …) or run arbitrary code (`custom`: `.refine()`, async or not). */\nconst REFUSED_CHECKS = new Set(['custom', 'overwrite']);\n\nfunction assertJsonSchema(schema: unknown, where: string, top = false): void {\n const def = (schema as { _zod?: { def?: core.$ZodTypeDef } } | null | undefined)?._zod?.def;\n if (def === undefined) throw new TypeError(`contract ${where}: not a Zod schema`);\n if (top && def.type !== 'object') {\n throw new TypeError(\n `contract ${where}: must be an object schema (z.looseObject / z.strictObject)`\n );\n }\n if (!ALLOWED.has(def.type)) {\n throw new TypeError(\n `contract ${where}: ${def.type} schemas are not supported (input and output must mean the same JSON, parsed synchronously)`\n );\n }\n if ((def as { coerce?: boolean }).coerce === true) {\n throw new TypeError(`contract ${where}: coercion (z.coerce) is not supported`);\n }\n for (const check of def.checks ?? []) {\n if (REFUSED_CHECKS.has(check._zod.def.check)) {\n throw new TypeError(\n `contract ${where}: ${check._zod.def.check === 'overwrite' ? 'value-changing checks (.overwrite, .trim, …)' : 'custom refinements'} are not supported`\n );\n }\n }\n const children: [string, unknown][] = [];\n switch (def.type) {\n case 'object': {\n const object = def as core.$ZodObjectDef;\n for (const [k, v] of Object.entries(object.shape)) children.push([`${where}.${k}`, v]);\n if (object.catchall !== undefined) children.push([`${where}.*`, object.catchall]);\n break;\n }\n case 'optional':\n case 'nullable':\n case 'readonly':\n case 'nonoptional':\n children.push([where, (def as unknown as { innerType: unknown }).innerType]);\n break;\n case 'array':\n children.push([`${where}[]`, (def as core.$ZodArrayDef).element]);\n break;\n case 'tuple': {\n const tuple = def as core.$ZodTupleDef;\n tuple.items.forEach((item, index) => children.push([`${where}[${index}]`, item]));\n if (tuple.rest !== null) children.push([`${where}[…]`, tuple.rest]);\n break;\n }\n case 'union':\n for (const option of (def as core.$ZodUnionDef).options) children.push([where, option]);\n break;\n case 'intersection': {\n const intersection = def as core.$ZodIntersectionDef;\n children.push([where, intersection.left], [where, intersection.right]);\n break;\n }\n case 'record': {\n const record = def as core.$ZodRecordDef;\n children.push([`${where}{key}`, record.keyType], [`${where}{}`, record.valueType]);\n break;\n }\n default:\n break;\n }\n for (const [path, child] of children) assertJsonSchema(child, path);\n}\n\nconst SCALARS = new Set(['string', 'number', 'boolean', 'enum', 'literal']);\n\nfunction assertFlatScalar(schema: unknown, where: string): void {\n let def = (schema as core.$ZodType)._zod.def;\n if (def.type === 'optional') def = (def as core.$ZodOptionalDef).innerType._zod.def;\n if (!SCALARS.has(def.type)) {\n throw new TypeError(\n `contract ${where}: profile fields must be flat scalars (string, number, boolean, enum, literal), got ${def.type}`\n );\n }\n}\n"],"mappings":"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"}
|
package/dist/dsl/action.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.cjs","names":["sha256Hex"],"sources":["../../src/dsl/action.ts"],"sourcesContent":["import { sha256Hex } from '../hash';\n\n/**\n * Custom actions — the escape hatch for side effects the built-in nodes do not\n * cover (CRM sync, coupon issuing, outbound webhooks with custom auth, …).\n *\n * Dual-plane model: the *code* solidifies into the Worker bundle at deploy\n * time (it ships with the app, like any other module), while the *identity*\n * solidifies into IR as `{ type: 'action', action: name, args, codeHash }`.\n * The runtime resolves the name against a registry (see engine/actions.ts) and\n * compares codeHash to detect version skew between a pinned IR and the\n * currently deployed implementation.\n *\n * Deliberately NOT supported: custom *conditions*. Branching stays declarative\n * so the canvas can always explain why a user took a path — an action that\n * needs to influence routing writes a property or emits an event, and the\n * decision is expressed as a normal predicate on that data.\n */\n\n/** Runtime context handed to the handler; deliberately small — facts and messaging have their own nodes. */\nexport interface ActionContext {\n userId: string;\n /**\n * `${instanceId}:${nodeId}` — stable across replays and retries of this one\n * action node. A handler with an external side effect (charging, issuing,\n * POSTing) must pass it along so the far side collapses the duplicate: the\n * step body can re-run after the handler already resolved.\n */\n idempotencyKey: string;\n}\n\n/**\n * The handler: plain code, executed inside a durable step (`step.do`), so a\n * throw is retried by the runtime and a success is checkpointed — it should be\n * idempotent under retry, which is what `ctx.idempotencyKey` is for. Args\n * arrive with user_property references already resolved to values.\n */\nexport type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => Promise<void> | void;\n\n/**\n * A registered custom action: the named asset `.run()` refers to. Carries the\n * implementation for the runtime registry and the codeHash for the IR pin.\n */\nexport interface ActionRef<A extends object =
|
|
1
|
+
{"version":3,"file":"action.cjs","names":["sha256Hex"],"sources":["../../src/dsl/action.ts"],"sourcesContent":["import { sha256Hex } from '../hash';\n\n/**\n * Custom actions — the escape hatch for side effects the built-in nodes do not\n * cover (CRM sync, coupon issuing, outbound webhooks with custom auth, …).\n *\n * Dual-plane model: the *code* solidifies into the Worker bundle at deploy\n * time (it ships with the app, like any other module), while the *identity*\n * solidifies into IR as `{ type: 'action', action: name, args, codeHash }`.\n * The runtime resolves the name against a registry (see engine/actions.ts) and\n * compares codeHash to detect version skew between a pinned IR and the\n * currently deployed implementation.\n *\n * Deliberately NOT supported: custom *conditions*. Branching stays declarative\n * so the canvas can always explain why a user took a path — an action that\n * needs to influence routing writes a property or emits an event, and the\n * decision is expressed as a normal predicate on that data.\n */\n\n/** Runtime context handed to the handler; deliberately small — facts and messaging have their own nodes. */\nexport interface ActionContext {\n userId: string;\n /**\n * `${instanceId}:${nodeId}` — stable across replays and retries of this one\n * action node. A handler with an external side effect (charging, issuing,\n * POSTing) must pass it along so the far side collapses the duplicate: the\n * step body can re-run after the handler already resolved.\n */\n idempotencyKey: string;\n}\n\n/**\n * The handler: plain code, executed inside a durable step (`step.do`), so a\n * throw is retried by the runtime and a success is checkpointed — it should be\n * idempotent under retry, which is what `ctx.idempotencyKey` is for. Args\n * arrive with user_property references already resolved to values.\n */\nexport type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => Promise<void> | void;\n\n/**\n * A registered custom action: the named asset `.run()` refers to. Carries the\n * implementation for the runtime registry and the codeHash for the IR pin.\n */\nexport interface ActionRef<A extends object = object> {\n readonly name: string;\n /** Hash of the handler's source text at definition time — the code-identity pin recorded in IR. */\n readonly codeHash: string;\n /** Method syntax on purpose: bivariant args let refs with different arg shapes share one list (compileBundle, registries). */\n handler(args: A, ctx: ActionContext): Promise<void> | void;\n}\n\n/**\n * Define a custom action. The generic `A` types the args `.run()` accepts —\n * the only type injection point, mirroring `template.sms<P>()`:\n *\n * const syncCrm = action<{ plan: string }>('sync_crm', async ({ plan }, { userId }) => {\n * await fetch('https://crm.example.com/sync', { method: 'POST', body: JSON.stringify({ userId, plan }) });\n * });\n *\n * workflow(...).run(syncCrm, { plan: u.subscription_plan })\n *\n * codeHash comes from `handler.toString()`, so it changes when the code\n * changes and stays put when it does not. Different build pipelines can print\n * the same function differently (minification), which is why the default\n * runtime policy on mismatch is warn, not fail.\n */\nexport function action<A extends object = Record<string, never>>(\n name: string,\n handler: ActionHandler<A>\n): ActionRef<A> {\n return { name, codeHash: sha256Hex(handler.toString()).slice(0, 32), handler };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAkEA,SAAgB,OACd,MACA,SACc;CACd,OAAO;EAAE;EAAM,UAAUA,aAAAA,UAAU,QAAQ,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE;EAAG;CAAQ;AAC/E"}
|
package/dist/dsl/action.d.cts
CHANGED
|
@@ -37,7 +37,7 @@ export type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => P
|
|
|
37
37
|
* A registered custom action: the named asset `.run()` refers to. Carries the
|
|
38
38
|
* implementation for the runtime registry and the codeHash for the IR pin.
|
|
39
39
|
*/
|
|
40
|
-
export interface ActionRef<A extends object =
|
|
40
|
+
export interface ActionRef<A extends object = object> {
|
|
41
41
|
readonly name: string;
|
|
42
42
|
/** Hash of the handler's source text at definition time — the code-identity pin recorded in IR. */
|
|
43
43
|
readonly codeHash: string;
|
|
@@ -59,6 +59,6 @@ export interface ActionRef<A extends object = Record<never, never>> {
|
|
|
59
59
|
* the same function differently (minification), which is why the default
|
|
60
60
|
* runtime policy on mismatch is warn, not fail.
|
|
61
61
|
*/
|
|
62
|
-
export declare function action<A extends object = Record<
|
|
62
|
+
export declare function action<A extends object = Record<string, never>>(name: string, handler: ActionHandler<A>): ActionRef<A>;
|
|
63
63
|
//#endregion
|
|
64
64
|
//# sourceMappingURL=action.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.d.cts","names":[],"sources":["../../src/dsl/action.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAoBiB;EACf;;;;;;;EAOA;;;;;;;;YASU,cAAc,qBAAqB,MAAM,GAAG,KAAK,kBAAkB;;;;;iBAM9D,UAAU
|
|
1
|
+
{"version":3,"file":"action.d.cts","names":[],"sources":["../../src/dsl/action.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAoBiB;EACf;;;;;;;EAOA;;;;;;;;YASU,cAAc,qBAAqB,MAAM,GAAG,KAAK,kBAAkB;;;;;iBAM9D,UAAU;WAChB;;WAEA;;EAET,QAAQ,MAAM,GAAG,KAAK,gBAAgB;;;;;;;;;;;;;;;;;wBAkBxB,OAAO,mBAAmB,uBACxC,cACA,SAAS,cAAc,KACtB,UAAU"}
|
package/dist/dsl/action.d.mts
CHANGED
|
@@ -37,7 +37,7 @@ export type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => P
|
|
|
37
37
|
* A registered custom action: the named asset `.run()` refers to. Carries the
|
|
38
38
|
* implementation for the runtime registry and the codeHash for the IR pin.
|
|
39
39
|
*/
|
|
40
|
-
export interface ActionRef<A extends object =
|
|
40
|
+
export interface ActionRef<A extends object = object> {
|
|
41
41
|
readonly name: string;
|
|
42
42
|
/** Hash of the handler's source text at definition time — the code-identity pin recorded in IR. */
|
|
43
43
|
readonly codeHash: string;
|
|
@@ -59,6 +59,6 @@ export interface ActionRef<A extends object = Record<never, never>> {
|
|
|
59
59
|
* the same function differently (minification), which is why the default
|
|
60
60
|
* runtime policy on mismatch is warn, not fail.
|
|
61
61
|
*/
|
|
62
|
-
export declare function action<A extends object = Record<
|
|
62
|
+
export declare function action<A extends object = Record<string, never>>(name: string, handler: ActionHandler<A>): ActionRef<A>;
|
|
63
63
|
//#endregion
|
|
64
64
|
//# sourceMappingURL=action.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.d.mts","names":[],"sources":["../../src/dsl/action.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAoBiB;EACf;;;;;;;EAOA;;;;;;;;YASU,cAAc,qBAAqB,MAAM,GAAG,KAAK,kBAAkB;;;;;iBAM9D,UAAU
|
|
1
|
+
{"version":3,"file":"action.d.mts","names":[],"sources":["../../src/dsl/action.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBAoBiB;EACf;;;;;;;EAOA;;;;;;;;YASU,cAAc,qBAAqB,MAAM,GAAG,KAAK,kBAAkB;;;;;iBAM9D,UAAU;WAChB;;WAEA;;EAET,QAAQ,MAAM,GAAG,KAAK,gBAAgB;;;;;;;;;;;;;;;;;wBAkBxB,OAAO,mBAAmB,uBACxC,cACA,SAAS,cAAc,KACtB,UAAU"}
|
package/dist/dsl/action.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action.mjs","names":[],"sources":["../../src/dsl/action.ts"],"sourcesContent":["import { sha256Hex } from '../hash';\n\n/**\n * Custom actions — the escape hatch for side effects the built-in nodes do not\n * cover (CRM sync, coupon issuing, outbound webhooks with custom auth, …).\n *\n * Dual-plane model: the *code* solidifies into the Worker bundle at deploy\n * time (it ships with the app, like any other module), while the *identity*\n * solidifies into IR as `{ type: 'action', action: name, args, codeHash }`.\n * The runtime resolves the name against a registry (see engine/actions.ts) and\n * compares codeHash to detect version skew between a pinned IR and the\n * currently deployed implementation.\n *\n * Deliberately NOT supported: custom *conditions*. Branching stays declarative\n * so the canvas can always explain why a user took a path — an action that\n * needs to influence routing writes a property or emits an event, and the\n * decision is expressed as a normal predicate on that data.\n */\n\n/** Runtime context handed to the handler; deliberately small — facts and messaging have their own nodes. */\nexport interface ActionContext {\n userId: string;\n /**\n * `${instanceId}:${nodeId}` — stable across replays and retries of this one\n * action node. A handler with an external side effect (charging, issuing,\n * POSTing) must pass it along so the far side collapses the duplicate: the\n * step body can re-run after the handler already resolved.\n */\n idempotencyKey: string;\n}\n\n/**\n * The handler: plain code, executed inside a durable step (`step.do`), so a\n * throw is retried by the runtime and a success is checkpointed — it should be\n * idempotent under retry, which is what `ctx.idempotencyKey` is for. Args\n * arrive with user_property references already resolved to values.\n */\nexport type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => Promise<void> | void;\n\n/**\n * A registered custom action: the named asset `.run()` refers to. Carries the\n * implementation for the runtime registry and the codeHash for the IR pin.\n */\nexport interface ActionRef<A extends object =
|
|
1
|
+
{"version":3,"file":"action.mjs","names":[],"sources":["../../src/dsl/action.ts"],"sourcesContent":["import { sha256Hex } from '../hash';\n\n/**\n * Custom actions — the escape hatch for side effects the built-in nodes do not\n * cover (CRM sync, coupon issuing, outbound webhooks with custom auth, …).\n *\n * Dual-plane model: the *code* solidifies into the Worker bundle at deploy\n * time (it ships with the app, like any other module), while the *identity*\n * solidifies into IR as `{ type: 'action', action: name, args, codeHash }`.\n * The runtime resolves the name against a registry (see engine/actions.ts) and\n * compares codeHash to detect version skew between a pinned IR and the\n * currently deployed implementation.\n *\n * Deliberately NOT supported: custom *conditions*. Branching stays declarative\n * so the canvas can always explain why a user took a path — an action that\n * needs to influence routing writes a property or emits an event, and the\n * decision is expressed as a normal predicate on that data.\n */\n\n/** Runtime context handed to the handler; deliberately small — facts and messaging have their own nodes. */\nexport interface ActionContext {\n userId: string;\n /**\n * `${instanceId}:${nodeId}` — stable across replays and retries of this one\n * action node. A handler with an external side effect (charging, issuing,\n * POSTing) must pass it along so the far side collapses the duplicate: the\n * step body can re-run after the handler already resolved.\n */\n idempotencyKey: string;\n}\n\n/**\n * The handler: plain code, executed inside a durable step (`step.do`), so a\n * throw is retried by the runtime and a success is checkpointed — it should be\n * idempotent under retry, which is what `ctx.idempotencyKey` is for. Args\n * arrive with user_property references already resolved to values.\n */\nexport type ActionHandler<A extends object> = (args: A, ctx: ActionContext) => Promise<void> | void;\n\n/**\n * A registered custom action: the named asset `.run()` refers to. Carries the\n * implementation for the runtime registry and the codeHash for the IR pin.\n */\nexport interface ActionRef<A extends object = object> {\n readonly name: string;\n /** Hash of the handler's source text at definition time — the code-identity pin recorded in IR. */\n readonly codeHash: string;\n /** Method syntax on purpose: bivariant args let refs with different arg shapes share one list (compileBundle, registries). */\n handler(args: A, ctx: ActionContext): Promise<void> | void;\n}\n\n/**\n * Define a custom action. The generic `A` types the args `.run()` accepts —\n * the only type injection point, mirroring `template.sms<P>()`:\n *\n * const syncCrm = action<{ plan: string }>('sync_crm', async ({ plan }, { userId }) => {\n * await fetch('https://crm.example.com/sync', { method: 'POST', body: JSON.stringify({ userId, plan }) });\n * });\n *\n * workflow(...).run(syncCrm, { plan: u.subscription_plan })\n *\n * codeHash comes from `handler.toString()`, so it changes when the code\n * changes and stays put when it does not. Different build pipelines can print\n * the same function differently (minification), which is why the default\n * runtime policy on mismatch is warn, not fail.\n */\nexport function action<A extends object = Record<string, never>>(\n name: string,\n handler: ActionHandler<A>\n): ActionRef<A> {\n return { name, codeHash: sha256Hex(handler.toString()).slice(0, 32), handler };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkEA,SAAgB,OACd,MACA,SACc;CACd,OAAO;EAAE;EAAM,UAAU,UAAU,QAAQ,SAAS,CAAC,CAAC,CAAC,MAAM,GAAG,EAAE;EAAG;CAAQ;AAC/E"}
|
package/dist/dsl/bundle.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle.cjs","names":["semanticHash","CHANNEL_OF","BundleIRSchema"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef<object>[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — name and code-identity hash; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({\n irVersion: IR_VERSION,\n name: a.name,\n codeHash: a.codeHash,\n }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and its codeHash must\n // agree, or two same-named definitions are competing for one identity.\n assertActionRefsResolve(workflows, actions);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n actions: readonly { name: string; codeHash: string }[]\n): void {\n const defined = new Map(actions.map((a) => [a.name, a.codeHash]));\n for (const workflow of workflows) {\n const used = new Map<string, Set<string | undefined>>();\n collectActionRefs(workflow.flow, used);\n for (const [name, codeHashes] of used) {\n const manifestHash = defined.get(name);\n if (manifestHash === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Every node's hash is checked, not just one per name: with two\n // definitions under one name, whichever the manifest holds, the *other*\n // node is the one that would meet a mismatched implementation at run time.\n for (const codeHash of codeHashes) {\n if (codeHash !== undefined && codeHash !== manifestHash) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`\n );\n }\n }\n }\n }\n}\n\n/** name → every codeHash seen under it; a name with two is exactly the case worth failing on. */\nfunction collectActionRefs(\n nodes: readonly NodeIR[],\n into: Map<string, Set<string | undefined>>\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'action': {\n const seen = into.get(node.action) ?? new Set<string | undefined>();\n seen.add(node.codeHash);\n into.set(node.action, seen);\n break;\n }\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);\n if (node.otherwise) collectActionRefs(node.otherwise, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectActionRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bundle.cjs","names":["semanticHash","CHANNEL_OF","BundleIRSchema"],"sources":["../../src/dsl/bundle.ts"],"sourcesContent":["import { semanticHash } from '../hash';\nimport {\n type BundleIR,\n BundleIR as BundleIRSchema,\n type ConditionIR,\n IR_VERSION,\n type NodeIR,\n type SegmentIR,\n type WorkflowIR,\n} from '../ir';\nimport { type AnyTemplate, CHANNEL_OF } from '../templates';\nimport type { ActionRef } from './action';\nimport type { SegmentInternal, SegmentRef } from './condition';\nimport type { WorkflowBuilder } from './workflow';\n\n/**\n * The deployment unit — compileBundle gathers workflows, segments and the\n * template manifest into one BundleIR snapshot (terraform-apply mental model);\n * the server diffs each definition by contentHash (see plan.ts).\n */\nexport function compileBundle(input: {\n workflows: readonly WorkflowBuilder[];\n segments?: readonly SegmentRef[];\n /** The template manifest: these definitions, by channel and id. */\n templates?: readonly AnyTemplate[];\n actions?: readonly ActionRef[];\n}): BundleIR {\n const segments = (input.segments ?? []).map((segment) => {\n const { id, definition, loc, meta } = segment as SegmentInternal;\n const compiled: SegmentIR = {\n irVersion: IR_VERSION,\n id,\n // Labels and provenance both live in meta, which stripMeta drops before hashing\n contentHash: semanticHash(definition),\n condition: definition,\n };\n const merged = {\n ...(meta?.name !== undefined ? { name: meta.name } : {}),\n ...(meta?.description !== undefined ? { description: meta.description } : {}),\n ...(loc !== undefined ? { loc } : {}),\n };\n if (Object.keys(merged).length > 0) compiled.meta = merged;\n return compiled;\n });\n\n const templates = (input.templates ?? []).map((template) => ({\n irVersion: IR_VERSION,\n key: template.id,\n channel: CHANNEL_OF[template.kind],\n }));\n\n // Manifest only — name and code-identity hash; the implementation ships in the Worker bundle, never in IR\n const actions = (input.actions ?? []).map((a) => ({\n irVersion: IR_VERSION,\n name: a.name,\n codeHash: a.codeHash,\n }));\n\n const workflows = input.workflows.map((builder) => builder.toIR());\n\n // Names are wire identity (routing tables, entry ledger, by-name refs):\n // duplicates would silently last-write-win on deploy, so fail the compile.\n assertUniqueNames(\n workflows.map((w) => w.id),\n 'workflow'\n );\n assertUniqueNames(\n segments.map((s) => s.id),\n 'segment'\n );\n // A message node names its template by channel and key, so the key is unique per channel:\n // an email and a push of one nudge may share it\n assertUniqueNames(\n templates.map((t) => `${t.channel}:${t.key}`),\n 'template'\n );\n assertUniqueNames(\n actions.map((a) => a.name),\n 'action'\n );\n\n // Deploy replaces the whole segment table, so every by-name reference must\n // resolve inside this bundle — catching it here turns a runtime \"Unknown\n // segment\" in some user's journey into a compile error.\n assertSegmentRefsResolve(workflows, segments);\n\n // Payload predicates and profile predicates share the Condition type, so\n // placement is checked here: payload only inside where-trees, and where-trees\n // contain nothing but payload predicates and combinators.\n assertConditionPlacement(workflows, segments);\n\n // Same rationale as segments: the runtime registry is looked up by name, so\n // an action a workflow runs must be in the bundle — and its codeHash must\n // agree, or two same-named definitions are competing for one identity.\n assertActionRefsResolve(workflows, actions);\n\n return BundleIRSchema.parse({ irVersion: IR_VERSION, workflows, segments, templates, actions });\n}\n\nfunction assertUniqueNames(names: readonly string[], kind: string): void {\n const seen = new Set<string>();\n for (const name of names) {\n if (seen.has(name)) throw new Error(`compileBundle: duplicate ${kind} name '${name}'`);\n seen.add(name);\n }\n}\n\nfunction assertSegmentRefsResolve(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n const defined = new Set(segments.map((s) => s.id));\n const check = (refs: ReadonlySet<string>, owner: string): void => {\n for (const ref of refs) {\n if (!defined.has(ref)) {\n throw new Error(\n `compileBundle: ${owner} references segment '${ref}' that is not in the bundle — pass it in 'segments'`\n );\n }\n }\n };\n for (const workflow of workflows) {\n check(referencedSegments(workflow), `workflow '${workflow.id}'`);\n }\n // Segments may reference other segments; those must resolve too\n for (const segment of segments) {\n const refs = new Set<string>();\n collectConditionRefs(segment.condition, refs);\n check(refs, `segment '${segment.id}'`);\n }\n}\n\nfunction referencedSegments(workflow: WorkflowIR): Set<string> {\n const refs = new Set<string>();\n if (workflow.trigger.type === 'segment') refs.add(workflow.trigger.segment);\n if (workflow.trigger.type === 'event' && workflow.trigger.filter !== undefined) {\n collectConditionRefs(workflow.trigger.filter, refs);\n }\n if (workflow.goal !== undefined) collectConditionRefs(workflow.goal.condition, refs);\n if (workflow.exitWhen !== undefined) collectConditionRefs(workflow.exitWhen, refs);\n collectNodeRefs(workflow.flow, refs);\n return refs;\n}\n\nfunction collectConditionRefs(condition: ConditionIR, into: Set<string>): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) collectConditionRefs(child, into);\n break;\n case 'not':\n collectConditionRefs(condition.condition, into);\n break;\n case 'segment':\n into.add(condition.segment);\n break;\n default:\n break;\n }\n}\n\nfunction collectNodeRefs(nodes: readonly NodeIR[], into: Set<string>): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n collectConditionRefs(node.condition, into);\n if (Array.isArray(node.onTimeout)) collectNodeRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n collectConditionRefs(branchCase.condition, into);\n collectNodeRefs(branchCase.flow, into);\n }\n if (node.otherwise) collectNodeRefs(node.otherwise, into);\n break;\n case 'filter':\n collectConditionRefs(node.condition, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectNodeRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\nfunction assertActionRefsResolve(\n workflows: readonly WorkflowIR[],\n actions: readonly { name: string; codeHash: string }[]\n): void {\n const defined = new Map(actions.map((a) => [a.name, a.codeHash]));\n for (const workflow of workflows) {\n const used = new Map<string, Set<string | undefined>>();\n collectActionRefs(workflow.flow, used);\n for (const [name, codeHashes] of used) {\n const manifestHash = defined.get(name);\n if (manifestHash === undefined) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' that is not in the bundle — pass it in 'actions'`\n );\n }\n // Every node's hash is checked, not just one per name: with two\n // definitions under one name, whichever the manifest holds, the *other*\n // node is the one that would meet a mismatched implementation at run time.\n for (const codeHash of codeHashes) {\n if (codeHash !== undefined && codeHash !== manifestHash) {\n throw new Error(\n `compileBundle: workflow '${workflow.id}' runs action '${name}' whose code differs from the one in 'actions' — two definitions are sharing that name`\n );\n }\n }\n }\n }\n}\n\n/** name → every codeHash seen under it; a name with two is exactly the case worth failing on. */\nfunction collectActionRefs(\n nodes: readonly NodeIR[],\n into: Map<string, Set<string | undefined>>\n): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'action': {\n const seen = into.get(node.action) ?? new Set<string | undefined>();\n seen.add(node.codeHash);\n into.set(node.action, seen);\n break;\n }\n case 'wait_until':\n if (Array.isArray(node.onTimeout)) collectActionRefs(node.onTimeout, into);\n break;\n case 'branch':\n for (const branchCase of node.cases) collectActionRefs(branchCase.flow, into);\n if (node.otherwise) collectActionRefs(node.otherwise, into);\n break;\n case 'cohort':\n for (const arm of node.arms) collectActionRefs(arm.flow, into);\n break;\n default:\n break;\n }\n }\n}\n\n/* --------------------- payload / profile placement checks --------------------- */\n\nfunction assertConditionPlacement(\n workflows: readonly WorkflowIR[],\n segments: readonly SegmentIR[]\n): void {\n for (const workflow of workflows) {\n if (workflow.trigger.type === 'event') {\n if (workflow.trigger.where !== undefined) {\n assertWhereTree(workflow.trigger.where, `workflow '${workflow.id}' trigger where`);\n }\n if (workflow.trigger.filter !== undefined) {\n assertProfileTree(workflow.trigger.filter, `workflow '${workflow.id}' trigger filter`);\n }\n }\n if (workflow.goal !== undefined) {\n assertProfileTree(workflow.goal.condition, `workflow '${workflow.id}' goal`);\n }\n if (workflow.exitWhen !== undefined) {\n assertProfileTree(workflow.exitWhen, `workflow '${workflow.id}' exitWhen`);\n }\n assertNodeTrees(workflow.flow, `workflow '${workflow.id}'`);\n }\n for (const segment of segments) {\n assertProfileTree(segment.condition, `segment '${segment.id}'`);\n }\n}\n\n/** A profile-context tree: payload predicates may appear only under a performed.where. */\nfunction assertProfileTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertProfileTree(child, owner);\n break;\n case 'not':\n assertProfileTree(condition.condition, owner);\n break;\n case 'performed':\n if (condition.where !== undefined) assertWhereTree(condition.where, owner);\n break;\n case 'payload':\n throw new Error(\n `compileBundle: ${owner} uses a payload predicate outside a where clause — payload refs are only valid inside performed({ where }) or trigger.event({ where })`\n );\n default:\n break; // property / segment\n }\n}\n\n/** A where tree: payload predicates and combinators only — no profile facts in event scope. */\nfunction assertWhereTree(condition: ConditionIR, owner: string): void {\n switch (condition.type) {\n case 'and':\n case 'or':\n for (const child of condition.conditions) assertWhereTree(child, owner);\n break;\n case 'not':\n assertWhereTree(condition.condition, owner);\n break;\n case 'payload':\n break;\n default:\n throw new Error(\n `compileBundle: ${owner} where clause may only contain payload predicates and and/or/not, found '${condition.type}'`\n );\n }\n}\n\nfunction assertNodeTrees(nodes: readonly NodeIR[], owner: string): void {\n for (const node of nodes) {\n switch (node.type) {\n case 'wait_until':\n assertProfileTree(node.condition, owner);\n if (Array.isArray(node.onTimeout)) assertNodeTrees(node.onTimeout, owner);\n break;\n case 'branch':\n for (const branchCase of node.cases) {\n assertProfileTree(branchCase.condition, owner);\n assertNodeTrees(branchCase.flow, owner);\n }\n if (node.otherwise) assertNodeTrees(node.otherwise, owner);\n break;\n case 'filter':\n assertProfileTree(node.condition, owner);\n break;\n case 'cohort':\n for (const arm of node.arms) assertNodeTrees(arm.flow, owner);\n break;\n default:\n break;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/dsl/bundle.d.cts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function compileBundle(input: {
|
|
|
14
14
|
segments?: readonly SegmentRef[];
|
|
15
15
|
/** The template manifest: these definitions, by channel and id. */
|
|
16
16
|
templates?: readonly AnyTemplate[];
|
|
17
|
-
actions?: readonly ActionRef
|
|
17
|
+
actions?: readonly ActionRef[];
|
|
18
18
|
}): BundleIR;
|
|
19
19
|
//#endregion
|
|
20
20
|
//# sourceMappingURL=bundle.d.cts.map
|
package/dist/dsl/bundle.d.mts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function compileBundle(input: {
|
|
|
14
14
|
segments?: readonly SegmentRef[];
|
|
15
15
|
/** The template manifest: these definitions, by channel and id. */
|
|
16
16
|
templates?: readonly AnyTemplate[];
|
|
17
|
-
actions?: readonly ActionRef
|
|
17
|
+
actions?: readonly ActionRef[];
|
|
18
18
|
}): BundleIR;
|
|
19
19
|
//#endregion
|
|
20
20
|
//# sourceMappingURL=bundle.d.mts.map
|