@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 +2 -1
- package/dist/guest/{constants-OZrYz4I2.d.ts → constants-B0wk-87t.d.ts} +1 -13
- package/dist/guest/{driver-D8cPyQgF.js → driver-DaNdNhuF.js} +18 -14
- package/dist/guest/driver.d.ts +1 -1
- package/dist/guest/driver.js +1 -1
- package/dist/guest/emails.d.ts +1 -1
- package/dist/guest/{index-heC1gFE_.d.ts → index-DAGLEHDn.d.ts} +11 -10
- package/dist/guest/{journeys-CR_wIAtP.js → journeys-B_xg_GnL.js} +190 -51
- package/dist/guest/journeys.d.ts +35 -11
- package/dist/guest/journeys.js +1 -1
- package/dist/guest/wasi.js +1 -1
- package/dist/index.js +1261 -448
- package/examples/abandoned-checkout/journeys/abandoned-checkout.ts +2 -1
- package/examples/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +2 -1
- package/examples/activity-decay/journeys/activity-decay.ts +1 -0
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +5 -1
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +2 -1
- package/examples/winback/journeys/winback.ts +2 -1
- package/examples/winback/scenarios/winback.json +4 -1
- package/package.json +3 -1
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
|
|
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 {
|
|
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-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
package/dist/guest/driver.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as ManifestOutput, i as JourneyStepOutput, o as TemplateRenderOutput } from "./index-
|
|
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
|
package/dist/guest/driver.js
CHANGED
package/dist/guest/emails.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
257
|
-
|
|
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
|
-
/**
|
|
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
|
|
172
|
-
"webhook
|
|
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
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
}),
|
package/dist/guest/journeys.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { n as Duration, r as GuestEvent, t as Trigger } from "./index-
|
|
2
|
-
import {
|
|
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<
|
|
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
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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. */
|
package/dist/guest/journeys.js
CHANGED
package/dist/guest/wasi.js
CHANGED