@powerhousedao/reactor-workflow 6.2.3-dev.11

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/index.js ADDED
@@ -0,0 +1,3771 @@
1
+ import { _ as FileTooLargeError, a as redactMessage, c as DEFAULT_EGRESS_POLICY, f as extractDedupeKey, n as containsRedactedMarker, o as rememberSecrets, r as redact, s as secretsFor, y as maxFileBytes } from "./redact-C7LWgAyD.js";
2
+ import { C as pieceModuleRef, S as localFirstResolver, _ as secretRefFromId, a as CompositeBlockExecutor, b as PieceWorkerError, c as storeHandlers, d as declaredConnectionIds, f as shapeConnection, g as parseSecretRef, h as SecretNotFoundError, i as ActivepiecesBlockExecutor, l as BoundConnectionResolver, m as SecretDeletedError, n as packagePieces, o as parseBlockType, r as runWorkflow, s as reactorHandlers, t as PieceRegistry, v as PieceWorkerPool, w as ensurePieceBundle, x as PieceWorkerTimeoutError, y as PieceWorker } from "./piece-registry-BWWihLyp.js";
3
+ import { WebhookHandshakeStrategy } from "@powerhousedao/pieces-framework";
4
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
+ import { open, readFile, rm } from "node:fs/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID } from "node:crypto";
8
+ import { Cron } from "croner";
9
+ import { AsyncLocalStorage } from "node:async_hooks";
10
+ import { childLogger, createAction } from "document-model";
11
+ import { actions } from "@powerhousedao/workflow/document-models/connection";
12
+ import { Kind, parse } from "graphql";
13
+ import { BaseReadModel, defaultReadModelIndexingConfig } from "@powerhousedao/reactor";
14
+ //#region src/reactor/attachment-port.ts
15
+ const logger$4 = childLogger(["workflow", "attachments"]);
16
+ function refOf(result) {
17
+ const direct = result.ref;
18
+ if (typeof direct === "string") return direct;
19
+ const nested = result.reservation?.ref;
20
+ if (typeof nested === "string") return nested;
21
+ throw new Error("The attachment store returned no reference for the upload");
22
+ }
23
+ async function writeCapped(body, destPath, limit) {
24
+ const reader = body.getReader();
25
+ const handle = await open(destPath, "w");
26
+ let written = 0;
27
+ try {
28
+ for (;;) {
29
+ const { done, value } = await reader.read();
30
+ if (done) break;
31
+ written += value.byteLength;
32
+ if (written > limit) {
33
+ await reader.cancel();
34
+ throw new FileTooLargeError(written, limit);
35
+ }
36
+ await handle.write(value);
37
+ }
38
+ } catch (error) {
39
+ await handle.close();
40
+ await rm(destPath, { force: true });
41
+ throw error;
42
+ }
43
+ await handle.close();
44
+ }
45
+ function createAttachmentPort(client, documentIdFor, canReadRef) {
46
+ return {
47
+ async read(ref, destPath) {
48
+ const documentId = documentIdFor();
49
+ if (!documentId) throw new Error(`Cannot resolve ${ref}: no workflow document is in scope to authorize the read`);
50
+ if (!await canReadRef(documentId, ref)) throw new Error(`Cannot resolve ${ref}: workflow document "${documentId}" does not reference it`);
51
+ const limit = maxFileBytes();
52
+ const { header, body } = await client.download({
53
+ documentId,
54
+ ref
55
+ });
56
+ if (typeof header.sizeBytes === "number" && Number.isFinite(header.sizeBytes) && header.sizeBytes > limit) {
57
+ await body.cancel().catch(() => void 0);
58
+ throw new FileTooLargeError(header.sizeBytes, limit);
59
+ }
60
+ await writeCapped(body, destPath, limit);
61
+ const contentType = header.mimeType !== void 0 && header.mimeType !== "" ? header.mimeType : void 0;
62
+ return {
63
+ fileName: header.fileName,
64
+ contentType
65
+ };
66
+ },
67
+ async write(file) {
68
+ const bytes = await readFile(file.path);
69
+ const ref = refOf(await client.upload({
70
+ file: new Blob([new Uint8Array(bytes)], { type: file.contentType ?? "application/octet-stream" }),
71
+ fileName: file.fileName,
72
+ mimeType: file.contentType
73
+ }));
74
+ logger$4.debug(`Ingested ${file.fileName} (${file.size} bytes) as ${ref}`);
75
+ return ref;
76
+ }
77
+ };
78
+ }
79
+ //#endregion
80
+ //#region src/reactor/package-name.ts
81
+ const WORKFLOW_PACKAGE_NAME = "@powerhousedao/workflow";
82
+ //#endregion
83
+ //#region src/reactor/reactor-piece.ts
84
+ const REACTOR_PIECE = "@powerhousedao/piece-reactor";
85
+ function action(name) {
86
+ return `${REACTOR_PIECE}#${name}`;
87
+ }
88
+ function trigger(name) {
89
+ return `${REACTOR_PIECE}#trigger:${name}`;
90
+ }
91
+ const DOCUMENT_CREATE_BLOCK = action("document-create");
92
+ const DOCUMENT_DISPATCH_BLOCK = action("document-dispatch");
93
+ const DOCUMENT_GET_BLOCK = action("document-get");
94
+ const DOCUMENT_FIND_BLOCK = action("document-find");
95
+ const DOCUMENT_SCHEMA_BLOCK = action("document-schema");
96
+ const DOCUMENT_TYPES_BLOCK = action("document-types");
97
+ const DOCUMENT_EVENT_BLOCK = trigger("document-event");
98
+ const DOCUMENT_CREATED_BLOCK = trigger("document-created");
99
+ const DOCUMENT_DELETED_BLOCK = trigger("document-deleted");
100
+ function staticString(value) {
101
+ if (typeof value !== "string") return void 0;
102
+ const trimmed = value.trim();
103
+ if (!trimmed || trimmed.includes("{{")) return void 0;
104
+ return trimmed;
105
+ }
106
+ //#endregion
107
+ //#region src/reactor/output-tree.ts
108
+ const MAX_DEPTH = 6;
109
+ function typeName(node) {
110
+ switch (node.kind) {
111
+ case Kind.NON_NULL_TYPE: {
112
+ const inner = typeName(node.type);
113
+ return {
114
+ name: inner.name,
115
+ display: `${inner.display}!`
116
+ };
117
+ }
118
+ case Kind.LIST_TYPE: {
119
+ const inner = typeName(node.type);
120
+ return {
121
+ name: inner.name,
122
+ display: `[${inner.display}]`
123
+ };
124
+ }
125
+ default: return {
126
+ name: node.name.value,
127
+ display: node.name.value
128
+ };
129
+ }
130
+ }
131
+ function fieldsFromSdl(sdl, rootType) {
132
+ let definitions;
133
+ try {
134
+ definitions = parse(sdl).definitions;
135
+ } catch {
136
+ return [];
137
+ }
138
+ const types = /* @__PURE__ */ new Map();
139
+ let firstType;
140
+ let stateType;
141
+ for (const def of definitions) {
142
+ if (def.kind !== Kind.OBJECT_TYPE_DEFINITION && def.kind !== Kind.INPUT_OBJECT_TYPE_DEFINITION) continue;
143
+ const name = def.name.value;
144
+ types.set(name, def.fields ?? []);
145
+ firstType ??= name;
146
+ if (name.endsWith("State") && !name.endsWith("LocalState")) stateType ??= name;
147
+ }
148
+ const root = rootType ?? stateType ?? firstType;
149
+ if (!root) return [];
150
+ const build = (name, depth) => {
151
+ const fields = types.get(name);
152
+ if (!fields || depth > MAX_DEPTH) return [];
153
+ return fields.map((field) => {
154
+ const { name: inner, display } = typeName(field.type);
155
+ const children = build(inner, depth + 1);
156
+ return {
157
+ name: field.name.value,
158
+ type: display,
159
+ description: field.description?.value,
160
+ ...children.length > 0 ? { children } : {}
161
+ };
162
+ });
163
+ };
164
+ return build(root, 0);
165
+ }
166
+ function mergeNodes(nodes) {
167
+ const byName = /* @__PURE__ */ new Map();
168
+ for (const node of nodes) {
169
+ const existing = byName.get(node.name);
170
+ if (existing?.children && node.children) existing.children = mergeNodes([...existing.children, ...node.children]);
171
+ else if (!byName.has(node.name)) byName.set(node.name, node);
172
+ }
173
+ return [...byName.values()];
174
+ }
175
+ function fromOutputSchema(schema) {
176
+ const fields = schema?.fields;
177
+ if (!Array.isArray(fields)) return [];
178
+ const convert = (field) => {
179
+ const inner = field.children ?? field.properties;
180
+ const items = field.listItems;
181
+ const childNodes = mergeNodes((inner ?? items ?? []).flatMap(convert));
182
+ const path = typeof field.value === "string" ? field.value : field.key ?? "";
183
+ if (path === "") return childNodes;
184
+ const segments = path.split(".");
185
+ let node = {
186
+ name: segments[segments.length - 1],
187
+ type: items ? "array" : field.format ?? (childNodes.length > 0 ? "object" : "value"),
188
+ description: field.description,
189
+ ...childNodes.length > 0 ? { children: childNodes } : {}
190
+ };
191
+ for (let i = segments.length - 2; i >= 0; i--) node = {
192
+ name: segments[i],
193
+ type: "object",
194
+ children: [node]
195
+ };
196
+ return [node];
197
+ };
198
+ return mergeNodes(fields.flatMap(convert)).filter((node) => node.name);
199
+ }
200
+ function hasOutputSchemaFields(schema) {
201
+ const fields = schema?.fields;
202
+ return Array.isArray(fields) && fields.length > 0;
203
+ }
204
+ function fromSample(value, depth = 0) {
205
+ if (value === null || typeof value !== "object" || depth > MAX_DEPTH) return [];
206
+ return (Array.isArray(value) ? value.slice(0, 1).map((item) => ["0", item]) : Object.entries(value)).map(([name, child]) => {
207
+ const kind = Array.isArray(child) ? "array" : child === null ? "null" : typeof child;
208
+ const children = fromSample(child, depth + 1);
209
+ return {
210
+ name,
211
+ type: kind,
212
+ ...children.length > 0 ? { children } : {}
213
+ };
214
+ });
215
+ }
216
+ const leaf = (name, type, description) => ({
217
+ name,
218
+ type,
219
+ ...description ? { description } : {}
220
+ });
221
+ const OPERATION_NODE = {
222
+ name: "operation",
223
+ type: "object",
224
+ children: [leaf("index", "Int!"), leaf("timestampUtcMs", "String!")]
225
+ };
226
+ function documentBlockTree(stateChildren) {
227
+ return [
228
+ leaf("documentId", "PHID!"),
229
+ leaf("documentType", "String!"),
230
+ leaf("name", "String"),
231
+ {
232
+ name: "state",
233
+ type: "object",
234
+ description: "Document global state after the actions applied",
235
+ ...stateChildren.length > 0 ? { children: stateChildren } : {}
236
+ }
237
+ ];
238
+ }
239
+ function documentGetTree(stateChildren) {
240
+ return [
241
+ ...documentBlockTree(stateChildren).filter((node) => node.name !== "state"),
242
+ leaf("slug", "String"),
243
+ {
244
+ name: "state",
245
+ type: "object",
246
+ description: "Document global state as read",
247
+ ...stateChildren.length > 0 ? { children: stateChildren } : {}
248
+ }
249
+ ];
250
+ }
251
+ function documentFindTree() {
252
+ return [leaf("count", "Int!"), {
253
+ name: "documents",
254
+ type: "array",
255
+ children: [
256
+ leaf("documentId", "PHID!"),
257
+ leaf("documentType", "String!"),
258
+ leaf("name", "String"),
259
+ leaf("slug", "String")
260
+ ]
261
+ }];
262
+ }
263
+ function documentTypesTree() {
264
+ return [leaf("count", "Int!"), {
265
+ name: "types",
266
+ type: "array",
267
+ children: [leaf("documentType", "String!"), leaf("name", "String")]
268
+ }];
269
+ }
270
+ function documentSchemaTree() {
271
+ return [
272
+ leaf("documentType", "String!"),
273
+ leaf("name", "String"),
274
+ leaf("stateSchema", "String", "SDL of the global state type"),
275
+ {
276
+ name: "actions",
277
+ type: "array",
278
+ description: "Dispatchable actions with their input SDL",
279
+ children: [
280
+ leaf("type", "String!"),
281
+ leaf("module", "String"),
282
+ leaf("inputSchema", "String")
283
+ ]
284
+ }
285
+ ];
286
+ }
287
+ function lifecycleTriggerTree() {
288
+ return [
289
+ leaf("documentId", "PHID!"),
290
+ leaf("documentType", "String"),
291
+ leaf("name", "String", "Set on creation only"),
292
+ leaf("driveId", "PHID", "Null for a document that belongs to no drive"),
293
+ leaf("parentId", "PHID"),
294
+ OPERATION_NODE
295
+ ];
296
+ }
297
+ function scheduleTriggerTree() {
298
+ return [
299
+ leaf("scheduledFor", "DateTime!", "The slot that came due (ISO 8601)"),
300
+ leaf("firedAt", "DateTime!", "When the run actually started"),
301
+ leaf("timezone", "String!"),
302
+ leaf("cron", "String", "Cron mode only"),
303
+ leaf("everyMs", "Int", "Interval mode only")
304
+ ];
305
+ }
306
+ function webhookTriggerTree() {
307
+ return [
308
+ leaf("method", "String!", "Uppercase HTTP method"),
309
+ leaf("path", "String!"),
310
+ leaf("headers", "JSONObject!", "Lowercased names; credentials redacted"),
311
+ leaf("queryParams", "JSONObject!"),
312
+ leaf("body", "Unknown", "Parsed JSON or form fields; text otherwise")
313
+ ];
314
+ }
315
+ function documentEventTree(actionInputChildren) {
316
+ return [
317
+ leaf("documentId", "PHID!"),
318
+ leaf("documentType", "String!"),
319
+ leaf("branch", "String!"),
320
+ leaf("scope", "String!"),
321
+ {
322
+ name: "action",
323
+ type: "object",
324
+ children: [leaf("type", "String!"), {
325
+ name: "input",
326
+ type: "object",
327
+ ...actionInputChildren.length > 0 ? { children: actionInputChildren } : {}
328
+ }]
329
+ },
330
+ OPERATION_NODE
331
+ ];
332
+ }
333
+ //#endregion
334
+ //#region src/reactor/first-party-logos.ts
335
+ const PAPERLESS_LOGO = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCA0OCI+PHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzE3NTQxZiIvPjxwYXRoIGQ9Ik0xNCAxMGgxM2w3IDd2MjFhMiAyIDAgMCAxLTIgMkgxNmEyIDIgMCAwIDEtMi0yVjEyYTIgMiAwIDAgMSAyLTJ6IiBmaWxsPSIjZmZmIi8+PHBhdGggZD0iTTI3IDEwbDcgN2gtN3oiIGZpbGw9IiM5ZmQzYTYiLz48ZyBmaWxsPSIjMTc1NDFmIj48cmVjdCB4PSIxOCIgeT0iMjIiIHdpZHRoPSIxNiIgaGVpZ2h0PSIyIiByeD0iMSIvPjxyZWN0IHg9IjE4IiB5PSIyNyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjIiIHJ4PSIxIi8+PHJlY3QgeD0iMTgiIHk9IjMyIiB3aWR0aD0iMTAiIGhlaWdodD0iMiIgcng9IjEiLz48L2c+PC9zdmc+";
336
+ //#endregion
337
+ //#region src/reactor/unsupported-pieces.ts
338
+ const SERVER_ONLY_PIECES = new Set([
339
+ "@activepieces/piece-ai",
340
+ "@activepieces/piece-text-ai",
341
+ "@activepieces/piece-image-ai",
342
+ "@activepieces/piece-utility-ai",
343
+ "@activepieces/piece-agent",
344
+ "@activepieces/piece-todos",
345
+ "@activepieces/piece-tables",
346
+ "@activepieces/piece-subflows"
347
+ ]);
348
+ //#endregion
349
+ //#region src/reactor/piece-catalog.ts
350
+ const CATALOG_URL = "https://cloud.activepieces.com/api/v1/pieces";
351
+ const CACHE_TTL_MS = 3600 * 1e3;
352
+ function aiLast(audience) {
353
+ return audience === "ai" ? 1 : 0;
354
+ }
355
+ function pieceUrl(packageName) {
356
+ return `${CATALOG_URL}/${packageName}?audience=all`;
357
+ }
358
+ async function fetchJson(url, timeoutMs = 3e4) {
359
+ const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
360
+ if (!response.ok) throw new Error(`${url} responded ${response.status}`);
361
+ return response.json();
362
+ }
363
+ async function fetchCatalogWithSuggestions() {
364
+ const raw = await fetchJson(`${CATALOG_URL}?suggestionType=ACTION_AND_TRIGGER`, 12e4);
365
+ return Array.isArray(raw) ? raw : [];
366
+ }
367
+ let catalogCache;
368
+ const FIRST_PARTY_PIECES = [{
369
+ name: "@powerhousedao/piece-paperless-ngx",
370
+ displayName: "Paperless-ngx",
371
+ description: "Manage documents in a self-hosted paperless-ngx archive: upload, search, tag, and react to new documents.",
372
+ logoUrl: PAPERLESS_LOGO,
373
+ version: "0.1.0",
374
+ actionCount: 9,
375
+ triggerCount: 2,
376
+ categories: ["CONTENT_AND_FILES"],
377
+ auth: {
378
+ type: "CUSTOM_AUTH",
379
+ displayName: "paperless-ngx",
380
+ required: true,
381
+ props: {
382
+ base_url: {
383
+ type: "SHORT_TEXT",
384
+ displayName: "Base URL",
385
+ required: true,
386
+ description: "e.g. https://paperless.example.com — no trailing slash, no /api suffix"
387
+ },
388
+ token: {
389
+ type: "SECRET_TEXT",
390
+ displayName: "API Token",
391
+ required: true,
392
+ description: "paperless web UI -> My Profile -> API Token"
393
+ }
394
+ }
395
+ }
396
+ }, {
397
+ name: "@powerhousedao/piece-docling",
398
+ displayName: "Docling",
399
+ description: "Convert documents (PDF, DOCX, PPTX, images, HTML, …) to Markdown, docling-document JSON, HTML, DocTags and plain text via a docling-serve v1 API (self-hosted or Docling for IBM watsonx).",
400
+ logoUrl: "data:image/svg+xml," + encodeURIComponent("<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 48 48\"><rect width=\"48\" height=\"48\" rx=\"10\" fill=\"#1e3a8a\"/><path d=\"M14 10h14l8 8v20a2 2 0 0 1-2 2H14a2 2 0 0 1-2-2V12a2 2 0 0 1 2-2z\" fill=\"#fff\"/><path d=\"M28 10v8h8\" fill=\"none\" stroke=\"#1e3a8a\" stroke-width=\"2\"/><path d=\"M18 24h12M18 29h12M18 34h8\" stroke=\"#1e3a8a\" stroke-width=\"2\"/></svg>"),
401
+ version: "1.0.0",
402
+ actionCount: 6,
403
+ triggerCount: 0,
404
+ categories: ["CONTENT_AND_FILES"],
405
+ auth: {
406
+ type: "CUSTOM_AUTH",
407
+ displayName: "Docling Serve",
408
+ required: true,
409
+ props: {
410
+ base_url: {
411
+ type: "SHORT_TEXT",
412
+ displayName: "Service URL",
413
+ required: true
414
+ },
415
+ api_key: {
416
+ type: "SECRET_TEXT",
417
+ displayName: "API Key",
418
+ required: false
419
+ }
420
+ }
421
+ }
422
+ }];
423
+ async function fetchPieceCatalog() {
424
+ if (catalogCache && catalogCache.expiresAt > Date.now()) return catalogCache.value;
425
+ const cloud = (await fetchJson(CATALOG_URL)).filter((entry) => typeof entry.name === "string" && typeof entry.version === "string" && !SERVER_ONLY_PIECES.has(entry.name) && (typeof entry.actions === "number" && entry.actions > 0 || typeof entry.triggers === "number" && entry.triggers > 0)).map((entry) => ({
426
+ name: entry.name,
427
+ displayName: entry.displayName ?? entry.name,
428
+ description: entry.description ?? "",
429
+ logoUrl: entry.logoUrl ?? "",
430
+ version: entry.version,
431
+ actionCount: typeof entry.actions === "number" ? entry.actions : 0,
432
+ triggerCount: typeof entry.triggers === "number" ? entry.triggers : 0,
433
+ categories: entry.categories ?? [],
434
+ auth: entry.auth ?? null
435
+ }));
436
+ const shortName = (n) => n.slice(n.lastIndexOf("/") + 1);
437
+ const cloudShorts = new Set(cloud.map((e) => shortName(e.name)));
438
+ const value = [...cloud, ...FIRST_PARTY_PIECES.filter((p) => !cloudShorts.has(shortName(p.name)))].sort((a, b) => a.displayName.localeCompare(b.displayName));
439
+ catalogCache = {
440
+ value,
441
+ expiresAt: Date.now() + CACHE_TTL_MS
442
+ };
443
+ return value;
444
+ }
445
+ const detailCache = /* @__PURE__ */ new Map();
446
+ async function fetchPieceDetail(packageName) {
447
+ const cached = detailCache.get(packageName);
448
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
449
+ const value = await fetchJson(pieceUrl(packageName));
450
+ detailCache.set(packageName, {
451
+ value,
452
+ expiresAt: Date.now() + CACHE_TTL_MS
453
+ });
454
+ return value;
455
+ }
456
+ const triggersCache = /* @__PURE__ */ new Map();
457
+ async function fetchPieceTriggers(packageName) {
458
+ const cached = triggersCache.get(packageName);
459
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
460
+ const detail = await fetchJson(pieceUrl(packageName));
461
+ const version = detail.version ?? "";
462
+ const triggersRecord = detail.triggers && typeof detail.triggers === "object" ? detail.triggers : {};
463
+ const triggers = Object.entries(triggersRecord).map(([name, trigger]) => ({
464
+ name,
465
+ displayName: trigger.displayName ?? name,
466
+ description: trigger.description ?? "",
467
+ strategy: trigger.type ?? "",
468
+ blockType: `${packageName}@${version}#trigger:${name}`
469
+ }));
470
+ const value = {
471
+ name: packageName,
472
+ displayName: detail.displayName ?? packageName,
473
+ version,
474
+ triggers,
475
+ auth: detail.auth ?? null
476
+ };
477
+ triggersCache.set(packageName, {
478
+ value,
479
+ expiresAt: Date.now() + CACHE_TTL_MS
480
+ });
481
+ return value;
482
+ }
483
+ const actionsCache = /* @__PURE__ */ new Map();
484
+ async function fetchPieceActions(packageName) {
485
+ const cached = actionsCache.get(packageName);
486
+ if (cached && cached.expiresAt > Date.now()) return cached.value;
487
+ const detail = await fetchJson(pieceUrl(packageName));
488
+ const version = detail.version ?? "";
489
+ const actionsRecord = detail.actions && typeof detail.actions === "object" ? detail.actions : {};
490
+ const actions = Object.entries(actionsRecord).map(([name, action]) => ({
491
+ name,
492
+ displayName: action.displayName ?? name,
493
+ description: action.description ?? "",
494
+ blockType: `${packageName}@${version}#${name}`,
495
+ audience: action.audience ?? null
496
+ })).sort((a, b) => aiLast(a.audience) - aiLast(b.audience));
497
+ const value = {
498
+ name: packageName,
499
+ displayName: detail.displayName ?? packageName,
500
+ version,
501
+ actions,
502
+ auth: detail.auth ?? null
503
+ };
504
+ actionsCache.set(packageName, {
505
+ value,
506
+ expiresAt: Date.now() + CACHE_TTL_MS
507
+ });
508
+ return value;
509
+ }
510
+ //#endregion
511
+ //#region src/reactor/local-catalog.ts
512
+ function localBlockType(pieceName, name, kind) {
513
+ return kind === "trigger" ? `${pieceName}#trigger:${name}` : `${pieceName}#${name}`;
514
+ }
515
+ function catalogEntry(descriptor, pieceName, version) {
516
+ return {
517
+ name: pieceName,
518
+ displayName: descriptor.displayName || pieceName,
519
+ description: descriptor.description ?? "",
520
+ logoUrl: descriptor.logoUrl ?? "",
521
+ version,
522
+ actionCount: descriptor.actions.length,
523
+ triggerCount: descriptor.triggers.length,
524
+ categories: descriptor.categories ?? [],
525
+ auth: descriptor.auth ?? null
526
+ };
527
+ }
528
+ function actionsResult(descriptor, pieceName, version) {
529
+ return {
530
+ name: pieceName,
531
+ displayName: descriptor.displayName || pieceName,
532
+ version,
533
+ actions: descriptor.actions.map((action) => ({
534
+ name: action.name,
535
+ displayName: action.displayName,
536
+ description: action.description ?? "",
537
+ blockType: localBlockType(pieceName, action.name, "action"),
538
+ audience: null
539
+ })),
540
+ auth: descriptor.auth ?? null
541
+ };
542
+ }
543
+ function triggersResult(descriptor, pieceName, version) {
544
+ return {
545
+ name: pieceName,
546
+ displayName: descriptor.displayName || pieceName,
547
+ version,
548
+ triggers: descriptor.triggers.map((trigger) => ({
549
+ name: trigger.name,
550
+ displayName: trigger.displayName,
551
+ description: trigger.description ?? "",
552
+ strategy: trigger.strategy,
553
+ blockType: localBlockType(pieceName, trigger.name, "trigger")
554
+ })),
555
+ auth: descriptor.auth ?? null
556
+ };
557
+ }
558
+ function localSearchHits(descriptor, pieceName) {
559
+ const pieceDisplayName = descriptor.displayName || pieceName;
560
+ const logoUrl = descriptor.logoUrl ?? "";
561
+ return [...descriptor.actions.map((action) => ({
562
+ blockType: localBlockType(pieceName, action.name, "action"),
563
+ pieceName,
564
+ pieceDisplayName,
565
+ logoUrl,
566
+ displayName: action.displayName,
567
+ description: action.description ?? "",
568
+ kind: "action",
569
+ strategy: null
570
+ })), ...descriptor.triggers.map((trigger) => ({
571
+ blockType: localBlockType(pieceName, trigger.name, "trigger"),
572
+ pieceName,
573
+ pieceDisplayName,
574
+ logoUrl,
575
+ displayName: trigger.displayName,
576
+ description: trigger.description ?? "",
577
+ kind: "trigger",
578
+ strategy: trigger.strategy
579
+ }))];
580
+ }
581
+ function detailResult(descriptor, pieceName, version) {
582
+ return {
583
+ name: pieceName,
584
+ displayName: descriptor.displayName || pieceName,
585
+ description: descriptor.description ?? "",
586
+ logoUrl: descriptor.logoUrl ?? "",
587
+ version,
588
+ categories: descriptor.categories ?? [],
589
+ auth: descriptor.auth ?? null,
590
+ actions: Object.fromEntries(descriptor.actions.map((action) => [action.name, {
591
+ name: action.name,
592
+ displayName: action.displayName,
593
+ description: action.description ?? "",
594
+ props: action.props,
595
+ requireAuth: action.requireAuth
596
+ }])),
597
+ triggers: Object.fromEntries(descriptor.triggers.map((trigger) => [trigger.name, {
598
+ name: trigger.name,
599
+ displayName: trigger.displayName,
600
+ description: trigger.description ?? "",
601
+ type: trigger.strategy,
602
+ props: trigger.props,
603
+ requireAuth: trigger.requireAuth
604
+ }]))
605
+ };
606
+ }
607
+ //#endregion
608
+ //#region src/reactor/block-search.ts
609
+ const INDEX_TTL_MS = 3600 * 1e3;
610
+ const DEFAULT_LIMIT = 30;
611
+ const MAX_LIMIT = 100;
612
+ function buildSearchIndex(raw) {
613
+ const entries = [];
614
+ let pieces = 0;
615
+ for (const entry of raw) {
616
+ if (typeof entry.name !== "string" || typeof entry.version !== "string" || SERVER_ONLY_PIECES.has(entry.name)) continue;
617
+ const pieceName = entry.name;
618
+ const pieceDisplayName = entry.displayName ?? pieceName;
619
+ const logoUrl = entry.logoUrl ?? "";
620
+ const push = (kind, item) => {
621
+ if (typeof item.name !== "string" || item.name === "") return;
622
+ const displayName = item.displayName ?? item.name;
623
+ const description = item.description ?? "";
624
+ entries.push({
625
+ hit: {
626
+ blockType: kind === "trigger" ? `${pieceName}@${entry.version}#trigger:${item.name}` : `${pieceName}@${entry.version}#${item.name}`,
627
+ pieceName,
628
+ pieceDisplayName,
629
+ logoUrl,
630
+ displayName,
631
+ description,
632
+ kind,
633
+ strategy: kind === "trigger" ? item.type ?? null : null
634
+ },
635
+ name: `${displayName} ${item.name}`.toLowerCase(),
636
+ description: description.toLowerCase(),
637
+ piece: pieceDisplayName.toLowerCase()
638
+ });
639
+ };
640
+ for (const action of entry.suggestedActions ?? []) push("action", action);
641
+ for (const trigger of entry.suggestedTriggers ?? []) push("trigger", trigger);
642
+ pieces += 1;
643
+ }
644
+ return {
645
+ entries,
646
+ pieces
647
+ };
648
+ }
649
+ function indexFromHits(hits) {
650
+ const pieces = /* @__PURE__ */ new Set();
651
+ return {
652
+ entries: hits.map((hit) => {
653
+ pieces.add(hit.pieceName);
654
+ return {
655
+ hit,
656
+ name: `${hit.displayName} ${hit.blockType.split("#").pop() ?? ""}`.toLowerCase(),
657
+ description: hit.description.toLowerCase(),
658
+ piece: hit.pieceDisplayName.toLowerCase()
659
+ };
660
+ }),
661
+ pieces: pieces.size
662
+ };
663
+ }
664
+ function merge(index, local) {
665
+ const localEntries = local?.entries ?? [];
666
+ const localNames = new Set(localEntries.map((entry) => entry.hit.pieceName));
667
+ const published = (index?.entries ?? []).filter((entry) => !localNames.has(entry.hit.pieceName));
668
+ const shadowed = new Set((index?.entries ?? []).map((entry) => entry.hit.pieceName).filter((name) => localNames.has(name)));
669
+ return {
670
+ entries: [...localEntries, ...published],
671
+ pieces: (local?.pieces ?? 0) + (index?.pieces ?? 0) - shadowed.size
672
+ };
673
+ }
674
+ function searchIndex(index, query, limit = DEFAULT_LIMIT) {
675
+ const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
676
+ if (tokens.length === 0) return [];
677
+ const scored = [];
678
+ for (const entry of index.entries) {
679
+ const haystack = `${entry.name} ${entry.piece} ${entry.description}`;
680
+ if (!tokens.every((token) => haystack.includes(token))) continue;
681
+ const first = tokens[0];
682
+ const score = entry.name.startsWith(first) ? 0 : entry.name.includes(first) ? 1 : entry.piece.includes(first) ? 2 : 3;
683
+ scored.push({
684
+ score,
685
+ entry
686
+ });
687
+ }
688
+ scored.sort((a, b) => a.score - b.score || a.entry.hit.displayName.localeCompare(b.entry.hit.displayName));
689
+ return scored.slice(0, Math.min(Math.max(limit, 1), MAX_LIMIT)).map(({ entry }) => entry.hit);
690
+ }
691
+ let cached;
692
+ function ensureIndex() {
693
+ if (cached && cached.expiresAt > Date.now() && !cached.error) return cached;
694
+ const entry = {
695
+ promise: fetchCatalogWithSuggestions().then(buildSearchIndex),
696
+ expiresAt: Date.now() + INDEX_TTL_MS
697
+ };
698
+ entry.promise.then((value) => {
699
+ entry.value = value;
700
+ }, (error) => {
701
+ entry.error = error instanceof Error ? error.message : String(error);
702
+ });
703
+ cached = entry;
704
+ return entry;
705
+ }
706
+ function searchBlocks(query, limit, local) {
707
+ const index = ensureIndex();
708
+ if (index.error) {
709
+ const message = index.error;
710
+ cached = void 0;
711
+ return {
712
+ status: "error",
713
+ hits: searchIndex(merge(void 0, local), query, limit),
714
+ indexedPieces: local?.pieces ?? 0,
715
+ error: message
716
+ };
717
+ }
718
+ if (!index.value) return {
719
+ status: "indexing",
720
+ hits: searchIndex(merge(void 0, local), query, limit),
721
+ indexedPieces: local?.pieces ?? 0,
722
+ error: null
723
+ };
724
+ const merged = merge(index.value, local);
725
+ return {
726
+ status: "ready",
727
+ hits: searchIndex(merged, query, limit),
728
+ indexedPieces: merged.pieces,
729
+ error: null
730
+ };
731
+ }
732
+ //#endregion
733
+ //#region src/reactor/reactor-port.ts
734
+ const DRIVE_DOCUMENT_TYPES = new Set(["powerhouse/document-drive", "powerhouse/reactor-drive"]);
735
+ const FIND_PAGE_LIMIT = 100;
736
+ const DESIGN_TIME_WRITES_REFUSED = "Reactor writes are not available while resolving design-time options";
737
+ const DESIGN_TIME_CALLER_REQUIRED = "Design-time reactor access requires an authenticated request";
738
+ const BASE_ACTIONS = [{
739
+ type: "SET_NAME",
740
+ module: "base",
741
+ inputSchema: "input SetNameInput {\n name: String!\n}"
742
+ }];
743
+ function stateValueAt(document, path) {
744
+ let current = document.state.global;
745
+ for (const segment of path.split(".")) {
746
+ if (typeof current !== "object" || current === null) return void 0;
747
+ current = current[segment];
748
+ }
749
+ return current;
750
+ }
751
+ function matchesState(document, match) {
752
+ if (!match) return true;
753
+ const value = stateValueAt(document, match.path);
754
+ if (typeof value === "string") return value === match.value;
755
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value) === match.value;
756
+ return false;
757
+ }
758
+ function documentSummary(document, withState) {
759
+ const globalState = document.state.global;
760
+ const stateName = globalState && typeof globalState === "object" ? globalState.name : void 0;
761
+ return {
762
+ documentId: document.header.id,
763
+ documentType: document.header.documentType,
764
+ name: typeof stateName === "string" && stateName || document.header.name || "",
765
+ slug: document.header.slug,
766
+ ...withState ? { state: globalState } : {}
767
+ };
768
+ }
769
+ function assertOperationsApplied(document, count) {
770
+ const failed = Object.values(document.operations).flat().sort((a, b) => a.index - b.index).slice(-Math.max(count, 1)).find((operation) => operation.error !== void 0);
771
+ if (failed) throw new Error(`Action ${failed.action.type} failed: ${failed.error ?? "unknown error"}`);
772
+ }
773
+ var SubgraphReactorPort = class {
774
+ constructor(host) {
775
+ this.host = host;
776
+ }
777
+ get client() {
778
+ return this.host.reactorClient;
779
+ }
780
+ async models() {
781
+ return (await this.client.getDocumentModelModules()).results.map((module) => module.documentModel.global).map((model) => ({
782
+ documentType: model.id,
783
+ name: model.name
784
+ })).filter((entry) => entry.documentType).sort((a, b) => a.documentType.localeCompare(b.documentType));
785
+ }
786
+ async model(documentType) {
787
+ const model = (await this.client.getDocumentModelModule(documentType)).documentModel.global;
788
+ const latest = model.specifications.at(-1);
789
+ return {
790
+ documentType,
791
+ name: model.name,
792
+ stateSchema: latest?.state.global.schema ?? null,
793
+ actions: [...(latest?.modules ?? []).flatMap((specModule) => specModule.operations.flatMap((operation) => operation.name ? [{
794
+ type: operation.name,
795
+ module: specModule.name,
796
+ inputSchema: operation.schema ?? null
797
+ }] : [])), ...BASE_ACTIONS]
798
+ };
799
+ }
800
+ async get(input) {
801
+ return documentSummary(await this.client.get(input.documentId), true);
802
+ }
803
+ async find(input) {
804
+ const limit = input.limit ?? FIND_PAGE_LIMIT;
805
+ let results;
806
+ if (input.documentType) results = await this.findByType(input.documentType, limit, input.parentId);
807
+ else if (input.parentId) results = (await this.client.find({ parentId: input.parentId }, void 0, {
808
+ cursor: "",
809
+ limit
810
+ })).results;
811
+ else {
812
+ const types = (await this.models()).map((model) => model.documentType);
813
+ results = (await Promise.all(types.map((type) => this.findByType(type, limit)))).flat();
814
+ }
815
+ const seen = /* @__PURE__ */ new Set();
816
+ return results.filter((document) => {
817
+ if (seen.has(document.header.id)) return false;
818
+ seen.add(document.header.id);
819
+ return true;
820
+ }).filter((document) => matchesState(document, input.match)).map((document) => documentSummary(document, input.withState === true));
821
+ }
822
+ async create(input) {
823
+ const target = input.parentId ? await this.resolveDriveTarget(input.parentId) : null;
824
+ if (!target) {
825
+ const created = await this.client.createEmpty(input.documentType, { parentIdentifier: input.parentId });
826
+ if (!input.name) return documentSummary(created, true);
827
+ const named = await this.client.execute(created.header.id, "main", [createAction("SET_NAME", { name: input.name })]);
828
+ assertOperationsApplied(named, 1);
829
+ return documentSummary(named, true);
830
+ }
831
+ const empty = (await this.client.getDocumentModelModule(input.documentType)).utils.createDocument();
832
+ if (input.name) empty.header.name = input.name;
833
+ return documentSummary(await this.client.drives.addFile(target.driveId, empty, target.parentFolder), true);
834
+ }
835
+ async execute(input) {
836
+ const actions = input.actions.map((entry) => createAction(entry.type, entry.input, void 0, void 0, entry.scope ?? "global"));
837
+ const document = await this.client.execute(input.documentId, input.branch ?? "main", actions);
838
+ assertOperationsApplied(document, actions.length);
839
+ return documentSummary(document, true);
840
+ }
841
+ async findByType(type, limit, parentId) {
842
+ try {
843
+ return (await this.client.find({
844
+ type,
845
+ ...parentId ? { parentId } : {}
846
+ }, void 0, {
847
+ cursor: "",
848
+ limit
849
+ })).results;
850
+ } catch {
851
+ return [];
852
+ }
853
+ }
854
+ async resolveDriveTarget(parentId) {
855
+ try {
856
+ const parent = await this.client.get(parentId);
857
+ return DRIVE_DOCUMENT_TYPES.has(parent.header.documentType) ? { driveId: parent.header.id } : null;
858
+ } catch {
859
+ return this.findFolderDrive(parentId);
860
+ }
861
+ }
862
+ async findFolderDrive(nodeId) {
863
+ const pages = await Promise.all([...DRIVE_DOCUMENT_TYPES].map((type) => this.findByType(type, FIND_PAGE_LIMIT)));
864
+ for (const drive of pages.flat()) try {
865
+ if ((await this.client.drives.getNode(drive.header.id, nodeId)).kind === "folder") return {
866
+ driveId: drive.header.id,
867
+ parentFolder: nodeId
868
+ };
869
+ } catch {}
870
+ return null;
871
+ }
872
+ };
873
+ var ScopedDesignTimeReactorPort = class {
874
+ inner;
875
+ constructor(host, caller) {
876
+ this.host = host;
877
+ this.caller = caller;
878
+ this.inner = new SubgraphReactorPort(host);
879
+ }
880
+ models() {
881
+ return this.inner.models();
882
+ }
883
+ model(documentType) {
884
+ return this.inner.model(documentType);
885
+ }
886
+ async get(input) {
887
+ if (!this.caller) throw new Error(DESIGN_TIME_CALLER_REQUIRED);
888
+ await this.host.assertCanRead(input.documentId, this.caller);
889
+ return this.inner.get(input);
890
+ }
891
+ async find(input) {
892
+ const found = await this.inner.find(input);
893
+ const allowed = await Promise.all(found.map((document) => this.canRead(document.documentId)));
894
+ return found.filter((_, index) => allowed[index]);
895
+ }
896
+ create(_input) {
897
+ return Promise.reject(new Error(DESIGN_TIME_WRITES_REFUSED));
898
+ }
899
+ execute(_input) {
900
+ return Promise.reject(new Error(DESIGN_TIME_WRITES_REFUSED));
901
+ }
902
+ async canRead(documentId) {
903
+ if (!this.caller) return false;
904
+ return this.host.assertCanRead(documentId, this.caller).then(() => true).catch(() => false);
905
+ }
906
+ };
907
+ //#endregion
908
+ //#region src/reactor/run-scope.ts
909
+ const storage = new AsyncLocalStorage();
910
+ function withRunScope(scope, fn) {
911
+ return storage.run(scope, fn);
912
+ }
913
+ function currentWorkflowId() {
914
+ return storage.getStore()?.workflowId;
915
+ }
916
+ function currentBoundConnections() {
917
+ return storage.getStore()?.connections;
918
+ }
919
+ function currentPieceWorker() {
920
+ return storage.getStore()?.pieceWorker;
921
+ }
922
+ //#endregion
923
+ //#region src/reactor/connector-id.ts
924
+ function packageFromConnectorId(connectorId) {
925
+ const separator = connectorId.lastIndexOf("#");
926
+ return separator > 0 ? connectorId.slice(0, separator) : connectorId;
927
+ }
928
+ //#endregion
929
+ //#region src/reactor/lib.ts
930
+ const pieceLogger = childLogger(["workflow", "piece"]);
931
+ const connectionLogger = childLogger(["workflow", "connection"]);
932
+ function assertConnectorMatches(state, request) {
933
+ const wanted = request?.piecePackage;
934
+ const owner = state.connectorId ? packageFromConnectorId(state.connectorId) : "";
935
+ if (!wanted || !owner || wanted !== owner) throw new ConnectorMismatchError();
936
+ }
937
+ var ConnectorMismatchError = class extends Error {
938
+ constructor() {
939
+ super("Connection is not available to this block");
940
+ this.name = "ConnectorMismatchError";
941
+ }
942
+ };
943
+ var DocumentConnectionResolver = class {
944
+ constructor(host, secrets) {
945
+ this.host = host;
946
+ this.secrets = secrets;
947
+ }
948
+ async resolve(connectionId, request) {
949
+ return (await this.resolveWithSecrets(connectionId, request)).auth;
950
+ }
951
+ async resolveWithSecrets(connectionId, request) {
952
+ return resolveConnectionWithSecrets(await this.host.reactorClient.get(connectionId), this.secrets, request);
953
+ }
954
+ };
955
+ async function resolveConnectionAuth(document, secrets, request) {
956
+ return (await resolveConnectionWithSecrets(document, secrets, request)).auth;
957
+ }
958
+ async function resolveConnectionWithSecrets(document, secrets, request) {
959
+ if (document.header.documentType !== "powerhouse/connection") throw new ConnectorMismatchError();
960
+ const state = document.state.global;
961
+ assertConnectorMatches(state, request);
962
+ if (state.status === "REVOKED") throw new Error(`Connection "${state.name || document.header.id}" is revoked`);
963
+ return shapeConnection({
964
+ authType: state.authType,
965
+ config: state.config ?? {},
966
+ secretRefs: state.secretRefs
967
+ }, secrets);
968
+ }
969
+ const EGRESS_ALLOW_ENV = "WORKFLOW_EGRESS_ALLOW_ADDRESSES";
970
+ function asCidr(entry) {
971
+ if (entry.includes("/")) return entry;
972
+ return entry.includes(":") ? `${entry}/128` : `${entry}/32`;
973
+ }
974
+ function configuredEgress() {
975
+ const raw = process.env[EGRESS_ALLOW_ENV];
976
+ if (raw === void 0 || raw.trim() === "") return void 0;
977
+ const allowAddresses = raw.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "").map(asCidr);
978
+ if (allowAddresses.length === 0) return void 0;
979
+ pieceLogger.info(`Egress policy widened by ${EGRESS_ALLOW_ENV}: ${allowAddresses.join(", ")}`);
980
+ return { allowAddresses };
981
+ }
982
+ const BUNDLE_CACHE_DIR = join(process.cwd(), ".ph", "ap-bundles");
983
+ const ATTACHMENT_STAGING_DIR = join(process.cwd(), ".ph", "ap-attachment-staging");
984
+ let resolver;
985
+ function fetchingResolver(cacheDir) {
986
+ return { async resolve(name, version) {
987
+ return {
988
+ name,
989
+ version,
990
+ bundleDir: (await ensurePieceBundle({
991
+ name,
992
+ version,
993
+ cacheDir
994
+ })).dir,
995
+ local: false
996
+ };
997
+ } };
998
+ }
999
+ function pieceResolver() {
1000
+ return resolver ??= localFirstResolver(async (name) => {
1001
+ await packagePieces.ready();
1002
+ return packagePieces.lookup(name);
1003
+ }, fetchingResolver(BUNDLE_CACHE_DIR));
1004
+ }
1005
+ function boundConnections(inner) {
1006
+ return new BoundConnectionResolver(inner, currentBoundConnections, (connectionId, request) => {
1007
+ connectionLogger.warn(`Step "${request?.stepKey ?? "?"}" of workflow "${currentWorkflowId() ?? "?"}" asked for connection "${connectionId}", which its definition does not declare`);
1008
+ });
1009
+ }
1010
+ function createBlockExecutor(host, secrets, attachments, pieceStore) {
1011
+ return new CompositeBlockExecutor(new ActivepiecesBlockExecutor({
1012
+ cacheDir: BUNDLE_CACHE_DIR,
1013
+ egress: configuredEgress(),
1014
+ worker: currentPieceWorker,
1015
+ resolver: pieceResolver(),
1016
+ packages: async () => {
1017
+ await packagePieces.ready();
1018
+ return packagePieces.versions();
1019
+ },
1020
+ reactor: new SubgraphReactorPort(host),
1021
+ connections: boundConnections(new DocumentConnectionResolver(host, secrets)),
1022
+ ...pieceStore ? { pieceStore } : {},
1023
+ onPieceLog: (entry, execution) => {
1024
+ const line = `[${execution.step.key}] ${entry.message}`;
1025
+ if (entry.level === "error") pieceLogger.error(line);
1026
+ else if (entry.level === "warn") pieceLogger.warn(line);
1027
+ else if (entry.level === "debug") pieceLogger.debug(line);
1028
+ else pieceLogger.info(line);
1029
+ },
1030
+ ...attachments ? {
1031
+ attachments,
1032
+ stagingRoot: ATTACHMENT_STAGING_DIR
1033
+ } : {}
1034
+ }));
1035
+ }
1036
+ function toWorkflowDefinition(state) {
1037
+ if (!state.trigger) throw new Error("Workflow has no trigger binding");
1038
+ return {
1039
+ name: state.name,
1040
+ trigger: {
1041
+ id: state.trigger.id,
1042
+ blockType: state.trigger.blockType,
1043
+ connectionId: state.trigger.connectionId,
1044
+ config: state.trigger.config,
1045
+ filter: state.trigger.filter
1046
+ },
1047
+ steps: state.steps.map((step) => ({
1048
+ id: step.id,
1049
+ key: step.key,
1050
+ name: step.name,
1051
+ blockType: step.blockType,
1052
+ connectionId: step.connectionId,
1053
+ config: step.config,
1054
+ timeoutSeconds: step.timeoutSeconds
1055
+ })),
1056
+ edges: state.edges.map((edge) => ({
1057
+ id: edge.id,
1058
+ from: edge.from,
1059
+ to: edge.to,
1060
+ port: edge.port,
1061
+ condition: edge.condition
1062
+ })),
1063
+ variables: state.variables.map((variable) => ({
1064
+ key: variable.key,
1065
+ value: variable.value
1066
+ }))
1067
+ };
1068
+ }
1069
+ //#endregion
1070
+ //#region src/reactor/schedule.ts
1071
+ const SCHEDULE_BLOCK = "core#schedule";
1072
+ const MIN_SCHEDULE_INTERVAL_MS = 6e4;
1073
+ const INTERVAL_UNIT_MS = {
1074
+ minutes: 6e4,
1075
+ hours: 36e5,
1076
+ days: 864e5
1077
+ };
1078
+ function asRecord$2(config) {
1079
+ if (config && typeof config === "object" && !Array.isArray(config)) return config;
1080
+ if (typeof config === "string") try {
1081
+ return asRecord$2(JSON.parse(config));
1082
+ } catch {
1083
+ return {};
1084
+ }
1085
+ return {};
1086
+ }
1087
+ function parseTimezone(value) {
1088
+ if (value === void 0 || value === null || value === "") return "UTC";
1089
+ if (typeof value !== "string") throw new Error(`${SCHEDULE_BLOCK}: "timezone" must be an IANA name`);
1090
+ try {
1091
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
1092
+ } catch {
1093
+ throw new Error(`${SCHEDULE_BLOCK}: unknown timezone "${value}" (use an IANA name such as Europe/Lisbon)`);
1094
+ }
1095
+ return value;
1096
+ }
1097
+ function toNumber$1(value) {
1098
+ if (typeof value === "number") return value;
1099
+ if (typeof value === "string" && value.trim() !== "") {
1100
+ const parsed = Number(value);
1101
+ return Number.isNaN(parsed) ? void 0 : parsed;
1102
+ }
1103
+ }
1104
+ function parseCronPattern(cron, timezone) {
1105
+ if (typeof cron !== "string" || cron.trim() === "") throw new Error(`${SCHEDULE_BLOCK}: "cron" is required in cron mode`);
1106
+ const pattern = cron.trim().replace(/\s+/g, " ");
1107
+ if (pattern.split(" ").length !== 5) throw new Error(`${SCHEDULE_BLOCK}: cron "${pattern}" must have exactly five fields (minute hour day month weekday)`);
1108
+ let cronJob;
1109
+ try {
1110
+ cronJob = new Cron(pattern, {
1111
+ timezone,
1112
+ legacyMode: false
1113
+ });
1114
+ } catch (error) {
1115
+ const message = error instanceof Error ? error.message : String(error);
1116
+ throw new Error(`${SCHEDULE_BLOCK}: invalid cron "${pattern}": ${message}`, { cause: error });
1117
+ }
1118
+ if (!cronJob.nextRun()) throw new Error(`${SCHEDULE_BLOCK}: cron "${pattern}" never fires`);
1119
+ return pattern;
1120
+ }
1121
+ function parseScheduleConfig(config) {
1122
+ const record = asRecord$2(config);
1123
+ const timezone = parseTimezone(record.timezone);
1124
+ const mode = record.mode === "cron" || record.mode === "interval" ? record.mode : record.cron ? "cron" : record.every !== void 0 || record.everyMs !== void 0 ? "interval" : void 0;
1125
+ if (!mode) throw new Error(`${SCHEDULE_BLOCK}: "mode" must be "cron" or "interval" (or set "cron" / "every")`);
1126
+ if (mode === "cron") return {
1127
+ mode,
1128
+ cron: parseCronPattern(record.cron, timezone),
1129
+ timezone
1130
+ };
1131
+ return {
1132
+ mode,
1133
+ everyMs: intervalMsFrom(record),
1134
+ timezone
1135
+ };
1136
+ }
1137
+ function intervalMsFrom(record) {
1138
+ const every = toNumber$1(record.every);
1139
+ let everyMs;
1140
+ if (every !== void 0) {
1141
+ const unit = record.unit ?? "minutes";
1142
+ if (!(unit in INTERVAL_UNIT_MS)) throw new Error(`${SCHEDULE_BLOCK}: "unit" must be one of ${Object.keys(INTERVAL_UNIT_MS).join(", ")}`);
1143
+ everyMs = every * INTERVAL_UNIT_MS[unit];
1144
+ } else everyMs = toNumber$1(record.everyMs);
1145
+ if (everyMs === void 0) throw new Error(`${SCHEDULE_BLOCK}: "every" (with "unit") or "everyMs" is required in interval mode`);
1146
+ if (!Number.isFinite(everyMs) || everyMs <= 0) throw new Error(`${SCHEDULE_BLOCK}: the interval must be a positive number`);
1147
+ if (everyMs < 6e4) throw new Error(`${SCHEDULE_BLOCK}: the interval must be at least ${MIN_SCHEDULE_INTERVAL_MS / 1e3}s`);
1148
+ return Math.round(everyMs);
1149
+ }
1150
+ function nextFireAt(schedule, from) {
1151
+ if (schedule.mode === "interval") return new Date(from.getTime() + schedule.everyMs);
1152
+ const next = new Cron(schedule.cron, {
1153
+ timezone: schedule.timezone,
1154
+ legacyMode: false
1155
+ }).nextRun(from);
1156
+ if (!next) throw new Error(`${SCHEDULE_BLOCK}: cron "${schedule.cron}" never fires`);
1157
+ return next;
1158
+ }
1159
+ function rescheduleAfterFire(schedule, scheduledFor, now) {
1160
+ if (schedule.mode === "interval") {
1161
+ const onPhase = new Date(scheduledFor.getTime() + schedule.everyMs);
1162
+ return onPhase > now ? onPhase : nextFireAt(schedule, now);
1163
+ }
1164
+ return nextFireAt(schedule, now);
1165
+ }
1166
+ function schedulePayload(schedule, scheduledFor, firedAt) {
1167
+ return {
1168
+ scheduledFor: scheduledFor.toISOString(),
1169
+ firedAt: firedAt.toISOString(),
1170
+ timezone: schedule.timezone,
1171
+ ...schedule.mode === "cron" ? { cron: schedule.cron } : { everyMs: schedule.everyMs }
1172
+ };
1173
+ }
1174
+ function cronIntervalMs(cron, from = /* @__PURE__ */ new Date()) {
1175
+ try {
1176
+ const runs = new Cron(cron.trim(), {
1177
+ timezone: "UTC",
1178
+ legacyMode: false
1179
+ }).nextRuns(2, from);
1180
+ if (runs.length < 2) return void 0;
1181
+ return Math.max(runs[1].getTime() - runs[0].getTime(), MIN_SCHEDULE_INTERVAL_MS);
1182
+ } catch {
1183
+ return;
1184
+ }
1185
+ }
1186
+ //#endregion
1187
+ //#region src/reactor/piece-store-port.ts
1188
+ const logger$3 = childLogger(["workflow", "piece-store"]);
1189
+ const PROJECT_SCOPE_KEY = "reactor";
1190
+ const TEST_PARTITION_SUFFIX = "#test";
1191
+ function testPartitionKey(scope, workflowId) {
1192
+ return (scope === "PROJECT" ? `${PROJECT_SCOPE_KEY}#${workflowId}` : workflowId) + TEST_PARTITION_SUFFIX;
1193
+ }
1194
+ const CURSOR_KEY = "lastPoll";
1195
+ const MAX_CURSOR_SKEW_MS = 1440 * 6e4;
1196
+ function isPlausibleCursor(value, now) {
1197
+ return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= now + MAX_CURSOR_SKEW_MS;
1198
+ }
1199
+ function isCursorShaped(value) {
1200
+ return value === null || typeof value === "number";
1201
+ }
1202
+ function showCursor(value) {
1203
+ return typeof value === "number" ? String(value) : JSON.stringify(value);
1204
+ }
1205
+ function isCursorRef(key) {
1206
+ return key === CURSOR_KEY || key.endsWith(`/${CURSOR_KEY}`);
1207
+ }
1208
+ function createPieceStorePort(store, workflowIdFor, sample = false, clock = Date.now) {
1209
+ const flowKey = () => {
1210
+ const workflowId = workflowIdFor();
1211
+ if (!workflowId) throw new Error("ctx.store is unavailable: no workflow is in scope");
1212
+ return workflowId;
1213
+ };
1214
+ const partition = (scope) => sample ? testPartitionKey(scope, flowKey()) : scope === "PROJECT" ? PROJECT_SCOPE_KEY : flowKey();
1215
+ return {
1216
+ get: async (key, scope) => store.getPieceStoreValue(scope, partition(scope), key),
1217
+ put: async (key, value, scope) => {
1218
+ const now = clock();
1219
+ if (!isCursorRef(key) || isPlausibleCursor(value, now)) return store.setPieceStoreValue(scope, partition(scope), key, value);
1220
+ const kept = await store.getPieceStoreValue(scope, partition(scope), key);
1221
+ if (!isCursorShaped(value) && !isPlausibleCursor(kept, now)) return store.setPieceStoreValue(scope, partition(scope), key, value);
1222
+ logger$3.warn(`Rejected ${key}=${showCursor(value)} from workflow ${flowKey()}; keeping ${isPlausibleCursor(kept, now) ? String(kept) : "no cursor"}`);
1223
+ if (!isPlausibleCursor(kept, now)) await store.deletePieceStoreValue(scope, partition(scope), key);
1224
+ },
1225
+ delete: async (key, scope) => store.deletePieceStoreValue(scope, partition(scope), key)
1226
+ };
1227
+ }
1228
+ //#endregion
1229
+ //#region src/reactor/secret-store.ts
1230
+ const IV_BYTES = 12;
1231
+ const TAG_BYTES = 16;
1232
+ const KEY_BYTES = 32;
1233
+ async function up$1(db) {
1234
+ await db.schema.createTable("secret").addColumn("id", "text", (col) => col.primaryKey()).addColumn("label", "text").addColumn("version", "integer", (col) => col.notNull()).addColumn("enc", "text").addColumn("status", "text", (col) => col.notNull()).addColumn("created_at", "text", (col) => col.notNull()).addColumn("updated_at", "text", (col) => col.notNull()).ifNotExists().execute();
1235
+ }
1236
+ function loadKey(options) {
1237
+ const hex = options.masterKeyHex ?? process.env.PH_SECRETS_MASTER_KEY;
1238
+ if (hex !== void 0) {
1239
+ if (!/^[0-9a-f]{64}$/i.test(hex)) throw new Error("Secrets master key must be 64 hex chars (32 bytes)");
1240
+ return Buffer.from(hex, "hex");
1241
+ }
1242
+ const file = options.keyFile ?? join(process.cwd(), ".ph", "secrets.key");
1243
+ try {
1244
+ const key = Buffer.from(readFileSync(file, "utf8").trim(), "hex");
1245
+ if (key.length !== KEY_BYTES) throw new Error(`Key file "${file}" is not 32 bytes of hex`);
1246
+ return key;
1247
+ } catch (error) {
1248
+ if (error.code !== "ENOENT") throw error;
1249
+ }
1250
+ const key = randomBytes(KEY_BYTES);
1251
+ mkdirSync(dirname(file), { recursive: true });
1252
+ writeFileSync(file, key.toString("hex") + "\n", { mode: 384 });
1253
+ return key;
1254
+ }
1255
+ var LocalEncryptedSecretStore = class LocalEncryptedSecretStore {
1256
+ constructor(db, key) {
1257
+ this.db = db;
1258
+ this.key = key;
1259
+ }
1260
+ static async create(relationalDb, options = {}) {
1261
+ const db = await relationalDb.createNamespace("secrets");
1262
+ await up$1(db);
1263
+ return new LocalEncryptedSecretStore(db, loadKey(options));
1264
+ }
1265
+ encrypt(value) {
1266
+ const iv = randomBytes(IV_BYTES);
1267
+ const cipher = createCipheriv("aes-256-gcm", this.key, iv);
1268
+ const ciphertext = Buffer.concat([cipher.update(value, "utf8"), cipher.final()]);
1269
+ return Buffer.concat([
1270
+ iv,
1271
+ cipher.getAuthTag(),
1272
+ ciphertext
1273
+ ]).toString("base64");
1274
+ }
1275
+ decrypt(enc) {
1276
+ const raw = Buffer.from(enc, "base64");
1277
+ const iv = raw.subarray(0, IV_BYTES);
1278
+ const tag = raw.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
1279
+ const decipher = createDecipheriv("aes-256-gcm", this.key, iv);
1280
+ decipher.setAuthTag(tag);
1281
+ return Buffer.concat([decipher.update(raw.subarray(IV_BYTES + TAG_BYTES)), decipher.final()]).toString("utf8");
1282
+ }
1283
+ async row(ref) {
1284
+ const id = parseSecretRef(ref);
1285
+ const row = await this.db.selectFrom("secret").selectAll().where("id", "=", id).executeTakeFirst();
1286
+ if (!row) throw new SecretNotFoundError(ref);
1287
+ return row;
1288
+ }
1289
+ toStat(row) {
1290
+ return {
1291
+ ref: secretRefFromId(row.id),
1292
+ label: row.label,
1293
+ version: row.version,
1294
+ status: row.status,
1295
+ createdAt: row.created_at,
1296
+ updatedAt: row.updated_at
1297
+ };
1298
+ }
1299
+ async create(input) {
1300
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1301
+ const row = {
1302
+ id: randomBytes(16).toString("hex"),
1303
+ label: input.label ?? null,
1304
+ version: 1,
1305
+ enc: this.encrypt(input.value),
1306
+ status: "ACTIVE",
1307
+ created_at: now,
1308
+ updated_at: now
1309
+ };
1310
+ await this.db.insertInto("secret").values(row).execute();
1311
+ return this.toStat(row);
1312
+ }
1313
+ async rotate(ref, value) {
1314
+ const row = await this.row(ref);
1315
+ if (row.status !== "ACTIVE") throw new SecretDeletedError(ref);
1316
+ const updated = {
1317
+ ...row,
1318
+ version: row.version + 1,
1319
+ enc: this.encrypt(value),
1320
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1321
+ };
1322
+ await this.db.updateTable("secret").set({
1323
+ version: updated.version,
1324
+ enc: updated.enc,
1325
+ updated_at: updated.updated_at
1326
+ }).where("id", "=", row.id).execute();
1327
+ return this.toStat(updated);
1328
+ }
1329
+ async get(ref) {
1330
+ const row = await this.row(ref);
1331
+ if (row.status !== "ACTIVE" || row.enc === null) throw new SecretDeletedError(ref);
1332
+ return this.decrypt(row.enc);
1333
+ }
1334
+ async stat(ref) {
1335
+ return this.toStat(await this.row(ref));
1336
+ }
1337
+ async list() {
1338
+ return (await this.db.selectFrom("secret").selectAll().where("status", "=", "ACTIVE").orderBy("created_at", "desc").execute()).map((row) => this.toStat(row));
1339
+ }
1340
+ async delete(ref) {
1341
+ const row = await this.row(ref);
1342
+ await this.db.updateTable("secret").set({
1343
+ status: "DELETED",
1344
+ enc: null,
1345
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1346
+ }).where("id", "=", row.id).execute();
1347
+ }
1348
+ };
1349
+ //#endregion
1350
+ //#region src/reactor/store.ts
1351
+ const logger$2 = childLogger([
1352
+ "workflow",
1353
+ "runtime",
1354
+ "store"
1355
+ ]);
1356
+ const ORPHANED_RUN_ERROR = "Reactor stopped before the run finished; steps completed before then were journaled";
1357
+ const ABANDONED_PENDING_RUN_ERROR = "Reactor stopped before the matched trigger started its run; rerun it to fire the workflow with the same payload";
1358
+ const PENDING_RUN_STATUS = "PENDING";
1359
+ function errorMessage(error) {
1360
+ return error instanceof Error ? error.message : String(error);
1361
+ }
1362
+ function isDuplicateObject(error) {
1363
+ if (typeof error !== "object" || error === null) return false;
1364
+ return error.code === "42P07";
1365
+ }
1366
+ async function up(db) {
1367
+ await db.schema.createTable("run").addColumn("id", "text", (col) => col.primaryKey()).addColumn("workflow_id", "text", (col) => col.notNull()).addColumn("workflow_name", "text", (col) => col.notNull()).addColumn("workflow_version", "integer", (col) => col.notNull()).addColumn("trigger_kind", "text", (col) => col.notNull()).addColumn("trigger_payload", "text").addColumn("status", "text", (col) => col.notNull()).addColumn("error", "text").addColumn("started_at", "text", (col) => col.notNull()).addColumn("ended_at", "text").addColumn("rerun_of", "text").ifNotExists().execute();
1368
+ try {
1369
+ await db.schema.alterTable("run").addColumn("rerun_of", "text").execute();
1370
+ } catch {}
1371
+ await db.schema.createTable("trigger_state").addColumn("workflow_id", "text", (col) => col.primaryKey()).addColumn("block_type", "text", (col) => col.notNull()).addColumn("config_hash", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull()).addColumn("store_state", "text", (col) => col.notNull()).addColumn("interval_ms", "integer", (col) => col.notNull()).addColumn("next_poll_at", "text").addColumn("last_poll_at", "text").addColumn("last_error", "text").addColumn("consecutive_failures", "integer", (col) => col.notNull()).addColumn("lease_owner", "text").addColumn("lease_expires_at", "text").addColumn("updated_at", "text", (col) => col.notNull()).ifNotExists().execute();
1372
+ await db.schema.createTable("trigger_dedupe").addColumn("workflow_id", "text", (col) => col.notNull()).addColumn("dedupe_key", "text", (col) => col.notNull()).addColumn("run_id", "text").addColumn("created_at", "text", (col) => col.notNull()).addPrimaryKeyConstraint("trigger_dedupe_pk", ["workflow_id", "dedupe_key"]).ifNotExists().execute();
1373
+ await db.schema.createTable("step_execution").addColumn("id", "text", (col) => col.primaryKey()).addColumn("run_id", "text", (col) => col.notNull()).addColumn("ordinal", "integer", (col) => col.notNull()).addColumn("step_id", "text", (col) => col.notNull()).addColumn("step_key", "text", (col) => col.notNull()).addColumn("block_type", "text", (col) => col.notNull()).addColumn("status", "text", (col) => col.notNull()).addColumn("input", "text").addColumn("output", "text").addColumn("port", "text").addColumn("error", "text").addUniqueConstraint("step_execution_run_step", ["run_id", "step_id"]).ifNotExists().execute();
1374
+ try {
1375
+ await db.schema.alterTable("step_execution").addUniqueConstraint("step_execution_run_step", ["run_id", "step_id"]).execute();
1376
+ } catch (error) {
1377
+ if (!isDuplicateObject(error)) throw new Error(`Could not add the step_execution (run_id, step_id) unique constraint, which per-step journaling upserts against: ${errorMessage(error)}`, { cause: error });
1378
+ }
1379
+ await db.schema.createTable("piece_store").addColumn("scope", "text", (col) => col.notNull()).addColumn("scope_key", "text", (col) => col.notNull()).addColumn("key", "text", (col) => col.notNull()).addColumn("value", "text", (col) => col.notNull()).addColumn("updated_at", "text", (col) => col.notNull()).addPrimaryKeyConstraint("piece_store_pk", [
1380
+ "scope",
1381
+ "scope_key",
1382
+ "key"
1383
+ ]).ifNotExists().execute();
1384
+ try {
1385
+ await db.schema.dropTable("webhook_endpoint").ifExists().execute();
1386
+ } catch {}
1387
+ return migrateTriggerStoreState(db);
1388
+ }
1389
+ async function migrateTriggerStoreState(db) {
1390
+ const unmigrated = /* @__PURE__ */ new Set();
1391
+ let rows;
1392
+ try {
1393
+ rows = await db.selectFrom("trigger_state").select(["workflow_id", "store_state"]).orderBy("updated_at", "asc").orderBy("workflow_id", "asc").execute();
1394
+ } catch (error) {
1395
+ logger$2.error("Could not read trigger_state to migrate it: @error", error);
1396
+ return unmigrated;
1397
+ }
1398
+ const pending = [];
1399
+ for (const row of rows) {
1400
+ const entries = parseLegacyBlob(row, unmigrated);
1401
+ if (entries) pending.push({
1402
+ workflowId: row.workflow_id,
1403
+ entries
1404
+ });
1405
+ }
1406
+ const projectWinner = resolveProjectCollisions(pending);
1407
+ for (const row of pending) try {
1408
+ await migrateOneRow(db, row, projectWinner);
1409
+ } catch (error) {
1410
+ unmigrated.add(row.workflowId);
1411
+ logger$2.error(`Could not migrate trigger store state for ${row.workflowId}`, error);
1412
+ }
1413
+ return unmigrated;
1414
+ }
1415
+ function parseLegacyBlob(row, unmigrated) {
1416
+ if (!row.store_state || row.store_state === "{}") return null;
1417
+ let state;
1418
+ try {
1419
+ state = JSON.parse(row.store_state);
1420
+ } catch {
1421
+ logger$2.warn(`Leaving unparseable store_state for ${row.workflow_id} in place`);
1422
+ unmigrated.add(row.workflow_id);
1423
+ return null;
1424
+ }
1425
+ if (typeof state !== "object" || state === null) {
1426
+ unmigrated.add(row.workflow_id);
1427
+ return null;
1428
+ }
1429
+ const entries = [];
1430
+ for (const [key, value] of Object.entries(state)) {
1431
+ if (/^testflow_.+\//.test(key)) continue;
1432
+ const flow = /^flow_(.+?)\/(.+)$/.exec(key);
1433
+ if (flow) {
1434
+ entries.push({
1435
+ scope: "FLOW",
1436
+ scopeKey: flow[1],
1437
+ key: flow[2],
1438
+ value
1439
+ });
1440
+ continue;
1441
+ }
1442
+ if (key.startsWith("test")) logger$2.info(`Migrating "${key}" for ${row.workflow_id} as a project key; it may be a test leftover`);
1443
+ entries.push({
1444
+ scope: "PROJECT",
1445
+ scopeKey: PROJECT_SCOPE_KEY,
1446
+ key,
1447
+ value
1448
+ });
1449
+ }
1450
+ return entries;
1451
+ }
1452
+ function resolveProjectCollisions(pending) {
1453
+ const winner = /* @__PURE__ */ new Map();
1454
+ const contested = /* @__PURE__ */ new Map();
1455
+ for (const row of pending) for (const entry of row.entries) {
1456
+ if (entry.scope !== "PROJECT") continue;
1457
+ const previous = winner.get(entry.key);
1458
+ if (previous !== void 0) {
1459
+ const seen = contested.get(entry.key) ?? [previous];
1460
+ contested.set(entry.key, [...seen, row.workflowId]);
1461
+ }
1462
+ winner.set(entry.key, row.workflowId);
1463
+ }
1464
+ for (const [key, workflows] of contested) logger$2.warn(`Project store key "${key}" was written by ${workflows.join(", ")}; keeping the value last updated, from ${winner.get(key)}, and discarding the rest`);
1465
+ return winner;
1466
+ }
1467
+ async function migrateOneRow(db, row, projectWinner) {
1468
+ for (const entry of row.entries) {
1469
+ if (entry.scope === "PROJECT" && projectWinner.get(entry.key) !== row.workflowId) continue;
1470
+ await insertPieceStoreIfAbsent(db, entry.scope, entry.scopeKey, entry.key, entry.value);
1471
+ }
1472
+ await db.updateTable("trigger_state").set({ store_state: "{}" }).where("workflow_id", "=", row.workflowId).execute();
1473
+ }
1474
+ async function insertPieceStoreIfAbsent(db, scope, scopeKey, key, value) {
1475
+ const encoded = jsonOrNull(value);
1476
+ if (encoded === null) return;
1477
+ warnIfOverPieceStoreLimits(scope, scopeKey, key, encoded);
1478
+ if (await db.selectFrom("piece_store").select("key").where("scope", "=", scope).where("scope_key", "=", scopeKey).where("key", "=", key).executeTakeFirst()) return;
1479
+ await db.insertInto("piece_store").values({
1480
+ scope,
1481
+ scope_key: scopeKey,
1482
+ key,
1483
+ value: encoded,
1484
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1485
+ }).onConflict((oc) => oc.columns([
1486
+ "scope",
1487
+ "scope_key",
1488
+ "key"
1489
+ ]).doNothing()).execute();
1490
+ }
1491
+ function warnIfOverPieceStoreLimits(scope, scopeKey, key, encoded) {
1492
+ const at = `${scope}/${scopeKey}/${key}`;
1493
+ if (key.length > 128) logger$2.warn(`Migrated store key ${at} is ${key.length} chars, over the 128 limit; writes to it will fail`);
1494
+ const size = Buffer.byteLength(encoded, "utf8");
1495
+ if (size > 524288) logger$2.warn(`Migrated store value ${at} is ${size} bytes, over the ${PIECE_STORE_MAX_VALUE_BYTES} limit; writes to it will fail`);
1496
+ }
1497
+ const PIECE_STORE_MAX_VALUE_BYTES = 512 * 1024;
1498
+ var PieceStoreLimitError = class extends Error {
1499
+ constructor(message) {
1500
+ super(message);
1501
+ this.name = "PieceStoreLimitError";
1502
+ }
1503
+ };
1504
+ function assertPieceStoreEntry(key, value) {
1505
+ if (key.length === 0 || key.length > 128) throw new PieceStoreLimitError(`Store key must be 1-128 characters, got ${key.length}`);
1506
+ const encoded = JSON.stringify(value);
1507
+ if (encoded === void 0) throw new PieceStoreLimitError(`Store value for "${key}" is not JSON`);
1508
+ const size = Buffer.byteLength(encoded, "utf8");
1509
+ if (size > 524288) throw new PieceStoreLimitError(`Store value for "${key}" is ${size} bytes, over the ${PIECE_STORE_MAX_VALUE_BYTES} byte limit`);
1510
+ }
1511
+ function stepValues(runId, ordinal, step) {
1512
+ return {
1513
+ run_id: runId,
1514
+ ordinal,
1515
+ step_id: step.stepId,
1516
+ step_key: step.key,
1517
+ block_type: step.blockType,
1518
+ status: step.status,
1519
+ input: jsonOrNull(redact(step.input)),
1520
+ output: jsonOrNull(redact(step.output)),
1521
+ port: step.port ?? null,
1522
+ error: step.error ? redactMessage(step.error) : null
1523
+ };
1524
+ }
1525
+ function jsonOrNull(value) {
1526
+ if (value === void 0) return null;
1527
+ try {
1528
+ return JSON.stringify(value) ?? null;
1529
+ } catch {
1530
+ return null;
1531
+ }
1532
+ }
1533
+ var WorkflowRunStore = class WorkflowRunStore {
1534
+ runsInFlight = /* @__PURE__ */ new Set();
1535
+ constructor(db, unmigrated) {
1536
+ this.db = db;
1537
+ this.unmigrated = unmigrated;
1538
+ }
1539
+ static async create(relationalDb) {
1540
+ const db = await relationalDb.createNamespace("workflow_runtime");
1541
+ const store = new WorkflowRunStore(db, await up(db));
1542
+ await store.recoverOrphanedRuns();
1543
+ await store.recoverAbandonedRuns();
1544
+ return store;
1545
+ }
1546
+ hasUnmigratedTriggerState(workflowId) {
1547
+ return this.unmigrated.has(workflowId);
1548
+ }
1549
+ clearUnmigratedTriggerState(workflowId) {
1550
+ this.unmigrated.delete(workflowId);
1551
+ }
1552
+ async recoverOrphanedRuns() {
1553
+ let query = this.db.updateTable("run").set({
1554
+ status: "FAILED",
1555
+ error: ORPHANED_RUN_ERROR,
1556
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
1557
+ }).where("status", "=", "RUNNING");
1558
+ if (this.runsInFlight.size > 0) query = query.where("id", "not in", [...this.runsInFlight]);
1559
+ const result = await query.executeTakeFirst();
1560
+ const recovered = Number(result.numUpdatedRows);
1561
+ if (recovered > 0) logger$2.warn(`Recovered ${recovered} workflow run(s) left RUNNING by a stopped reactor; they are now FAILED and rerunnable`);
1562
+ return recovered;
1563
+ }
1564
+ async recoverAbandonedRuns() {
1565
+ let query = this.db.updateTable("run").set({
1566
+ status: "FAILED",
1567
+ error: ABANDONED_PENDING_RUN_ERROR,
1568
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
1569
+ }).where("status", "=", PENDING_RUN_STATUS);
1570
+ if (this.runsInFlight.size > 0) query = query.where("id", "not in", [...this.runsInFlight]);
1571
+ const result = await query.executeTakeFirst();
1572
+ const recovered = Number(result.numUpdatedRows);
1573
+ if (recovered > 0) logger$2.warn(`Recovered ${recovered} workflow run(s) journaled by a stopped reactor but never started; they are now FAILED and rerunnable`);
1574
+ return recovered;
1575
+ }
1576
+ async enqueueRun(options) {
1577
+ const id = randomUUID();
1578
+ await this.db.insertInto("run").values({
1579
+ id,
1580
+ workflow_id: options.workflowId,
1581
+ workflow_name: "",
1582
+ workflow_version: 0,
1583
+ trigger_kind: options.triggerKind,
1584
+ trigger_payload: jsonOrNull(redact(options.triggerPayload)),
1585
+ status: PENDING_RUN_STATUS,
1586
+ error: null,
1587
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1588
+ ended_at: null,
1589
+ rerun_of: null
1590
+ }).execute();
1591
+ this.runsInFlight.add(id);
1592
+ return id;
1593
+ }
1594
+ async beginRun(runId, details) {
1595
+ this.runsInFlight.add(runId);
1596
+ await this.db.updateTable("run").set({
1597
+ status: "RUNNING",
1598
+ workflow_name: details.workflowName,
1599
+ workflow_version: details.workflowVersion,
1600
+ started_at: (/* @__PURE__ */ new Date()).toISOString()
1601
+ }).where("id", "=", runId).execute();
1602
+ }
1603
+ async startRun(options) {
1604
+ const id = randomUUID();
1605
+ await this.db.insertInto("run").values({
1606
+ id,
1607
+ workflow_id: options.workflowId,
1608
+ workflow_name: options.workflowName,
1609
+ workflow_version: options.workflowVersion,
1610
+ trigger_kind: options.triggerKind,
1611
+ trigger_payload: jsonOrNull(redact(options.triggerPayload)),
1612
+ status: "RUNNING",
1613
+ error: null,
1614
+ started_at: (/* @__PURE__ */ new Date()).toISOString(),
1615
+ ended_at: null,
1616
+ rerun_of: options.rerunOf ?? null
1617
+ }).execute();
1618
+ this.runsInFlight.add(id);
1619
+ return id;
1620
+ }
1621
+ async recordStep(runId, ordinal, step) {
1622
+ const values = stepValues(runId, ordinal, step);
1623
+ const { run_id: _run, step_id: _step, ...mutable } = values;
1624
+ await this.db.insertInto("step_execution").values({
1625
+ id: randomUUID(),
1626
+ ...values
1627
+ }).onConflict((oc) => oc.columns(["run_id", "step_id"]).doUpdateSet(mutable)).execute();
1628
+ }
1629
+ async finishRun(runId, result, executionOrder) {
1630
+ this.runsInFlight.delete(runId);
1631
+ if (result.steps.length > 0) try {
1632
+ await this.sweepSteps(runId, result, executionOrder);
1633
+ } catch (error) {
1634
+ logger$2.warn(`Run ${runId}: writing the closing step journal failed; the run is closed out without it`, error);
1635
+ }
1636
+ await this.db.updateTable("run").set({
1637
+ status: result.status,
1638
+ error: result.error ? redactMessage(result.error) : null,
1639
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
1640
+ }).where("id", "=", runId).execute();
1641
+ }
1642
+ async sweepSteps(runId, result, executionOrder) {
1643
+ const journaled = await this.db.selectFrom("step_execution").select(["step_id", "ordinal"]).where("run_id", "=", runId).execute();
1644
+ const ordinals = new Map(journaled.map((row) => [row.step_id, row.ordinal]));
1645
+ let nextOrdinal = journaled.reduce((max, row) => Math.max(max, row.ordinal + 1), 0);
1646
+ for (const step of result.steps) {
1647
+ const ran = executionOrder?.get(step.stepId);
1648
+ if (ran === void 0 || ordinals.has(step.stepId)) continue;
1649
+ ordinals.set(step.stepId, ran);
1650
+ nextOrdinal = Math.max(nextOrdinal, ran + 1);
1651
+ }
1652
+ const ordinalFor = (step) => ordinals.get(step.stepId) ?? nextOrdinal++;
1653
+ await this.db.insertInto("step_execution").values(result.steps.map((step) => ({
1654
+ id: randomUUID(),
1655
+ ...stepValues(runId, ordinalFor(step), step)
1656
+ }))).onConflict((oc) => oc.columns(["run_id", "step_id"]).doUpdateSet((eb) => ({
1657
+ ordinal: eb.ref("excluded.ordinal"),
1658
+ step_key: eb.ref("excluded.step_key"),
1659
+ block_type: eb.ref("excluded.block_type"),
1660
+ status: eb.ref("excluded.status"),
1661
+ input: eb.ref("excluded.input"),
1662
+ output: eb.ref("excluded.output"),
1663
+ port: eb.ref("excluded.port"),
1664
+ error: eb.ref("excluded.error")
1665
+ }))).execute();
1666
+ }
1667
+ async failRun(runId, error) {
1668
+ this.runsInFlight.delete(runId);
1669
+ await this.db.updateTable("run").set({
1670
+ status: "FAILED",
1671
+ error: redactMessage(error),
1672
+ ended_at: (/* @__PURE__ */ new Date()).toISOString()
1673
+ }).where("id", "=", runId).execute();
1674
+ }
1675
+ async listRuns(workflowId, limit = 25) {
1676
+ if (Array.isArray(workflowId) && workflowId.length === 0) return [];
1677
+ let query = this.db.selectFrom("run").selectAll().orderBy("started_at", "desc").limit(Math.min(Math.max(limit, 1), 100));
1678
+ if (Array.isArray(workflowId)) query = query.where("workflow_id", "in", workflowId);
1679
+ else if (workflowId) query = query.where("workflow_id", "=", workflowId);
1680
+ return query.execute();
1681
+ }
1682
+ async getRun(id) {
1683
+ return this.db.selectFrom("run").selectAll().where("id", "=", id).executeTakeFirst();
1684
+ }
1685
+ async getSteps(runId) {
1686
+ return this.db.selectFrom("step_execution").selectAll().where("run_id", "=", runId).orderBy("ordinal", "asc").execute();
1687
+ }
1688
+ async getTriggerState(workflowId) {
1689
+ return this.db.selectFrom("trigger_state").selectAll().where("workflow_id", "=", workflowId).executeTakeFirst();
1690
+ }
1691
+ async upsertTriggerState(row) {
1692
+ const values = {
1693
+ ...row,
1694
+ last_error: row.last_error ? redactMessage(row.last_error) : null
1695
+ };
1696
+ await this.db.insertInto("trigger_state").values(values).onConflict((oc) => {
1697
+ const { workflow_id: _, ...rest } = values;
1698
+ return oc.column("workflow_id").doUpdateSet(rest);
1699
+ }).execute();
1700
+ }
1701
+ async setTriggerStatus(workflowId, status, error) {
1702
+ await this.db.updateTable("trigger_state").set({
1703
+ status,
1704
+ last_error: error ? redactMessage(error) : null,
1705
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
1706
+ }).where("workflow_id", "=", workflowId).execute();
1707
+ }
1708
+ async listDueTriggerStates(nowIso) {
1709
+ return (await this.db.selectFrom("trigger_state").selectAll().where("status", "=", "ENABLED").where("next_poll_at", "<=", nowIso).execute()).filter((row) => !this.unmigrated.has(row.workflow_id));
1710
+ }
1711
+ async listTriggerStates() {
1712
+ return this.db.selectFrom("trigger_state").selectAll().orderBy("workflow_id", "asc").execute();
1713
+ }
1714
+ async recordPollSuccess(workflowId, storeState, nowIso, nextPollAtIso) {
1715
+ await this.db.updateTable("trigger_state").set({
1716
+ store_state: storeState,
1717
+ last_poll_at: nowIso,
1718
+ next_poll_at: nextPollAtIso,
1719
+ last_error: null,
1720
+ consecutive_failures: 0,
1721
+ updated_at: nowIso
1722
+ }).where("workflow_id", "=", workflowId).execute();
1723
+ }
1724
+ async recordPollFailure(workflowId, error, nowIso, nextPollAtIso, consecutiveFailures) {
1725
+ await this.db.updateTable("trigger_state").set({
1726
+ last_poll_at: nowIso,
1727
+ next_poll_at: nextPollAtIso,
1728
+ last_error: redactMessage(error),
1729
+ consecutive_failures: consecutiveFailures,
1730
+ updated_at: nowIso
1731
+ }).where("workflow_id", "=", workflowId).execute();
1732
+ }
1733
+ async claimDedupe(workflowId, dedupeKey, ttlMs, nowIso) {
1734
+ const cutoff = new Date(Date.parse(nowIso) - ttlMs).toISOString();
1735
+ await this.db.deleteFrom("trigger_dedupe").where("workflow_id", "=", workflowId).where("created_at", "<", cutoff).execute();
1736
+ return await this.db.insertInto("trigger_dedupe").values({
1737
+ workflow_id: workflowId,
1738
+ dedupe_key: dedupeKey,
1739
+ run_id: null,
1740
+ created_at: nowIso
1741
+ }).onConflict((oc) => oc.columns(["workflow_id", "dedupe_key"]).doNothing()).returning("dedupe_key").executeTakeFirst() !== void 0;
1742
+ }
1743
+ async recordDedupeRun(workflowId, dedupeKey, runId) {
1744
+ await this.db.updateTable("trigger_dedupe").set({ run_id: runId }).where("workflow_id", "=", workflowId).where("dedupe_key", "=", dedupeKey).execute();
1745
+ }
1746
+ async getPieceStoreValue(scope, scopeKey, key) {
1747
+ const row = await this.db.selectFrom("piece_store").select("value").where("scope", "=", scope).where("scope_key", "=", scopeKey).where("key", "=", key).executeTakeFirst();
1748
+ if (!row) return null;
1749
+ try {
1750
+ return JSON.parse(row.value);
1751
+ } catch {
1752
+ return null;
1753
+ }
1754
+ }
1755
+ async setPieceStoreValue(scope, scopeKey, key, value) {
1756
+ assertPieceStoreEntry(key, value);
1757
+ const encoded = JSON.stringify(value);
1758
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
1759
+ await this.db.insertInto("piece_store").values({
1760
+ scope,
1761
+ scope_key: scopeKey,
1762
+ key,
1763
+ value: encoded,
1764
+ updated_at: nowIso
1765
+ }).onConflict((oc) => oc.columns([
1766
+ "scope",
1767
+ "scope_key",
1768
+ "key"
1769
+ ]).doUpdateSet({
1770
+ value: encoded,
1771
+ updated_at: nowIso
1772
+ })).execute();
1773
+ }
1774
+ async deletePieceStoreValue(scope, scopeKey, key) {
1775
+ await this.db.deleteFrom("piece_store").where("scope", "=", scope).where("scope_key", "=", scopeKey).where("key", "=", key).execute();
1776
+ }
1777
+ async listPieceStore(scope, scopeKey) {
1778
+ const rows = await this.db.selectFrom("piece_store").selectAll().where("scope", "=", scope).where("scope_key", "=", scopeKey).execute();
1779
+ const out = {};
1780
+ for (const row of rows) try {
1781
+ out[row.key] = JSON.parse(row.value);
1782
+ } catch {}
1783
+ return out;
1784
+ }
1785
+ async deletePieceStore(scope, scopeKey) {
1786
+ await this.db.deleteFrom("piece_store").where("scope", "=", scope).where("scope_key", "=", scopeKey).execute();
1787
+ }
1788
+ };
1789
+ //#endregion
1790
+ //#region src/reactor/trigger-supervisor.ts
1791
+ const logger$1 = childLogger(["workflow", "trigger-supervisor"]);
1792
+ const SCHEDULE_TRIGGER_KIND = "schedule";
1793
+ const MIN_INTERVAL_MS = MIN_SCHEDULE_INTERVAL_MS;
1794
+ const MAX_BACKOFF_MS = 30 * 6e4;
1795
+ const DEDUPE_TTL_MS = 3e4;
1796
+ const DEFAULT_RECONCILE_INTERVAL_MS = 15 * 6e4;
1797
+ function isSchedule(binding) {
1798
+ return binding.kind === "schedule";
1799
+ }
1800
+ function configHash(blockType, config) {
1801
+ return createHash("sha256").update(blockType).update(JSON.stringify(config ?? {})).digest("hex").slice(0, 16);
1802
+ }
1803
+ function intervalFromSchedules(schedules, defaultMs) {
1804
+ const schedule = schedules?.at(-1);
1805
+ if (!schedule) return Math.max(defaultMs, MIN_INTERVAL_MS);
1806
+ if ("intervalMs" in schedule) return Math.max(schedule.intervalMs, MIN_INTERVAL_MS);
1807
+ const cron = schedule.cronExpression;
1808
+ const intervalMs = cronIntervalMs(cron);
1809
+ if (intervalMs === void 0) {
1810
+ logger$1.warn(`Unsupported setSchedule cron "${cron}"; using the default`);
1811
+ return Math.max(defaultMs, MIN_INTERVAL_MS);
1812
+ }
1813
+ return intervalMs;
1814
+ }
1815
+ function pollIntervalFor(binding, schedules, defaultMs) {
1816
+ if (binding.pollIntervalMs !== void 0) return Math.max(binding.pollIntervalMs, MIN_INTERVAL_MS);
1817
+ return intervalFromSchedules(schedules, defaultMs);
1818
+ }
1819
+ const VESTIGIAL_STORE_STATE = "{}";
1820
+ var MissingJournalError = class extends Error {
1821
+ constructor(what) {
1822
+ super(`${what} needs a run journal, and none is configured`);
1823
+ this.name = "MissingJournalError";
1824
+ }
1825
+ };
1826
+ var TriggerConfigError = class extends Error {
1827
+ constructor(message) {
1828
+ super(message);
1829
+ this.name = "TriggerConfigError";
1830
+ }
1831
+ };
1832
+ function isPermanentFailure(error) {
1833
+ if (error instanceof TriggerConfigError) return true;
1834
+ return error instanceof PieceWorkerError && error.serialized.unsupportedMember !== void 0;
1835
+ }
1836
+ function backoffMs(intervalMs, failures) {
1837
+ return Math.min(intervalMs * 2 ** failures, MAX_BACKOFF_MS);
1838
+ }
1839
+ var TriggerSupervisor = class {
1840
+ bindings = /* @__PURE__ */ new Map();
1841
+ worker;
1842
+ tickMs;
1843
+ defaultIntervalMs;
1844
+ hookTimeoutMs;
1845
+ now;
1846
+ egress;
1847
+ timer;
1848
+ ops = Promise.resolve();
1849
+ ticking = false;
1850
+ warnedMissingJournal = false;
1851
+ constructor(options) {
1852
+ this.options = options;
1853
+ this.worker = options.worker ?? new PieceWorker();
1854
+ this.tickMs = options.tickMs ?? 15e3;
1855
+ this.defaultIntervalMs = options.defaultIntervalMs ?? 3e5;
1856
+ this.hookTimeoutMs = options.hookTimeoutMs ?? 6e4;
1857
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
1858
+ this.egress = options.egress === void 0 ? DEFAULT_EGRESS_POLICY : options.egress ?? void 0;
1859
+ }
1860
+ start() {
1861
+ if (this.timer) return;
1862
+ this.timer = setInterval(() => {
1863
+ this.tick().catch((error) => {
1864
+ logger$1.error("Trigger tick failed: @error", error);
1865
+ });
1866
+ }, this.tickMs);
1867
+ this.timer.unref();
1868
+ logger$1.info(`Trigger supervisor started (tick ${this.tickMs}ms)`);
1869
+ }
1870
+ stop() {
1871
+ if (this.timer) clearInterval(this.timer);
1872
+ this.timer = void 0;
1873
+ if (!this.options.worker) this.worker.dispose();
1874
+ logger$1.info("Trigger supervisor stopped");
1875
+ }
1876
+ enqueue(task) {
1877
+ const run = this.ops.then(task);
1878
+ this.ops = run.catch(() => void 0);
1879
+ return run;
1880
+ }
1881
+ enabledOk = /* @__PURE__ */ new Set();
1882
+ descriptors = /* @__PURE__ */ new Map();
1883
+ own;
1884
+ resolver() {
1885
+ return this.options.resolver ?? (this.own ??= fetchingResolver(this.options.cacheDir));
1886
+ }
1887
+ enableRetries = /* @__PURE__ */ new Map();
1888
+ upsert(binding) {
1889
+ const previous = this.bindings.get(binding.workflowId);
1890
+ if (previous && this.enabledOk.has(binding.workflowId) && JSON.stringify(previous) === JSON.stringify(binding)) return Promise.resolve();
1891
+ this.bindings.set(binding.workflowId, binding);
1892
+ return this.enqueue(() => this.enable(binding, previous));
1893
+ }
1894
+ remove(workflowId) {
1895
+ const binding = this.bindings.get(workflowId);
1896
+ this.bindings.delete(workflowId);
1897
+ this.enabledOk.delete(workflowId);
1898
+ this.enableRetries.delete(workflowId);
1899
+ return this.enqueue(() => this.disable(workflowId, binding));
1900
+ }
1901
+ test(binding) {
1902
+ return this.enqueue(async () => {
1903
+ const store = await this.options.store();
1904
+ try {
1905
+ return (await this.hook(binding, "test")).output;
1906
+ } finally {
1907
+ await this.dropTestPartitions(store, binding.workflowId);
1908
+ }
1909
+ });
1910
+ }
1911
+ async dropTestPartitions(store, workflowId) {
1912
+ if (!store) return;
1913
+ try {
1914
+ await store.deletePieceStore("FLOW", testPartitionKey("FLOW", workflowId));
1915
+ await store.deletePieceStore("PROJECT", testPartitionKey("PROJECT", workflowId));
1916
+ } catch (error) {
1917
+ logger$1.warn(`Could not clear test store for ${workflowId}`, error);
1918
+ }
1919
+ }
1920
+ handshake(binding, payload) {
1921
+ return this.enqueue(() => this.hook(binding, "onHandshake", { payload }));
1922
+ }
1923
+ deliverWebhook(workflowId, payload) {
1924
+ return this.enqueue(async () => {
1925
+ const binding = this.bindings.get(workflowId);
1926
+ if (!binding || isSchedule(binding)) {
1927
+ logger$1.warn(`Webhook delivery for unknown workflow ${workflowId}`);
1928
+ return;
1929
+ }
1930
+ const store = await this.options.store();
1931
+ if (!store) throw new MissingJournalError("Webhook delivery");
1932
+ const row = await store.getTriggerState(workflowId);
1933
+ const now = this.now();
1934
+ const rewind = await this.cursorRewind(store, workflowId);
1935
+ try {
1936
+ const result = await this.hook(binding, "run", { payload });
1937
+ if (!Array.isArray(result.output)) throw new Error(`Trigger run returned ${typeof result.output}, expected an array`);
1938
+ if (row?.status === "ENABLED") await store.recordPollSuccess(workflowId, VESTIGIAL_STORE_STATE, now.toISOString(), new Date(now.getTime() + row.interval_ms).toISOString());
1939
+ for (const item of result.output) await this.fireItem(store, binding, item, now);
1940
+ } catch (error) {
1941
+ await rewind();
1942
+ throw error;
1943
+ }
1944
+ });
1945
+ }
1946
+ async cursorRewind(store, workflowId) {
1947
+ const before = await store.listPieceStore("FLOW", workflowId);
1948
+ return async () => {
1949
+ try {
1950
+ await store.deletePieceStore("FLOW", workflowId);
1951
+ for (const [key, value] of Object.entries(before)) await store.setPieceStoreValue("FLOW", workflowId, key, value);
1952
+ } catch (error) {
1953
+ logger$1.warn(`Could not rewind the cursor for ${workflowId}`, error);
1954
+ }
1955
+ };
1956
+ }
1957
+ async hook(binding, hook, options = {}) {
1958
+ const store = await this.options.store();
1959
+ if (!store && hook !== "test") throw new MissingJournalError(`Trigger hook "${hook}"`);
1960
+ const pieceStore = store ? createPieceStorePort(store, () => binding.workflowId, hook === "test") : void 0;
1961
+ const piece = await this.resolver().resolve(binding.packageName, binding.version);
1962
+ const auth = await this.options.resolveAuth(binding.connectionId, {
1963
+ blockType: binding.blockType,
1964
+ piecePackage: binding.packageName
1965
+ });
1966
+ const redactValues = secretsFor(auth);
1967
+ return this.worker.runTriggerHook({
1968
+ ...pieceModuleRef(piece),
1969
+ triggerName: binding.triggerName,
1970
+ hook,
1971
+ propsValue: binding.config,
1972
+ auth,
1973
+ ...redactValues.length > 0 ? { redactValues } : {},
1974
+ ...pieceStore ? { durableStore: true } : {},
1975
+ identity: { flowId: binding.workflowId },
1976
+ isRepublish: options.isRepublish,
1977
+ payload: options.payload,
1978
+ webhookUrl: options.webhookUrl ?? `http://localhost:0/v1/webhooks/${binding.workflowId}`,
1979
+ ...this.egress ? { egress: this.egress } : {}
1980
+ }, {
1981
+ timeoutMs: this.hookTimeoutMs,
1982
+ ...pieceStore ? { hostCalls: storeHandlers(pieceStore) } : {}
1983
+ });
1984
+ }
1985
+ async strategyFor(binding) {
1986
+ const key = `${binding.packageName}@${binding.version}`;
1987
+ let descriptor = this.descriptors.get(key);
1988
+ if (!descriptor) {
1989
+ const piece = await this.resolver().resolve(binding.packageName, binding.version);
1990
+ descriptor = (await this.worker.describePiece({
1991
+ ...pieceModuleRef(piece),
1992
+ packageName: binding.packageName,
1993
+ version: binding.version,
1994
+ ...this.egress ? { egress: this.egress } : {}
1995
+ }, { timeoutMs: this.hookTimeoutMs })).output;
1996
+ this.descriptors.set(key, descriptor);
1997
+ }
1998
+ return descriptor.triggers.find((candidate) => candidate.name === binding.triggerName)?.strategy ?? "POLLING";
1999
+ }
2000
+ async enable(binding, superseded) {
2001
+ const store = await this.options.store();
2002
+ if (!store) throw new MissingJournalError("Enabling a trigger");
2003
+ const hash = configHash(binding.blockType, binding.config);
2004
+ const existing = await store.getTriggerState(binding.workflowId);
2005
+ const now = this.now();
2006
+ const isRepublish = existing?.config_hash === hash && existing.status === "ENABLED" && !store.hasUnmigratedTriggerState(binding.workflowId);
2007
+ if (existing && !isRepublish && existing.status === "ENABLED") await this.disableRow(binding.workflowId, existing, superseded);
2008
+ const pending = this.enableRetries.get(binding.workflowId);
2009
+ if (isSchedule(binding)) {
2010
+ this.enableRetries.delete(binding.workflowId);
2011
+ if (pending?.release && existing && superseded && !isSchedule(superseded)) await this.releaseRegistration(superseded);
2012
+ await this.enableSchedule(store, binding, hash, existing);
2013
+ return;
2014
+ }
2015
+ if (this.deferToStoredRetry(binding, hash, existing, now)) return;
2016
+ if (pending?.release && existing) await this.releaseRegistration(binding);
2017
+ if (!isRepublish) await store.deletePieceStore("FLOW", binding.workflowId);
2018
+ let reachedProvider = false;
2019
+ try {
2020
+ const strategy = await this.strategyFor(binding);
2021
+ const webhook = strategy === "WEBHOOK" || strategy === "APP_WEBHOOK";
2022
+ const webhookUrl = webhook ? await this.webhookUrlOrThrow(binding.workflowId) : void 0;
2023
+ if (webhook && !webhookUrl) throw new Error("This trigger delivers by webhook, but no public webhook endpoint is configured for the reactor");
2024
+ reachedProvider = true;
2025
+ const result = await this.hook(binding, "onEnable", {
2026
+ isRepublish,
2027
+ webhookUrl
2028
+ });
2029
+ const intervalMs = webhook ? this.options.reconcileIntervalMs ?? DEFAULT_RECONCILE_INTERVAL_MS : pollIntervalFor(binding, result.schedules, this.defaultIntervalMs);
2030
+ await store.upsertTriggerState({
2031
+ workflow_id: binding.workflowId,
2032
+ block_type: binding.blockType,
2033
+ config_hash: hash,
2034
+ status: "ENABLED",
2035
+ store_state: VESTIGIAL_STORE_STATE,
2036
+ interval_ms: intervalMs,
2037
+ next_poll_at: new Date(now.getTime() + intervalMs).toISOString(),
2038
+ last_poll_at: null,
2039
+ last_error: null,
2040
+ consecutive_failures: 0,
2041
+ lease_owner: null,
2042
+ lease_expires_at: null,
2043
+ updated_at: now.toISOString()
2044
+ });
2045
+ this.enabledOk.add(binding.workflowId);
2046
+ this.enableRetries.delete(binding.workflowId);
2047
+ store.clearUnmigratedTriggerState(binding.workflowId);
2048
+ logger$1.info(`Enabled ${binding.blockType} for workflow ${binding.workflowId} (every ${intervalMs}ms)`);
2049
+ } catch (error) {
2050
+ this.enabledOk.delete(binding.workflowId);
2051
+ const message = error instanceof Error ? error.message : String(error);
2052
+ const failures = (existing?.consecutive_failures ?? 0) + 1;
2053
+ const intervalMs = pollIntervalFor(binding, void 0, this.defaultIntervalMs);
2054
+ const retryAt = isPermanentFailure(error) ? void 0 : new Date(now.getTime() + backoffMs(intervalMs, failures));
2055
+ if (retryAt) this.enableRetries.set(binding.workflowId, {
2056
+ at: retryAt.getTime(),
2057
+ failures,
2058
+ release: reachedProvider
2059
+ });
2060
+ else this.enableRetries.delete(binding.workflowId);
2061
+ await store.upsertTriggerState({
2062
+ workflow_id: binding.workflowId,
2063
+ block_type: binding.blockType,
2064
+ config_hash: hash,
2065
+ status: "ERROR",
2066
+ store_state: VESTIGIAL_STORE_STATE,
2067
+ interval_ms: intervalMs,
2068
+ next_poll_at: retryAt?.toISOString() ?? null,
2069
+ last_poll_at: null,
2070
+ last_error: message,
2071
+ consecutive_failures: failures,
2072
+ lease_owner: null,
2073
+ lease_expires_at: null,
2074
+ updated_at: now.toISOString()
2075
+ });
2076
+ logger$1.error(`onEnable failed for workflow ${binding.workflowId} (${failures}x): ${message}` + (retryAt ? `; retrying at ${retryAt.toISOString()}` : "; not retrying"));
2077
+ }
2078
+ }
2079
+ async webhookUrlOrThrow(workflowId) {
2080
+ const mint = this.options.webhookUrlFor;
2081
+ if (!mint) throw new TriggerConfigError("This trigger delivers by webhook, but no public webhook endpoint is configured for the reactor");
2082
+ const url = await mint(workflowId);
2083
+ if (!url) throw new Error("The reactor's webhook endpoint is not available yet for this workflow");
2084
+ return url;
2085
+ }
2086
+ deferToStoredRetry(binding, hash, existing, now) {
2087
+ if (existing?.status !== "ERROR" || existing.config_hash !== hash) return false;
2088
+ const pending = this.enableRetries.get(binding.workflowId);
2089
+ const stored = existing.next_poll_at ? Date.parse(existing.next_poll_at) : NaN;
2090
+ const at = pending?.at ?? (Number.isFinite(stored) ? stored : void 0);
2091
+ if (at === void 0 || at <= now.getTime()) return false;
2092
+ if (!pending) {
2093
+ this.enableRetries.set(binding.workflowId, {
2094
+ at,
2095
+ failures: existing.consecutive_failures,
2096
+ release: true
2097
+ });
2098
+ logger$1.info(`Enable for workflow ${binding.workflowId} still backing off until ${existing.next_poll_at}`);
2099
+ }
2100
+ return true;
2101
+ }
2102
+ async releaseRegistration(binding) {
2103
+ try {
2104
+ await this.hook(binding, "onDisable");
2105
+ } catch (error) {
2106
+ logger$1.warn(`onDisable before retrying workflow ${binding.workflowId} failed`, error);
2107
+ }
2108
+ }
2109
+ async enableSchedule(store, binding, hash, existing) {
2110
+ const now = this.now();
2111
+ const base = {
2112
+ workflow_id: binding.workflowId,
2113
+ block_type: binding.blockType,
2114
+ config_hash: hash,
2115
+ store_state: VESTIGIAL_STORE_STATE,
2116
+ last_poll_at: existing?.last_poll_at ?? null,
2117
+ lease_owner: null,
2118
+ lease_expires_at: null,
2119
+ updated_at: now.toISOString()
2120
+ };
2121
+ try {
2122
+ const schedule = parseScheduleConfig(binding.config);
2123
+ const carried = existing?.status === "ENABLED" && existing.config_hash === hash ? existing.next_poll_at : null;
2124
+ const nextAt = carried ? new Date(carried) : nextFireAt(schedule, now);
2125
+ await store.upsertTriggerState({
2126
+ ...base,
2127
+ status: "ENABLED",
2128
+ interval_ms: schedule.mode === "interval" ? schedule.everyMs : MIN_SCHEDULE_INTERVAL_MS,
2129
+ next_poll_at: nextAt.toISOString(),
2130
+ last_error: null,
2131
+ consecutive_failures: 0
2132
+ });
2133
+ this.enabledOk.add(binding.workflowId);
2134
+ logger$1.info(`Scheduled workflow ${binding.workflowId}: next fire ${nextAt.toISOString()}` + (carried ? " (carried over)" : ""));
2135
+ } catch (error) {
2136
+ this.enabledOk.delete(binding.workflowId);
2137
+ const message = error instanceof Error ? error.message : String(error);
2138
+ await store.upsertTriggerState({
2139
+ ...base,
2140
+ status: "ERROR",
2141
+ interval_ms: MIN_SCHEDULE_INTERVAL_MS,
2142
+ next_poll_at: null,
2143
+ last_error: message,
2144
+ consecutive_failures: (existing?.consecutive_failures ?? 0) + 1
2145
+ });
2146
+ logger$1.error(`Invalid schedule for workflow ${binding.workflowId}: ${message}`);
2147
+ }
2148
+ }
2149
+ async disable(workflowId, binding) {
2150
+ const store = await this.options.store();
2151
+ if (!store) throw new MissingJournalError("Disabling a trigger");
2152
+ const row = await store.getTriggerState(workflowId);
2153
+ if (!row || row.status === "DISABLED") return;
2154
+ await this.disableRow(workflowId, row, binding);
2155
+ }
2156
+ async disableRow(workflowId, row, binding) {
2157
+ const store = await this.options.store();
2158
+ if (!store) return;
2159
+ const target = binding ?? this.bindingFromRow(row);
2160
+ if (target && !isSchedule(target) && row.block_type !== "core#schedule") try {
2161
+ await this.hook(target, "onDisable");
2162
+ } catch (error) {
2163
+ logger$1.warn(`onDisable failed for workflow ${workflowId}`, error);
2164
+ }
2165
+ await store.setTriggerStatus(workflowId, "DISABLED");
2166
+ }
2167
+ bindingFromRow(row) {
2168
+ return this.bindings.get(row.workflow_id);
2169
+ }
2170
+ async tick() {
2171
+ if (this.ticking) return;
2172
+ this.ticking = true;
2173
+ try {
2174
+ await this.enqueue(() => this.pollDue());
2175
+ } finally {
2176
+ this.ticking = false;
2177
+ }
2178
+ }
2179
+ async retryOneEnable() {
2180
+ const now = this.now().getTime();
2181
+ const next = [...this.enableRetries].filter(([, retry]) => retry.at <= now).sort((a, b) => a[1].at - b[1].at).at(0);
2182
+ if (!next) return;
2183
+ const [workflowId, retry] = next;
2184
+ const binding = this.bindings.get(workflowId);
2185
+ if (!binding) {
2186
+ this.enableRetries.delete(workflowId);
2187
+ return;
2188
+ }
2189
+ try {
2190
+ await this.enable(binding);
2191
+ } catch (error) {
2192
+ const failures = retry.failures + 1;
2193
+ const intervalMs = isSchedule(binding) ? MIN_INTERVAL_MS : pollIntervalFor(binding, void 0, this.defaultIntervalMs);
2194
+ this.enableRetries.set(workflowId, {
2195
+ at: now + backoffMs(intervalMs, failures),
2196
+ failures,
2197
+ release: retry.release
2198
+ });
2199
+ logger$1.error(`Enable retry for workflow ${workflowId} threw`, error);
2200
+ }
2201
+ }
2202
+ async pollDue() {
2203
+ const store = await this.options.store();
2204
+ if (!store) {
2205
+ if (!this.warnedMissingJournal) {
2206
+ this.warnedMissingJournal = true;
2207
+ logger$1.error("Trigger polling is off: @error", new MissingJournalError("Polling"));
2208
+ }
2209
+ return;
2210
+ }
2211
+ const due = await store.listDueTriggerStates(this.now().toISOString());
2212
+ for (const row of due) {
2213
+ const binding = this.bindings.get(row.workflow_id);
2214
+ if (!binding) {
2215
+ await store.setTriggerStatus(row.workflow_id, "DISABLED");
2216
+ continue;
2217
+ }
2218
+ if (isSchedule(binding)) await this.fireSchedule(store, row, binding);
2219
+ else await this.poll(store, row, binding);
2220
+ }
2221
+ await this.retryOneEnable();
2222
+ }
2223
+ async fireSchedule(store, row, binding) {
2224
+ const now = this.now();
2225
+ try {
2226
+ const schedule = parseScheduleConfig(binding.config);
2227
+ const scheduledFor = row.next_poll_at ? new Date(row.next_poll_at) : now;
2228
+ const nextAt = rescheduleAfterFire(schedule, scheduledFor, now);
2229
+ await store.recordPollSuccess(row.workflow_id, "{}", now.toISOString(), nextAt.toISOString());
2230
+ this.options.fire(binding.workflowId, schedulePayload(schedule, scheduledFor, now), SCHEDULE_TRIGGER_KIND);
2231
+ } catch (error) {
2232
+ const message = error instanceof Error ? error.message : String(error);
2233
+ this.enabledOk.delete(binding.workflowId);
2234
+ await store.setTriggerStatus(row.workflow_id, "ERROR", message);
2235
+ logger$1.error(`Schedule fire failed for workflow ${row.workflow_id}: ${message}`);
2236
+ }
2237
+ }
2238
+ async poll(store, row, binding) {
2239
+ const now = this.now();
2240
+ const rewind = await this.cursorRewind(store, row.workflow_id);
2241
+ try {
2242
+ const result = await this.hook(binding, "run");
2243
+ if (!Array.isArray(result.output)) throw new Error(`Trigger run returned ${typeof result.output}, expected an array`);
2244
+ await store.recordPollSuccess(row.workflow_id, VESTIGIAL_STORE_STATE, now.toISOString(), new Date(now.getTime() + row.interval_ms).toISOString());
2245
+ for (const item of result.output) await this.fireItem(store, binding, item, now);
2246
+ } catch (error) {
2247
+ await rewind();
2248
+ const message = error instanceof Error ? error.message : String(error);
2249
+ const failures = row.consecutive_failures + 1;
2250
+ const backoff = backoffMs(row.interval_ms, failures);
2251
+ await store.recordPollFailure(row.workflow_id, message, now.toISOString(), new Date(now.getTime() + backoff).toISOString(), failures);
2252
+ logger$1.warn(`Poll failed for workflow ${row.workflow_id} (${failures}x): ${message}`);
2253
+ }
2254
+ }
2255
+ async fireItem(store, binding, item, now) {
2256
+ const dedupeKey = extractDedupeKey(item);
2257
+ if (dedupeKey) {
2258
+ if (!await store.claimDedupe(binding.workflowId, dedupeKey, DEDUPE_TTL_MS, now.toISOString())) return;
2259
+ }
2260
+ this.options.fire(binding.workflowId, item, `piece:${binding.blockType}`);
2261
+ }
2262
+ };
2263
+ //#endregion
2264
+ //#region src/reactor/webhook.ts
2265
+ const WEBHOOK_BLOCK = "core#webhook";
2266
+ const WEBHOOK_TRIGGER_KIND = "webhook";
2267
+ const ALGORITHMS = new Set([
2268
+ "sha1",
2269
+ "sha256",
2270
+ "sha512"
2271
+ ]);
2272
+ const ENCODINGS = new Set(["hex", "base64"]);
2273
+ const SIGNED_SCHEMES = new Set([
2274
+ "token",
2275
+ "hmac",
2276
+ "hmac-prefixed",
2277
+ "hmac-timestamped"
2278
+ ]);
2279
+ const SCHEMES = new Set([
2280
+ "none",
2281
+ "token",
2282
+ "hmac",
2283
+ "hmac-prefixed",
2284
+ "hmac-timestamped"
2285
+ ]);
2286
+ const DEFAULT_HEADER = {
2287
+ none: "",
2288
+ token: "x-webhook-token",
2289
+ hmac: "x-signature",
2290
+ "hmac-prefixed": "x-hub-signature-256",
2291
+ "hmac-timestamped": "stripe-signature"
2292
+ };
2293
+ const HTTP_METHODS = [
2294
+ "GET",
2295
+ "POST",
2296
+ "PUT",
2297
+ "PATCH",
2298
+ "DELETE",
2299
+ "HEAD"
2300
+ ];
2301
+ function asRecord$1(config) {
2302
+ if (config && typeof config === "object" && !Array.isArray(config)) return config;
2303
+ if (typeof config === "string") try {
2304
+ return asRecord$1(JSON.parse(config));
2305
+ } catch {
2306
+ return {};
2307
+ }
2308
+ return {};
2309
+ }
2310
+ function toNumber(value) {
2311
+ if (typeof value === "number") return value;
2312
+ if (typeof value === "string" && value.trim() !== "") {
2313
+ const parsed = Number(value);
2314
+ return Number.isNaN(parsed) ? void 0 : parsed;
2315
+ }
2316
+ }
2317
+ function nonEmptyString(value) {
2318
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
2319
+ }
2320
+ function parseWebhookField(value) {
2321
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2322
+ const record = value;
2323
+ const header = nonEmptyString(record.header);
2324
+ if (header) return { header: header.toLowerCase() };
2325
+ const body = nonEmptyString(record.body);
2326
+ if (body) return { body };
2327
+ throw new Error(`${WEBHOOK_BLOCK}: a field source must name either "header" or "body"`);
2328
+ }
2329
+ const text = nonEmptyString(value);
2330
+ if (!text) return void 0;
2331
+ const match = /^(header|body)\s*:\s*(\S.*)$/i.exec(text);
2332
+ if (!match) return text;
2333
+ const source = match[1].toLowerCase();
2334
+ const name = match[2].trim();
2335
+ return source === "header" ? { header: name.toLowerCase() } : { body: name };
2336
+ }
2337
+ function parseEnum(value, allowed, field) {
2338
+ const text = nonEmptyString(value)?.toLowerCase();
2339
+ if (!text) return void 0;
2340
+ if (!allowed.has(text)) throw new Error(`${WEBHOOK_BLOCK}: "${field}" must be one of ${[...allowed].join(", ")}`);
2341
+ return text;
2342
+ }
2343
+ function parseMethods(value) {
2344
+ const list = typeof value === "string" ? value === "" || value.toUpperCase() === "ANY" ? [] : [value] : Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
2345
+ if (list.length === 0) return void 0;
2346
+ const methods = list.map((method) => method.trim().toUpperCase());
2347
+ for (const method of methods) if (!HTTP_METHODS.includes(method)) throw new Error(`${WEBHOOK_BLOCK}: "${method}" is not one of ${HTTP_METHODS.join(", ")}`);
2348
+ return methods;
2349
+ }
2350
+ function parseWebhookConfig(config) {
2351
+ const record = asRecord$1(config);
2352
+ const rawScheme = nonEmptyString(record.scheme) ?? "none";
2353
+ if (!SCHEMES.has(rawScheme)) throw new Error(`${WEBHOOK_BLOCK}: "scheme" must be one of ${[...SCHEMES].join(", ")}`);
2354
+ const scheme = rawScheme;
2355
+ const secretRef = nonEmptyString(record.secretRef);
2356
+ if (SIGNED_SCHEMES.has(scheme) && !secretRef) throw new Error(`${WEBHOOK_BLOCK}: the "${scheme}" scheme needs a "secretRef"`);
2357
+ const responseMode = record.responseMode === "sync" ? "sync" : "async";
2358
+ const status = toNumber(record.responseStatus) ?? (responseMode === "sync" ? 200 : 202);
2359
+ if (!Number.isInteger(status) || status < 200 || status > 599) throw new Error(`${WEBHOOK_BLOCK}: "responseStatus" must be an integer between 200 and 599`);
2360
+ const tolerance = toNumber(record.toleranceSeconds) ?? 300;
2361
+ if (!Number.isFinite(tolerance) || tolerance <= 0) throw new Error(`${WEBHOOK_BLOCK}: "toleranceSeconds" must be a positive number`);
2362
+ const dedupeTtl = toNumber(record.dedupeTtlSeconds) ?? 300;
2363
+ if (!Number.isFinite(dedupeTtl) || dedupeTtl <= 0) throw new Error(`${WEBHOOK_BLOCK}: "dedupeTtlSeconds" must be a positive number`);
2364
+ return {
2365
+ methods: parseMethods(record.methods),
2366
+ scheme,
2367
+ header: (nonEmptyString(record.header) ?? DEFAULT_HEADER[scheme]).toLowerCase(),
2368
+ secretRef,
2369
+ toleranceSeconds: tolerance,
2370
+ responseMode,
2371
+ responseStatus: status,
2372
+ algorithm: parseEnum(record.algorithm, ALGORITHMS, "algorithm"),
2373
+ encoding: parseEnum(record.encoding, ENCODINGS, "encoding"),
2374
+ prefix: typeof record.prefix === "string" ? record.prefix : void 0,
2375
+ challengeField: parseWebhookField(record.challengeField),
2376
+ dedupeField: parseWebhookField(record.dedupeField),
2377
+ dedupeTtlSeconds: dedupeTtl
2378
+ };
2379
+ }
2380
+ //#endregion
2381
+ //#region src/reactor/piece-handshake.ts
2382
+ const strategies = WebhookHandshakeStrategy;
2383
+ function handshakeMatches(handshake, request) {
2384
+ if (handshake.strategy === strategies.HEAD_REQUEST) return request.method.toUpperCase() === "HEAD";
2385
+ const name = handshake.paramName;
2386
+ if (!name) return false;
2387
+ switch (handshake.strategy) {
2388
+ case strategies.HEADER_PRESENT: return Object.hasOwn(request.headers, name.toLowerCase());
2389
+ case strategies.QUERY_PRESENT: return Object.hasOwn(request.queryParams, name);
2390
+ case strategies.BODY_PARAM_PRESENT: return typeof request.body === "object" && request.body !== null && !Array.isArray(request.body) && name in request.body;
2391
+ default: return false;
2392
+ }
2393
+ }
2394
+ function handshakeReply(output) {
2395
+ if (typeof output !== "object" || output === null) return { status: 200 };
2396
+ const record = output;
2397
+ const status = typeof record.status === "number" ? record.status : 200;
2398
+ const body = record.body;
2399
+ if (body === void 0 || body === null) return { status };
2400
+ if (typeof body === "string") return {
2401
+ status,
2402
+ body
2403
+ };
2404
+ return {
2405
+ status,
2406
+ body: JSON.stringify(body),
2407
+ contentType: "application/json"
2408
+ };
2409
+ }
2410
+ //#endregion
2411
+ //#region src/reactor/trigger-filters.ts
2412
+ const TRIGGER_KIND_BY_BLOCK = {
2413
+ [DOCUMENT_EVENT_BLOCK]: "document-event",
2414
+ [DOCUMENT_CREATED_BLOCK]: "document-created",
2415
+ [DOCUMENT_DELETED_BLOCK]: "document-deleted"
2416
+ };
2417
+ function lifecycleKindForDocumentAction(actionType) {
2418
+ if (actionType === "CREATE_DOCUMENT") return "document-created";
2419
+ if (actionType === "DELETE_DOCUMENT") return "document-deleted";
2420
+ }
2421
+ function lifecycleKindForDriveAction(actionType) {
2422
+ if (actionType === "ADD_FILE") return "document-created";
2423
+ if (actionType === "DELETE_NODE") return "document-deleted";
2424
+ }
2425
+ function toList(value) {
2426
+ if (typeof value === "string") return value ? [value] : void 0;
2427
+ if (Array.isArray(value)) {
2428
+ const strings = value.filter((item) => typeof item === "string");
2429
+ return strings.length > 0 ? strings : void 0;
2430
+ }
2431
+ }
2432
+ function asRecord(config) {
2433
+ if (config === null || typeof config !== "object") return {};
2434
+ return config;
2435
+ }
2436
+ function parseEventFilter(config) {
2437
+ const record = asRecord(config);
2438
+ return {
2439
+ documentType: toList(record.documentType),
2440
+ documentId: toList(record.documentId),
2441
+ actionType: toList(record.actionType)
2442
+ };
2443
+ }
2444
+ function parseLifecycleFilter(config) {
2445
+ const record = asRecord(config);
2446
+ return {
2447
+ documentType: toList(record.documentType),
2448
+ driveId: toList(record.driveId)
2449
+ };
2450
+ }
2451
+ const ok = (list, value) => !list || value !== void 0 && list.includes(value);
2452
+ function matchesEventFilter(filter, documentType, documentId, actionType) {
2453
+ return ok(filter.documentType, documentType) && ok(filter.documentId, documentId) && ok(filter.actionType, actionType);
2454
+ }
2455
+ function matchesLifecycleFilter(filter, documentType, driveId) {
2456
+ return ok(filter.documentType, documentType ?? void 0) && ok(filter.driveId, driveId ?? void 0);
2457
+ }
2458
+ //#endregion
2459
+ //#region src/reactor/service.ts
2460
+ const CHECK_TIMEOUT_MS = 3e4;
2461
+ const DESCRIBE_TIMEOUT_MS = 3e4;
2462
+ const SEED_ATTEMPTS = 3;
2463
+ const SEED_RETRY_BASE_MS = 250;
2464
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms).unref());
2465
+ const logger = childLogger(["workflow", "runtime"]);
2466
+ const DRIVE_DOCUMENT_TYPE = "powerhouse/document-drive";
2467
+ const WORKFLOW_DOCUMENT_TYPE = "powerhouse/workflow";
2468
+ const DOCUMENT_SCOPE = "document";
2469
+ const CHILD_RELATIONSHIP = "child";
2470
+ function operationKey(op) {
2471
+ return op.context.ordinal > 0 ? `o:${op.context.ordinal}` : `${op.context.documentId}:${op.context.scope}:${op.context.branch}:${op.operation.index}`;
2472
+ }
2473
+ const OPERATION_DEDUPE_TTL_MS = 1440 * 6e4;
2474
+ function stringField(record, key) {
2475
+ const value = record[key];
2476
+ return typeof value === "string" && value !== "" ? value : void 0;
2477
+ }
2478
+ function inputRecord(input) {
2479
+ if (input === null || typeof input !== "object") return {};
2480
+ return input;
2481
+ }
2482
+ function collectLifecycleParentHints(operations) {
2483
+ const hints = /* @__PURE__ */ new Map();
2484
+ const merge = (documentId, hint) => {
2485
+ const existing = hints.get(documentId);
2486
+ hints.set(documentId, existing ? {
2487
+ ...existing,
2488
+ ...hint
2489
+ } : hint);
2490
+ };
2491
+ for (const { operation, context } of operations) {
2492
+ const actionType = operation.action.type;
2493
+ const input = inputRecord(operation.action.input);
2494
+ if (context.scope === DOCUMENT_SCOPE) {
2495
+ if (actionType !== "ADD_RELATIONSHIP" && actionType !== "REMOVE_RELATIONSHIP") continue;
2496
+ if (stringField(input, "relationshipType") !== CHILD_RELATIONSHIP) continue;
2497
+ const target = stringField(input, "targetId");
2498
+ const source = stringField(input, "sourceId");
2499
+ if (!target || !source) continue;
2500
+ merge(target, {
2501
+ parentId: source,
2502
+ parentCandidate: source
2503
+ });
2504
+ continue;
2505
+ }
2506
+ if (context.documentType !== DRIVE_DOCUMENT_TYPE) continue;
2507
+ if (actionType !== "ADD_FILE" && actionType !== "DELETE_NODE") continue;
2508
+ const nodeId = stringField(input, "id");
2509
+ if (!nodeId) continue;
2510
+ merge(nodeId, {
2511
+ driveId: context.documentId,
2512
+ parentId: stringField(input, "parentFolder")
2513
+ });
2514
+ }
2515
+ return hints;
2516
+ }
2517
+ function accountLabelFromCheckResult(result) {
2518
+ if (!result || typeof result !== "object") return void 0;
2519
+ const record = result;
2520
+ for (const key of [
2521
+ "name",
2522
+ "username",
2523
+ "email",
2524
+ "sub"
2525
+ ]) {
2526
+ const value = record[key];
2527
+ if (typeof value === "string" && value !== "") return value;
2528
+ }
2529
+ }
2530
+ function pieceFailureDetail(error, timeoutDetail) {
2531
+ if (error instanceof PieceWorkerTimeoutError) return timeoutDetail;
2532
+ if (error instanceof PieceWorkerError) return error.serialized.message;
2533
+ return error instanceof Error ? error.message : String(error);
2534
+ }
2535
+ function checkFailureDetail(error) {
2536
+ return pieceFailureDetail(error, `Connection check timed out after ${Math.round(CHECK_TIMEOUT_MS / 1e3)}s`);
2537
+ }
2538
+ const PIECE_WEBHOOK_KIND = "piece-webhook";
2539
+ const SUPERVISED_KINDS = new Set([
2540
+ "piece",
2541
+ "schedule",
2542
+ PIECE_WEBHOOK_KIND
2543
+ ]);
2544
+ const UNAUTHORIZED = { status: 401 };
2545
+ const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
2546
+ const DELIVERY_TIMEOUT_MS = Number(process.env.WORKFLOW_WEBHOOK_TIMEOUT_MS) || 3e4;
2547
+ const TIMED_OUT = Symbol("webhook delivery timed out");
2548
+ /** Resolves to TIMED_OUT, and never keeps the process alive waiting to. */
2549
+ function timeout(ms) {
2550
+ return new Promise((resolve) => {
2551
+ setTimeout(() => resolve(TIMED_OUT), ms).unref();
2552
+ });
2553
+ }
2554
+ /** Shaped like Activepieces' catch-webhook contract so authored expressions and adapted
2555
+ * pieces agree where a request's parts are; headers arrive redacted, body decoded. */
2556
+ function webhookPayload(request) {
2557
+ return {
2558
+ method: request.method,
2559
+ path: request.path,
2560
+ headers: request.headers,
2561
+ queryParams: request.queryParams,
2562
+ body: request.body
2563
+ };
2564
+ }
2565
+ const POLL_INTERVAL_CONFIG_KEY = "pollEverySeconds";
2566
+ function splitPollInterval(config) {
2567
+ if (!("pollEverySeconds" in config)) return { config };
2568
+ const { [POLL_INTERVAL_CONFIG_KEY]: raw, ...rest } = config;
2569
+ const seconds = typeof raw === "string" ? Number(raw) : raw;
2570
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds <= 0) {
2571
+ logger.warn(`Ignoring ${POLL_INTERVAL_CONFIG_KEY}=${JSON.stringify(raw)}: expected a positive number of seconds`);
2572
+ return { config: rest };
2573
+ }
2574
+ return {
2575
+ config: rest,
2576
+ pollIntervalMs: Math.round(seconds * 1e3)
2577
+ };
2578
+ }
2579
+ function parseWorkflowState(resultingState) {
2580
+ if (!resultingState) return void 0;
2581
+ try {
2582
+ return JSON.parse(resultingState);
2583
+ } catch {
2584
+ return;
2585
+ }
2586
+ }
2587
+ function configRecord(config) {
2588
+ if (config && typeof config === "object" && !Array.isArray(config)) return config;
2589
+ if (typeof config === "string") try {
2590
+ const parsed = JSON.parse(config);
2591
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
2592
+ } catch {}
2593
+ return {};
2594
+ }
2595
+ var WorkflowRuntimeService = class {
2596
+ host;
2597
+ logger;
2598
+ attachments;
2599
+ executor;
2600
+ pieceWorkers;
2601
+ storePromise;
2602
+ secretsPromise;
2603
+ registry = /* @__PURE__ */ new Map();
2604
+ seedPromise;
2605
+ seedError;
2606
+ constructor(host) {
2607
+ this.host = host;
2608
+ this.logger = host.logger ?? logger;
2609
+ this.attachments = host.attachments ? createAttachmentPort(host.attachments, () => currentWorkflowId(), (documentId, ref) => host.canReadAttachmentRef?.(documentId, ref) ?? Promise.resolve(false)) : void 0;
2610
+ this.storePromise = WorkflowRunStore.create(host.relationalDb);
2611
+ this.storePromise.catch((error) => {
2612
+ this.logger.error("Failed to open the workflow run store: @error", error);
2613
+ });
2614
+ this.seedPromise = this.seedWithRetries();
2615
+ }
2616
+ async store() {
2617
+ try {
2618
+ return await this.storePromise;
2619
+ } catch {
2620
+ return;
2621
+ }
2622
+ }
2623
+ secrets() {
2624
+ this.secretsPromise ??= this.host.secrets !== void 0 ? Promise.resolve(this.host.secrets) : LocalEncryptedSecretStore.create(this.host.relationalDb);
2625
+ return this.secretsPromise;
2626
+ }
2627
+ secretProvider() {
2628
+ return { get: (ref) => this.secrets().then((store) => store.get(ref)) };
2629
+ }
2630
+ /** The seeding failure a restart is needed to clear, or undefined while the
2631
+ * registry is seeded. Resolves once seeding has finished either way. */
2632
+ async seedFailure() {
2633
+ await this.seedPromise;
2634
+ return this.seedError;
2635
+ }
2636
+ async seedWithRetries() {
2637
+ for (let attempt = 1; attempt <= SEED_ATTEMPTS; attempt += 1) try {
2638
+ await this.seedRegistry();
2639
+ this.seedError = void 0;
2640
+ return;
2641
+ } catch (error) {
2642
+ this.seedError = error;
2643
+ if (attempt === SEED_ATTEMPTS) break;
2644
+ this.logger.warn(`Seeding the trigger registry failed (attempt ${attempt}/${SEED_ATTEMPTS}), retrying: @error`, error);
2645
+ await sleep(SEED_RETRY_BASE_MS * 2 ** (attempt - 1));
2646
+ }
2647
+ this.logger.error(`Failed to seed the trigger registry after ${SEED_ATTEMPTS} attempts; its workflows stay inactive until the reactor restarts: @error`, this.seedError);
2648
+ }
2649
+ async seedRegistry() {
2650
+ const page = await this.host.reactorClient.find({ type: "powerhouse/workflow" });
2651
+ for (const document of page.results) await this.updateRegistration(document.header.id, document.state.global);
2652
+ if (this.registry.size === 0 && await this.hasWebhookEndpoints()) {
2653
+ this.logger.warn("Trigger registry seeded no workflows, but @count webhook endpoint(s) exist: their deliveries will be refused as unknown tokens", await this.endpointCount());
2654
+ return;
2655
+ }
2656
+ this.logger.info(`Trigger registry seeded: ${this.registry.size} workflow(s)`);
2657
+ }
2658
+ async endpointCount() {
2659
+ const endpoints = await this.endpoints();
2660
+ return endpoints ? (await endpoints.list()).length : 0;
2661
+ }
2662
+ async hasWebhookEndpoints() {
2663
+ return await this.endpointCount() > 0;
2664
+ }
2665
+ async updateRegistration(workflowId, state) {
2666
+ await packagePieces.ready();
2667
+ const trigger = state.status === "ENABLED" ? state.trigger : void 0;
2668
+ if (trigger?.blockType === "core#webhook") {
2669
+ await this.registerWebhook(workflowId, trigger.config);
2670
+ return;
2671
+ }
2672
+ const kind = trigger ? TRIGGER_KIND_BY_BLOCK[trigger.blockType] : void 0;
2673
+ const supervised = trigger && !kind ? this.supervisedBinding(workflowId, trigger) : void 0;
2674
+ if (!kind && !supervised) {
2675
+ const had = this.registry.get(workflowId);
2676
+ this.registry.delete(workflowId);
2677
+ if (had && SUPERVISED_KINDS.has(had.kind)) this.dropSupervised(workflowId);
2678
+ return;
2679
+ }
2680
+ if (supervised) {
2681
+ if (supervised.kind === "schedule") {
2682
+ this.registry.set(workflowId, {
2683
+ workflowId,
2684
+ kind: "schedule"
2685
+ });
2686
+ this.enableSupervised(workflowId, supervised);
2687
+ return;
2688
+ }
2689
+ this.registry.set(workflowId, {
2690
+ workflowId,
2691
+ kind: "piece"
2692
+ });
2693
+ await this.registerPieceTrigger(workflowId, supervised);
2694
+ return;
2695
+ }
2696
+ const had = this.registry.get(workflowId);
2697
+ if (had && SUPERVISED_KINDS.has(had.kind)) this.dropSupervised(workflowId);
2698
+ const config = trigger?.config;
2699
+ this.registry.set(workflowId, kind === "document-event" ? {
2700
+ workflowId,
2701
+ kind,
2702
+ filter: parseEventFilter(config)
2703
+ } : {
2704
+ workflowId,
2705
+ kind,
2706
+ filter: parseLifecycleFilter(config)
2707
+ });
2708
+ }
2709
+ enableSupervised(workflowId, binding) {
2710
+ this.supervisor().upsert(binding).catch((error) => {
2711
+ this.logger.error(`Trigger enable failed for ${workflowId}`, error);
2712
+ });
2713
+ }
2714
+ async registerPieceTrigger(workflowId, binding) {
2715
+ const delivery = await this.pieceDelivery(binding);
2716
+ const resolved = {
2717
+ ...binding,
2718
+ delivery
2719
+ };
2720
+ if (delivery === "webhook") {
2721
+ this.registry.set(workflowId, {
2722
+ workflowId,
2723
+ kind: PIECE_WEBHOOK_KIND,
2724
+ binding: resolved
2725
+ });
2726
+ await (await this.endpoints())?.endpointFor(workflowId);
2727
+ }
2728
+ this.enableSupervised(workflowId, resolved);
2729
+ }
2730
+ async pieceDelivery(binding) {
2731
+ try {
2732
+ const { triggers } = await this.pieceTriggers(binding.packageName);
2733
+ return triggers.find((entry) => entry.name === binding.triggerName)?.strategy === "WEBHOOK" ? "webhook" : "poll";
2734
+ } catch (error) {
2735
+ this.logger.warn("Could not resolve the trigger strategy for @block; polling", binding.blockType, error);
2736
+ return "poll";
2737
+ }
2738
+ }
2739
+ async registerWebhook(workflowId, rawConfig) {
2740
+ const had = this.registry.get(workflowId);
2741
+ if (had && SUPERVISED_KINDS.has(had.kind)) this.dropSupervised(workflowId);
2742
+ let config;
2743
+ try {
2744
+ config = parseWebhookConfig(configRecord(rawConfig));
2745
+ } catch (error) {
2746
+ this.registry.delete(workflowId);
2747
+ const message = error instanceof Error ? error.message : String(error);
2748
+ this.logger.error(`Webhook trigger rejected for ${workflowId}: ${message}`);
2749
+ return;
2750
+ }
2751
+ this.registry.set(workflowId, {
2752
+ workflowId,
2753
+ kind: WEBHOOK_TRIGGER_KIND,
2754
+ config
2755
+ });
2756
+ await (await this.endpoints())?.endpointFor(workflowId);
2757
+ }
2758
+ supervisedBinding(workflowId, trigger) {
2759
+ if (trigger.blockType === "core#schedule") return {
2760
+ kind: "schedule",
2761
+ workflowId,
2762
+ blockType: SCHEDULE_BLOCK,
2763
+ config: configRecord(trigger.config)
2764
+ };
2765
+ return this.pieceBinding(workflowId, trigger);
2766
+ }
2767
+ pieceBinding(workflowId, trigger) {
2768
+ const parsed = parseBlockType(trigger.blockType, packagePieces.versions());
2769
+ if (!parsed || parsed.kind !== "trigger") return void 0;
2770
+ const { config, pollIntervalMs } = splitPollInterval(configRecord(trigger.config));
2771
+ return {
2772
+ workflowId,
2773
+ blockType: trigger.blockType,
2774
+ packageName: parsed.packageName,
2775
+ version: parsed.version,
2776
+ triggerName: parsed.name,
2777
+ config,
2778
+ connectionId: trigger.connectionId,
2779
+ pollIntervalMs
2780
+ };
2781
+ }
2782
+ dropSupervised(workflowId) {
2783
+ this.supervisor().remove(workflowId).catch((error) => {
2784
+ this.logger.error(`Trigger disable failed for ${workflowId}`, error);
2785
+ });
2786
+ }
2787
+ async refreshRegistration(workflowId, resultingState) {
2788
+ const carried = parseWorkflowState(resultingState);
2789
+ if (carried) {
2790
+ await this.updateRegistration(workflowId, carried);
2791
+ return;
2792
+ }
2793
+ const document = await this.host.reactorClient.get(workflowId);
2794
+ await this.updateRegistration(workflowId, document.state.global);
2795
+ }
2796
+ seenOps = /* @__PURE__ */ new Set();
2797
+ seenOpsQueue = [];
2798
+ alreadySeen(key) {
2799
+ if (this.seenOps.has(key)) return true;
2800
+ this.seenOps.add(key);
2801
+ this.seenOpsQueue.push(key);
2802
+ if (this.seenOpsQueue.length > 8192) {
2803
+ const evicted = this.seenOpsQueue.shift();
2804
+ if (evicted) this.seenOps.delete(evicted);
2805
+ }
2806
+ return false;
2807
+ }
2808
+ async onOperations(operations) {
2809
+ const hints = collectLifecycleParentHints(operations);
2810
+ for (const { operation, context } of operations) {
2811
+ if (context.scope !== DOCUMENT_SCOPE && context.scope !== "global") continue;
2812
+ const opKey = operationKey({
2813
+ operation,
2814
+ context
2815
+ });
2816
+ if (this.alreadySeen(opKey)) continue;
2817
+ if (context.scope === DOCUMENT_SCOPE) {
2818
+ await this.matchDocumentLifecycle(operation, context, hints, opKey);
2819
+ continue;
2820
+ }
2821
+ if (context.documentType === "powerhouse/workflow") await this.refreshRegistration(context.documentId, operation.resultingState);
2822
+ if (operation.error !== void 0) continue;
2823
+ for (const registration of this.registry.values()) {
2824
+ if (registration.kind !== "document-event") continue;
2825
+ if (!matchesEventFilter(registration.filter, context.documentType, context.documentId, operation.action.type)) continue;
2826
+ const payload = {
2827
+ documentId: context.documentId,
2828
+ documentType: context.documentType,
2829
+ branch: context.branch,
2830
+ scope: context.scope,
2831
+ action: {
2832
+ type: operation.action.type,
2833
+ input: operation.action.input
2834
+ },
2835
+ operation: {
2836
+ index: operation.index,
2837
+ timestampUtcMs: operation.timestampUtcMs
2838
+ }
2839
+ };
2840
+ await this.enqueueFire(registration.workflowId, payload, registration.kind, opKey);
2841
+ }
2842
+ if (context.documentType === DRIVE_DOCUMENT_TYPE) await this.matchDriveLifecycle(context.documentId, operation.action.type, operation.action.input, {
2843
+ index: operation.index,
2844
+ timestampUtcMs: operation.timestampUtcMs
2845
+ }, opKey);
2846
+ }
2847
+ }
2848
+ async enqueueFire(workflowId, payload, kind, opKey) {
2849
+ const store = await this.store();
2850
+ if (!store) {
2851
+ this.fireFromTrigger(workflowId, payload, kind);
2852
+ return;
2853
+ }
2854
+ if (!await store.claimDedupe(workflowId, `op:${opKey}`, OPERATION_DEDUPE_TTL_MS, (/* @__PURE__ */ new Date()).toISOString())) return;
2855
+ let runId;
2856
+ try {
2857
+ runId = await store.enqueueRun({
2858
+ workflowId,
2859
+ triggerKind: kind,
2860
+ triggerPayload: payload
2861
+ });
2862
+ } catch (error) {
2863
+ this.logger.error(`Could not journal the ${kind} fire for workflow ${workflowId}; running it without a durable record`, error);
2864
+ this.fireFromTrigger(workflowId, payload, kind);
2865
+ return;
2866
+ }
2867
+ this.fireFromTrigger(workflowId, payload, kind, runId);
2868
+ }
2869
+ fireFromTrigger(workflowId, payload, kind, enqueuedRunId) {
2870
+ this.fire(workflowId, payload, kind, void 0, void 0, enqueuedRunId).then((run) => {
2871
+ this.logger.info(`${kind} fired workflow ${workflowId}: ${run.status}`);
2872
+ }, (error) => {
2873
+ this.logger.error(`${kind} run failed for workflow ${workflowId}`, error);
2874
+ });
2875
+ }
2876
+ firedLifecycle = /* @__PURE__ */ new Set();
2877
+ firedLifecycleQueue = [];
2878
+ lifecycleAlreadyFired(kind, documentId) {
2879
+ return this.firedLifecycle.has(`${kind}:${documentId}`);
2880
+ }
2881
+ recordLifecycleFired(kind, documentId) {
2882
+ const key = `${kind}:${documentId}`;
2883
+ if (this.firedLifecycle.has(key)) return;
2884
+ this.firedLifecycle.add(key);
2885
+ this.firedLifecycleQueue.push(key);
2886
+ if (this.firedLifecycleQueue.length > 4096) {
2887
+ const evicted = this.firedLifecycleQueue.shift();
2888
+ if (evicted) this.firedLifecycle.delete(evicted);
2889
+ }
2890
+ }
2891
+ lifecycleTargets(kind) {
2892
+ const targets = [];
2893
+ for (const registration of this.registry.values()) {
2894
+ if (registration.kind !== kind) continue;
2895
+ if (registration.kind === "document-event") continue;
2896
+ targets.push({
2897
+ workflowId: registration.workflowId,
2898
+ filter: registration.filter
2899
+ });
2900
+ }
2901
+ return targets;
2902
+ }
2903
+ async fireLifecycle(kind, payload, opKey) {
2904
+ let matched = false;
2905
+ for (const target of this.lifecycleTargets(kind)) {
2906
+ if (!matchesLifecycleFilter(target.filter, payload.documentType, payload.driveId)) continue;
2907
+ matched = true;
2908
+ await this.enqueueFire(target.workflowId, payload, kind, opKey);
2909
+ }
2910
+ if (matched) this.recordLifecycleFired(kind, payload.documentId);
2911
+ }
2912
+ driveParentCache = /* @__PURE__ */ new Map();
2913
+ async driveIdFromParent(parentId) {
2914
+ if (!parentId) return void 0;
2915
+ const cached = this.driveParentCache.get(parentId);
2916
+ if (cached !== void 0) return cached ? parentId : void 0;
2917
+ try {
2918
+ const isDrive = (await this.host.reactorClient.get(parentId)).header.documentType === DRIVE_DOCUMENT_TYPE;
2919
+ if (this.driveParentCache.size > 1024) this.driveParentCache.clear();
2920
+ this.driveParentCache.set(parentId, isDrive);
2921
+ return isDrive ? parentId : void 0;
2922
+ } catch {
2923
+ return;
2924
+ }
2925
+ }
2926
+ async matchDocumentLifecycle(operation, context, hints, opKey) {
2927
+ const kind = lifecycleKindForDocumentAction(operation.action.type);
2928
+ if (!kind) return;
2929
+ if (operation.error !== void 0) return;
2930
+ const input = inputRecord(operation.action.input);
2931
+ const documentId = stringField(input, "documentId") ?? context.documentId;
2932
+ if (this.lifecycleAlreadyFired(kind, documentId)) return;
2933
+ if (this.lifecycleTargets(kind).length === 0) return;
2934
+ const hint = hints.get(documentId);
2935
+ const driveId = hint?.driveId ?? await this.driveIdFromParent(hint?.parentCandidate);
2936
+ const created = kind === "document-created";
2937
+ await this.fireLifecycle(kind, {
2938
+ documentId,
2939
+ documentType: (created ? stringField(input, "model") : void 0) ?? (context.documentType || null),
2940
+ name: stringField(input, "name") ?? null,
2941
+ driveId: driveId ?? null,
2942
+ parentId: hint?.parentId ?? null,
2943
+ operation: {
2944
+ index: operation.index,
2945
+ timestampUtcMs: operation.timestampUtcMs
2946
+ }
2947
+ }, opKey);
2948
+ }
2949
+ async matchDriveLifecycle(driveId, actionType, input, operation, opKey) {
2950
+ const kind = lifecycleKindForDriveAction(actionType);
2951
+ if (!kind) return;
2952
+ if (this.lifecycleTargets(kind).length === 0) return;
2953
+ const record = inputRecord(input);
2954
+ const documentId = stringField(record, "id");
2955
+ if (!documentId) return;
2956
+ if (this.lifecycleAlreadyFired(kind, documentId)) return;
2957
+ let documentType = stringField(record, "documentType");
2958
+ let name = stringField(record, "name") ?? null;
2959
+ if (kind === "document-deleted") try {
2960
+ const document = await this.host.reactorClient.get(documentId);
2961
+ documentType = document.header.documentType;
2962
+ name ??= document.header.name;
2963
+ } catch {
2964
+ documentType = void 0;
2965
+ }
2966
+ await this.fireLifecycle(kind, {
2967
+ documentId,
2968
+ documentType: documentType ?? null,
2969
+ name,
2970
+ driveId,
2971
+ parentId: stringField(record, "parentFolder") ?? null,
2972
+ operation
2973
+ }, opKey);
2974
+ }
2975
+ triggerSupervisor;
2976
+ supervisor() {
2977
+ this.triggerSupervisor ??= new TriggerSupervisor({
2978
+ store: () => this.store(),
2979
+ resolveAuth: async (connectionId, request) => {
2980
+ if (!connectionId) return void 0;
2981
+ const resolved = await new DocumentConnectionResolver(this.host, this.secretProvider()).resolveWithSecrets(connectionId, request);
2982
+ return rememberSecrets(resolved.auth, resolved.secretValues);
2983
+ },
2984
+ fire: (workflowId, payload, kind) => {
2985
+ this.fireFromTrigger(workflowId, payload, kind);
2986
+ },
2987
+ webhookUrlFor: async (workflowId) => (await this.mintWebhookEndpoint(workflowId))?.url,
2988
+ cacheDir: BUNDLE_CACHE_DIR,
2989
+ resolver: pieceResolver(),
2990
+ egress: configuredEgress(),
2991
+ defaultIntervalMs: Number(process.env.WORKFLOW_POLL_INTERVAL_MS) || void 0,
2992
+ reconcileIntervalMs: Number(process.env.WORKFLOW_WEBHOOK_RECONCILE_MS) || void 0
2993
+ });
2994
+ return this.triggerSupervisor;
2995
+ }
2996
+ startTriggerSupervisor() {
2997
+ this.supervisor().start();
2998
+ }
2999
+ stopTriggerSupervisor() {
3000
+ this.triggerSupervisor?.stop();
3001
+ }
3002
+ shutdown() {
3003
+ this.stopTriggerSupervisor();
3004
+ this.pieceWorkers?.dispose();
3005
+ this.designWorker?.dispose();
3006
+ this.designWorker = void 0;
3007
+ }
3008
+ async triggerStates(ctx) {
3009
+ const store = await this.store();
3010
+ if (!store) return [];
3011
+ const rows = await store.listTriggerStates();
3012
+ return this.readableRows(rows, (row) => row.workflow_id, ctx);
3013
+ }
3014
+ webhookEndpoints;
3015
+ webhookScope;
3016
+ webhookRegistration;
3017
+ /** Registers the workflow endpoint family with the reactor's webhook service
3018
+ * (idempotent). Everything transport-shaped is the service's; only workflow identity is ours. */
3019
+ async registerWebhookEndpoint() {
3020
+ if (this.webhookRegistration) {
3021
+ await this.webhookRegistration;
3022
+ return;
3023
+ }
3024
+ const webhooks = this.host.webhooks;
3025
+ if (!webhooks) {
3026
+ this.logger.warn("This host serves no webhooks; workflows with a webhook trigger will not arm");
3027
+ return;
3028
+ }
3029
+ this.webhookScope = webhooks;
3030
+ this.webhookRegistration = webhooks.register({
3031
+ name: "trigger",
3032
+ policyFor: (workflowId) => this.webhookPolicy(workflowId),
3033
+ onRequest: (request) => this.deliverWebhook(request)
3034
+ }).then((endpoints) => {
3035
+ this.webhookEndpoints = endpoints;
3036
+ return endpoints;
3037
+ }).catch((error) => {
3038
+ this.logger.warn("Webhook triggers are unavailable on this host; other triggers are unaffected: @error", error);
3039
+ });
3040
+ await this.webhookRegistration;
3041
+ }
3042
+ /** The endpoint family, once registered. Seeding runs before the host starts
3043
+ * the runtime, so a caller that needs a token waits rather than finds it missing. */
3044
+ async endpoints() {
3045
+ if (this.webhookEndpoints) return this.webhookEndpoints;
3046
+ if (!this.webhookRegistration && this.host.webhooks) await this.registerWebhookEndpoint();
3047
+ return await this.webhookRegistration;
3048
+ }
3049
+ /** The per-document policy the service enforces before a delivery reaches this code;
3050
+ * undefined means the workflow is not armed, answered exactly as an unknown token is. */
3051
+ async webhookPolicy(workflowId) {
3052
+ await this.seedPromise;
3053
+ const registration = this.registry.get(workflowId);
3054
+ if (!registration) return void 0;
3055
+ if (registration.kind === "piece-webhook") return {};
3056
+ if (registration.kind !== "webhook") return void 0;
3057
+ const { config } = registration;
3058
+ return {
3059
+ methods: config.methods,
3060
+ challengeField: config.challengeField,
3061
+ dedupe: config.dedupeField ? {
3062
+ field: config.dedupeField,
3063
+ ttlSeconds: config.dedupeTtlSeconds
3064
+ } : void 0,
3065
+ verify: config.scheme === "none" ? void 0 : {
3066
+ scheme: config.scheme,
3067
+ header: config.header,
3068
+ secret: await this.webhookSecret(config, workflowId),
3069
+ toleranceSeconds: config.toleranceSeconds,
3070
+ algorithm: config.algorithm,
3071
+ encoding: config.encoding,
3072
+ prefix: config.prefix
3073
+ }
3074
+ };
3075
+ }
3076
+ async webhookEndpoint(workflowId, ctx) {
3077
+ await this.assertCanReadDocument(workflowId, ctx);
3078
+ return this.mintWebhookEndpoint(workflowId);
3079
+ }
3080
+ async mintWebhookEndpoint(workflowId) {
3081
+ const endpoints = await this.endpoints();
3082
+ if (!endpoints) return null;
3083
+ const registration = this.registry.get(workflowId);
3084
+ const armed = registration?.kind === "webhook" || registration?.kind === "piece-webhook";
3085
+ const absoluteUrl = this.webhookScope?.hasPublicOrigin ?? false;
3086
+ const minted = await endpoints.endpointFor(workflowId);
3087
+ return {
3088
+ workflowId,
3089
+ url: minted.url,
3090
+ absoluteUrl,
3091
+ armed,
3092
+ createdAt: minted.createdAt
3093
+ };
3094
+ }
3095
+ /** A delivery the service has already rate-limited, verified, de-duplicated and
3096
+ * answered any challenge for; all that is left is deciding what it means. */
3097
+ async deliverWebhook(request) {
3098
+ const workflowId = request.key;
3099
+ const registration = this.registry.get(workflowId);
3100
+ if (!registration) return UNAUTHORIZED;
3101
+ if (registration.kind === "piece-webhook") return this.deliverToPiece(registration.binding, request);
3102
+ if (registration.kind !== "webhook") return UNAUTHORIZED;
3103
+ const { config } = registration;
3104
+ const payload = webhookPayload(request);
3105
+ if (config.responseMode === "async") {
3106
+ this.fireFromTrigger(workflowId, payload, WEBHOOK_TRIGGER_KIND);
3107
+ return { status: config.responseStatus };
3108
+ }
3109
+ const run = await Promise.race([this.fire(workflowId, payload, WEBHOOK_TRIGGER_KIND).then((result) => ({
3110
+ ok: true,
3111
+ result
3112
+ }), (error) => ({
3113
+ ok: false,
3114
+ error
3115
+ })), timeout(DELIVERY_TIMEOUT_MS)]);
3116
+ if (run === TIMED_OUT) {
3117
+ this.logger.warn(`Webhook run for ${workflowId} exceeded ${DELIVERY_TIMEOUT_MS}ms; answering 504 while it continues`);
3118
+ return {
3119
+ status: 504,
3120
+ contentType: JSON_CONTENT_TYPE,
3121
+ body: JSON.stringify({
3122
+ status: "RUNNING",
3123
+ error: "The run did not finish in time"
3124
+ })
3125
+ };
3126
+ }
3127
+ if (!run.ok) {
3128
+ const message = run.error instanceof Error ? run.error.message : String(run.error);
3129
+ this.logger.error(`Webhook run failed for ${workflowId}: ${message}`);
3130
+ return {
3131
+ status: 500,
3132
+ contentType: JSON_CONTENT_TYPE,
3133
+ body: JSON.stringify({ error: message })
3134
+ };
3135
+ }
3136
+ return {
3137
+ status: run.result.status === "SUCCEEDED" ? config.responseStatus : 500,
3138
+ contentType: JSON_CONTENT_TYPE,
3139
+ body: JSON.stringify({
3140
+ runId: run.result.runId,
3141
+ status: run.result.status,
3142
+ error: run.result.error ?? null
3143
+ })
3144
+ };
3145
+ }
3146
+ async pieceHandshake(binding, request) {
3147
+ const handshake = await this.pieceHandshakeConfig(binding);
3148
+ if (!handshake || !handshakeMatches(handshake, request)) return void 0;
3149
+ try {
3150
+ return handshakeReply((await this.supervisor().handshake(binding, webhookPayload(request))).output);
3151
+ } catch (error) {
3152
+ this.logger.error("Handshake failed for @block on workflow @workflow", binding.blockType, binding.workflowId, error);
3153
+ return { status: 500 };
3154
+ }
3155
+ }
3156
+ async pieceHandshakeConfig(binding) {
3157
+ try {
3158
+ return (await this.pieceDescriptor(binding.packageName, binding.version)).triggers.find((entry) => entry.name === binding.triggerName)?.handshake;
3159
+ } catch (error) {
3160
+ this.logger.warn("Could not read the handshake config for @block", binding.blockType, error);
3161
+ return;
3162
+ }
3163
+ }
3164
+ async deliverToPiece(binding, request) {
3165
+ const probe = await this.pieceHandshake(binding, request);
3166
+ if (probe) return probe;
3167
+ const payload = webhookPayload(request);
3168
+ this.supervisor().deliverWebhook(binding.workflowId, payload).then(() => {
3169
+ this.logger.info("Webhook delivered to @block for workflow @workflow", binding.blockType, binding.workflowId);
3170
+ }, (error) => {
3171
+ this.logger.error(`Webhook delivery failed for workflow ${binding.workflowId}`, error);
3172
+ });
3173
+ return { status: 200 };
3174
+ }
3175
+ async webhookSecret(config, workflowId) {
3176
+ if (!config.secretRef) return void 0;
3177
+ try {
3178
+ return await (await this.secrets()).get(config.secretRef);
3179
+ } catch (error) {
3180
+ const message = error instanceof Error ? error.message : String(error);
3181
+ this.logger.error(`Webhook secret unavailable for ${workflowId}: ${message}`);
3182
+ return;
3183
+ }
3184
+ }
3185
+ descriptors = /* @__PURE__ */ new Map();
3186
+ designWorker;
3187
+ designEgress = configuredEgress() ?? DEFAULT_EGRESS_POLICY;
3188
+ async pieceDescriptor(packageName, version) {
3189
+ const cacheKey = `${packageName}@${version}`;
3190
+ let descriptor = this.descriptors.get(cacheKey);
3191
+ if (!descriptor) {
3192
+ const piece = await pieceResolver().resolve(packageName, version);
3193
+ this.designWorker ??= new PieceWorker();
3194
+ let output;
3195
+ try {
3196
+ output = (await this.designWorker.describePiece({
3197
+ ...pieceModuleRef(piece),
3198
+ packageName,
3199
+ version,
3200
+ ...this.designEgress ? { egress: this.designEgress } : {}
3201
+ }, { timeoutMs: DESCRIBE_TIMEOUT_MS })).output;
3202
+ } catch (error) {
3203
+ throw new Error(pieceFailureDetail(error, `Loading piece "${packageName}" timed out after ${Math.round(DESCRIBE_TIMEOUT_MS / 1e3)}s`), { cause: error });
3204
+ }
3205
+ descriptor = output;
3206
+ this.descriptors.set(cacheKey, descriptor);
3207
+ }
3208
+ return descriptor;
3209
+ }
3210
+ async driveWorkflowIds(driveId, ctx) {
3211
+ await this.assertCanReadDocument(driveId, ctx);
3212
+ let page = await this.host.reactorClient.drives.listNodes(driveId);
3213
+ const nodes = [...page.results];
3214
+ while (page.next) {
3215
+ page = await page.next();
3216
+ nodes.push(...page.results);
3217
+ }
3218
+ const ids = nodes.filter((node) => "documentType" in node && node.documentType === WORKFLOW_DOCUMENT_TYPE).map((node) => node.id);
3219
+ return this.readableRows(ids, (id) => id, ctx);
3220
+ }
3221
+ async runs(args, ctx) {
3222
+ const store = await this.store();
3223
+ if (!store) return [];
3224
+ let scope;
3225
+ if (args.workflowId) {
3226
+ await this.assertCanReadDocument(args.workflowId, ctx);
3227
+ scope = args.workflowId;
3228
+ } else if (args.driveId) {
3229
+ scope = await this.driveWorkflowIds(args.driveId, ctx);
3230
+ if (scope.length === 0) return [];
3231
+ } else if (!ctx) return [];
3232
+ const rows = await store.listRuns(scope, args.limit ?? 25);
3233
+ const readable = await this.readableRows(rows, (row) => row.workflow_id, ctx);
3234
+ return Promise.all(readable.map(async (row) => ({
3235
+ row,
3236
+ steps: await store.getSteps(row.id)
3237
+ })));
3238
+ }
3239
+ async run(runId, ctx) {
3240
+ const store = await this.store();
3241
+ if (!store || !ctx) return null;
3242
+ const row = await store.getRun(runId);
3243
+ if (!row) return null;
3244
+ if (!await this.canReadDocument(row.workflow_id, ctx)) return null;
3245
+ return {
3246
+ row,
3247
+ steps: await store.getSteps(row.id)
3248
+ };
3249
+ }
3250
+ async connections(ctx) {
3251
+ const page = await this.host.reactorClient.find({ type: "powerhouse/connection" });
3252
+ return (await this.readableDocuments(page.results, ctx)).map((document) => {
3253
+ const state = document.state.global;
3254
+ return {
3255
+ id: document.header.id,
3256
+ name: state.name,
3257
+ connectorId: state.connectorId,
3258
+ authType: state.authType,
3259
+ status: state.status,
3260
+ accountLabel: state.accountLabel ?? null
3261
+ };
3262
+ });
3263
+ }
3264
+ async checkConnection(connectionId, ctx) {
3265
+ await this.assertCanReadDocument(connectionId, ctx);
3266
+ await this.assertCanWriteDocument(connectionId, ctx);
3267
+ const document = await this.host.reactorClient.get(connectionId);
3268
+ if (document.header.documentType !== "powerhouse/connection") throw new Error(`Document "${connectionId}" is not a powerhouse/connection`);
3269
+ const state = document.state.global;
3270
+ const accountLabel = state.accountLabel ?? null;
3271
+ if (state.status === "REVOKED") return {
3272
+ ok: false,
3273
+ detail: "Connection is revoked",
3274
+ accountLabel
3275
+ };
3276
+ if (state.status === "UNCONFIGURED") return this.recordCheckResult(document, {
3277
+ ok: false,
3278
+ detail: "Connection is not configured",
3279
+ accountLabel
3280
+ });
3281
+ if (state.authType === "OAUTH2" || state.authType === "OIDC") return this.recordCheckResult(document, {
3282
+ ok: false,
3283
+ detail: `${state.authType} connections are not supported by the runtime yet`,
3284
+ accountLabel
3285
+ });
3286
+ const packageName = packageFromConnectorId(state.connectorId);
3287
+ let moduleRef;
3288
+ try {
3289
+ const version = await this.pieceVersion(packageName);
3290
+ moduleRef = pieceModuleRef(await pieceResolver().resolve(packageName, version));
3291
+ } catch (error) {
3292
+ return this.recordCheckResult(document, {
3293
+ ok: false,
3294
+ detail: error instanceof Error ? error.message : String(error),
3295
+ accountLabel
3296
+ });
3297
+ }
3298
+ let shapedAuth;
3299
+ try {
3300
+ shapedAuth = await resolveConnectionAuth(document, this.secretProvider(), {
3301
+ blockType: state.connectorId,
3302
+ piecePackage: packageName
3303
+ });
3304
+ } catch (error) {
3305
+ return this.recordCheckResult(document, {
3306
+ ok: false,
3307
+ detail: error instanceof Error ? error.message : String(error),
3308
+ accountLabel
3309
+ });
3310
+ }
3311
+ let outcome;
3312
+ try {
3313
+ this.designWorker ??= new PieceWorker();
3314
+ outcome = (await this.designWorker.checkConnection({
3315
+ ...moduleRef,
3316
+ auth: shapedAuth,
3317
+ ...this.designEgress ? { egress: this.designEgress } : {}
3318
+ }, { timeoutMs: CHECK_TIMEOUT_MS })).output;
3319
+ } catch (error) {
3320
+ return this.recordCheckResult(document, {
3321
+ ok: false,
3322
+ detail: checkFailureDetail(error),
3323
+ accountLabel
3324
+ });
3325
+ }
3326
+ if (!outcome.declared) return this.recordCheckResult(document, {
3327
+ ok: true,
3328
+ detail: "piece declares no connection check; credentials resolved",
3329
+ accountLabel
3330
+ });
3331
+ if (outcome.result === false) return this.recordCheckResult(document, {
3332
+ ok: false,
3333
+ detail: "Connection check failed",
3334
+ accountLabel
3335
+ });
3336
+ return this.recordCheckResult(document, {
3337
+ ok: true,
3338
+ detail: null,
3339
+ accountLabel: accountLabelFromCheckResult(outcome.result) ?? accountLabel
3340
+ });
3341
+ }
3342
+ async pieceVersion(packageName) {
3343
+ await packagePieces.ready();
3344
+ const local = packagePieces.lookup(packageName);
3345
+ if (local) return local.version;
3346
+ try {
3347
+ const version = (await fetchPieceCatalog()).find((entry) => entry.name === packageName)?.version;
3348
+ if (version) return version;
3349
+ } catch {}
3350
+ const detail = await fetchPieceDetail(packageName);
3351
+ if (typeof detail.version === "string" && detail.version !== "") return detail.version;
3352
+ throw new Error(`Could not resolve a version for piece "${packageName}"`);
3353
+ }
3354
+ async recordCheckResult(document, result) {
3355
+ const action = actions.recordCheckResult({
3356
+ status: result.ok ? "OK" : "ERROR",
3357
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
3358
+ error: result.ok ? void 0 : result.detail ?? void 0
3359
+ });
3360
+ await this.host.reactorClient.execute(document.header.id, "main", [action]);
3361
+ return result;
3362
+ }
3363
+ async localPieces() {
3364
+ await packagePieces.ready();
3365
+ return (await Promise.all(packagePieces.entries().map(async (piece) => {
3366
+ try {
3367
+ return {
3368
+ piece,
3369
+ descriptor: await this.pieceDescriptor(piece.name, piece.version)
3370
+ };
3371
+ } catch (error) {
3372
+ this.logger.warn(`Could not describe the package piece "${piece.name}": ${String(error)}`);
3373
+ return;
3374
+ }
3375
+ }))).filter((entry) => entry !== void 0);
3376
+ }
3377
+ async localPiece(packageName) {
3378
+ await packagePieces.ready();
3379
+ const piece = packagePieces.lookup(packageName);
3380
+ if (!piece) return void 0;
3381
+ return {
3382
+ piece,
3383
+ descriptor: await this.pieceDescriptor(piece.name, piece.version)
3384
+ };
3385
+ }
3386
+ async pieceCatalog() {
3387
+ const entries = (await this.localPieces()).map(({ piece, descriptor }) => catalogEntry(descriptor, piece.name, piece.version));
3388
+ const names = new Set(entries.map((entry) => entry.name));
3389
+ let published;
3390
+ try {
3391
+ published = await fetchPieceCatalog();
3392
+ } catch (error) {
3393
+ if (entries.length === 0) throw error;
3394
+ this.logger.warn(`Serving package pieces only: ${String(error)}`);
3395
+ published = [];
3396
+ }
3397
+ return [...entries, ...published.filter((entry) => !names.has(entry.name))].sort((a, b) => a.displayName.localeCompare(b.displayName));
3398
+ }
3399
+ async pieceActions(packageName) {
3400
+ const local = await this.localPiece(packageName);
3401
+ return local ? actionsResult(local.descriptor, local.piece.name, local.piece.version) : fetchPieceActions(packageName);
3402
+ }
3403
+ async pieceTriggers(packageName) {
3404
+ const local = await this.localPiece(packageName);
3405
+ return local ? triggersResult(local.descriptor, local.piece.name, local.piece.version) : fetchPieceTriggers(packageName);
3406
+ }
3407
+ async searchBlocks(query, limit) {
3408
+ let local;
3409
+ try {
3410
+ local = indexFromHits((await this.localPieces()).flatMap(({ piece, descriptor }) => localSearchHits(descriptor, piece.name)));
3411
+ } catch (error) {
3412
+ this.logger.warn(`Could not index the package pieces: ${String(error)}`);
3413
+ }
3414
+ return searchBlocks(query, limit, local);
3415
+ }
3416
+ async pieceDetail(packageName) {
3417
+ const local = await this.localPiece(packageName);
3418
+ return local ? detailResult(local.descriptor, local.piece.name, local.piece.version) : fetchPieceDetail(packageName);
3419
+ }
3420
+ async blockDescriptor(blockType) {
3421
+ await packagePieces.ready();
3422
+ const parsed = parseBlockType(blockType, packagePieces.versions());
3423
+ if (!parsed) return null;
3424
+ const descriptor = await this.pieceDescriptor(parsed.packageName, parsed.version);
3425
+ const common = {
3426
+ displayName: descriptor.displayName,
3427
+ logoUrl: descriptor.logoUrl,
3428
+ auth: descriptor.auth ?? null
3429
+ };
3430
+ if (parsed.kind === "trigger") {
3431
+ const trigger = descriptor.triggers.find((entry) => entry.name === parsed.name);
3432
+ return trigger ? {
3433
+ ...common,
3434
+ trigger
3435
+ } : null;
3436
+ }
3437
+ const action = descriptor.actions.find((entry) => entry.name === parsed.name);
3438
+ return action ? {
3439
+ ...common,
3440
+ action
3441
+ } : null;
3442
+ }
3443
+ async readableDocuments(documents, ctx) {
3444
+ if (!ctx) return [];
3445
+ const host = this.host;
3446
+ const allowed = await Promise.all(documents.map((document) => host.assertCanRead(document.header.id, ctx).then(() => true).catch(() => false)));
3447
+ return documents.filter((_, index) => allowed[index]);
3448
+ }
3449
+ async readableRows(rows, documentIdOf, ctx) {
3450
+ if (!ctx) return [];
3451
+ const allowed = await Promise.all(rows.map((row) => this.canReadDocument(documentIdOf(row), ctx)));
3452
+ return rows.filter((_, index) => allowed[index]);
3453
+ }
3454
+ canReadDocument(documentId, ctx) {
3455
+ return this.host.assertCanRead(documentId, ctx).then(() => true).catch(() => false);
3456
+ }
3457
+ async assertCanReadDocument(documentId, ctx) {
3458
+ if (!ctx) throw new Error("Connection access requires an authenticated request");
3459
+ await this.host.assertCanRead(documentId, ctx);
3460
+ }
3461
+ async assertCanWriteDocument(documentId, ctx) {
3462
+ if (!ctx) throw new Error("Connection access requires an authenticated request");
3463
+ await this.host.assertCanWrite(documentId, ctx);
3464
+ }
3465
+ async blockOptions(blockType, propName, input, connectionId, ctx) {
3466
+ await packagePieces.ready();
3467
+ const parsed = parseBlockType(blockType, packagePieces.versions());
3468
+ if (!parsed) throw new Error(`Not a piece block type: "${blockType}"`);
3469
+ let auth;
3470
+ if (connectionId) {
3471
+ await this.assertCanReadDocument(connectionId, ctx);
3472
+ auth = await new DocumentConnectionResolver(this.host, this.secretProvider()).resolve(connectionId, {
3473
+ blockType,
3474
+ piecePackage: parsed.packageName
3475
+ });
3476
+ }
3477
+ const piece = await pieceResolver().resolve(parsed.packageName, parsed.version);
3478
+ this.designWorker ??= new PieceWorker();
3479
+ return (await this.designWorker.resolveOptions({
3480
+ ...pieceModuleRef(piece),
3481
+ actionName: parsed.name,
3482
+ kind: parsed.kind,
3483
+ propName,
3484
+ refresherValues: input ?? {},
3485
+ auth,
3486
+ ...piece.local ? { reactorAccess: true } : {},
3487
+ ...this.designEgress ? { egress: this.designEgress } : {}
3488
+ }, piece.local ? { hostCalls: reactorHandlers(new ScopedDesignTimeReactorPort(this.host, ctx)) } : {})).output;
3489
+ }
3490
+ async blockOutputTree(blockType, config) {
3491
+ await packagePieces.ready();
3492
+ const record = config ?? {};
3493
+ switch (blockType) {
3494
+ case "core#manual": return {
3495
+ source: "none",
3496
+ nodes: []
3497
+ };
3498
+ case SCHEDULE_BLOCK: return {
3499
+ source: "static",
3500
+ nodes: scheduleTriggerTree()
3501
+ };
3502
+ case WEBHOOK_BLOCK: return {
3503
+ source: "static",
3504
+ nodes: webhookTriggerTree()
3505
+ };
3506
+ case "core#branch": return {
3507
+ source: "static",
3508
+ nodes: [{
3509
+ name: "condition",
3510
+ type: "value"
3511
+ }]
3512
+ };
3513
+ case "core#assert": return {
3514
+ source: "static",
3515
+ nodes: [{
3516
+ name: "value",
3517
+ type: "value"
3518
+ }]
3519
+ };
3520
+ case DOCUMENT_CREATED_BLOCK:
3521
+ case DOCUMENT_DELETED_BLOCK: return {
3522
+ source: "static",
3523
+ nodes: lifecycleTriggerTree()
3524
+ };
3525
+ case DOCUMENT_EVENT_BLOCK: {
3526
+ const inputChildren = await this.operationInputFields(staticString(record.documentType), staticString(record.actionType));
3527
+ return {
3528
+ source: inputChildren.length > 0 ? "schema" : "static",
3529
+ nodes: documentEventTree(inputChildren)
3530
+ };
3531
+ }
3532
+ case DOCUMENT_FIND_BLOCK: return {
3533
+ source: "static",
3534
+ nodes: documentFindTree()
3535
+ };
3536
+ case DOCUMENT_SCHEMA_BLOCK: return {
3537
+ source: "static",
3538
+ nodes: documentSchemaTree()
3539
+ };
3540
+ case DOCUMENT_TYPES_BLOCK: return {
3541
+ source: "static",
3542
+ nodes: documentTypesTree()
3543
+ };
3544
+ case DOCUMENT_GET_BLOCK: {
3545
+ const stateChildren = await this.stateFields(staticString(record.documentType));
3546
+ return {
3547
+ source: stateChildren.length > 0 ? "schema" : "static",
3548
+ nodes: documentGetTree(stateChildren)
3549
+ };
3550
+ }
3551
+ case DOCUMENT_CREATE_BLOCK:
3552
+ case DOCUMENT_DISPATCH_BLOCK: {
3553
+ const stateChildren = await this.stateFields(staticString(record.documentType));
3554
+ return {
3555
+ source: stateChildren.length > 0 ? "schema" : "static",
3556
+ nodes: documentBlockTree(stateChildren)
3557
+ };
3558
+ }
3559
+ default: {
3560
+ const parsed = parseBlockType(blockType, packagePieces.versions());
3561
+ if (!parsed) return {
3562
+ source: "none",
3563
+ nodes: []
3564
+ };
3565
+ const detail = await this.pieceDetail(parsed.packageName);
3566
+ const entry = (parsed.kind === "trigger" ? detail.triggers : detail.actions)?.[parsed.name];
3567
+ if (entry?.outputSchema) {
3568
+ const nodes = fromOutputSchema(entry.outputSchema);
3569
+ if (nodes.length > 0) return {
3570
+ source: "schema",
3571
+ nodes
3572
+ };
3573
+ if (hasOutputSchemaFields(entry.outputSchema)) return {
3574
+ source: "schema",
3575
+ nodes: []
3576
+ };
3577
+ }
3578
+ if (entry?.sampleData !== void 0 && entry.sampleData !== null) {
3579
+ const nodes = fromSample(entry.sampleData);
3580
+ if (nodes.length > 0) return {
3581
+ source: "sample",
3582
+ nodes
3583
+ };
3584
+ }
3585
+ return {
3586
+ source: "none",
3587
+ nodes: []
3588
+ };
3589
+ }
3590
+ }
3591
+ }
3592
+ async stateFields(documentType) {
3593
+ if (!documentType) return [];
3594
+ try {
3595
+ const sdl = (await this.host.reactorClient.getDocumentModelModule(documentType)).documentModel.global.specifications.at(-1)?.state.global.schema;
3596
+ return sdl ? fieldsFromSdl(sdl) : [];
3597
+ } catch {
3598
+ return [];
3599
+ }
3600
+ }
3601
+ async operationInputFields(documentType, actionType) {
3602
+ if (!documentType || !actionType) return [];
3603
+ try {
3604
+ const latest = (await this.host.reactorClient.getDocumentModelModule(documentType)).documentModel.global.specifications.at(-1);
3605
+ for (const specModule of latest?.modules ?? []) for (const operation of specModule.operations) if (operation.name === actionType && operation.schema) return fieldsFromSdl(operation.schema);
3606
+ return [];
3607
+ } catch {
3608
+ return [];
3609
+ }
3610
+ }
3611
+ async testTrigger(workflowId, ctx) {
3612
+ await this.assertCanReadDocument(workflowId, ctx);
3613
+ const trigger = (await this.host.reactorClient.get(workflowId)).state.global.trigger;
3614
+ if (!trigger) throw new Error("Workflow has no trigger");
3615
+ if (trigger.connectionId) await this.assertCanReadDocument(trigger.connectionId, ctx);
3616
+ await packagePieces.ready();
3617
+ const binding = this.pieceBinding(workflowId, trigger);
3618
+ if (!binding) throw new Error(`"${trigger.blockType}" is not a piece trigger`);
3619
+ return this.supervisor().test(binding);
3620
+ }
3621
+ workers() {
3622
+ return this.pieceWorkers ??= new PieceWorkerPool({
3623
+ size: Number(process.env.WORKFLOW_RUN_CONCURRENCY) || void 0,
3624
+ maxQueueDepth: Number(process.env.WORKFLOW_RUN_QUEUE_DEPTH) || void 0
3625
+ });
3626
+ }
3627
+ async fire(workflowId, triggerPayload, triggerKind = "manual", resume, ctx, enqueuedRunId) {
3628
+ const store = await this.store();
3629
+ let state;
3630
+ let definition;
3631
+ try {
3632
+ if (triggerKind === "manual") await this.assertCanReadDocument(workflowId, ctx);
3633
+ const document = await this.host.reactorClient.get(workflowId);
3634
+ if (document.header.documentType !== "powerhouse/workflow") throw new Error(`Document "${workflowId}" is not a powerhouse/workflow`);
3635
+ state = document.state.global;
3636
+ if (state.status !== "ENABLED") throw new Error(`Workflow is ${state.status}; only ENABLED workflows can fire`);
3637
+ definition = toWorkflowDefinition(state);
3638
+ } catch (error) {
3639
+ if (enqueuedRunId) await store?.failRun(enqueuedRunId, error instanceof Error ? error.message : String(error));
3640
+ throw error;
3641
+ }
3642
+ const connections = declaredConnectionIds(definition);
3643
+ this.executor ??= createBlockExecutor(this.host, this.secretProvider(), this.attachments, store ? createPieceStorePort(store, currentWorkflowId) : void 0);
3644
+ let runId = enqueuedRunId ?? null;
3645
+ if (enqueuedRunId) await store?.beginRun(enqueuedRunId, {
3646
+ workflowName: state.name,
3647
+ workflowVersion: state.version
3648
+ });
3649
+ else runId = await store?.startRun({
3650
+ workflowId,
3651
+ workflowName: state.name,
3652
+ workflowVersion: state.version,
3653
+ triggerKind,
3654
+ triggerPayload,
3655
+ rerunOf: resume?.rerunOf
3656
+ }) ?? null;
3657
+ let journalFailed = false;
3658
+ const executionOrder = /* @__PURE__ */ new Map();
3659
+ let session;
3660
+ try {
3661
+ session = this.workers().session();
3662
+ const result = await withRunScope({
3663
+ workflowId,
3664
+ runId,
3665
+ connections,
3666
+ pieceWorker: session
3667
+ }, () => runWorkflow({
3668
+ definition,
3669
+ executor: this.executor,
3670
+ triggerPayload,
3671
+ completedSteps: resume?.completedSteps,
3672
+ onStep: store && runId ? async (record, ordinal) => {
3673
+ executionOrder.set(record.stepId, ordinal);
3674
+ try {
3675
+ await store.recordStep(runId, ordinal, record);
3676
+ } catch (error) {
3677
+ if (journalFailed) return;
3678
+ journalFailed = true;
3679
+ this.logger.warn(`Run ${runId}: journaling step "${record.key}" failed; the run continues without per-step durability`, error);
3680
+ }
3681
+ } : void 0
3682
+ }));
3683
+ if (store && runId) try {
3684
+ await store.finishRun(runId, result, executionOrder);
3685
+ } catch (error) {
3686
+ this.logger.warn(`Run ${runId}: closing the run journal out failed; the run's outcome stands`, error);
3687
+ }
3688
+ return {
3689
+ ...result,
3690
+ runId
3691
+ };
3692
+ } catch (error) {
3693
+ if (store && runId) await store.failRun(runId, error instanceof Error ? error.message : String(error));
3694
+ throw error;
3695
+ } finally {
3696
+ session?.close();
3697
+ }
3698
+ }
3699
+ async rerun(runId, ctx) {
3700
+ const store = await this.store();
3701
+ if (!store) throw new Error("Run journal is unavailable");
3702
+ const run = await store.getRun(runId);
3703
+ if (!run) throw new Error(`Run "${runId}" not found`);
3704
+ await this.assertCanReadDocument(run.workflow_id, ctx);
3705
+ if (run.status !== "FAILED") throw new Error(`Only FAILED runs can be rerun; run is ${run.status}`);
3706
+ const triggerPayload = run.trigger_payload === null ? void 0 : JSON.parse(run.trigger_payload);
3707
+ if (containsRedactedMarker(triggerPayload)) throw new Error(`Trigger payload of run "${runId}" was redacted and cannot be replayed; fire the workflow again instead of rerunning it`);
3708
+ const document = await this.host.reactorClient.get(run.workflow_id);
3709
+ const currentSteps = new Map(document.state.global.steps.map((step) => [step.id, step]));
3710
+ const completedSteps = /* @__PURE__ */ new Map();
3711
+ for (const row of await store.getSteps(runId)) {
3712
+ if (row.status !== "SUCCEEDED" && row.status !== "REPLAYED") continue;
3713
+ const current = currentSteps.get(row.step_id);
3714
+ if (!current || current.blockType !== row.block_type || current.key !== row.step_key) continue;
3715
+ completedSteps.set(row.step_id, {
3716
+ output: row.output === null ? void 0 : JSON.parse(row.output),
3717
+ port: row.port
3718
+ });
3719
+ }
3720
+ return this.fire(run.workflow_id, triggerPayload, "rerun", {
3721
+ completedSteps,
3722
+ rerunOf: runId
3723
+ });
3724
+ }
3725
+ };
3726
+ /** The runtime a host composes: one instance, its lifetime the host's. */
3727
+ function createWorkflowRuntime(deps) {
3728
+ return new WorkflowRuntimeService(deps);
3729
+ }
3730
+ //#endregion
3731
+ //#region src/reactor/workflow-triggers-read-model.ts
3732
+ const WORKFLOW_TRIGGERS_READ_MODEL = "workflow-triggers";
3733
+ const WORKFLOW_TRIGGERS_READ_MODEL_STAGE = "post_ready";
3734
+ var WorkflowTriggersReadModel = class extends BaseReadModel {
3735
+ freshRegistration = false;
3736
+ constructor(db, operationIndex, writeCache, consistencyTracker, runtime) {
3737
+ super(db, operationIndex, writeCache, consistencyTracker, {
3738
+ readModelId: WORKFLOW_TRIGGERS_READ_MODEL,
3739
+ rebuildStateOnInit: false,
3740
+ indexing: defaultReadModelIndexingConfig
3741
+ });
3742
+ this.runtime = runtime;
3743
+ }
3744
+ async init() {
3745
+ if (await this.loadState() === void 0) {
3746
+ this.freshRegistration = true;
3747
+ return;
3748
+ }
3749
+ this.freshRegistration = false;
3750
+ await super.init();
3751
+ }
3752
+ async indexOperations(items) {
3753
+ if (items.length === 0) return;
3754
+ await super.indexOperations(items);
3755
+ this.freshRegistration = false;
3756
+ }
3757
+ async commitOperations(items) {
3758
+ await this.runtime.onOperations(items);
3759
+ }
3760
+ async saveState(trx, items) {
3761
+ if (this.freshRegistration) await trx.insertInto("ViewState").values({
3762
+ readModelId: this.config.readModelId,
3763
+ lastOrdinal: 0
3764
+ }).onConflict((oc) => oc.column("readModelId").doNothing()).execute();
3765
+ await super.saveState(trx, items);
3766
+ }
3767
+ };
3768
+ //#endregion
3769
+ export { PieceRegistry, WORKFLOW_PACKAGE_NAME, WORKFLOW_TRIGGERS_READ_MODEL, WORKFLOW_TRIGGERS_READ_MODEL_STAGE, WorkflowRuntimeService, WorkflowTriggersReadModel, createAttachmentPort, createWorkflowRuntime, packagePieces };
3770
+
3771
+ //# sourceMappingURL=index.js.map