@cowliss/cli 0.3.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-BcbIxxI-.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-M6LQVMr2.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,42 +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
- /**
95
- * Overrides the dashboard `cow login` opens. Paired with `apiUrl`: a
96
- * login against one Cowliss's dashboard yields a token the other's API
97
- * rejects, so a config that names an API names its dashboard too.
98
- */
99
- webUrl: z.url().optional()
100
- }).meta({
101
- title: "cow.json",
102
- description: "A cow project: the organization and the project it deploys to."
103
- });
104
-
105
69
  //#endregion
106
70
  //#region ../../packages/shared/src/environments.ts
107
71
  /**
@@ -147,6 +111,50 @@ const journeyKeySchema = z.string().max(64).regex(JOURNEY_KEY_PATTERN, "key must
147
111
  */
148
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)]);
149
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
+ /**
150
158
  * A matcher field: one pattern or a non-empty list of them, a list being a
151
159
  * disjunction. See `matchesPattern` in ../patterns for the dialect (`*`
152
160
  * only) and for why a pattern whose literal prefix is not `system.` never
@@ -168,14 +176,29 @@ const triggerSchema = z.union([z.strictObject({
168
176
  event: patternSchema,
169
177
  appId: patternSchema.optional()
170
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");
171
189
  /** A content address: `sha256:` plus the lowercase hex digest. */
172
190
  const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/, "digest must be sha256:<64 hex>");
173
- /** 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
+ */
174
197
  const COMMAND_NAMES = [
175
198
  "sleep",
176
199
  "waitForEvent",
177
- "email.send",
178
- "webhook.send",
200
+ "send.email",
201
+ "send.webhook",
179
202
  "traits.set",
180
203
  "traits.unset",
181
204
  "profile.get",
@@ -184,21 +207,58 @@ const COMMAND_NAMES = [
184
207
  "restart"
185
208
  ];
186
209
  /**
187
- * One entry of the step spine `cow build` records by running `run` once
188
- * against a recording stub. Display only, never trusted: a journey's real
189
- * control flow is whatever its code does at runtime. `detail` names the
190
- * 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.
191
214
  */
215
+ const SPINE_ENTRY_NAMES = [
216
+ ...COMMAND_NAMES,
217
+ "if",
218
+ "loop",
219
+ "end"
220
+ ];
192
221
  const spineEntrySchema = z.object({
193
- name: z.enum(COMMAND_NAMES),
194
- detail: z.string().max(200).optional()
195
- });
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" });
196
240
  const manifestJourneySchema = z.object({
197
241
  key: journeyKeySchema,
198
242
  /** The author's labels; the dashboard's only grouping. */
199
243
  tags: tagsSchema,
200
244
  trigger: triggerSchema,
201
- 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(),
202
262
  /** The author's rollout gate: the journey is active only in these. */
203
263
  environments: environmentsSchema,
204
264
  spine: z.array(spineEntrySchema),
@@ -230,18 +290,37 @@ function uniqueKeys(items, ctx, path) {
230
290
  seen.add(item.key);
231
291
  }
232
292
  }
233
- /** 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
+ */
234
306
  const manifestSchema = z.object({
235
- protocol: z.literal(1),
307
+ protocol: z.number().int().positive(),
236
308
  /** The `@cowliss/cli` version the project was built with. */
237
309
  sdk: z.string().min(1),
238
310
  journeys: z.array(manifestJourneySchema).max(RELEASE_LIMITS.journeys),
239
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(),
240
318
  /** Digest of the gzipped source tarball. */
241
319
  source: digestSchema
242
320
  }).superRefine((manifest, ctx) => {
243
321
  uniqueKeys(manifest.journeys, ctx, "journeys");
244
322
  uniqueKeys(manifest.templates, ctx, "templates");
323
+ uniqueKeys(manifest.purposes ?? [], ctx, "purposes");
245
324
  });
246
325
  /**
247
326
  * The manifest as the release row stores it once compilation succeeded:
@@ -266,6 +345,49 @@ const compiledManifestSchema = manifestSchema.safeExtend({
266
345
  plugin: digestSchema
267
346
  });
268
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
+
269
391
  //#endregion
270
392
  //#region ../../packages/shared/src/journeys-v2/guest.ts
271
393
  /**
@@ -308,14 +430,21 @@ const commandSchema = z.discriminatedUnion("name", [
308
430
  })
309
431
  }),
310
432
  z.object({
311
- name: z.literal("email.send"),
433
+ name: z.literal("send.email"),
312
434
  args: z.strictObject({
313
435
  template: journeyKeySchema,
314
- 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
315
444
  })
316
445
  }),
317
446
  z.object({
318
- name: z.literal("webhook.send"),
447
+ name: z.literal("send.webhook"),
319
448
  args: z.strictObject({
320
449
  destination: z.string().min(1),
321
450
  payload: properties
@@ -392,7 +521,7 @@ const executionLimitsSchema = z.object({
392
521
  logLineBytes: z.number().int().positive()
393
522
  });
394
523
  const journeyStepInputSchema = z.object({
395
- protocol: z.literal(1),
524
+ protocol: z.literal(2),
396
525
  kind: z.literal("journey"),
397
526
  key: journeyKeySchema,
398
527
  event: guestEventSchema,
@@ -432,7 +561,7 @@ const journeyStepOutputSchema = z.discriminatedUnion("status", [
432
561
  })
433
562
  ]);
434
563
  const templateRenderInputSchema = z.object({
435
- protocol: z.literal(1),
564
+ protocol: z.literal(2),
436
565
  kind: z.literal("template"),
437
566
  key: journeyKeySchema,
438
567
  props: properties
@@ -451,6 +580,7 @@ const manifestInputSchema = z.object({ kind: z.literal("manifest") });
451
580
  const manifestOutputSchema = z.discriminatedUnion("kind", [manifestJourneySchema.pick({
452
581
  trigger: true,
453
582
  purpose: true,
583
+ senderIdentity: true,
454
584
  environments: true,
455
585
  tags: true
456
586
  }).extend({ kind: z.literal("journey") }), manifestTemplateSchema.pick({
@@ -563,21 +693,24 @@ var CapabilityError = class extends Error {
563
693
  };
564
694
  /**
565
695
  * The config half of a journey, validated with the same schema the release
566
- * manifest is validated with, so a bad trigger or an unknown purpose fails
567
- * 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.
568
699
  */
569
700
  const journeyConfigSchema = manifestJourneySchema.pick({
570
701
  trigger: true,
571
702
  purpose: true,
703
+ senderIdentity: true,
572
704
  environments: true,
573
705
  tags: true
574
- });
706
+ }).extend({ senderIdentity: destinationNameSchema });
575
707
  /** Author a journey. Throws at definition time on an invalid config. */
576
708
  function defineJourney(input) {
577
709
  return {
578
710
  ...journeyConfigSchema.parse({
579
711
  trigger: input.trigger,
580
712
  purpose: input.purpose,
713
+ senderIdentity: input.senderIdentity,
581
714
  environments: input.environments ?? [...ENVIRONMENTS],
582
715
  tags: input.tags
583
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-BcbIxxI-.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-M6LQVMr2.js";
1
+ import { t as runGuest } from "./driver-DaNdNhuF.js";
2
2
 
3
3
  //#region src/guest/wasi.ts
4
4
  const STDIN = 0;