@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 +2 -1
- package/dist/guest/{constants-OZrYz4I2.d.ts → constants-B0wk-87t.d.ts} +1 -13
- package/dist/guest/{driver-M6LQVMr2.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-BcbIxxI-.js → journeys-B_xg_GnL.js} +190 -57
- 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 +978 -302
- 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,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
|
-
/**
|
|
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
|
|
178
|
-
"webhook
|
|
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
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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(
|
|
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
|
-
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
}),
|
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