@cowliss/cli 0.1.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/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/guest/constants-OZrYz4I2.d.ts +42 -0
- package/dist/guest/driver-BOe7jBFw.js +5647 -0
- package/dist/guest/driver.d.ts +21 -0
- package/dist/guest/driver.js +3 -0
- package/dist/guest/emails.d.ts +24 -0
- package/dist/guest/emails.js +0 -0
- package/dist/guest/index-heC1gFE_.d.ts +296 -0
- package/dist/guest/journeys-CxuS-mHn.js +571 -0
- package/dist/guest/journeys.d.ts +127 -0
- package/dist/guest/journeys.js +3 -0
- package/dist/guest/prelude.d.ts +1 -0
- package/dist/guest/prelude.js +91 -0
- package/dist/guest/wasi.d.ts +6 -0
- package/dist/guest/wasi.js +43 -0
- package/dist/index.js +11235 -0
- package/examples/abandoned-checkout/emails/abandoned-checkout.tsx +104 -0
- package/examples/abandoned-checkout/journeys/abandoned-checkout.ts +39 -0
- package/examples/abandoned-checkout/scenarios/abandoned-checkout.recovered.json +10 -0
- package/examples/abandoned-checkout/scenarios/abandoned-checkout.timeout.json +16 -0
- package/examples/activity-decay/journeys/activity-decay.ts +36 -0
- package/examples/activity-decay/scenarios/activity-decay.json +8 -0
- package/examples/cross-app-pitch/emails/cross-app-pitch.tsx +77 -0
- package/examples/cross-app-pitch/journeys/cross-app-pitch.ts +30 -0
- package/examples/cross-app-pitch/scenarios/cross-app-pitch.json +16 -0
- package/examples/winback/emails/winback.tsx +78 -0
- package/examples/winback/journeys/winback.ts +33 -0
- package/examples/winback/scenarios/winback.json +18 -0
- package/package.json +82 -0
- package/project/tsconfig.json +18 -0
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
//#region ../../packages/shared/src/constants.ts
|
|
4
|
+
/** The public docs site, for the mail and the pages that point people at it. */
|
|
5
|
+
const DOCS_URL = "https://docs.cowliss.com";
|
|
6
|
+
/**
|
|
7
|
+
* Clerk ID prefix marking an organization-scoped subject. Ingestion API keys
|
|
8
|
+
* must resolve to an org subject; user-scoped keys are rejected.
|
|
9
|
+
*/
|
|
10
|
+
const CLERK_ORG_SUBJECT_PREFIX = "org_";
|
|
11
|
+
/**
|
|
12
|
+
* The two fixed environments every org has (spec: Environments). An app
|
|
13
|
+
* belongs to one, so the environment of every write is the app's;
|
|
14
|
+
* profiles, identifiers, events, memberships, journey instances,
|
|
15
|
+
* deliveries, violations, quarantine, the address ledger, and idempotency
|
|
16
|
+
* keys are per environment, while the catalog, sending domains, the
|
|
17
|
+
* suppression mirror, billing, and journey code are shared. A third
|
|
18
|
+
* environment is a one-line change here plus the mirrored db enum.
|
|
19
|
+
*/
|
|
20
|
+
const ENVIRONMENTS = ["development", "production"];
|
|
21
|
+
/**
|
|
22
|
+
* Fixed consent purposes for the prototype. Consent is a per-purpose map on
|
|
23
|
+
* the profile, checked at send-step execution time.
|
|
24
|
+
*
|
|
25
|
+
* The two names describe what the recipient agreed to, not the pipe it
|
|
26
|
+
* arrives on: "emails I did not ask for individually" and "my data leaving
|
|
27
|
+
* for somewhere else". Naming them after the channel (`email`, `webhook`)
|
|
28
|
+
* said nothing a recipient could consent to, and transactional mail already
|
|
29
|
+
* bypasses the email purpose, so it was only ever marketing consent.
|
|
30
|
+
*/
|
|
31
|
+
const CONSENT_PURPOSES = ["emailMarketing", "dataProcessing"];
|
|
32
|
+
/**
|
|
33
|
+
* The two classes of send, declared on the template rather than passed per
|
|
34
|
+
* call so the class cannot drift between two sends of the same message.
|
|
35
|
+
*
|
|
36
|
+
* Marketing is everything a journey sends on the org's behalf: it carries
|
|
37
|
+
* the RFC 8058 unsubscribe headers and runs the full gate list.
|
|
38
|
+
* Transactional is the developer's own operational mail (a receipt, an
|
|
39
|
+
* export-is-ready notice): it carries neither header and skips consent and
|
|
40
|
+
* the per-user frequency cap, because an unsubscribe link on an invoice is
|
|
41
|
+
* wrong and a receipt must not silently vanish for anyone who once left a
|
|
42
|
+
* newsletter. Suppression, the quota, the sending pause, and the from-domain
|
|
43
|
+
* check still apply to both: those bound cost and protect the shared SES
|
|
44
|
+
* account, and none of them are about what the recipient asked for.
|
|
45
|
+
*/
|
|
46
|
+
const SEND_CLASSES = ["marketing", "transactional"];
|
|
47
|
+
/** Per execution: capability calls, journal size, and captured `api.log` lines. */
|
|
48
|
+
const EXECUTION_LIMITS = {
|
|
49
|
+
calls: 1e3,
|
|
50
|
+
journalBytes: 1048576,
|
|
51
|
+
logLines: 100,
|
|
52
|
+
logLineBytes: 1024
|
|
53
|
+
};
|
|
54
|
+
/** Per release: manifest counts, bundle size, and the source tarball. */
|
|
55
|
+
const RELEASE_LIMITS = {
|
|
56
|
+
journeys: 100,
|
|
57
|
+
templates: 200,
|
|
58
|
+
bundleBytes: 2097152,
|
|
59
|
+
sourceBytes: 5242880
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Where the docs site serves the `cow.json` JSON Schema (the `$schema` a
|
|
63
|
+
* project file points at). Generated from `cowConfigSchema` by the docs
|
|
64
|
+
* generator; the path is stable because project files link to it.
|
|
65
|
+
*/
|
|
66
|
+
const COW_CONFIG_SCHEMA_PATH = "/schemas/cow.json";
|
|
67
|
+
const COW_CONFIG_SCHEMA_URL = `${DOCS_URL}${COW_CONFIG_SCHEMA_PATH}`;
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region ../../packages/shared/src/journeys-v2/config.ts
|
|
71
|
+
/**
|
|
72
|
+
* `cow.json` (spec: Project layout): the org id and nothing more. The
|
|
73
|
+
* folder name is the project's local name, the environment is always a
|
|
74
|
+
* flag, and auth never lives in the project. Strict, so a typo'd key is a
|
|
75
|
+
* build error rather than a silently ignored setting.
|
|
76
|
+
*/
|
|
77
|
+
const cowConfigSchema = z.strictObject({
|
|
78
|
+
$schema: z.url().optional(),
|
|
79
|
+
orgId: z.string().startsWith(CLERK_ORG_SUBJECT_PREFIX, "orgId must be a Clerk org id"),
|
|
80
|
+
/** Overrides the API the CLI talks to; the hosted product needs none. */
|
|
81
|
+
apiUrl: z.url().optional()
|
|
82
|
+
}).meta({
|
|
83
|
+
title: "cow.json",
|
|
84
|
+
description: "A cow project: the organization it deploys to."
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region ../../packages/shared/src/environments.ts
|
|
89
|
+
/**
|
|
90
|
+
* The environment on the wire: the source's `environment` field, the
|
|
91
|
+
* `X-Cow-Environment` header admin calls select with, and the
|
|
92
|
+
* `environment` column every per-environment DTO carries.
|
|
93
|
+
*/
|
|
94
|
+
const environmentSchema = z.enum(ENVIRONMENTS);
|
|
95
|
+
/**
|
|
96
|
+
* The environments a shared definition (a segment, a journey) declares it
|
|
97
|
+
* works on: a non-empty list of distinct values. A definition declaring none
|
|
98
|
+
* would be dead code with a row behind it, and a repeated value is a typo
|
|
99
|
+
* rather than an intent, which is the rule `defineJourney` applies too.
|
|
100
|
+
*/
|
|
101
|
+
const environmentsSchema = z.array(environmentSchema).min(1, "at least one environment is required").refine((values) => new Set(values).size === values.length, { message: "environments must not repeat" });
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region ../../packages/shared/src/journeys-v2/manifest.ts
|
|
105
|
+
/**
|
|
106
|
+
* The release manifest (spec: Build; Push and compile): what `cow build`
|
|
107
|
+
* extracts from a project and `cow push` uploads with the bundles. The
|
|
108
|
+
* server validates it with these schemas, compiles every bundle, and stores
|
|
109
|
+
* the compiled form on the release row.
|
|
110
|
+
*/
|
|
111
|
+
/**
|
|
112
|
+
* A journey or template key: the file basename under `journeys/` or
|
|
113
|
+
* `emails/`, kebab-case and unique across the project. Becomes part of the
|
|
114
|
+
* Temporal workflow id and travels in the journey chain, so it stays short.
|
|
115
|
+
*/
|
|
116
|
+
const JOURNEY_KEY_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
117
|
+
const journeyKeySchema = z.string().max(64).regex(JOURNEY_KEY_PATTERN, "key must be kebab-case (a-z, 0-9, hyphens)");
|
|
118
|
+
/**
|
|
119
|
+
* An author's labels on a journey or a template: how the dashboard groups
|
|
120
|
+
* and filters them, and the only grouping there is. Case is kept as
|
|
121
|
+
* written, each entry is trimmed and non-empty, the list is deduplicated,
|
|
122
|
+
* and both ceilings are low on purpose: tags are a handful of words, not a
|
|
123
|
+
* taxonomy. Absent means `[]`.
|
|
124
|
+
*
|
|
125
|
+
* The count is capped on the list as written, before the deduplication, so
|
|
126
|
+
* a 21st entry is an error even when it is a repeat: that keeps `maxItems`
|
|
127
|
+
* in the generated JSON Schema, and an author who wrote 21 tags wants to
|
|
128
|
+
* hear about it.
|
|
129
|
+
*/
|
|
130
|
+
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)]);
|
|
131
|
+
/**
|
|
132
|
+
* A matcher field: one pattern or a non-empty list of them, a list being a
|
|
133
|
+
* disjunction. See `matchesPattern` in ../patterns for the dialect (`*`
|
|
134
|
+
* only) and for why a pattern whose literal prefix is not `system.` never
|
|
135
|
+
* reaches a system event.
|
|
136
|
+
*/
|
|
137
|
+
const onePatternSchema = z.string().min(1).max(200, "a pattern must be at most 200 characters");
|
|
138
|
+
const patternSchema = z.union([onePatternSchema, z.array(onePatternSchema).min(1).max(20, "a matcher takes at most 20 patterns")]);
|
|
139
|
+
/**
|
|
140
|
+
* What starts a journey: an event (optionally narrowed to an app id or a
|
|
141
|
+
* list of them) or a segment entry. The registry DTO in `../journeys`
|
|
142
|
+
* reuses it.
|
|
143
|
+
*
|
|
144
|
+
* Both members are strict, so a journey holding the retired `source` key
|
|
145
|
+
* fails to compile a release instead of silently triggering on every app.
|
|
146
|
+
* There is deliberately no pipe filter: a trigger narrows by the app the
|
|
147
|
+
* write is attributed to, the same token a segment definition names.
|
|
148
|
+
*/
|
|
149
|
+
const triggerSchema = z.union([z.strictObject({
|
|
150
|
+
event: patternSchema,
|
|
151
|
+
appId: patternSchema.optional()
|
|
152
|
+
}), z.strictObject({ segment: z.string().min(1) })]);
|
|
153
|
+
/** A content address: `sha256:` plus the lowercase hex digest. */
|
|
154
|
+
const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/, "digest must be sha256:<64 hex>");
|
|
155
|
+
/** The names of the capability calls a journey may make. */
|
|
156
|
+
const COMMAND_NAMES = [
|
|
157
|
+
"sleep",
|
|
158
|
+
"waitForEvent",
|
|
159
|
+
"email.send",
|
|
160
|
+
"webhook.send",
|
|
161
|
+
"traits.set",
|
|
162
|
+
"traits.unset",
|
|
163
|
+
"profile.get",
|
|
164
|
+
"profiles.get",
|
|
165
|
+
"events.track",
|
|
166
|
+
"restart"
|
|
167
|
+
];
|
|
168
|
+
/**
|
|
169
|
+
* One entry of the step spine `cow build` records by running `run` once
|
|
170
|
+
* against a recording stub. Display only, never trusted: a journey's real
|
|
171
|
+
* control flow is whatever its code does at runtime. `detail` names the
|
|
172
|
+
* template, destination, event, or trait key when the call has one.
|
|
173
|
+
*/
|
|
174
|
+
const spineEntrySchema = z.object({
|
|
175
|
+
name: z.enum(COMMAND_NAMES),
|
|
176
|
+
detail: z.string().max(200).optional()
|
|
177
|
+
});
|
|
178
|
+
const manifestJourneySchema = z.object({
|
|
179
|
+
key: journeyKeySchema,
|
|
180
|
+
/** The author's labels; the dashboard's only grouping. */
|
|
181
|
+
tags: tagsSchema,
|
|
182
|
+
trigger: triggerSchema,
|
|
183
|
+
purpose: z.enum(CONSENT_PURPOSES),
|
|
184
|
+
/** The author's rollout gate: the journey is active only in these. */
|
|
185
|
+
environments: environmentsSchema,
|
|
186
|
+
spine: z.array(spineEntrySchema),
|
|
187
|
+
bundle: digestSchema
|
|
188
|
+
});
|
|
189
|
+
const manifestTemplateSchema = z.object({
|
|
190
|
+
key: journeyKeySchema,
|
|
191
|
+
/** The author's labels; the dashboard's only grouping. */
|
|
192
|
+
tags: tagsSchema,
|
|
193
|
+
sendClass: z.enum(SEND_CLASSES),
|
|
194
|
+
/** True asks the host to mint a signed `verifyUrl` prop at send time. */
|
|
195
|
+
verifyLink: z.boolean(),
|
|
196
|
+
/** JSON Schema of the template's `props`, converted by `cow build`. */
|
|
197
|
+
propsSchema: z.record(z.string(), z.unknown()),
|
|
198
|
+
bundle: digestSchema
|
|
199
|
+
});
|
|
200
|
+
function uniqueKeys(items, ctx, path) {
|
|
201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
202
|
+
for (const [index, item] of items.entries()) {
|
|
203
|
+
if (seen.has(item.key)) ctx.addIssue({
|
|
204
|
+
code: "custom",
|
|
205
|
+
message: `duplicate ${path} key "${item.key}"`,
|
|
206
|
+
path: [
|
|
207
|
+
path,
|
|
208
|
+
index,
|
|
209
|
+
"key"
|
|
210
|
+
]
|
|
211
|
+
});
|
|
212
|
+
seen.add(item.key);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/** What `cow build` writes to `.cow/build/manifest.json` and `cow push` sends. */
|
|
216
|
+
const manifestSchema = z.object({
|
|
217
|
+
protocol: z.literal(1),
|
|
218
|
+
/** The `@cowliss/cli` version the project was built with. */
|
|
219
|
+
sdk: z.string().min(1),
|
|
220
|
+
journeys: z.array(manifestJourneySchema).max(RELEASE_LIMITS.journeys),
|
|
221
|
+
templates: z.array(manifestTemplateSchema).max(RELEASE_LIMITS.templates),
|
|
222
|
+
/** Digest of the gzipped source tarball. */
|
|
223
|
+
source: digestSchema
|
|
224
|
+
}).superRefine((manifest, ctx) => {
|
|
225
|
+
uniqueKeys(manifest.journeys, ctx, "journeys");
|
|
226
|
+
uniqueKeys(manifest.templates, ctx, "templates");
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* The manifest as the release row stores it once compilation succeeded:
|
|
230
|
+
* the pushed manifest plus the compiled module digest per key, and the
|
|
231
|
+
* digest of the Javy engine plugin the toolchain that compiled them was
|
|
232
|
+
* built from.
|
|
233
|
+
*
|
|
234
|
+
* Journeys and templates are keyed separately because they share a key
|
|
235
|
+
* space: `welcome.ts` and `welcome.tsx` are one journey and the email it
|
|
236
|
+
* sends in every example, and a flat map would let one overwrite the other.
|
|
237
|
+
*
|
|
238
|
+
* `plugin` is a toolchain record, not a linked artifact: modules are
|
|
239
|
+
* statically linked, so the plugin bytes are inside each module. It says
|
|
240
|
+
* which engine compiled the release, which is what a later bug report or a
|
|
241
|
+
* reproducible rebuild needs.
|
|
242
|
+
*/
|
|
243
|
+
const compiledManifestSchema = manifestSchema.safeExtend({
|
|
244
|
+
modules: z.object({
|
|
245
|
+
journeys: z.record(journeyKeySchema, digestSchema),
|
|
246
|
+
templates: z.record(journeyKeySchema, digestSchema)
|
|
247
|
+
}),
|
|
248
|
+
plugin: digestSchema
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
//#endregion
|
|
252
|
+
//#region ../../packages/shared/src/journeys-v2/guest.ts
|
|
253
|
+
/**
|
|
254
|
+
* The guest protocol (spec: Guest protocol): the JSON a compiled module
|
|
255
|
+
* reads on stdin and writes on stdout. The sandbox worker parses every byte
|
|
256
|
+
* a guest returns with these schemas before anything acts on it; the guest
|
|
257
|
+
* SDK and the Node simulator produce and consume the same shapes.
|
|
258
|
+
*/
|
|
259
|
+
/** A duration as journey code writes it: an ms-style string ("2d") or milliseconds. */
|
|
260
|
+
const durationSchema = z.union([z.string().min(1), z.number().int().nonnegative()]);
|
|
261
|
+
/** The event a journey runs for, or waits on: name, properties, and when. */
|
|
262
|
+
const guestEventSchema = z.object({
|
|
263
|
+
name: z.string().min(1),
|
|
264
|
+
properties: z.record(z.string(), z.unknown()),
|
|
265
|
+
/** Milliseconds since the epoch; the guest's clock starts here. */
|
|
266
|
+
timestamp: z.number().int().nonnegative()
|
|
267
|
+
});
|
|
268
|
+
const properties = z.record(z.string(), z.unknown());
|
|
269
|
+
/**
|
|
270
|
+
* One capability call as the guest suspends on it: the runner dispatches by
|
|
271
|
+
* `name` and validates `args` with this schema before any activity sees
|
|
272
|
+
* them. A name outside the union is rejected, never dispatched.
|
|
273
|
+
*
|
|
274
|
+
* Every `args` is strict, and that is the tenancy boundary made mechanical:
|
|
275
|
+
* a module that returns an `orgId`, an `environment`, or any other field
|
|
276
|
+
* beside the ones a capability takes fails the parse instead of having it
|
|
277
|
+
* quietly dropped. Tenant context comes from the workflow input, never from
|
|
278
|
+
* guest output, and this is where saying so becomes checkable.
|
|
279
|
+
*/
|
|
280
|
+
const commandSchema = z.discriminatedUnion("name", [
|
|
281
|
+
z.object({
|
|
282
|
+
name: z.literal("sleep"),
|
|
283
|
+
args: z.strictObject({ duration: durationSchema })
|
|
284
|
+
}),
|
|
285
|
+
z.object({
|
|
286
|
+
name: z.literal("waitForEvent"),
|
|
287
|
+
args: z.strictObject({
|
|
288
|
+
event: patternSchema,
|
|
289
|
+
timeout: durationSchema
|
|
290
|
+
})
|
|
291
|
+
}),
|
|
292
|
+
z.object({
|
|
293
|
+
name: z.literal("email.send"),
|
|
294
|
+
args: z.strictObject({
|
|
295
|
+
template: journeyKeySchema,
|
|
296
|
+
props: properties
|
|
297
|
+
})
|
|
298
|
+
}),
|
|
299
|
+
z.object({
|
|
300
|
+
name: z.literal("webhook.send"),
|
|
301
|
+
args: z.strictObject({
|
|
302
|
+
destination: z.string().min(1),
|
|
303
|
+
payload: properties
|
|
304
|
+
})
|
|
305
|
+
}),
|
|
306
|
+
z.object({
|
|
307
|
+
name: z.literal("traits.set"),
|
|
308
|
+
args: z.strictObject({
|
|
309
|
+
key: z.string().min(1),
|
|
310
|
+
value: z.unknown()
|
|
311
|
+
})
|
|
312
|
+
}),
|
|
313
|
+
z.object({
|
|
314
|
+
name: z.literal("traits.unset"),
|
|
315
|
+
args: z.strictObject({ key: z.string().min(1) })
|
|
316
|
+
}),
|
|
317
|
+
z.object({
|
|
318
|
+
name: z.literal("profile.get"),
|
|
319
|
+
args: z.strictObject({})
|
|
320
|
+
}),
|
|
321
|
+
z.object({
|
|
322
|
+
name: z.literal("profiles.get"),
|
|
323
|
+
args: z.strictObject({ id: z.string().min(1) })
|
|
324
|
+
}),
|
|
325
|
+
z.object({
|
|
326
|
+
name: z.literal("events.track"),
|
|
327
|
+
args: z.strictObject({
|
|
328
|
+
event: z.string().min(1),
|
|
329
|
+
properties
|
|
330
|
+
})
|
|
331
|
+
}),
|
|
332
|
+
z.object({
|
|
333
|
+
name: z.literal("restart"),
|
|
334
|
+
args: z.strictObject({ event: guestEventSchema.optional() })
|
|
335
|
+
})
|
|
336
|
+
]);
|
|
337
|
+
/** A journaled call's outcome: the value the capability returned, or why it threw. */
|
|
338
|
+
const journalResultSchema = z.discriminatedUnion("ok", [z.object({
|
|
339
|
+
ok: z.literal(true),
|
|
340
|
+
value: z.unknown()
|
|
341
|
+
}), z.object({
|
|
342
|
+
ok: z.literal(false),
|
|
343
|
+
error: z.object({
|
|
344
|
+
code: z.string().min(1),
|
|
345
|
+
message: z.string()
|
|
346
|
+
})
|
|
347
|
+
})]);
|
|
348
|
+
/**
|
|
349
|
+
* One line of the journal the runner carries per execution. `at` is the
|
|
350
|
+
* workflow time the result was recorded, in epoch milliseconds: it is what
|
|
351
|
+
* `Date.now()` returns to journey code between this call and the next.
|
|
352
|
+
*/
|
|
353
|
+
const journalEntrySchema = z.object({
|
|
354
|
+
call: commandSchema,
|
|
355
|
+
result: journalResultSchema,
|
|
356
|
+
at: z.number().int().nonnegative()
|
|
357
|
+
});
|
|
358
|
+
const LOG_LEVELS = [
|
|
359
|
+
"debug",
|
|
360
|
+
"info",
|
|
361
|
+
"warn",
|
|
362
|
+
"error"
|
|
363
|
+
];
|
|
364
|
+
/** One `api.log` line, emitted during the step's new segment only. */
|
|
365
|
+
const logLineSchema = z.object({
|
|
366
|
+
level: z.enum(LOG_LEVELS),
|
|
367
|
+
message: z.string().max(EXECUTION_LIMITS.logLineBytes)
|
|
368
|
+
});
|
|
369
|
+
/** The per-execution caps the guest is told about, so it can fail early. */
|
|
370
|
+
const executionLimitsSchema = z.object({
|
|
371
|
+
calls: z.number().int().positive(),
|
|
372
|
+
journalBytes: z.number().int().positive(),
|
|
373
|
+
logLines: z.number().int().positive(),
|
|
374
|
+
logLineBytes: z.number().int().positive()
|
|
375
|
+
});
|
|
376
|
+
const journeyStepInputSchema = z.object({
|
|
377
|
+
protocol: z.literal(1),
|
|
378
|
+
kind: z.literal("journey"),
|
|
379
|
+
key: journeyKeySchema,
|
|
380
|
+
event: guestEventSchema,
|
|
381
|
+
/** Workflow time at the start of this step, epoch milliseconds. */
|
|
382
|
+
now: z.number().int().nonnegative(),
|
|
383
|
+
/** Seeds `Math.random()`; derived from the execution id. */
|
|
384
|
+
seed: z.string().min(1),
|
|
385
|
+
journal: z.array(journalEntrySchema).max(EXECUTION_LIMITS.calls),
|
|
386
|
+
limits: executionLimitsSchema
|
|
387
|
+
});
|
|
388
|
+
const logs = z.array(logLineSchema).max(EXECUTION_LIMITS.logLines).default([]);
|
|
389
|
+
/** The error codes a guest itself reports; traps are mapped host-side. */
|
|
390
|
+
const GUEST_ERROR_CODES = ["journey_error", "journey_nondeterministic"];
|
|
391
|
+
const journeyStepOutputSchema = z.discriminatedUnion("status", [
|
|
392
|
+
z.object({
|
|
393
|
+
status: z.literal("suspend"),
|
|
394
|
+
call: commandSchema,
|
|
395
|
+
logs
|
|
396
|
+
}),
|
|
397
|
+
z.object({
|
|
398
|
+
status: z.literal("done"),
|
|
399
|
+
logs
|
|
400
|
+
}),
|
|
401
|
+
z.object({
|
|
402
|
+
status: z.literal("error"),
|
|
403
|
+
error: z.object({
|
|
404
|
+
code: z.enum(GUEST_ERROR_CODES),
|
|
405
|
+
message: z.string(),
|
|
406
|
+
stack: z.string().optional()
|
|
407
|
+
}),
|
|
408
|
+
/** Set when a replayed call diverged from the journal: both are named. */
|
|
409
|
+
nondeterministic: z.object({
|
|
410
|
+
expected: commandSchema,
|
|
411
|
+
actual: commandSchema
|
|
412
|
+
}).optional(),
|
|
413
|
+
logs
|
|
414
|
+
})
|
|
415
|
+
]);
|
|
416
|
+
const templateRenderInputSchema = z.object({
|
|
417
|
+
protocol: z.literal(1),
|
|
418
|
+
kind: z.literal("template"),
|
|
419
|
+
key: journeyKeySchema,
|
|
420
|
+
props: properties
|
|
421
|
+
});
|
|
422
|
+
const templateRenderOutputSchema = z.object({
|
|
423
|
+
subject: z.string(),
|
|
424
|
+
html: z.string(),
|
|
425
|
+
text: z.string()
|
|
426
|
+
});
|
|
427
|
+
/** Asks a module for its own config; the compile step checks it against the push. */
|
|
428
|
+
const manifestInputSchema = z.object({ kind: z.literal("manifest") });
|
|
429
|
+
/**
|
|
430
|
+
* A module's self-report. The host knows which key it ran, so the report
|
|
431
|
+
* carries the config only.
|
|
432
|
+
*/
|
|
433
|
+
const manifestOutputSchema = z.discriminatedUnion("kind", [manifestJourneySchema.pick({
|
|
434
|
+
trigger: true,
|
|
435
|
+
purpose: true,
|
|
436
|
+
environments: true,
|
|
437
|
+
tags: true
|
|
438
|
+
}).extend({ kind: z.literal("journey") }), manifestTemplateSchema.pick({
|
|
439
|
+
sendClass: true,
|
|
440
|
+
verifyLink: true,
|
|
441
|
+
propsSchema: true,
|
|
442
|
+
tags: true
|
|
443
|
+
}).extend({ kind: z.literal("template") })]);
|
|
444
|
+
/** Everything a driver may be handed on stdin. */
|
|
445
|
+
const guestInputSchema = z.discriminatedUnion("kind", [
|
|
446
|
+
journeyStepInputSchema,
|
|
447
|
+
templateRenderInputSchema,
|
|
448
|
+
manifestInputSchema
|
|
449
|
+
]);
|
|
450
|
+
/**
|
|
451
|
+
* Execution lifecycle as the runner records it. `waiting` is a suspended
|
|
452
|
+
* step (a timer or a signal wait); `cancelled` is the runner honouring a
|
|
453
|
+
* release cancellation at its next step.
|
|
454
|
+
*/
|
|
455
|
+
const EXECUTION_STATUSES = [
|
|
456
|
+
"running",
|
|
457
|
+
"waiting",
|
|
458
|
+
"completed",
|
|
459
|
+
"failed",
|
|
460
|
+
"cancelled"
|
|
461
|
+
];
|
|
462
|
+
const executionStatusSchema = z.enum(EXECUTION_STATUSES);
|
|
463
|
+
|
|
464
|
+
//#endregion
|
|
465
|
+
//#region ../../packages/shared/src/journeys-v2/sandbox.ts
|
|
466
|
+
/**
|
|
467
|
+
* What the sandbox worker's activities return (spec: Sandbox worker). A
|
|
468
|
+
* guest failure is a value, not a throw: the runner has to journal it and
|
|
469
|
+
* record it on the execution, and a Temporal retry would only reproduce it.
|
|
470
|
+
* Infrastructure failures (a missing binary, a spawn error, Postgres down)
|
|
471
|
+
* throw and retry on the activity's bounded policy.
|
|
472
|
+
*/
|
|
473
|
+
/** Every way a guest invocation can fail, as the runner records it. */
|
|
474
|
+
const SANDBOX_FAILURE_CODES = [
|
|
475
|
+
"journey_fuel_exceeded",
|
|
476
|
+
"journey_timeout",
|
|
477
|
+
"journey_memory_exceeded",
|
|
478
|
+
"journey_error",
|
|
479
|
+
"journey_output_invalid"
|
|
480
|
+
];
|
|
481
|
+
const sandboxFailureCodeSchema = z.enum(SANDBOX_FAILURE_CODES);
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region ../../packages/shared/src/journeys-v2/scenario.ts
|
|
485
|
+
/**
|
|
486
|
+
* Scenario files for `cow test`: a journey run scripted as JSON and replayed
|
|
487
|
+
* on a virtual clock, so a 2-day journey is asserted in seconds. The shape is deliberately small (a user, some later
|
|
488
|
+
* events, the side effects the run must produce) because a scenario is meant
|
|
489
|
+
* to be written by hand or by an agent, not generated.
|
|
490
|
+
*
|
|
491
|
+
* The schema lives here rather than in packages/journeys so the CLI can reject
|
|
492
|
+
* a bad file before it boots a Temporal environment, and so the docs site can
|
|
493
|
+
* render the format from one source.
|
|
494
|
+
*/
|
|
495
|
+
const journeyScenarioSchema = z.object({
|
|
496
|
+
/** The user the instance runs for; traits are what getProfile returns. */
|
|
497
|
+
user: z.object({
|
|
498
|
+
id: z.string().min(1),
|
|
499
|
+
traits: z.record(z.string(), z.unknown()).default({})
|
|
500
|
+
}).default({
|
|
501
|
+
id: "user_1",
|
|
502
|
+
traits: {}
|
|
503
|
+
}),
|
|
504
|
+
/**
|
|
505
|
+
* Later events, each at an offset from instance start: ms-style string
|
|
506
|
+
* ("1h", "2d") or milliseconds. The trigger event itself is delivered
|
|
507
|
+
* automatically at start, exactly as production does.
|
|
508
|
+
*/
|
|
509
|
+
events: z.array(z.object({
|
|
510
|
+
at: z.union([z.string().min(1), z.number().int().nonnegative()]),
|
|
511
|
+
event: z.string().min(1)
|
|
512
|
+
})).default([]),
|
|
513
|
+
/**
|
|
514
|
+
* Every side-effect activity call the run must make, in order. `input` is
|
|
515
|
+
* matched as a subset of the actual activity input, so a scenario asserts
|
|
516
|
+
* the fields it cares about and ignores the rest.
|
|
517
|
+
*/
|
|
518
|
+
expect: z.array(z.object({
|
|
519
|
+
activity: z.enum([
|
|
520
|
+
"sendEmail",
|
|
521
|
+
"sendWebhook",
|
|
522
|
+
"setTrait",
|
|
523
|
+
"unsetTrait",
|
|
524
|
+
"trackEvent",
|
|
525
|
+
"restart"
|
|
526
|
+
]),
|
|
527
|
+
input: z.record(z.string(), z.unknown()).optional()
|
|
528
|
+
}))
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/guest/journeys.ts
|
|
533
|
+
/**
|
|
534
|
+
* A capability that failed host-side (an unknown destination, invalid
|
|
535
|
+
* props, a profile that is not there). The failure is journaled, so a
|
|
536
|
+
* journey that catches it takes the same branch on every replay.
|
|
537
|
+
*/
|
|
538
|
+
var CapabilityError = class extends Error {
|
|
539
|
+
code;
|
|
540
|
+
constructor(code, message) {
|
|
541
|
+
super(message);
|
|
542
|
+
this.name = "CapabilityError";
|
|
543
|
+
this.code = code;
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
/**
|
|
547
|
+
* The config half of a journey, validated with the same schema the release
|
|
548
|
+
* manifest is validated with, so a bad trigger or an unknown purpose fails
|
|
549
|
+
* at build time rather than at the first execution.
|
|
550
|
+
*/
|
|
551
|
+
const journeyConfigSchema = manifestJourneySchema.pick({
|
|
552
|
+
trigger: true,
|
|
553
|
+
purpose: true,
|
|
554
|
+
environments: true,
|
|
555
|
+
tags: true
|
|
556
|
+
});
|
|
557
|
+
/** Author a journey. Throws at definition time on an invalid config. */
|
|
558
|
+
function defineJourney(input) {
|
|
559
|
+
return {
|
|
560
|
+
...journeyConfigSchema.parse({
|
|
561
|
+
trigger: input.trigger,
|
|
562
|
+
purpose: input.purpose,
|
|
563
|
+
environments: input.environments ?? [...ENVIRONMENTS],
|
|
564
|
+
tags: input.tags
|
|
565
|
+
}),
|
|
566
|
+
run: input.run
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
//#endregion
|
|
571
|
+
export { manifestOutputSchema as a, journeyStepOutputSchema as i, defineJourney as n, templateRenderOutputSchema as o, guestInputSchema as r, CapabilityError as t };
|
|
@@ -0,0 +1,127 @@
|
|
|
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";
|
|
3
|
+
//#region src/guest/journeys.d.ts
|
|
4
|
+
/** The event a journey runs for, or the one a `waitForEvent` resolved with. */
|
|
5
|
+
type Event = GuestEvent;
|
|
6
|
+
/**
|
|
7
|
+
* A profile as the capability layer hands it back: the recipient of this
|
|
8
|
+
* execution (`api.profile.get()`) or another profile in the same org and
|
|
9
|
+
* environment (`api.profiles.get(id)`). A fresh read on every call.
|
|
10
|
+
*/
|
|
11
|
+
type Profile = {
|
|
12
|
+
/** The Cowliss-generated profile id (`usr_`). */
|
|
13
|
+
id: string;
|
|
14
|
+
traits: Record<string, unknown>;
|
|
15
|
+
/** Consent state per purpose; the send gates read it host-side too. */
|
|
16
|
+
consent: Record<ConsentPurpose, boolean>;
|
|
17
|
+
identifiers: Record<string, string>;
|
|
18
|
+
/** Names of the segments the profile is currently in. */
|
|
19
|
+
segments: string[];
|
|
20
|
+
};
|
|
21
|
+
/** What `api.email.send` resolves to once the host accepted the send. */
|
|
22
|
+
type EmailSendResult = {
|
|
23
|
+
deliveryId: string;
|
|
24
|
+
status: string;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* A capability that failed host-side (an unknown destination, invalid
|
|
28
|
+
* props, a profile that is not there). The failure is journaled, so a
|
|
29
|
+
* journey that catches it takes the same branch on every replay.
|
|
30
|
+
*/
|
|
31
|
+
declare class CapabilityError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
constructor(code: string, message: string);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The project's templates, keyed by template key. `cow build` writes the
|
|
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
|
|
39
|
+
* project that has not built yet still compiles, with string keys.
|
|
40
|
+
*/
|
|
41
|
+
interface CowTemplates {}
|
|
42
|
+
type TemplateKey = keyof CowTemplates extends never ? string : keyof CowTemplates;
|
|
43
|
+
type TemplateProps<Key> = Key extends keyof CowTemplates ? CowTemplates[Key] : Record<string, unknown>;
|
|
44
|
+
/**
|
|
45
|
+
* The capability object a journey's `run` receives. Every method is async
|
|
46
|
+
* and journaled (except `log`), which is what makes a replay reproduce the
|
|
47
|
+
* run exactly: the SDK compares each call against the journal and returns
|
|
48
|
+
* the recorded result instead of doing the work again.
|
|
49
|
+
*/
|
|
50
|
+
type Api = {
|
|
51
|
+
/** Durable wait; the runner turns it into a Temporal timer. */
|
|
52
|
+
sleep(duration: Duration): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* The next event matching this pattern, or `null` once the timeout
|
|
55
|
+
* elapses. `*` matches any run of characters, a list of patterns is a
|
|
56
|
+
* disjunction, and the resolved event carries its own `name`, so a wait on
|
|
57
|
+
* a list can branch on which one arrived. A pattern whose literal prefix
|
|
58
|
+
* is not `system.` never resolves with an event Cowliss wrote itself.
|
|
59
|
+
*/
|
|
60
|
+
waitForEvent(pattern: string | string[], options: {
|
|
61
|
+
timeout: Duration;
|
|
62
|
+
}): Promise<Event | null>;
|
|
63
|
+
email: {
|
|
64
|
+
send<Key extends TemplateKey>(args: {
|
|
65
|
+
template: Key;
|
|
66
|
+
props: TemplateProps<Key>;
|
|
67
|
+
}): Promise<EmailSendResult>;
|
|
68
|
+
};
|
|
69
|
+
webhook: {
|
|
70
|
+
send(args: {
|
|
71
|
+
destination: string;
|
|
72
|
+
payload: Record<string, unknown>;
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
};
|
|
75
|
+
traits: {
|
|
76
|
+
/** A `null` value deletes the key (RFC 7386 merge, as identify does). */
|
|
77
|
+
set(key: string, value: unknown): Promise<void>;
|
|
78
|
+
unset(key: string): Promise<void>;
|
|
79
|
+
};
|
|
80
|
+
profile: {
|
|
81
|
+
get(): Promise<Profile>;
|
|
82
|
+
};
|
|
83
|
+
profiles: {
|
|
84
|
+
get(id: string): Promise<Profile>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Writes an event on the recipient's timeline; may trigger other journeys.
|
|
88
|
+
* The name may not start with `system.`, which Cowliss reserves for its
|
|
89
|
+
* own events: that call throws a CapabilityError instead of writing.
|
|
90
|
+
*/
|
|
91
|
+
events: {
|
|
92
|
+
track(name: string, properties: Record<string, unknown>): Promise<void>;
|
|
93
|
+
};
|
|
94
|
+
/** Not journaled; captured per execution and shown on the execution. */
|
|
95
|
+
log(...args: unknown[]): void;
|
|
96
|
+
/**
|
|
97
|
+
* Ends this execution and starts a fresh one under the same workflow id
|
|
98
|
+
* with an empty journal. Never resolves: the execution is over.
|
|
99
|
+
*/
|
|
100
|
+
restart(args?: {
|
|
101
|
+
event?: Event;
|
|
102
|
+
}): Promise<never>;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* A journey as `defineJourney` returns it: the validated config plus `run`.
|
|
106
|
+
* The manifest path reports the config back to the host, which compares it
|
|
107
|
+
* with what `cow push` uploaded.
|
|
108
|
+
*/
|
|
109
|
+
type JourneyConfig = {
|
|
110
|
+
trigger: Trigger;
|
|
111
|
+
purpose: ConsentPurpose;
|
|
112
|
+
/** The author's rollout gate; defaults to every environment. */
|
|
113
|
+
environments: Environment[];
|
|
114
|
+
/** The author's labels; the dashboard's only grouping. Defaults to none. */
|
|
115
|
+
tags: string[];
|
|
116
|
+
};
|
|
117
|
+
type Journey = JourneyConfig & {
|
|
118
|
+
run: (event: Event, api: Api) => Promise<void>;
|
|
119
|
+
};
|
|
120
|
+
/** Author a journey. Throws at definition time on an invalid config. */
|
|
121
|
+
declare function defineJourney(input: Omit<JourneyConfig, "environments" | "tags"> & {
|
|
122
|
+
environments?: Environment[];
|
|
123
|
+
tags?: string[];
|
|
124
|
+
run: (event: Event, api: Api) => Promise<void>;
|
|
125
|
+
}): Journey;
|
|
126
|
+
//#endregion
|
|
127
|
+
export { Api, CapabilityError, CowTemplates, type Duration, EmailSendResult, Event, Journey, JourneyConfig, Profile, type Trigger, defineJourney };
|