@cowliss/cli 0.2.0 → 0.4.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 CHANGED
@@ -45,6 +45,7 @@ import { defineJourney } from "@cowliss/cli/journeys";
45
45
  export default defineJourney({
46
46
  trigger: { event: "checkout_started" },
47
47
  purpose: "emailMarketing",
48
+ senderIdentity: "cowliss-default",
48
49
  run: async (event, api) => {
49
50
  const purchased = await api.waitForEvent("purchase_completed", {
50
51
  timeout: "24h",
@@ -52,7 +53,7 @@ export default defineJourney({
52
53
  if (purchased) {
53
54
  return;
54
55
  }
55
- await api.email.send({
56
+ await api.send.email({
56
57
  template: "abandoned-checkout",
57
58
  props: { checkoutUrl: event.properties.checkoutUrl },
58
59
  });
@@ -10,18 +10,6 @@
10
10
  */
11
11
  declare const ENVIRONMENTS: readonly ["development", "production"];
12
12
  type Environment = (typeof ENVIRONMENTS)[number];
13
- /**
14
- * Fixed consent purposes for the prototype. Consent is a per-purpose map on
15
- * the profile, checked at send-step execution time.
16
- *
17
- * The two names describe what the recipient agreed to, not the pipe it
18
- * arrives on: "emails I did not ask for individually" and "my data leaving
19
- * for somewhere else". Naming them after the channel (`email`, `webhook`)
20
- * said nothing a recipient could consent to, and transactional mail already
21
- * bypasses the email purpose, so it was only ever marketing consent.
22
- */
23
- declare const CONSENT_PURPOSES: readonly ["emailMarketing", "dataProcessing"];
24
- type ConsentPurpose = (typeof CONSENT_PURPOSES)[number];
25
13
  /**
26
14
  * The two classes of send, declared on the template rather than passed per
27
15
  * call so the class cannot drift between two sends of the same message.
@@ -39,4 +27,4 @@ type ConsentPurpose = (typeof CONSENT_PURPOSES)[number];
39
27
  declare const SEND_CLASSES: readonly ["marketing", "transactional"];
40
28
  type SendClass = (typeof SEND_CLASSES)[number];
41
29
  //#endregion
42
- export { Environment as n, SendClass as r, ConsentPurpose as t };
30
+ export { SendClass as n, Environment as t };
@@ -1,4 +1,4 @@
1
- import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-CR_wIAtP.js";
1
+ import { a as manifestOutputSchema, i as journeyStepOutputSchema, o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-B_xg_GnL.js";
2
2
  import { z } from "zod";
3
3
  import { createElement } from "react";
4
4
  import { renderToStaticMarkup } from "react-dom/server.browser";
@@ -78,7 +78,7 @@ function formatLogArg(value) {
78
78
  * The returned state is the driver's window into the run: the pending
79
79
  * call, the captured logs, and the clock the shims read.
80
80
  */
81
- function createApi(input) {
81
+ function createApi(input, journey) {
82
82
  let cursor = 0;
83
83
  let markSuspended = () => {};
84
84
  const state = {
@@ -118,17 +118,20 @@ function createApi(input) {
118
118
  timeout: options.timeout
119
119
  }
120
120
  }),
121
- email: { send: (args) => call({
122
- name: "email.send",
123
- args: {
124
- template: args.template,
125
- props: args.props
126
- }
127
- }) },
128
- webhook: { send: (args) => call({
129
- name: "webhook.send",
130
- args
131
- }) },
121
+ send: {
122
+ email: (args) => call({
123
+ name: "send.email",
124
+ args: {
125
+ template: args.template,
126
+ props: args.props,
127
+ senderIdentity: args.senderIdentity ?? journey.senderIdentity
128
+ }
129
+ }),
130
+ webhook: (args) => call({
131
+ name: "send.webhook",
132
+ args
133
+ })
134
+ },
132
135
  traits: {
133
136
  set: (key, value) => call({
134
137
  name: "traits.set",
@@ -5569,6 +5572,7 @@ function readManifest(module) {
5569
5572
  kind: "journey",
5570
5573
  trigger: journey.trigger,
5571
5574
  purpose: journey.purpose,
5575
+ senderIdentity: journey.senderIdentity,
5572
5576
  environments: journey.environments,
5573
5577
  tags: journey.tags
5574
5578
  });
@@ -5584,7 +5588,7 @@ function readManifest(module) {
5584
5588
  }
5585
5589
  async function runJourneyStep(module, input) {
5586
5590
  const journey = asJourney(module);
5587
- const { api, state } = createApi(input);
5591
+ const { api, state } = createApi(input, journey);
5588
5592
  const restore = installShims({
5589
5593
  clock: state.clock,
5590
5594
  seed: input.seed
@@ -1,4 +1,4 @@
1
- import { a as ManifestOutput, i as JourneyStepOutput, o as TemplateRenderOutput } from "./index-heC1gFE_.js";
1
+ import { a as ManifestOutput, i as JourneyStepOutput, o as TemplateRenderOutput } from "./index-DAGLEHDn.js";
2
2
  //#region src/guest/driver.d.ts
3
3
  /**
4
4
  * The in-process driver: JSON in, JSON out (spec: Guest protocol). One
@@ -1,3 +1,3 @@
1
- import { t as runGuest } from "./driver-D8cPyQgF.js";
1
+ import { t as runGuest } from "./driver-DaNdNhuF.js";
2
2
 
3
3
  export { runGuest };
@@ -1,4 +1,4 @@
1
- import { r as SendClass } from "./constants-OZrYz4I2.js";
1
+ import { n as SendClass } from "./constants-B0wk-87t.js";
2
2
  import { z } from "zod";
3
3
  import { ReactElement } from "react";
4
4
  //#region src/guest/emails.d.ts
@@ -30,13 +30,14 @@ declare const journeyStepOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
30
30
  timeout: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
31
31
  }, z.core.$strict>;
32
32
  }, z.core.$strip>, z.ZodObject<{
33
- name: z.ZodLiteral<"email.send">;
33
+ name: z.ZodLiteral<"send.email">;
34
34
  args: z.ZodObject<{
35
35
  template: z.ZodString;
36
36
  props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
37
+ senderIdentity: z.ZodString;
37
38
  }, z.core.$strict>;
38
39
  }, z.core.$strip>, z.ZodObject<{
39
- name: z.ZodLiteral<"webhook.send">;
40
+ name: z.ZodLiteral<"send.webhook">;
40
41
  args: z.ZodObject<{
41
42
  destination: z.ZodString;
42
43
  payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -119,13 +120,14 @@ declare const journeyStepOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
119
120
  timeout: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
120
121
  }, z.core.$strict>;
121
122
  }, z.core.$strip>, z.ZodObject<{
122
- name: z.ZodLiteral<"email.send">;
123
+ name: z.ZodLiteral<"send.email">;
123
124
  args: z.ZodObject<{
124
125
  template: z.ZodString;
125
126
  props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
127
+ senderIdentity: z.ZodString;
126
128
  }, z.core.$strict>;
127
129
  }, z.core.$strip>, z.ZodObject<{
128
- name: z.ZodLiteral<"webhook.send">;
130
+ name: z.ZodLiteral<"send.webhook">;
129
131
  args: z.ZodObject<{
130
132
  destination: z.ZodString;
131
133
  payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -177,13 +179,14 @@ declare const journeyStepOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
177
179
  timeout: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
178
180
  }, z.core.$strict>;
179
181
  }, z.core.$strip>, z.ZodObject<{
180
- name: z.ZodLiteral<"email.send">;
182
+ name: z.ZodLiteral<"send.email">;
181
183
  args: z.ZodObject<{
182
184
  template: z.ZodString;
183
185
  props: z.ZodRecord<z.ZodString, z.ZodUnknown>;
186
+ senderIdentity: z.ZodString;
184
187
  }, z.core.$strict>;
185
188
  }, z.core.$strip>, z.ZodObject<{
186
- name: z.ZodLiteral<"webhook.send">;
189
+ name: z.ZodLiteral<"send.webhook">;
187
190
  args: z.ZodObject<{
188
191
  destination: z.ZodString;
189
192
  payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
@@ -253,10 +256,8 @@ declare const manifestOutputSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
253
256
  }, z.core.$strict>, z.ZodObject<{
254
257
  segment: z.ZodString;
255
258
  }, z.core.$strict>]>;
256
- purpose: z.ZodEnum<{
257
- dataProcessing: "dataProcessing";
258
- emailMarketing: "emailMarketing";
259
- }>;
259
+ purpose: z.ZodString;
260
+ senderIdentity: z.ZodOptional<z.ZodString>;
260
261
  environments: z.ZodArray<z.ZodEnum<{
261
262
  development: "development";
262
263
  production: "production";
@@ -66,36 +66,6 @@ const RELEASE_LIMITS = {
66
66
  const COW_CONFIG_SCHEMA_PATH = "/schemas/cow.json";
67
67
  const COW_CONFIG_SCHEMA_URL = `${DOCS_URL}${COW_CONFIG_SCHEMA_PATH}`;
68
68
 
69
- //#endregion
70
- //#region ../../packages/shared/src/journeys-v2/config.ts
71
- /**
72
- * A project's name: what `cow init` asked for, and what the `projects` row
73
- * stores. Lives here rather than beside the release DTOs because `cow.json`
74
- * is the file a developer types it into; `createProjectBodySchema` reuses it.
75
- */
76
- const projectNameSchema = z.string().trim().min(1).max(200);
77
- /**
78
- * `cow.json` (spec: Project layout): the org, and which of its projects this
79
- * directory is. The environment is always a flag, and auth never lives in
80
- * the project. Strict, so a typo'd key is a build error rather than a
81
- * silently ignored setting.
82
- */
83
- const cowConfigSchema = z.strictObject({
84
- $schema: z.url().optional(),
85
- orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
86
- /**
87
- * Which project of the org this directory pushes to. One org can hold
88
- * several, one per repo, each with its own release sequence and its own
89
- * slice of the deployed journeys, so every project says which it is.
90
- */
91
- project: projectNameSchema,
92
- /** Overrides the API the CLI talks to; the hosted product needs none. */
93
- apiUrl: z.url().optional()
94
- }).meta({
95
- title: "cow.json",
96
- description: "A cow project: the organization and the project it deploys to."
97
- });
98
-
99
69
  //#endregion
100
70
  //#region ../../packages/shared/src/environments.ts
101
71
  /**
@@ -141,6 +111,50 @@ const journeyKeySchema = z.string().max(64).regex(JOURNEY_KEY_PATTERN, "key must
141
111
  */
142
112
  const tagsSchema = z.array(z.string().trim().min(1).max(50, "a tag must be at most 50 characters")).max(20, "a journey or template takes at most 20 tags").default([]).transform((tags) => [...new Set(tags)]);
143
113
  /**
114
+ * A consent purpose key: camelCase, matching the fixed `emailMarketing` and
115
+ * `dataProcessing`. Purposes are keys in the `consent` map a customer reads
116
+ * on their own profile, which is why they are not the kebab-case of a
117
+ * journey key.
118
+ */
119
+ const CONSENT_PURPOSE_KEY_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
120
+ /**
121
+ * One purpose key wherever a key is named rather than declared: a journey's
122
+ * `purpose`, and the keys of the consent patch an identify carries. The set
123
+ * a key is checked against is the org's declared rows, which no schema can
124
+ * see, so validation here is the shape only; the deploy and the ingestion
125
+ * write refuse a key the org has not declared.
126
+ */
127
+ const consentPurposeKeySchema = z.string().max(50, "a purpose key must be at most 50 characters").regex(CONSENT_PURPOSE_KEY_PATTERN, "a consent purpose key must be camelCase (a letter first, then letters and digits)");
128
+ /**
129
+ * One purpose a project declares in `cow.json` (spec: Decisions). A declared
130
+ * purpose is marketing-class and sits under the `emailMarketing` umbrella,
131
+ * so `denied` is the only default it may carry: the purpose is absent on
132
+ * every profile that already exists, and a granted default would answer for
133
+ * all of them at once.
134
+ *
135
+ * The field stays required rather than disappearing, so every `cow.json` and
136
+ * every stored manifest written before this still parses, and the column
137
+ * behind it still holds `granted` for the seeded `dataProcessing` row: this
138
+ * is a refusal at declaration time, not a narrower storage shape.
139
+ *
140
+ * The two fixed purposes cannot be declared. They are seeded for every org
141
+ * and owned by no project, so a project redeclaring one would be renaming
142
+ * the master switch every other project's journeys hang off.
143
+ */
144
+ const declaredPurposeSchema = z.strictObject({
145
+ key: consentPurposeKeySchema.refine((key) => !CONSENT_PURPOSES.includes(key), `${CONSENT_PURPOSES.join(" and ")} are fixed purposes and cannot be declared`),
146
+ /** What the dashboard and the account modal render beside the switch. */
147
+ label: z.string().trim().min(1).max(50, "a purpose label must be at most 50 characters"),
148
+ /** What the purpose means for a profile whose map does not answer it. */
149
+ default: z.literal("denied", "a declared purpose's \"default\" must be \"denied\": nobody has answered it yet, and a granted default would opt every profile you already have into it")
150
+ });
151
+ /**
152
+ * The purposes one project declares. Capped low the way tags are: a purpose
153
+ * is a category a recipient reads on a preferences switch, not a taxonomy.
154
+ * Absent means the project declares none, which is every project today.
155
+ */
156
+ const purposesSchema = z.array(declaredPurposeSchema).max(20, "a project declares at most 20 consent purposes");
157
+ /**
144
158
  * A matcher field: one pattern or a non-empty list of them, a list being a
145
159
  * disjunction. See `matchesPattern` in ../patterns for the dialect (`*`
146
160
  * only) and for why a pattern whose literal prefix is not `system.` never
@@ -162,14 +176,29 @@ const triggerSchema = z.union([z.strictObject({
162
176
  event: patternSchema,
163
177
  appId: patternSchema.optional()
164
178
  }), z.strictObject({ segment: z.string().min(1) })]);
179
+ /**
180
+ * A destination's name: the token a journey addresses it by, in a
181
+ * `send.webhook` call or a journey's `senderIdentity`.
182
+ *
183
+ * Defined here rather than beside the destinations contract, and imported
184
+ * from here by it, because a journey manifest names one and the guest layer
185
+ * is bundled into every tenant module: importing it the other way round
186
+ * would pull the Drizzle destinations table into all of them.
187
+ */
188
+ const destinationNameSchema = z.string().trim().min(1, "name is required").max(100, "name must be at most 100 characters");
165
189
  /** A content address: `sha256:` plus the lowercase hex digest. */
166
190
  const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/, "digest must be sha256:<64 hex>");
167
- /** The names of the capability calls a journey may make. */
191
+ /**
192
+ * The names of the capability calls a journey may make. Grouped by verb and
193
+ * then channel (`send.email`, not `email.send`), because the verb is the
194
+ * thing a journey author is choosing between and the name should read the
195
+ * way the code is written.
196
+ */
168
197
  const COMMAND_NAMES = [
169
198
  "sleep",
170
199
  "waitForEvent",
171
- "email.send",
172
- "webhook.send",
200
+ "send.email",
201
+ "send.webhook",
173
202
  "traits.set",
174
203
  "traits.unset",
175
204
  "profile.get",
@@ -178,21 +207,58 @@ const COMMAND_NAMES = [
178
207
  "restart"
179
208
  ];
180
209
  /**
181
- * One entry of the step spine `cow build` records by running `run` once
182
- * against a recording stub. Display only, never trusted: a journey's real
183
- * control flow is whatever its code does at runtime. `detail` names the
184
- * template, destination, event, or trait key when the call has one.
210
+ * What a spine entry can be: every capability call, plus the control flow
211
+ * `cow build` reads off the journey's source: an `if` (a condition, with
212
+ * `steps` and `otherwise`), a `loop` (with `steps` as its body), and the `end`
213
+ * of a path.
185
214
  */
215
+ const SPINE_ENTRY_NAMES = [
216
+ ...COMMAND_NAMES,
217
+ "if",
218
+ "loop",
219
+ "end"
220
+ ];
186
221
  const spineEntrySchema = z.object({
187
- name: z.enum(COMMAND_NAMES),
188
- detail: z.string().max(200).optional()
189
- });
222
+ name: z.enum(SPINE_ENTRY_NAMES),
223
+ detail: z.string().max(200).optional(),
224
+ /**
225
+ * The sender identity a `send.email` call named for itself, overriding
226
+ * the journey's own. Present only when the author wrote one on the call,
227
+ * which is what lets the deploy warning and the journey detail page name
228
+ * the override without re-reading the code.
229
+ */
230
+ senderIdentity: z.string().max(100).optional(),
231
+ /** A `waitForEvent` timeout, as the author wrote it. */
232
+ timeout: z.string().max(50).optional(),
233
+ get steps() {
234
+ return z.array(spineEntrySchema).optional();
235
+ },
236
+ get otherwise() {
237
+ return z.array(spineEntrySchema).optional();
238
+ }
239
+ }).meta({ id: "JourneySpineEntry" });
190
240
  const manifestJourneySchema = z.object({
191
241
  key: journeyKeySchema,
192
242
  /** The author's labels; the dashboard's only grouping. */
193
243
  tags: tagsSchema,
194
244
  trigger: triggerSchema,
195
- purpose: z.enum(CONSENT_PURPOSES),
245
+ /**
246
+ * A fixed purpose or one the project declares. Checked against the org's
247
+ * declared set at deploy time, not here: a manifest is built and pushed
248
+ * without ever reaching the org whose rows say what exists.
249
+ */
250
+ purpose: consentPurposeKeySchema,
251
+ /**
252
+ * The sender identity every `send.email` in this journey goes out as,
253
+ * unless the call names its own. A name, never a `dst_` id: an org has one
254
+ * row per environment, so an id would send in production and fail in
255
+ * development, which is the one thing a journey must not do.
256
+ *
257
+ * Optional here and required at `defineJourney`, exactly like `purposes`: a
258
+ * release pushed before the field existed carries none and its stored
259
+ * manifest still parses. The author's build is where the error is useful.
260
+ */
261
+ senderIdentity: destinationNameSchema.optional(),
196
262
  /** The author's rollout gate: the journey is active only in these. */
197
263
  environments: environmentsSchema,
198
264
  spine: z.array(spineEntrySchema),
@@ -224,18 +290,37 @@ function uniqueKeys(items, ctx, path) {
224
290
  seen.add(item.key);
225
291
  }
226
292
  }
227
- /** What `cow build` writes to `.cow/build/manifest.json` and `cow push` sends. */
293
+ /**
294
+ * What `cow build` writes to `.cow/build/manifest.json`, and the shape a
295
+ * release row stores for good.
296
+ *
297
+ * `protocol` is any positive integer rather than the current constant on
298
+ * purpose: a release is immutable and an execution stays pinned to the one it
299
+ * started on, so the day the protocol is bumped every stored release must
300
+ * still parse, or the runner, the deploy, and every API response the client
301
+ * validates all break at once for any org with history. The literal lives at
302
+ * push time only (`createReleaseBodySchema`), which is where a stale CLI is
303
+ * the developer's own fixable problem, and deploy plus the runner refuse a
304
+ * release built for another protocol with a message naming both numbers.
305
+ */
228
306
  const manifestSchema = z.object({
229
- protocol: z.literal(1),
307
+ protocol: z.number().int().positive(),
230
308
  /** The `@cowliss/cli` version the project was built with. */
231
309
  sdk: z.string().min(1),
232
310
  journeys: z.array(manifestJourneySchema).max(RELEASE_LIMITS.journeys),
233
311
  templates: z.array(manifestTemplateSchema).max(RELEASE_LIMITS.templates),
312
+ /**
313
+ * The consent purposes this project declares, copied from `cow.json`.
314
+ * Optional rather than defaulted: a release pushed before purposes
315
+ * existed carries none, and its stored manifest still parses.
316
+ */
317
+ purposes: purposesSchema.optional(),
234
318
  /** Digest of the gzipped source tarball. */
235
319
  source: digestSchema
236
320
  }).superRefine((manifest, ctx) => {
237
321
  uniqueKeys(manifest.journeys, ctx, "journeys");
238
322
  uniqueKeys(manifest.templates, ctx, "templates");
323
+ uniqueKeys(manifest.purposes ?? [], ctx, "purposes");
239
324
  });
240
325
  /**
241
326
  * The manifest as the release row stores it once compilation succeeded:
@@ -260,6 +345,49 @@ const compiledManifestSchema = manifestSchema.safeExtend({
260
345
  plugin: digestSchema
261
346
  });
262
347
 
348
+ //#endregion
349
+ //#region ../../packages/shared/src/journeys-v2/config.ts
350
+ /**
351
+ * A project's name: what `cow init` asked for, and what the `projects` row
352
+ * stores. Lives here rather than beside the release DTOs because `cow.json`
353
+ * is the file a developer types it into; `createProjectBodySchema` reuses it.
354
+ */
355
+ const projectNameSchema = z.string().trim().min(1).max(200);
356
+ /**
357
+ * `cow.json` (spec: Project layout): the org, and which of its projects this
358
+ * directory is. The environment is always a flag, and auth never lives in
359
+ * the project. Strict, so a typo'd key is a build error rather than a
360
+ * silently ignored setting.
361
+ */
362
+ const cowConfigSchema = z.strictObject({
363
+ $schema: z.url().optional(),
364
+ orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
365
+ /**
366
+ * Which project of the org this directory pushes to. One org can hold
367
+ * several, one per repo, each with its own release sequence and its own
368
+ * slice of the deployed journeys, so every project says which it is.
369
+ */
370
+ project: projectNameSchema,
371
+ /**
372
+ * The consent purposes this project declares. They are org-wide, so two
373
+ * projects declaring one key must agree on its label and default or the
374
+ * deploy is refused; a deploy adds and updates them and never deletes
375
+ * one, because profiles hold answers against them.
376
+ */
377
+ purposes: purposesSchema.optional(),
378
+ /** Overrides the API the CLI talks to; the hosted product needs none. */
379
+ apiUrl: z.url().optional(),
380
+ /**
381
+ * Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
382
+ * login against one Cowliss's dashboard yields a token the other's API
383
+ * rejects, so a config that names an API names its dashboard too.
384
+ */
385
+ webUrl: z.url().optional()
386
+ }).meta({
387
+ title: "cow.json",
388
+ description: "A cow project: the organization and the project it deploys to."
389
+ });
390
+
263
391
  //#endregion
264
392
  //#region ../../packages/shared/src/journeys-v2/guest.ts
265
393
  /**
@@ -302,14 +430,21 @@ const commandSchema = z.discriminatedUnion("name", [
302
430
  })
303
431
  }),
304
432
  z.object({
305
- name: z.literal("email.send"),
433
+ name: z.literal("send.email"),
306
434
  args: z.strictObject({
307
435
  template: journeyKeySchema,
308
- props: properties
436
+ props: properties,
437
+ /**
438
+ * Which sender identity this mail leaves as, by name. Required, and
439
+ * the guest SDK fills in the journey's own when the call does not name
440
+ * one, so the host has one resolution path and never has to read the
441
+ * manifest to find a sender.
442
+ */
443
+ senderIdentity: destinationNameSchema
309
444
  })
310
445
  }),
311
446
  z.object({
312
- name: z.literal("webhook.send"),
447
+ name: z.literal("send.webhook"),
313
448
  args: z.strictObject({
314
449
  destination: z.string().min(1),
315
450
  payload: properties
@@ -386,7 +521,7 @@ const executionLimitsSchema = z.object({
386
521
  logLineBytes: z.number().int().positive()
387
522
  });
388
523
  const journeyStepInputSchema = z.object({
389
- protocol: z.literal(1),
524
+ protocol: z.literal(2),
390
525
  kind: z.literal("journey"),
391
526
  key: journeyKeySchema,
392
527
  event: guestEventSchema,
@@ -426,7 +561,7 @@ const journeyStepOutputSchema = z.discriminatedUnion("status", [
426
561
  })
427
562
  ]);
428
563
  const templateRenderInputSchema = z.object({
429
- protocol: z.literal(1),
564
+ protocol: z.literal(2),
430
565
  kind: z.literal("template"),
431
566
  key: journeyKeySchema,
432
567
  props: properties
@@ -445,6 +580,7 @@ const manifestInputSchema = z.object({ kind: z.literal("manifest") });
445
580
  const manifestOutputSchema = z.discriminatedUnion("kind", [manifestJourneySchema.pick({
446
581
  trigger: true,
447
582
  purpose: true,
583
+ senderIdentity: true,
448
584
  environments: true,
449
585
  tags: true
450
586
  }).extend({ kind: z.literal("journey") }), manifestTemplateSchema.pick({
@@ -557,21 +693,24 @@ var CapabilityError = class extends Error {
557
693
  };
558
694
  /**
559
695
  * The config half of a journey, validated with the same schema the release
560
- * manifest is validated with, so a bad trigger or an unknown purpose fails
561
- * at build time rather than at the first execution.
696
+ * manifest is validated with, so a bad trigger or a malformed purpose key
697
+ * fails at build time rather than at the first execution. Whether the org
698
+ * declares that purpose is a question only the deploy can answer.
562
699
  */
563
700
  const journeyConfigSchema = manifestJourneySchema.pick({
564
701
  trigger: true,
565
702
  purpose: true,
703
+ senderIdentity: true,
566
704
  environments: true,
567
705
  tags: true
568
- });
706
+ }).extend({ senderIdentity: destinationNameSchema });
569
707
  /** Author a journey. Throws at definition time on an invalid config. */
570
708
  function defineJourney(input) {
571
709
  return {
572
710
  ...journeyConfigSchema.parse({
573
711
  trigger: input.trigger,
574
712
  purpose: input.purpose,
713
+ senderIdentity: input.senderIdentity,
575
714
  environments: input.environments ?? [...ENVIRONMENTS],
576
715
  tags: input.tags
577
716
  }),
@@ -1,5 +1,5 @@
1
- import { n as Duration, r as GuestEvent, t as Trigger } from "./index-heC1gFE_.js";
2
- import { n as Environment, t as ConsentPurpose } from "./constants-OZrYz4I2.js";
1
+ import { n as Duration, r as GuestEvent, t as Trigger } from "./index-DAGLEHDn.js";
2
+ import { t as Environment } from "./constants-B0wk-87t.js";
3
3
  //#region src/guest/journeys.d.ts
4
4
  /** The event a journey runs for, or the one a `waitForEvent` resolved with. */
5
5
  type Event = GuestEvent;
@@ -13,12 +13,12 @@ type Profile = {
13
13
  id: string;
14
14
  traits: Record<string, unknown>;
15
15
  /** Consent state per purpose; the send gates read it host-side too. */
16
- consent: Record<ConsentPurpose, boolean>;
16
+ consent: Record<string, boolean>;
17
17
  identifiers: Record<string, string>;
18
18
  /** Names of the segments the profile is currently in. */
19
19
  segments: string[];
20
20
  };
21
- /** What `api.email.send` resolves to once the host accepted the send. */
21
+ /** What `api.send.email` resolves to once the host accepted the send. */
22
22
  type EmailSendResult = {
23
23
  deliveryId: string;
24
24
  status: string;
@@ -35,7 +35,7 @@ declare class CapabilityError extends Error {
35
35
  /**
36
36
  * The project's templates, keyed by template key. `cow build` writes the
37
37
  * real interface into `.cow/types.d.ts` and TypeScript merges it into this
38
- * one, which is what types `api.email.send`. Empty here on purpose: a
38
+ * one, which is what types `api.send.email`. Empty here on purpose: a
39
39
  * project that has not built yet still compiles, with string keys.
40
40
  */
41
41
  interface CowTemplates {}
@@ -60,14 +60,22 @@ type Api = {
60
60
  waitForEvent(pattern: string | string[], options: {
61
61
  timeout: Duration;
62
62
  }): Promise<Event | null>;
63
- email: {
64
- send<Key extends TemplateKey>(args: {
63
+ /**
64
+ * The two things a journey sends. Grouped by the verb rather than by the
65
+ * channel, because the verb is what an author is choosing between.
66
+ */
67
+ send: {
68
+ email<Key extends TemplateKey>(args: {
65
69
  template: Key;
66
70
  props: TemplateProps<Key>;
71
+ /**
72
+ * Send this one mail from a different sender identity than the
73
+ * journey's own. A name the organization has configured, resolved in
74
+ * the environment the execution is running in.
75
+ */
76
+ senderIdentity?: string;
67
77
  }): Promise<EmailSendResult>;
68
- };
69
- webhook: {
70
- send(args: {
78
+ webhook(args: {
71
79
  destination: string;
72
80
  payload: Record<string, unknown>;
73
81
  }): Promise<void>;
@@ -108,7 +116,23 @@ type Api = {
108
116
  */
109
117
  type JourneyConfig = {
110
118
  trigger: Trigger;
111
- purpose: ConsentPurpose;
119
+ /**
120
+ * A fixed purpose (`emailMarketing`, `dataProcessing`) or one this
121
+ * project declares in `cow.json`. The key's shape is checked here; that
122
+ * the org declares it is checked when the release is deployed.
123
+ */
124
+ purpose: string;
125
+ /**
126
+ * The sender identity every `api.send.email` in this journey goes out as:
127
+ * the name of one your organization has configured, or `cowliss-default`
128
+ * for the address Cowliss gives you with nothing to set up. A single send
129
+ * can override it.
130
+ *
131
+ * Required, and named here rather than in a settings page, so the address
132
+ * a journey sends from is readable in the journey's own source. It is
133
+ * resolved per environment, which is why it is a name and not an id.
134
+ */
135
+ senderIdentity: string;
112
136
  /** The author's rollout gate; defaults to every environment. */
113
137
  environments: Environment[];
114
138
  /** The author's labels; the dashboard's only grouping. Defaults to none. */
@@ -1,3 +1,3 @@
1
- import { n as defineJourney, t as CapabilityError } from "./journeys-CR_wIAtP.js";
1
+ import { n as defineJourney, t as CapabilityError } from "./journeys-B_xg_GnL.js";
2
2
 
3
3
  export { CapabilityError, defineJourney };
@@ -1,4 +1,4 @@
1
- import { t as runGuest } from "./driver-D8cPyQgF.js";
1
+ import { t as runGuest } from "./driver-DaNdNhuF.js";
2
2
 
3
3
  //#region src/guest/wasi.ts
4
4
  const STDIN = 0;