@cowliss/cli 0.8.0 → 0.10.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/dist/guest/driver-emails.d.ts +12 -0
- package/dist/guest/{driver-BXuYP007.js → driver-emails.js} +11 -312
- package/dist/guest/driver-poSdZIj8.js +320 -0
- package/dist/guest/driver.d.ts +18 -5
- package/dist/guest/driver.js +2 -2
- package/dist/guest/emails-5GindgMQ.d.ts +47 -0
- package/dist/guest/emails.d.ts +1 -46
- package/dist/guest/wasi.d.ts +4 -2
- package/dist/guest/wasi.js +1 -3
- package/dist/index.js +49 -14
- package/package.json +17 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { GuestModule, GuestOutput } from "./driver.js";
|
|
2
|
+
//#region src/guest/driver-emails.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* The template half of the driver (spec: Guest protocol). It answers the
|
|
5
|
+
* two inputs a bundle built from `emails/` can get, `manifest` and
|
|
6
|
+
* `template`, and it is the only guest entry that imports `render.ts`:
|
|
7
|
+
* react and react-dom reach a bundle through this file or not at all.
|
|
8
|
+
* `driver.ts` is the journey half and holds everything both kinds share.
|
|
9
|
+
*/
|
|
10
|
+
declare function runGuest(module: GuestModule, input: unknown): Promise<GuestOutput>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { runGuest };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { o as templateRenderOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
|
|
2
|
+
import { n as readManifest, t as asTemplate } from "./driver-poSdZIj8.js";
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { renderToStaticMarkup } from "react-dom/server.browser";
|
|
5
5
|
|
|
@@ -30,154 +30,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
enumerable: true
|
|
31
31
|
}) : target, mod));
|
|
32
32
|
|
|
33
|
-
//#endregion
|
|
34
|
-
//#region src/guest/api.ts
|
|
35
|
-
/**
|
|
36
|
-
* The journaled capability implementation (spec: Guest protocol). Every
|
|
37
|
-
* `api` method funnels into `call`, which either replays the matching
|
|
38
|
-
* journal entry or records the pending call and hangs, letting the driver
|
|
39
|
-
* report the suspension.
|
|
40
|
-
*/
|
|
41
|
-
/** A replayed call did not match the journal: the execution cannot continue. */
|
|
42
|
-
var NondeterminismError = class extends Error {
|
|
43
|
-
expected;
|
|
44
|
-
actual;
|
|
45
|
-
constructor(expected, actual) {
|
|
46
|
-
super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
|
|
47
|
-
this.name = "NondeterminismError";
|
|
48
|
-
this.expected = expected;
|
|
49
|
-
this.actual = actual;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
/**
|
|
53
|
-
* Structural equality over JSON values. Key order differs between a
|
|
54
|
-
* literal in journey code and the same object round-tripped through the
|
|
55
|
-
* journal, so comparing serialized forms would report a divergence that
|
|
56
|
-
* is not one.
|
|
57
|
-
*/
|
|
58
|
-
function sameValue(a, b) {
|
|
59
|
-
if (a === b) return true;
|
|
60
|
-
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
|
|
61
|
-
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
62
|
-
const left = a;
|
|
63
|
-
const right = b;
|
|
64
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
65
|
-
for (const key of keys) if (!sameValue(left[key], right[key])) return false;
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
function formatLogArg(value) {
|
|
69
|
-
if (typeof value === "string") return value;
|
|
70
|
-
try {
|
|
71
|
-
return JSON.stringify(value) ?? String(value);
|
|
72
|
-
} catch {
|
|
73
|
-
return String(value);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Builds the `api` a journey's `run` receives, over the step's journal.
|
|
78
|
-
* The returned state is the driver's window into the run: the pending
|
|
79
|
-
* call, the captured logs, and the clock the shims read.
|
|
80
|
-
*/
|
|
81
|
-
function createApi(input, journey) {
|
|
82
|
-
let cursor = 0;
|
|
83
|
-
let markSuspended = () => {};
|
|
84
|
-
const state = {
|
|
85
|
-
clock: { now: input.now },
|
|
86
|
-
logs: [],
|
|
87
|
-
pending: null,
|
|
88
|
-
suspended: new Promise((resolve) => {
|
|
89
|
-
markSuspended = resolve;
|
|
90
|
-
})
|
|
91
|
-
};
|
|
92
|
-
function call(command) {
|
|
93
|
-
if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
|
|
94
|
-
const entry = input.journal[cursor];
|
|
95
|
-
if (!entry) {
|
|
96
|
-
if (!state.pending) {
|
|
97
|
-
state.pending = command;
|
|
98
|
-
markSuspended();
|
|
99
|
-
}
|
|
100
|
-
return new Promise(() => {});
|
|
101
|
-
}
|
|
102
|
-
if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
|
|
103
|
-
cursor += 1;
|
|
104
|
-
state.clock.now = cursor < input.journal.length ? entry.at : input.now;
|
|
105
|
-
if (entry.result.ok) return Promise.resolve(entry.result.value);
|
|
106
|
-
return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
|
|
107
|
-
}
|
|
108
|
-
return {
|
|
109
|
-
api: {
|
|
110
|
-
sleep: (duration) => call({
|
|
111
|
-
name: "sleep",
|
|
112
|
-
args: { duration }
|
|
113
|
-
}),
|
|
114
|
-
waitForEvent: (name, options) => call({
|
|
115
|
-
name: "waitForEvent",
|
|
116
|
-
args: {
|
|
117
|
-
event: name,
|
|
118
|
-
timeout: options.timeout
|
|
119
|
-
}
|
|
120
|
-
}),
|
|
121
|
-
send: {
|
|
122
|
-
email: (args) => call({
|
|
123
|
-
name: "send.email",
|
|
124
|
-
args: {
|
|
125
|
-
template: args.template,
|
|
126
|
-
props: args.props,
|
|
127
|
-
from: args.from ?? journey.from,
|
|
128
|
-
replyTo: args.replyTo
|
|
129
|
-
}
|
|
130
|
-
}),
|
|
131
|
-
webhook: (args) => call({
|
|
132
|
-
name: "send.webhook",
|
|
133
|
-
args
|
|
134
|
-
})
|
|
135
|
-
},
|
|
136
|
-
traits: {
|
|
137
|
-
set: (key, value) => call({
|
|
138
|
-
name: "traits.set",
|
|
139
|
-
args: {
|
|
140
|
-
key,
|
|
141
|
-
value
|
|
142
|
-
}
|
|
143
|
-
}),
|
|
144
|
-
unset: (key) => call({
|
|
145
|
-
name: "traits.unset",
|
|
146
|
-
args: { key }
|
|
147
|
-
})
|
|
148
|
-
},
|
|
149
|
-
profile: { get: () => call({
|
|
150
|
-
name: "profile.get",
|
|
151
|
-
args: {}
|
|
152
|
-
}) },
|
|
153
|
-
profiles: { get: (id) => call({
|
|
154
|
-
name: "profiles.get",
|
|
155
|
-
args: { id }
|
|
156
|
-
}) },
|
|
157
|
-
events: { track: (name, properties) => call({
|
|
158
|
-
name: "events.track",
|
|
159
|
-
args: {
|
|
160
|
-
event: name,
|
|
161
|
-
properties
|
|
162
|
-
}
|
|
163
|
-
}) },
|
|
164
|
-
log: (...args) => {
|
|
165
|
-
if (cursor < input.journal.length) return;
|
|
166
|
-
if (state.logs.length >= input.limits.logLines) return;
|
|
167
|
-
state.logs.push({
|
|
168
|
-
level: "info",
|
|
169
|
-
message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
|
|
170
|
-
});
|
|
171
|
-
},
|
|
172
|
-
restart: (args) => call({
|
|
173
|
-
name: "restart",
|
|
174
|
-
args: { event: args?.event }
|
|
175
|
-
})
|
|
176
|
-
},
|
|
177
|
-
state
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
33
|
//#endregion
|
|
182
34
|
//#region ../../node_modules/.pnpm/domelementtype@2.3.0/node_modules/domelementtype/lib/esm/index.js
|
|
183
35
|
/** Types of elements found in htmlparser2's DOM */
|
|
@@ -5482,173 +5334,20 @@ function renderTemplateModule(template, props) {
|
|
|
5482
5334
|
}
|
|
5483
5335
|
|
|
5484
5336
|
//#endregion
|
|
5485
|
-
//#region src/guest/
|
|
5337
|
+
//#region src/guest/driver-emails.ts
|
|
5486
5338
|
/**
|
|
5487
|
-
*
|
|
5488
|
-
*
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
for (let index = 0; index < seed.length; index += 1) {
|
|
5493
|
-
hash ^= seed.charCodeAt(index);
|
|
5494
|
-
hash = Math.imul(hash, 16777619);
|
|
5495
|
-
}
|
|
5496
|
-
return hash >>> 0;
|
|
5497
|
-
}
|
|
5498
|
-
/** mulberry32: small, fast, and good enough for journey branching. */
|
|
5499
|
-
function mulberry32(seed) {
|
|
5500
|
-
let state = hashSeed(seed);
|
|
5501
|
-
return () => {
|
|
5502
|
-
state = state + 1831565813 | 0;
|
|
5503
|
-
let value = Math.imul(state ^ state >>> 15, 1 | state);
|
|
5504
|
-
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
|
|
5505
|
-
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
5506
|
-
};
|
|
5507
|
-
}
|
|
5508
|
-
const BLOCKED = [
|
|
5509
|
-
"setTimeout",
|
|
5510
|
-
"setInterval",
|
|
5511
|
-
"setImmediate",
|
|
5512
|
-
"fetch",
|
|
5513
|
-
"XMLHttpRequest",
|
|
5514
|
-
"queueMicrotask"
|
|
5515
|
-
];
|
|
5516
|
-
function blocked(name) {
|
|
5517
|
-
return () => {
|
|
5518
|
-
throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
|
|
5519
|
-
};
|
|
5520
|
-
}
|
|
5521
|
-
/**
|
|
5522
|
-
* Installs the shims and returns the undo. Call it before user code runs
|
|
5523
|
-
* and restore in a `finally`, so a driver embedded in a Node process does
|
|
5524
|
-
* not leak a frozen clock into its host.
|
|
5525
|
-
*/
|
|
5526
|
-
function installShims(options) {
|
|
5527
|
-
const globals = globalThis;
|
|
5528
|
-
const saved = /* @__PURE__ */ new Map();
|
|
5529
|
-
const save = (name) => {
|
|
5530
|
-
saved.set(name, name in globals ? globals[name] : void 0);
|
|
5531
|
-
};
|
|
5532
|
-
const RealDate = Date;
|
|
5533
|
-
class GuestDate extends RealDate {
|
|
5534
|
-
constructor(...args) {
|
|
5535
|
-
if (args.length === 0) super(options.clock.now);
|
|
5536
|
-
else super(...args);
|
|
5537
|
-
}
|
|
5538
|
-
static now() {
|
|
5539
|
-
return options.clock.now;
|
|
5540
|
-
}
|
|
5541
|
-
}
|
|
5542
|
-
save("Date");
|
|
5543
|
-
globals.Date = GuestDate;
|
|
5544
|
-
const realRandom = Math.random;
|
|
5545
|
-
Math.random = mulberry32(options.seed);
|
|
5546
|
-
for (const name of BLOCKED) {
|
|
5547
|
-
save(name);
|
|
5548
|
-
globals[name] = blocked(name);
|
|
5549
|
-
}
|
|
5550
|
-
return () => {
|
|
5551
|
-
Math.random = realRandom;
|
|
5552
|
-
for (const [name, value] of saved) if (value === void 0) delete globals[name];
|
|
5553
|
-
else globals[name] = value;
|
|
5554
|
-
};
|
|
5555
|
-
}
|
|
5556
|
-
|
|
5557
|
-
//#endregion
|
|
5558
|
-
//#region src/guest/driver.ts
|
|
5559
|
-
function asJourney(module) {
|
|
5560
|
-
const journey = module.default;
|
|
5561
|
-
if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
|
|
5562
|
-
return journey;
|
|
5563
|
-
}
|
|
5564
|
-
function asTemplate(module) {
|
|
5565
|
-
const template = module;
|
|
5566
|
-
if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
|
|
5567
|
-
return template;
|
|
5568
|
-
}
|
|
5569
|
-
function readManifest(module) {
|
|
5570
|
-
if (module.default && typeof module.default.run === "function") {
|
|
5571
|
-
const journey = asJourney(module);
|
|
5572
|
-
return manifestOutputSchema.parse({
|
|
5573
|
-
kind: "journey",
|
|
5574
|
-
trigger: journey.trigger,
|
|
5575
|
-
purpose: journey.purpose,
|
|
5576
|
-
enrollment: journey.enrollment,
|
|
5577
|
-
description: journey.description,
|
|
5578
|
-
from: journey.from,
|
|
5579
|
-
tags: journey.tags
|
|
5580
|
-
});
|
|
5581
|
-
}
|
|
5582
|
-
const template = asTemplate(module);
|
|
5583
|
-
return manifestOutputSchema.parse({
|
|
5584
|
-
kind: "template",
|
|
5585
|
-
tags: template.tags,
|
|
5586
|
-
sendClass: template.sendClass ?? "marketing",
|
|
5587
|
-
verifyLink: template.verifyLink ?? false,
|
|
5588
|
-
unsubscribeLink: template.unsubscribeLink ?? false,
|
|
5589
|
-
propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
|
|
5590
|
-
});
|
|
5591
|
-
}
|
|
5592
|
-
async function runJourneyStep(module, input) {
|
|
5593
|
-
const journey = asJourney(module);
|
|
5594
|
-
const { api, state } = createApi(input, journey);
|
|
5595
|
-
const restore = installShims({
|
|
5596
|
-
clock: state.clock,
|
|
5597
|
-
seed: input.seed
|
|
5598
|
-
});
|
|
5599
|
-
try {
|
|
5600
|
-
if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
|
|
5601
|
-
status: "done",
|
|
5602
|
-
logs: state.logs
|
|
5603
|
-
});
|
|
5604
|
-
return journeyStepOutput({
|
|
5605
|
-
status: "suspend",
|
|
5606
|
-
call: state.pending,
|
|
5607
|
-
logs: state.logs
|
|
5608
|
-
});
|
|
5609
|
-
} catch (error) {
|
|
5610
|
-
if (error instanceof NondeterminismError) return journeyStepOutput({
|
|
5611
|
-
status: "error",
|
|
5612
|
-
error: {
|
|
5613
|
-
code: "journey_nondeterministic",
|
|
5614
|
-
message: error.message
|
|
5615
|
-
},
|
|
5616
|
-
nondeterministic: {
|
|
5617
|
-
expected: error.expected,
|
|
5618
|
-
actual: error.actual
|
|
5619
|
-
},
|
|
5620
|
-
logs: state.logs
|
|
5621
|
-
});
|
|
5622
|
-
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
5623
|
-
return journeyStepOutput({
|
|
5624
|
-
status: "error",
|
|
5625
|
-
error: {
|
|
5626
|
-
code: "journey_error",
|
|
5627
|
-
message: thrown.message,
|
|
5628
|
-
stack: thrown.stack
|
|
5629
|
-
},
|
|
5630
|
-
logs: state.logs
|
|
5631
|
-
});
|
|
5632
|
-
} finally {
|
|
5633
|
-
restore();
|
|
5634
|
-
}
|
|
5635
|
-
}
|
|
5636
|
-
function journeyStepOutput(output) {
|
|
5637
|
-
return journeyStepOutputSchema.parse(output);
|
|
5638
|
-
}
|
|
5639
|
-
/**
|
|
5640
|
-
* Runs one guest invocation. `module` is the tenant module namespace the
|
|
5641
|
-
* bundle's entry imported; `input` is the raw JSON the host handed over.
|
|
5339
|
+
* The template half of the driver (spec: Guest protocol). It answers the
|
|
5340
|
+
* two inputs a bundle built from `emails/` can get, `manifest` and
|
|
5341
|
+
* `template`, and it is the only guest entry that imports `render.ts`:
|
|
5342
|
+
* react and react-dom reach a bundle through this file or not at all.
|
|
5343
|
+
* `driver.ts` is the journey half and holds everything both kinds share.
|
|
5642
5344
|
*/
|
|
5643
5345
|
async function runGuest(module, input) {
|
|
5644
5346
|
const parsed = guestInputSchema.parse(input);
|
|
5645
5347
|
if (parsed.kind === "manifest") return readManifest(module);
|
|
5646
|
-
if (parsed.kind === "
|
|
5647
|
-
return
|
|
5648
|
-
}
|
|
5649
|
-
function renderTemplate(module, input) {
|
|
5650
|
-
return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), input.props));
|
|
5348
|
+
if (parsed.kind === "journey") throw new Error("A template bundle cannot run a journey step.");
|
|
5349
|
+
return templateRenderOutputSchema.parse(renderTemplateModule(asTemplate(module), parsed.props));
|
|
5651
5350
|
}
|
|
5652
5351
|
|
|
5653
5352
|
//#endregion
|
|
5654
|
-
export { runGuest
|
|
5353
|
+
export { runGuest };
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { a as manifestOutputSchema, i as journeyStepOutputSchema, r as guestInputSchema, t as CapabilityError } from "./journeys-F8Yk8s1P.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/guest/api.ts
|
|
5
|
+
/**
|
|
6
|
+
* The journaled capability implementation (spec: Guest protocol). Every
|
|
7
|
+
* `api` method funnels into `call`, which either replays the matching
|
|
8
|
+
* journal entry or records the pending call and hangs, letting the driver
|
|
9
|
+
* report the suspension.
|
|
10
|
+
*/
|
|
11
|
+
/** A replayed call did not match the journal: the execution cannot continue. */
|
|
12
|
+
var NondeterminismError = class extends Error {
|
|
13
|
+
expected;
|
|
14
|
+
actual;
|
|
15
|
+
constructor(expected, actual) {
|
|
16
|
+
super(`Journey replay diverged: the journal recorded "${expected.name}" but the code called "${actual.name}" with different arguments`);
|
|
17
|
+
this.name = "NondeterminismError";
|
|
18
|
+
this.expected = expected;
|
|
19
|
+
this.actual = actual;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Structural equality over JSON values. Key order differs between a
|
|
24
|
+
* literal in journey code and the same object round-tripped through the
|
|
25
|
+
* journal, so comparing serialized forms would report a divergence that
|
|
26
|
+
* is not one.
|
|
27
|
+
*/
|
|
28
|
+
function sameValue(a, b) {
|
|
29
|
+
if (a === b) return true;
|
|
30
|
+
if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, index) => sameValue(item, b[index]));
|
|
31
|
+
if (typeof a !== "object" || typeof b !== "object" || !a || !b) return false;
|
|
32
|
+
const left = a;
|
|
33
|
+
const right = b;
|
|
34
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)]);
|
|
35
|
+
for (const key of keys) if (!sameValue(left[key], right[key])) return false;
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
function formatLogArg(value) {
|
|
39
|
+
if (typeof value === "string") return value;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(value) ?? String(value);
|
|
42
|
+
} catch {
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Builds the `api` a journey's `run` receives, over the step's journal.
|
|
48
|
+
* The returned state is the driver's window into the run: the pending
|
|
49
|
+
* call, the captured logs, and the clock the shims read.
|
|
50
|
+
*/
|
|
51
|
+
function createApi(input, journey) {
|
|
52
|
+
let cursor = 0;
|
|
53
|
+
let markSuspended = () => {};
|
|
54
|
+
const state = {
|
|
55
|
+
clock: { now: input.now },
|
|
56
|
+
logs: [],
|
|
57
|
+
pending: null,
|
|
58
|
+
suspended: new Promise((resolve) => {
|
|
59
|
+
markSuspended = resolve;
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
function call(command) {
|
|
63
|
+
if (command.name !== "restart" && cursor >= input.limits.calls) throw new CapabilityError("call_limit_exceeded", `Journey exceeded the per-execution limit of ${input.limits.calls} capability calls; use api.restart() for loops`);
|
|
64
|
+
const entry = input.journal[cursor];
|
|
65
|
+
if (!entry) {
|
|
66
|
+
if (!state.pending) {
|
|
67
|
+
state.pending = command;
|
|
68
|
+
markSuspended();
|
|
69
|
+
}
|
|
70
|
+
return new Promise(() => {});
|
|
71
|
+
}
|
|
72
|
+
if (entry.call.name !== command.name || !sameValue(entry.call.args, command.args)) throw new NondeterminismError(entry.call, command);
|
|
73
|
+
cursor += 1;
|
|
74
|
+
state.clock.now = cursor < input.journal.length ? entry.at : input.now;
|
|
75
|
+
if (entry.result.ok) return Promise.resolve(entry.result.value);
|
|
76
|
+
return Promise.reject(new CapabilityError(entry.result.error.code, entry.result.error.message));
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
api: {
|
|
80
|
+
sleep: (duration) => call({
|
|
81
|
+
name: "sleep",
|
|
82
|
+
args: { duration }
|
|
83
|
+
}),
|
|
84
|
+
waitForEvent: (name, options) => call({
|
|
85
|
+
name: "waitForEvent",
|
|
86
|
+
args: {
|
|
87
|
+
event: name,
|
|
88
|
+
timeout: options.timeout
|
|
89
|
+
}
|
|
90
|
+
}),
|
|
91
|
+
send: {
|
|
92
|
+
email: (args) => call({
|
|
93
|
+
name: "send.email",
|
|
94
|
+
args: {
|
|
95
|
+
template: args.template,
|
|
96
|
+
props: args.props,
|
|
97
|
+
from: args.from ?? journey.from,
|
|
98
|
+
replyTo: args.replyTo
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
webhook: (args) => call({
|
|
102
|
+
name: "send.webhook",
|
|
103
|
+
args
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
traits: {
|
|
107
|
+
set: (key, value) => call({
|
|
108
|
+
name: "traits.set",
|
|
109
|
+
args: {
|
|
110
|
+
key,
|
|
111
|
+
value
|
|
112
|
+
}
|
|
113
|
+
}),
|
|
114
|
+
unset: (key) => call({
|
|
115
|
+
name: "traits.unset",
|
|
116
|
+
args: { key }
|
|
117
|
+
})
|
|
118
|
+
},
|
|
119
|
+
profile: { get: () => call({
|
|
120
|
+
name: "profile.get",
|
|
121
|
+
args: {}
|
|
122
|
+
}) },
|
|
123
|
+
profiles: { get: (id) => call({
|
|
124
|
+
name: "profiles.get",
|
|
125
|
+
args: { id }
|
|
126
|
+
}) },
|
|
127
|
+
events: { track: (name, properties) => call({
|
|
128
|
+
name: "events.track",
|
|
129
|
+
args: {
|
|
130
|
+
event: name,
|
|
131
|
+
properties
|
|
132
|
+
}
|
|
133
|
+
}) },
|
|
134
|
+
log: (...args) => {
|
|
135
|
+
if (cursor < input.journal.length) return;
|
|
136
|
+
if (state.logs.length >= input.limits.logLines) return;
|
|
137
|
+
state.logs.push({
|
|
138
|
+
level: "info",
|
|
139
|
+
message: args.map(formatLogArg).join(" ").slice(0, input.limits.logLineBytes)
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
restart: (args) => call({
|
|
143
|
+
name: "restart",
|
|
144
|
+
args: { event: args?.event }
|
|
145
|
+
})
|
|
146
|
+
},
|
|
147
|
+
state
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
//#endregion
|
|
152
|
+
//#region src/guest/shims.ts
|
|
153
|
+
/**
|
|
154
|
+
* A hash of the seed string into a 32-bit state (FNV-1a). The seed derives
|
|
155
|
+
* from the execution id, so the same execution replays the same numbers.
|
|
156
|
+
*/
|
|
157
|
+
function hashSeed(seed) {
|
|
158
|
+
let hash = 2166136261;
|
|
159
|
+
for (let index = 0; index < seed.length; index += 1) {
|
|
160
|
+
hash ^= seed.charCodeAt(index);
|
|
161
|
+
hash = Math.imul(hash, 16777619);
|
|
162
|
+
}
|
|
163
|
+
return hash >>> 0;
|
|
164
|
+
}
|
|
165
|
+
/** mulberry32: small, fast, and good enough for journey branching. */
|
|
166
|
+
function mulberry32(seed) {
|
|
167
|
+
let state = hashSeed(seed);
|
|
168
|
+
return () => {
|
|
169
|
+
state = state + 1831565813 | 0;
|
|
170
|
+
let value = Math.imul(state ^ state >>> 15, 1 | state);
|
|
171
|
+
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
|
|
172
|
+
return ((value ^ value >>> 14) >>> 0) / 4294967296;
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const BLOCKED = [
|
|
176
|
+
"setTimeout",
|
|
177
|
+
"setInterval",
|
|
178
|
+
"setImmediate",
|
|
179
|
+
"fetch",
|
|
180
|
+
"XMLHttpRequest",
|
|
181
|
+
"queueMicrotask"
|
|
182
|
+
];
|
|
183
|
+
function blocked(name) {
|
|
184
|
+
return () => {
|
|
185
|
+
throw new CapabilityError("capability_unavailable", `${name} is not available in journeys: use the api object, which is journaled and replays exactly`);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Installs the shims and returns the undo. Call it before user code runs
|
|
190
|
+
* and restore in a `finally`, so a driver embedded in a Node process does
|
|
191
|
+
* not leak a frozen clock into its host.
|
|
192
|
+
*/
|
|
193
|
+
function installShims(options) {
|
|
194
|
+
const globals = globalThis;
|
|
195
|
+
const saved = /* @__PURE__ */ new Map();
|
|
196
|
+
const save = (name) => {
|
|
197
|
+
saved.set(name, name in globals ? globals[name] : void 0);
|
|
198
|
+
};
|
|
199
|
+
const RealDate = Date;
|
|
200
|
+
class GuestDate extends RealDate {
|
|
201
|
+
constructor(...args) {
|
|
202
|
+
if (args.length === 0) super(options.clock.now);
|
|
203
|
+
else super(...args);
|
|
204
|
+
}
|
|
205
|
+
static now() {
|
|
206
|
+
return options.clock.now;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
save("Date");
|
|
210
|
+
globals.Date = GuestDate;
|
|
211
|
+
const realRandom = Math.random;
|
|
212
|
+
Math.random = mulberry32(options.seed);
|
|
213
|
+
for (const name of BLOCKED) {
|
|
214
|
+
save(name);
|
|
215
|
+
globals[name] = blocked(name);
|
|
216
|
+
}
|
|
217
|
+
return () => {
|
|
218
|
+
Math.random = realRandom;
|
|
219
|
+
for (const [name, value] of saved) if (value === void 0) delete globals[name];
|
|
220
|
+
else globals[name] = value;
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/guest/driver.ts
|
|
226
|
+
function asJourney(module) {
|
|
227
|
+
const journey = module.default;
|
|
228
|
+
if (!journey || typeof journey.run !== "function") throw new Error("Journey module must default-export defineJourney({ ... }) with a run function");
|
|
229
|
+
return journey;
|
|
230
|
+
}
|
|
231
|
+
function asTemplate(module) {
|
|
232
|
+
const template = module;
|
|
233
|
+
if (typeof template.default !== "function" || !template.props) throw new Error("Template module must export a default component and a zod `props` schema");
|
|
234
|
+
return template;
|
|
235
|
+
}
|
|
236
|
+
function readManifest(module) {
|
|
237
|
+
if (module.default && typeof module.default.run === "function") {
|
|
238
|
+
const journey = asJourney(module);
|
|
239
|
+
return manifestOutputSchema.parse({
|
|
240
|
+
kind: "journey",
|
|
241
|
+
trigger: journey.trigger,
|
|
242
|
+
purpose: journey.purpose,
|
|
243
|
+
enrollment: journey.enrollment,
|
|
244
|
+
description: journey.description,
|
|
245
|
+
from: journey.from,
|
|
246
|
+
tags: journey.tags
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const template = asTemplate(module);
|
|
250
|
+
return manifestOutputSchema.parse({
|
|
251
|
+
kind: "template",
|
|
252
|
+
tags: template.tags,
|
|
253
|
+
sendClass: template.sendClass ?? "marketing",
|
|
254
|
+
verifyLink: template.verifyLink ?? false,
|
|
255
|
+
unsubscribeLink: template.unsubscribeLink ?? false,
|
|
256
|
+
propsSchema: z.toJSONSchema(template.props, { target: "draft-2020-12" })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async function runJourneyStep(module, input) {
|
|
260
|
+
const journey = asJourney(module);
|
|
261
|
+
const { api, state } = createApi(input, journey);
|
|
262
|
+
const restore = installShims({
|
|
263
|
+
clock: state.clock,
|
|
264
|
+
seed: input.seed
|
|
265
|
+
});
|
|
266
|
+
try {
|
|
267
|
+
if (await Promise.race([journey.run(input.event, api).then(() => true), state.suspended.then(() => false)])) return journeyStepOutput({
|
|
268
|
+
status: "done",
|
|
269
|
+
logs: state.logs
|
|
270
|
+
});
|
|
271
|
+
return journeyStepOutput({
|
|
272
|
+
status: "suspend",
|
|
273
|
+
call: state.pending,
|
|
274
|
+
logs: state.logs
|
|
275
|
+
});
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error instanceof NondeterminismError) return journeyStepOutput({
|
|
278
|
+
status: "error",
|
|
279
|
+
error: {
|
|
280
|
+
code: "journey_nondeterministic",
|
|
281
|
+
message: error.message
|
|
282
|
+
},
|
|
283
|
+
nondeterministic: {
|
|
284
|
+
expected: error.expected,
|
|
285
|
+
actual: error.actual
|
|
286
|
+
},
|
|
287
|
+
logs: state.logs
|
|
288
|
+
});
|
|
289
|
+
const thrown = error instanceof Error ? error : new Error(String(error));
|
|
290
|
+
return journeyStepOutput({
|
|
291
|
+
status: "error",
|
|
292
|
+
error: {
|
|
293
|
+
code: "journey_error",
|
|
294
|
+
message: thrown.message,
|
|
295
|
+
stack: thrown.stack
|
|
296
|
+
},
|
|
297
|
+
logs: state.logs
|
|
298
|
+
});
|
|
299
|
+
} finally {
|
|
300
|
+
restore();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function journeyStepOutput(output) {
|
|
304
|
+
return journeyStepOutputSchema.parse(output);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Runs one guest invocation against a journey bundle. `module` is the tenant
|
|
308
|
+
* module namespace the bundle's entry imported; `input` is the raw JSON the
|
|
309
|
+
* host handed over. A `template` input is a host bug, not a tenant one: this
|
|
310
|
+
* bundle was built from `journeys/` and carries no renderer.
|
|
311
|
+
*/
|
|
312
|
+
async function runGuest(module, input) {
|
|
313
|
+
const parsed = guestInputSchema.parse(input);
|
|
314
|
+
if (parsed.kind === "manifest") return readManifest(module);
|
|
315
|
+
if (parsed.kind === "template") throw new Error("A journey bundle cannot render a template.");
|
|
316
|
+
return runJourneyStep(module, parsed);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
//#endregion
|
|
320
|
+
export { readManifest as n, runGuest as r, asTemplate as t };
|
package/dist/guest/driver.d.ts
CHANGED
|
@@ -1,21 +1,34 @@
|
|
|
1
1
|
import { a as JourneyStepOutput, o as ManifestOutput, s as TemplateRenderOutput } from "./index-EqBCZnpq.js";
|
|
2
|
+
import { n as Template } from "./emails-5GindgMQ.js";
|
|
2
3
|
//#region src/guest/driver.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* The in-process driver: JSON in, JSON out (spec: Guest protocol). One
|
|
5
|
-
* bundle is one module is one journey or one template, and this turns
|
|
6
|
-
*
|
|
6
|
+
* bundle is one module is one journey or one template, and this turns that
|
|
7
|
+
* bundle's inputs into its outputs. `wasi.ts` wraps it for the sandbox;
|
|
7
8
|
* the simulator and the tests call `runGuest` directly.
|
|
8
9
|
*
|
|
10
|
+
* There are two drivers, one per kind, because a bundle only pays for the
|
|
11
|
+
* one it is. This is the journey half: `manifest` and `journey`. The
|
|
12
|
+
* template half lives in `driver-emails.ts` and is the only one that
|
|
13
|
+
* imports `render.ts`, so react and react-dom (197KB minified) stay out of
|
|
14
|
+
* every journey bundle. `build/index.ts` picks the driver by source kind
|
|
15
|
+
* when it composes an entry; everything both kinds need is here and
|
|
16
|
+
* imported by that file.
|
|
17
|
+
*
|
|
9
18
|
* Every output is validated with the shared schema before it leaves, so a
|
|
10
19
|
* guest bug surfaces here rather than as a host-side parse failure.
|
|
11
20
|
*/
|
|
12
21
|
/** A compiled bundle's module namespace, as `import * as module` sees it. */
|
|
13
22
|
type GuestModule = Record<string, unknown>;
|
|
14
23
|
type GuestOutput = JourneyStepOutput | TemplateRenderOutput | ManifestOutput;
|
|
24
|
+
declare function asTemplate(module: GuestModule): Template;
|
|
25
|
+
declare function readManifest(module: GuestModule): ManifestOutput;
|
|
15
26
|
/**
|
|
16
|
-
* Runs one guest invocation. `module` is the tenant
|
|
17
|
-
* bundle's entry imported; `input` is the raw JSON the
|
|
27
|
+
* Runs one guest invocation against a journey bundle. `module` is the tenant
|
|
28
|
+
* module namespace the bundle's entry imported; `input` is the raw JSON the
|
|
29
|
+
* host handed over. A `template` input is a host bug, not a tenant one: this
|
|
30
|
+
* bundle was built from `journeys/` and carries no renderer.
|
|
18
31
|
*/
|
|
19
32
|
declare function runGuest(module: GuestModule, input: unknown): Promise<GuestOutput>;
|
|
20
33
|
//#endregion
|
|
21
|
-
export { GuestModule, GuestOutput, runGuest };
|
|
34
|
+
export { GuestModule, GuestOutput, asTemplate, readManifest, runGuest };
|
package/dist/guest/driver.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as readManifest, r as runGuest, t as asTemplate } from "./driver-poSdZIj8.js";
|
|
2
2
|
|
|
3
|
-
export { runGuest };
|
|
3
|
+
export { asTemplate, readManifest, runGuest };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ReactElement } from "react";
|
|
3
|
+
//#region ../../packages/shared/src/constants.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The two classes of send, declared on the template rather than passed per
|
|
6
|
+
* call so the class cannot drift between two sends of the same message.
|
|
7
|
+
*
|
|
8
|
+
* Marketing is everything a journey sends on the org's behalf: it carries
|
|
9
|
+
* the RFC 8058 unsubscribe headers and runs the full gate list.
|
|
10
|
+
* Transactional is the developer's own operational mail (a receipt, an
|
|
11
|
+
* export-is-ready notice): it carries neither header and skips consent and
|
|
12
|
+
* the per-user frequency cap, because an unsubscribe link on an invoice is
|
|
13
|
+
* wrong and a receipt must not silently vanish for anyone who once left a
|
|
14
|
+
* newsletter. Suppression, the quota, the sending pause, and the from-domain
|
|
15
|
+
* check still apply to both: those bound cost and protect the shared SES
|
|
16
|
+
* account, and none of them are about what the recipient asked for.
|
|
17
|
+
*/
|
|
18
|
+
declare const SEND_CLASSES: readonly ["marketing", "transactional"];
|
|
19
|
+
type SendClass = (typeof SEND_CLASSES)[number];
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/guest/emails.d.ts
|
|
22
|
+
/** A template's subject line, computed from the props it declares. */
|
|
23
|
+
type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
|
|
24
|
+
/**
|
|
25
|
+
* The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
|
|
26
|
+
* into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
|
|
27
|
+
* and the optional metadata below it; the sandbox renders `default`.
|
|
28
|
+
*/
|
|
29
|
+
type Template<Schema extends z.ZodType = z.ZodType> = {
|
|
30
|
+
default: (props: z.infer<Schema>) => ReactElement;
|
|
31
|
+
props: Schema;
|
|
32
|
+
subject: Subject<Schema> | string;
|
|
33
|
+
/** Defaults to "marketing". */
|
|
34
|
+
sendClass?: SendClass;
|
|
35
|
+
/** The author's labels; the dashboard's only grouping. Defaults to none. */
|
|
36
|
+
tags?: string[];
|
|
37
|
+
/** True asks the host to mint a signed `verifyUrl` prop at send time. */
|
|
38
|
+
verifyLink?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* True asks the host to mint a signed `unsubscribeUrl` prop at send time,
|
|
41
|
+
* for the author's own footer link. Leave it off and a marketing template
|
|
42
|
+
* still gets a platform footer with the link appended at send time.
|
|
43
|
+
*/
|
|
44
|
+
unsubscribeLink?: boolean;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Template as n, SendClass as r, Subject as t };
|
package/dist/guest/emails.d.ts
CHANGED
|
@@ -1,47 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { ReactElement } from "react";
|
|
3
|
-
//#region ../../packages/shared/src/constants.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* The two classes of send, declared on the template rather than passed per
|
|
6
|
-
* call so the class cannot drift between two sends of the same message.
|
|
7
|
-
*
|
|
8
|
-
* Marketing is everything a journey sends on the org's behalf: it carries
|
|
9
|
-
* the RFC 8058 unsubscribe headers and runs the full gate list.
|
|
10
|
-
* Transactional is the developer's own operational mail (a receipt, an
|
|
11
|
-
* export-is-ready notice): it carries neither header and skips consent and
|
|
12
|
-
* the per-user frequency cap, because an unsubscribe link on an invoice is
|
|
13
|
-
* wrong and a receipt must not silently vanish for anyone who once left a
|
|
14
|
-
* newsletter. Suppression, the quota, the sending pause, and the from-domain
|
|
15
|
-
* check still apply to both: those bound cost and protect the shared SES
|
|
16
|
-
* account, and none of them are about what the recipient asked for.
|
|
17
|
-
*/
|
|
18
|
-
declare const SEND_CLASSES: readonly ["marketing", "transactional"];
|
|
19
|
-
type SendClass = (typeof SEND_CLASSES)[number];
|
|
20
|
-
//#endregion
|
|
21
|
-
//#region src/guest/emails.d.ts
|
|
22
|
-
/** A template's subject line, computed from the props it declares. */
|
|
23
|
-
type Subject<Schema extends z.ZodType> = (props: z.infer<Schema>) => string;
|
|
24
|
-
/**
|
|
25
|
-
* The shape `emails/<key>.tsx` exports. `cow build` reads `props` (turned
|
|
26
|
-
* into JSON Schema in the manifest and into `.cow/types.d.ts`), `subject`,
|
|
27
|
-
* and the optional metadata below it; the sandbox renders `default`.
|
|
28
|
-
*/
|
|
29
|
-
type Template<Schema extends z.ZodType = z.ZodType> = {
|
|
30
|
-
default: (props: z.infer<Schema>) => ReactElement;
|
|
31
|
-
props: Schema;
|
|
32
|
-
subject: Subject<Schema> | string;
|
|
33
|
-
/** Defaults to "marketing". */
|
|
34
|
-
sendClass?: SendClass;
|
|
35
|
-
/** The author's labels; the dashboard's only grouping. Defaults to none. */
|
|
36
|
-
tags?: string[];
|
|
37
|
-
/** True asks the host to mint a signed `verifyUrl` prop at send time. */
|
|
38
|
-
verifyLink?: boolean;
|
|
39
|
-
/**
|
|
40
|
-
* True asks the host to mint a signed `unsubscribeUrl` prop at send time,
|
|
41
|
-
* for the author's own footer link. Leave it off and a marketing template
|
|
42
|
-
* still gets a platform footer with the link appended at send time.
|
|
43
|
-
*/
|
|
44
|
-
unsubscribeLink?: boolean;
|
|
45
|
-
};
|
|
46
|
-
//#endregion
|
|
1
|
+
import { n as Template, r as SendClass, t as Subject } from "./emails-5GindgMQ.js";
|
|
47
2
|
export { type SendClass, Subject, Template };
|
package/dist/guest/wasi.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { GuestModule } from "./driver.js";
|
|
1
|
+
import { GuestModule, GuestOutput } from "./driver.js";
|
|
2
2
|
//#region src/guest/wasi.d.ts
|
|
3
|
+
/** The kind-specific driver the entry hands over. */
|
|
4
|
+
type RunGuest = (module: GuestModule, input: unknown) => Promise<GuestOutput>;
|
|
3
5
|
/** Drives one invocation of the module this bundle was built for. */
|
|
4
|
-
declare function main(module: GuestModule): void;
|
|
6
|
+
declare function main(module: GuestModule, runGuest: RunGuest): void;
|
|
5
7
|
//#endregion
|
|
6
8
|
export { main };
|
package/dist/guest/wasi.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { t as runGuest } from "./driver-BXuYP007.js";
|
|
2
|
-
|
|
3
1
|
//#region src/guest/wasi.ts
|
|
4
2
|
const STDIN = 0;
|
|
5
3
|
const STDOUT = 1;
|
|
@@ -27,7 +25,7 @@ function write(fd, text) {
|
|
|
27
25
|
Javy.IO.writeSync(fd, new TextEncoder().encode(text));
|
|
28
26
|
}
|
|
29
27
|
/** Drives one invocation of the module this bundle was built for. */
|
|
30
|
-
function main(module) {
|
|
28
|
+
function main(module, runGuest) {
|
|
31
29
|
const fail = (error) => {
|
|
32
30
|
write(STDERR, error instanceof Error ? error.message : String(error));
|
|
33
31
|
throw error;
|
package/dist/index.js
CHANGED
|
@@ -3170,8 +3170,15 @@ const insertAppSchema = createInsertSchema(apps$1);
|
|
|
3170
3170
|
const bytea = customType({ dataType: () => "bytea" });
|
|
3171
3171
|
/**
|
|
3172
3172
|
* What an artifact is: a journey or template bundle as `cow build` emitted
|
|
3173
|
-
* it, the
|
|
3174
|
-
*
|
|
3173
|
+
* it, or the gzipped source tarball of the repo the push was built from.
|
|
3174
|
+
*
|
|
3175
|
+
* `module` is still a value of the Postgres enum and no longer a value here
|
|
3176
|
+
* (ticket 22/05). Compiled modules are not stored: a module is a
|
|
3177
|
+
* deterministic function of its bundle, so the rows were Postgres caching
|
|
3178
|
+
* what the sandbox's own disk cache already holds, at about 2MB each of
|
|
3179
|
+
* which 1.34MB is the QuickJS engine Javy links into every one. Dropping
|
|
3180
|
+
* the enum value itself would mean recreating the type and rewriting the
|
|
3181
|
+
* column, for a label nothing writes.
|
|
3175
3182
|
*/
|
|
3176
3183
|
const artifactKindEnum = pgEnum("artifact_kind", [
|
|
3177
3184
|
"bundle",
|
|
@@ -4395,10 +4402,16 @@ const versionStatusEnum = pgEnum("version_status", [
|
|
|
4395
4402
|
* use; an execution pins the journey version it started on for its whole
|
|
4396
4403
|
* life (ADR 0004).
|
|
4397
4404
|
*
|
|
4398
|
-
*
|
|
4399
|
-
*
|
|
4400
|
-
*
|
|
4401
|
-
*
|
|
4405
|
+
* The content address is the manifest's `bundle`, which is what the
|
|
4406
|
+
* developer actually pushed. `moduleDigest` is a derived compile result,
|
|
4407
|
+
* null until the compile settles and rewritable afterwards: the toolchain
|
|
4408
|
+
* that produced it is ours, not the tenant's, so bumping Javy is a
|
|
4409
|
+
* background recompile rather than a new version in a customer's history
|
|
4410
|
+
* (ADR 0011, amended 2026-09-19).
|
|
4411
|
+
*
|
|
4412
|
+
* The uniqueness that follows is "one ready version per key per bundle": a
|
|
4413
|
+
* key whose bundle does not compile records every attempt, and the next
|
|
4414
|
+
* push of that bundle is a retry rather than a duplicate.
|
|
4402
4415
|
*
|
|
4403
4416
|
* `pushedAt` is the ordering key rather than a creation stamp, because an
|
|
4404
4417
|
* unchanged key gets no new version: a push that re-sends bytes a version
|
|
@@ -4430,7 +4443,7 @@ const versions$1 = pgTable("versions", {
|
|
|
4430
4443
|
}),
|
|
4431
4444
|
error: text("error")
|
|
4432
4445
|
}, (table) => [
|
|
4433
|
-
uniqueIndex("
|
|
4446
|
+
uniqueIndex("versions_org_id_kind_key_bundle_unique").on(table.orgId, table.kind, table.key, sql`(${table.manifest}->>'bundle')`).where(sql`${table.status} = 'ready'`),
|
|
4434
4447
|
index("versions_org_id_kind_key_pushed_at_idx").on(table.orgId, table.kind, table.key, table.pushedAt),
|
|
4435
4448
|
index("versions_org_id_push_id_idx").on(table.orgId, table.pushId),
|
|
4436
4449
|
foreignKey({
|
|
@@ -6343,15 +6356,23 @@ const versionSchema = selectVersionSchema.extend({
|
|
|
6343
6356
|
* version" column shows (the short digest, when it was pushed, whether it
|
|
6344
6357
|
* compiled) and the push its source archive lives on. The manifest entry is
|
|
6345
6358
|
* left out, because a journey row already carries what its own entry says.
|
|
6359
|
+
*
|
|
6360
|
+
* `bundle` is lifted out of that entry because it is the version's address:
|
|
6361
|
+
* it is there from the moment the push lands, and it does not move when a
|
|
6362
|
+
* recompile rewrites `moduleDigest` (ADR 0011, amended 2026-09-19). The
|
|
6363
|
+
* compiled module's digest is not on the wire at all; nothing outside the
|
|
6364
|
+
* sandbox has a use for it.
|
|
6346
6365
|
*/
|
|
6347
6366
|
const versionSummarySchema = selectVersionSchema.pick({
|
|
6348
6367
|
id: true,
|
|
6349
6368
|
status: true,
|
|
6350
|
-
moduleDigest: true,
|
|
6351
6369
|
pushId: true,
|
|
6352
6370
|
appId: true,
|
|
6353
6371
|
error: true
|
|
6354
|
-
}).extend({
|
|
6372
|
+
}).extend({
|
|
6373
|
+
pushedAt: z.iso.datetime(),
|
|
6374
|
+
bundle: digestSchema
|
|
6375
|
+
});
|
|
6355
6376
|
/**
|
|
6356
6377
|
* A push as the API reports it, with the versions it created: `cow push`
|
|
6357
6378
|
* returns as soon as the upload is stored, so what is compiling, what is
|
|
@@ -8136,11 +8157,15 @@ const CONFIG_FILES = [
|
|
|
8136
8157
|
* cannot be inlined). There is no `src/` fallback: a missing
|
|
8137
8158
|
* build surfaces here as an unresolved import rather than as a second code
|
|
8138
8159
|
* path whose only job is to hide it.
|
|
8160
|
+
*
|
|
8161
|
+
* `guest` and `guest-emails` are the two drivers and a bundle links exactly
|
|
8162
|
+
* one, picked by source kind (`DRIVER`).
|
|
8139
8163
|
*/
|
|
8140
8164
|
const GUEST_ALIAS = {
|
|
8141
8165
|
"@cowliss/cli/journeys": guestFile("journeys"),
|
|
8142
8166
|
"@cowliss/cli/emails": guestFile("emails"),
|
|
8143
8167
|
"@cowliss/cli/guest": guestFile("guest"),
|
|
8168
|
+
"@cowliss/cli/guest-emails": guestFile("guest-emails"),
|
|
8144
8169
|
"@cowliss/cli/guest-wasi": guestFile("guest-wasi"),
|
|
8145
8170
|
"@cowliss/cli/guest-prelude": guestFile("guest-prelude")
|
|
8146
8171
|
};
|
|
@@ -8325,23 +8350,33 @@ async function bundle(projectDir, outFile, entry) {
|
|
|
8325
8350
|
};
|
|
8326
8351
|
}
|
|
8327
8352
|
/**
|
|
8353
|
+
* The driver a bundle links, one per kind. Only the template one imports
|
|
8354
|
+
* the renderer, so naming it from a journey entry is what used to put react
|
|
8355
|
+
* and react-dom in all 95 journey bundles (`guest/driver.ts`).
|
|
8356
|
+
*/
|
|
8357
|
+
const DRIVER = {
|
|
8358
|
+
journeys: "@cowliss/cli/guest",
|
|
8359
|
+
emails: "@cowliss/cli/guest-emails"
|
|
8360
|
+
};
|
|
8361
|
+
/**
|
|
8328
8362
|
* The artifact the sandbox compiles: stdin in, stdout out, no exports. The
|
|
8329
8363
|
* prelude is imported first on purpose: ESM evaluates in import order, and
|
|
8330
8364
|
* its `MessageChannel` and `console` walls have to stand before react-dom
|
|
8331
8365
|
* or tenant code initializes.
|
|
8332
8366
|
*/
|
|
8333
|
-
function wasiEntry(file) {
|
|
8367
|
+
function wasiEntry(file, kind) {
|
|
8334
8368
|
return `import "@cowliss/cli/guest-prelude";
|
|
8335
8369
|
import * as module from ${JSON.stringify(file)};
|
|
8336
8370
|
import { main } from "@cowliss/cli/guest-wasi";
|
|
8337
|
-
|
|
8371
|
+
import { runGuest } from ${JSON.stringify(DRIVER[kind])};
|
|
8372
|
+
main(module, runGuest);
|
|
8338
8373
|
`;
|
|
8339
8374
|
}
|
|
8340
8375
|
/** The local twin: the same graph, importable by Node for the manifest and `cow test`. */
|
|
8341
|
-
function nodeEntry(file) {
|
|
8376
|
+
function nodeEntry(file, kind) {
|
|
8342
8377
|
return `import * as module from ${JSON.stringify(file)};
|
|
8343
8378
|
export { module };
|
|
8344
|
-
export { runGuest } from
|
|
8379
|
+
export { runGuest } from ${JSON.stringify(DRIVER[kind])};
|
|
8345
8380
|
`;
|
|
8346
8381
|
}
|
|
8347
8382
|
let messagePortsUnreffed = false;
|
|
@@ -8495,7 +8530,7 @@ async function buildProject(projectDir) {
|
|
|
8495
8530
|
const bundles = await Promise.all(files.map(async (source) => {
|
|
8496
8531
|
const nodeFile = join(outDir, "node", source.kind, `${source.key}.mjs`);
|
|
8497
8532
|
const wasiFile = join(outDir, "bundles", source.kind, `${source.key}.js`);
|
|
8498
|
-
const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file)), bundle(projectDir, nodeFile, nodeEntry(source.file))]);
|
|
8533
|
+
const [wasi, node] = await Promise.all([bundle(projectDir, wasiFile, wasiEntry(source.file, source.kind)), bundle(projectDir, nodeFile, nodeEntry(source.file, source.kind))]);
|
|
8499
8534
|
await writeFile(nodeFile, node.code);
|
|
8500
8535
|
await writeFile(`${nodeFile}.map`, node.map);
|
|
8501
8536
|
await writeFile(wasiFile, wasi.code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cowliss/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cow": "./dist/index.js"
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
"types": "./dist/guest/driver.d.ts",
|
|
19
19
|
"default": "./dist/guest/driver.js"
|
|
20
20
|
},
|
|
21
|
+
"./guest-emails": {
|
|
22
|
+
"types": "./dist/guest/driver-emails.d.ts",
|
|
23
|
+
"default": "./dist/guest/driver-emails.js"
|
|
24
|
+
},
|
|
21
25
|
"./guest-wasi": {
|
|
22
26
|
"types": "./dist/guest/wasi.d.ts",
|
|
23
27
|
"default": "./dist/guest/wasi.js"
|
|
@@ -37,6 +41,18 @@
|
|
|
37
41
|
"tar": "^7.5.22",
|
|
38
42
|
"zod": "^4.4.3"
|
|
39
43
|
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"react": "^19",
|
|
46
|
+
"react-dom": "^19"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"react": {
|
|
50
|
+
"optional": true
|
|
51
|
+
},
|
|
52
|
+
"react-dom": {
|
|
53
|
+
"optional": true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
40
56
|
"devDependencies": {
|
|
41
57
|
"@babel/types": "^7.29.8",
|
|
42
58
|
"@cowliss/api-client": "0.0.0",
|