@workflow/web-shared 4.0.1-beta.9 → 4.1.0-beta.46

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.
Files changed (137) hide show
  1. package/README.md +2 -0
  2. package/dist/api/workflow-api-client.d.ts +325 -85
  3. package/dist/api/workflow-api-client.d.ts.map +1 -1
  4. package/dist/api/workflow-api-client.js +370 -214
  5. package/dist/api/workflow-api-client.js.map +1 -1
  6. package/dist/api/workflow-server-actions.d.ts +136 -3
  7. package/dist/api/workflow-server-actions.d.ts.map +1 -1
  8. package/dist/api/workflow-server-actions.js +649 -116
  9. package/dist/api/workflow-server-actions.js.map +1 -1
  10. package/dist/components/ui/card.d.ts +9 -0
  11. package/dist/components/ui/card.d.ts.map +1 -0
  12. package/dist/components/ui/card.js +18 -0
  13. package/dist/components/ui/card.js.map +1 -0
  14. package/dist/components/ui/error-card.d.ts +15 -0
  15. package/dist/components/ui/error-card.d.ts.map +1 -0
  16. package/dist/components/ui/error-card.js +14 -0
  17. package/dist/components/ui/error-card.js.map +1 -0
  18. package/dist/components/ui/skeleton.d.ts +3 -0
  19. package/dist/components/ui/skeleton.d.ts.map +1 -0
  20. package/dist/components/ui/skeleton.js +7 -0
  21. package/dist/components/ui/skeleton.js.map +1 -0
  22. package/dist/error-boundary.d.ts +28 -0
  23. package/dist/error-boundary.d.ts.map +1 -0
  24. package/dist/error-boundary.js +51 -0
  25. package/dist/error-boundary.js.map +1 -0
  26. package/dist/event-list-view.d.ts +13 -0
  27. package/dist/event-list-view.d.ts.map +1 -0
  28. package/dist/event-list-view.js +183 -0
  29. package/dist/event-list-view.js.map +1 -0
  30. package/dist/hook-actions.d.ts +59 -0
  31. package/dist/hook-actions.d.ts.map +1 -0
  32. package/dist/hook-actions.js +76 -0
  33. package/dist/hook-actions.js.map +1 -0
  34. package/dist/hooks/use-dark-mode.d.ts +9 -0
  35. package/dist/hooks/use-dark-mode.d.ts.map +1 -0
  36. package/dist/hooks/use-dark-mode.js +30 -0
  37. package/dist/hooks/use-dark-mode.js.map +1 -0
  38. package/dist/index.d.ts +14 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +9 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/lib/event-analysis.d.ts +55 -0
  43. package/dist/lib/event-analysis.d.ts.map +1 -0
  44. package/dist/lib/event-analysis.js +161 -0
  45. package/dist/lib/event-analysis.js.map +1 -0
  46. package/dist/lib/utils.d.ts +44 -0
  47. package/dist/lib/utils.d.ts.map +1 -1
  48. package/dist/lib/utils.js +109 -0
  49. package/dist/lib/utils.js.map +1 -1
  50. package/dist/run-trace-view.d.ts.map +1 -1
  51. package/dist/run-trace-view.js +1 -1
  52. package/dist/run-trace-view.js.map +1 -1
  53. package/dist/sidebar/attribute-panel.d.ts +12 -2
  54. package/dist/sidebar/attribute-panel.d.ts.map +1 -1
  55. package/dist/sidebar/attribute-panel.js +368 -23
  56. package/dist/sidebar/attribute-panel.js.map +1 -1
  57. package/dist/sidebar/conversation-view.d.ts +7 -0
  58. package/dist/sidebar/conversation-view.d.ts.map +1 -0
  59. package/dist/sidebar/conversation-view.js +125 -0
  60. package/dist/sidebar/conversation-view.js.map +1 -0
  61. package/dist/sidebar/detail-card.d.ts.map +1 -1
  62. package/dist/sidebar/detail-card.js +2 -2
  63. package/dist/sidebar/detail-card.js.map +1 -1
  64. package/dist/sidebar/entity-detail-panel.d.ts +12 -0
  65. package/dist/sidebar/entity-detail-panel.d.ts.map +1 -0
  66. package/dist/sidebar/entity-detail-panel.js +190 -0
  67. package/dist/sidebar/entity-detail-panel.js.map +1 -0
  68. package/dist/sidebar/events-list.d.ts +2 -1
  69. package/dist/sidebar/events-list.d.ts.map +1 -1
  70. package/dist/sidebar/events-list.js +11 -10
  71. package/dist/sidebar/events-list.js.map +1 -1
  72. package/dist/sidebar/resolve-hook-modal.d.ts +16 -0
  73. package/dist/sidebar/resolve-hook-modal.d.ts.map +1 -0
  74. package/dist/sidebar/resolve-hook-modal.js +74 -0
  75. package/dist/sidebar/resolve-hook-modal.js.map +1 -0
  76. package/dist/stream-viewer.d.ts +13 -0
  77. package/dist/stream-viewer.d.ts.map +1 -0
  78. package/dist/stream-viewer.js +109 -0
  79. package/dist/stream-viewer.js.map +1 -0
  80. package/dist/trace-viewer/components/markers.d.ts.map +1 -1
  81. package/dist/trace-viewer/components/markers.js +3 -2
  82. package/dist/trace-viewer/components/markers.js.map +1 -1
  83. package/dist/trace-viewer/components/node.d.ts.map +1 -1
  84. package/dist/trace-viewer/components/node.js +1 -0
  85. package/dist/trace-viewer/components/node.js.map +1 -1
  86. package/dist/trace-viewer/components/search.d.ts.map +1 -1
  87. package/dist/trace-viewer/components/search.js +1 -0
  88. package/dist/trace-viewer/components/search.js.map +1 -1
  89. package/dist/trace-viewer/components/span-detail-panel.js +2 -2
  90. package/dist/trace-viewer/components/span-detail-panel.js.map +1 -1
  91. package/dist/trace-viewer/context.d.ts.map +1 -1
  92. package/dist/trace-viewer/context.js +1 -0
  93. package/dist/trace-viewer/context.js.map +1 -1
  94. package/dist/trace-viewer/trace-viewer.module.css +47 -30
  95. package/dist/trace-viewer/types.d.ts +11 -0
  96. package/dist/trace-viewer/types.d.ts.map +1 -1
  97. package/dist/trace-viewer/util/timing.d.ts +7 -1
  98. package/dist/trace-viewer/util/timing.d.ts.map +1 -1
  99. package/dist/trace-viewer/util/timing.js +7 -12
  100. package/dist/trace-viewer/util/timing.js.map +1 -1
  101. package/dist/trace-viewer/util/tree.d.ts.map +1 -1
  102. package/dist/trace-viewer/util/tree.js +4 -0
  103. package/dist/trace-viewer/util/tree.js.map +1 -1
  104. package/dist/trace-viewer/util/use-immediate-style.d.ts.map +1 -1
  105. package/dist/trace-viewer/util/use-immediate-style.js +1 -0
  106. package/dist/trace-viewer/util/use-immediate-style.js.map +1 -1
  107. package/dist/trace-viewer/util/use-streaming-spans.d.ts.map +1 -1
  108. package/dist/trace-viewer/util/use-streaming-spans.js +2 -1
  109. package/dist/trace-viewer/util/use-streaming-spans.js.map +1 -1
  110. package/dist/trace-viewer/util/use-trackpad-zoom.d.ts.map +1 -1
  111. package/dist/trace-viewer/util/use-trackpad-zoom.js +1 -0
  112. package/dist/trace-viewer/util/use-trackpad-zoom.js.map +1 -1
  113. package/dist/trace-viewer/worker.js +1 -1
  114. package/dist/trace-viewer/worker.js.map +1 -1
  115. package/dist/workflow-trace-view.d.ts +3 -1
  116. package/dist/workflow-trace-view.d.ts.map +1 -1
  117. package/dist/workflow-trace-view.js +28 -11
  118. package/dist/workflow-trace-view.js.map +1 -1
  119. package/dist/workflow-traces/event-colors.d.ts +1 -1
  120. package/dist/workflow-traces/event-colors.js +2 -2
  121. package/dist/workflow-traces/event-colors.js.map +1 -1
  122. package/dist/workflow-traces/trace-colors.d.ts.map +1 -1
  123. package/dist/workflow-traces/trace-colors.js +1 -3
  124. package/dist/workflow-traces/trace-colors.js.map +1 -1
  125. package/dist/workflow-traces/trace-span-construction.d.ts +18 -3
  126. package/dist/workflow-traces/trace-span-construction.d.ts.map +1 -1
  127. package/dist/workflow-traces/trace-span-construction.js +84 -31
  128. package/dist/workflow-traces/trace-span-construction.js.map +1 -1
  129. package/dist/workflow-traces/trace-time-utils.d.ts +2 -2
  130. package/dist/workflow-traces/trace-time-utils.d.ts.map +1 -1
  131. package/dist/workflow-traces/trace-time-utils.js +9 -0
  132. package/dist/workflow-traces/trace-time-utils.js.map +1 -1
  133. package/package.json +24 -14
  134. package/dist/sidebar/workflow-detail-panel.d.ts +0 -8
  135. package/dist/sidebar/workflow-detail-panel.d.ts.map +0 -1
  136. package/dist/sidebar/workflow-detail-panel.js +0 -56
  137. package/dist/sidebar/workflow-detail-panel.js.map +0 -1
@@ -1,47 +1,379 @@
1
1
  'use server';
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  import { hydrateResourceIO } from '@workflow/core/observability';
3
- import { createWorld, start } from '@workflow/core/runtime';
4
- function getWorldFromEnv(envMap) {
5
- for (const [key, value] of Object.entries(envMap)) {
6
- if (value === undefined || value === null || value === '') {
5
+ import { createWorld, healthCheck, resumeHook as resumeHookRuntime, start, } from '@workflow/core/runtime';
6
+ import { getDeserializeStream, getExternalRevivers, } from '@workflow/core/serialization';
7
+ import { WorkflowAPIError, WorkflowRunNotFoundError } from '@workflow/errors';
8
+ import { findWorkflowDataDir } from '@workflow/utils/check-data-dir';
9
+ import { isLegacySpecVersion, SPEC_VERSION_LEGACY, } from '@workflow/world';
10
+ import { createVercelWorld } from '@workflow/world-vercel';
11
+ /**
12
+ * Map from WORKFLOW_TARGET_WORLD value to human-readable display name
13
+ */
14
+ function getBackendDisplayName(targetWorld) {
15
+ if (!targetWorld)
16
+ return 'Local';
17
+ switch (targetWorld) {
18
+ case 'local':
19
+ return 'Local';
20
+ case 'vercel':
21
+ return 'Vercel';
22
+ case '@workflow/world-postgres':
23
+ case 'postgres':
24
+ return 'PostgreSQL';
25
+ default:
26
+ // For custom worlds, try to make a readable name
27
+ if (targetWorld.startsWith('@')) {
28
+ // Extract package name without scope for display
29
+ const parts = targetWorld.split('/');
30
+ return parts[parts.length - 1] || targetWorld;
31
+ }
32
+ return targetWorld;
33
+ }
34
+ }
35
+ function getEffectiveBackendId() {
36
+ const targetWorld = process.env.WORKFLOW_TARGET_WORLD;
37
+ if (targetWorld) {
38
+ return targetWorld;
39
+ }
40
+ // Match @workflow/core/runtime defaulting: vercel if VERCEL_DEPLOYMENT_ID is set, else local.
41
+ return process.env.VERCEL_DEPLOYMENT_ID ? 'vercel' : 'local';
42
+ }
43
+ function getObservabilityCwd() {
44
+ const raw = process.env.WORKFLOW_OBSERVABILITY_CWD;
45
+ if (!raw) {
46
+ return process.cwd();
47
+ }
48
+ return path.isAbsolute(raw) ? raw : path.resolve(process.cwd(), raw);
49
+ }
50
+ /**
51
+ * Ensure local-world env is derived consistently when running `packages/web` directly.
52
+ *
53
+ * Without this, the UI may *display* a dataDir detected from WORKFLOW_OBSERVABILITY_CWD,
54
+ * while the actual World reads from `WORKFLOW_LOCAL_DATA_DIR` (defaulting to `.workflow-data`
55
+ * under the web package cwd), resulting in "no runs" even though data exists.
56
+ */
57
+ async function ensureLocalWorldDataDirEnv() {
58
+ if (process.env.WORKFLOW_LOCAL_DATA_DIR)
59
+ return;
60
+ const cwd = getObservabilityCwd();
61
+ const info = await findWorkflowDataDir(cwd);
62
+ // Prefer a discovered workflow-data directory (e.g. `.next/workflow-data`).
63
+ if (info.dataDir) {
64
+ process.env.WORKFLOW_LOCAL_DATA_DIR = info.dataDir;
65
+ return;
66
+ }
67
+ // Fall back to a canonical location under the target project directory.
68
+ process.env.WORKFLOW_LOCAL_DATA_DIR = path.resolve(cwd, '.workflow-data');
69
+ }
70
+ /**
71
+ * Extract hostname from a database URL without exposing credentials.
72
+ */
73
+ function extractHostnameFromUrl(url) {
74
+ if (!url)
75
+ return undefined;
76
+ try {
77
+ const parsed = new URL(url);
78
+ return parsed.hostname || undefined;
79
+ }
80
+ catch {
81
+ return undefined;
82
+ }
83
+ }
84
+ /**
85
+ * Extract database name from a URL where pathname is like "/dbname".
86
+ * (Works for postgres/mongodb-style URLs; returns undefined when not applicable.)
87
+ */
88
+ function extractDatabaseFromUrl(url) {
89
+ if (!url)
90
+ return undefined;
91
+ try {
92
+ const parsed = new URL(url);
93
+ const dbName = parsed.pathname?.slice(1);
94
+ return dbName || undefined;
95
+ }
96
+ catch {
97
+ return undefined;
98
+ }
99
+ }
100
+ // Keep this list in sync with `worlds-manifest.json` env + credentialsNote.
101
+ const WORLD_ENV_ALLOWLIST_BY_TARGET_WORLD = {
102
+ // Official
103
+ local: [
104
+ 'WORKFLOW_TARGET_WORLD',
105
+ 'WORKFLOW_LOCAL_DATA_DIR',
106
+ 'WORKFLOW_MANIFEST_PATH',
107
+ 'WORKFLOW_OBSERVABILITY_CWD',
108
+ 'PORT',
109
+ ],
110
+ '@workflow/world-local': [
111
+ 'WORKFLOW_TARGET_WORLD',
112
+ 'WORKFLOW_LOCAL_DATA_DIR',
113
+ 'WORKFLOW_MANIFEST_PATH',
114
+ 'WORKFLOW_OBSERVABILITY_CWD',
115
+ 'PORT',
116
+ ],
117
+ postgres: ['WORKFLOW_TARGET_WORLD', 'WORKFLOW_POSTGRES_URL'],
118
+ '@workflow/world-postgres': [
119
+ 'WORKFLOW_TARGET_WORLD',
120
+ 'WORKFLOW_POSTGRES_URL',
121
+ ],
122
+ vercel: [
123
+ 'WORKFLOW_TARGET_WORLD',
124
+ 'WORKFLOW_VERCEL_ENV',
125
+ 'WORKFLOW_VERCEL_TEAM',
126
+ 'WORKFLOW_VERCEL_PROJECT',
127
+ 'WORKFLOW_VERCEL_AUTH_TOKEN',
128
+ ],
129
+ '@workflow/world-vercel': [
130
+ 'WORKFLOW_TARGET_WORLD',
131
+ 'WORKFLOW_VERCEL_ENV',
132
+ 'WORKFLOW_VERCEL_TEAM',
133
+ 'WORKFLOW_VERCEL_PROJECT',
134
+ 'WORKFLOW_VERCEL_AUTH_TOKEN',
135
+ ],
136
+ // Community (from worlds-manifest.json)
137
+ '@workflow-worlds/starter': ['WORKFLOW_TARGET_WORLD'],
138
+ '@workflow-worlds/turso': [
139
+ 'WORKFLOW_TARGET_WORLD',
140
+ 'WORKFLOW_TURSO_DATABASE_URL',
141
+ ],
142
+ '@workflow-worlds/mongodb': [
143
+ 'WORKFLOW_TARGET_WORLD',
144
+ 'WORKFLOW_MONGODB_URI',
145
+ 'WORKFLOW_MONGODB_DATABASE_NAME',
146
+ ],
147
+ '@workflow-worlds/redis': ['WORKFLOW_TARGET_WORLD', 'WORKFLOW_REDIS_URI'],
148
+ 'workflow-world-jazz': [
149
+ 'WORKFLOW_TARGET_WORLD',
150
+ // credentialsNote:
151
+ 'JAZZ_API_KEY',
152
+ 'JAZZ_WORKER_ACCOUNT',
153
+ 'JAZZ_WORKER_SECRET',
154
+ ],
155
+ };
156
+ function getAllowedEnvKeysForBackend(backendId) {
157
+ return (WORLD_ENV_ALLOWLIST_BY_TARGET_WORLD[backendId] ?? ['WORKFLOW_TARGET_WORLD']);
158
+ }
159
+ // Keep this list in sync with `worlds-manifest.json` env + credentialsNote.
160
+ //
161
+ // IMPORTANT: This is intentionally explicit (no heuristics). We only redact values for env
162
+ // vars that are known + whitelisted and that we *know* contain secrets/credentials.
163
+ const WORLD_SENSITIVE_ENV_KEYS = new Set([
164
+ // Official
165
+ 'WORKFLOW_POSTGRES_URL',
166
+ 'WORKFLOW_VERCEL_AUTH_TOKEN',
167
+ // Community
168
+ 'WORKFLOW_TURSO_DATABASE_URL',
169
+ 'WORKFLOW_MONGODB_URI',
170
+ 'WORKFLOW_REDIS_URI',
171
+ 'JAZZ_API_KEY',
172
+ 'JAZZ_WORKER_SECRET',
173
+ ]);
174
+ function isSet(value) {
175
+ return value !== undefined && value !== null && value !== '';
176
+ }
177
+ function deriveDbInfoForKey(key, value) {
178
+ // Only attempt for URL-like strings.
179
+ if (!value.includes(':'))
180
+ return null;
181
+ try {
182
+ const parsed = new URL(value);
183
+ const protocol = (parsed.protocol || '').replace(':', '');
184
+ // file: URIs are not useful for hostname/db display
185
+ if (protocol === 'file')
186
+ return null;
187
+ const hostname = extractHostnameFromUrl(value);
188
+ const database = extractDatabaseFromUrl(value);
189
+ const out = {};
190
+ if (hostname)
191
+ out[`derived.${key}.hostname`] = hostname;
192
+ if (database)
193
+ out[`derived.${key}.database`] = database;
194
+ if (protocol)
195
+ out[`derived.${key}.protocol`] = protocol;
196
+ return Object.keys(out).length ? out : null;
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ }
202
+ async function getLocalDisplayInfo() {
203
+ const cwd = getObservabilityCwd();
204
+ const dataDirInfo = await findWorkflowDataDir(cwd);
205
+ const out = {
206
+ 'local.shortName': dataDirInfo.shortName,
207
+ 'local.projectDir': dataDirInfo.projectDir,
208
+ };
209
+ if (dataDirInfo.dataDir) {
210
+ out['local.dataDirPath'] = dataDirInfo.dataDir;
211
+ }
212
+ return out;
213
+ }
214
+ function collectAllowedEnv(allowedKeys) {
215
+ const publicEnv = {};
216
+ const sensitiveEnvKeys = [];
217
+ const derivedDisplayInfo = {};
218
+ for (const key of allowedKeys) {
219
+ const value = process.env[key];
220
+ if (!isSet(value))
221
+ continue;
222
+ if (WORLD_SENSITIVE_ENV_KEYS.has(key)) {
223
+ sensitiveEnvKeys.push(key);
224
+ const derived = deriveDbInfoForKey(key, value);
225
+ if (derived)
226
+ Object.assign(derivedDisplayInfo, derived);
7
227
  continue;
8
228
  }
9
- process.env[key] = value;
229
+ publicEnv[key] = value;
230
+ }
231
+ return {
232
+ publicEnv,
233
+ sensitiveEnvKeys: Array.from(new Set(sensitiveEnvKeys)).sort(),
234
+ derivedDisplayInfo,
235
+ };
236
+ }
237
+ /**
238
+ * Get public configuration info that is safe to send to the client.
239
+ *
240
+ * This is the ONLY server action that intentionally exposes env-derived data,
241
+ * and that data is strictly whitelisted per world backend.
242
+ */
243
+ export async function getPublicServerConfig() {
244
+ const backendId = getEffectiveBackendId();
245
+ const backendDisplayName = getBackendDisplayName(backendId);
246
+ const allowedKeys = getAllowedEnvKeysForBackend(backendId);
247
+ const { publicEnv, sensitiveEnvKeys, derivedDisplayInfo } = collectAllowedEnv(allowedKeys);
248
+ const displayInfo = { ...derivedDisplayInfo };
249
+ if (backendId === 'local' || backendId === '@workflow/world-local') {
250
+ Object.assign(displayInfo, await getLocalDisplayInfo());
251
+ }
252
+ const config = {
253
+ backendDisplayName,
254
+ backendId,
255
+ publicEnv,
256
+ sensitiveEnvKeys,
257
+ displayInfo: Object.keys(displayInfo).length ? displayInfo : undefined,
258
+ };
259
+ // Provide defaults for commonly expected keys without revealing extra secrets.
260
+ if ((backendId === 'vercel' || backendId === '@workflow/world-vercel') &&
261
+ !publicEnv.WORKFLOW_VERCEL_ENV) {
262
+ config.publicEnv.WORKFLOW_VERCEL_ENV = 'production';
10
263
  }
11
- return createWorld();
264
+ return config;
265
+ }
266
+ /**
267
+ * Cache for World instances.
268
+ *
269
+ * IMPORTANT:
270
+ * - We only cache non-vercel worlds.
271
+ * - Cache keys are derived from **server-side** WORKFLOW_* env vars only.
272
+ */
273
+ const worldCache = new Map();
274
+ /**
275
+ * Get or create a World instance based on configuration.
276
+ *
277
+ * The @workflow/web UI should always pass `{}` for envMap.
278
+ */
279
+ async function getWorldFromEnv(userEnvMap) {
280
+ const backendId = getEffectiveBackendId();
281
+ const isVercelWorld = ['vercel', '@workflow/world-vercel'].includes(backendId);
282
+ // For the vercel world specifically, we do not cache the world,
283
+ // and allow user-provided env, as it can be a multi-tenant environment,
284
+ // and we instantiate the world per-user directly to avoid having to set
285
+ // process.env.
286
+ if (isVercelWorld) {
287
+ return createVercelWorld({
288
+ token: userEnvMap.WORKFLOW_VERCEL_AUTH_TOKEN ||
289
+ process.env.WORKFLOW_VERCEL_AUTH_TOKEN,
290
+ projectConfig: {
291
+ environment: userEnvMap.WORKFLOW_VERCEL_ENV || process.env.WORKFLOW_VERCEL_ENV,
292
+ projectId: userEnvMap.WORKFLOW_VERCEL_PROJECT ||
293
+ process.env.WORKFLOW_VERCEL_PROJECT,
294
+ teamId: userEnvMap.WORKFLOW_VERCEL_TEAM || process.env.WORKFLOW_VERCEL_TEAM,
295
+ },
296
+ });
297
+ }
298
+ // For other worlds, we intentionally do not trust or apply client-provided env,
299
+ // to avoid potential security risks in self-hosted scenarios.
300
+ // Ensure local-world reads from the same project directory the UI is inspecting.
301
+ if (backendId === 'local' || backendId === '@workflow/world-local') {
302
+ await ensureLocalWorldDataDirEnv();
303
+ }
304
+ // Cache key derived ONLY from WORKFLOW_* env vars.
305
+ const workflowEnvEntries = Object.entries(process.env).filter(([key]) => key.startsWith('WORKFLOW_'));
306
+ workflowEnvEntries.sort(([a], [b]) => a.localeCompare(b));
307
+ const cacheKey = JSON.stringify(Object.fromEntries(workflowEnvEntries));
308
+ const cachedWorld = worldCache.get(cacheKey);
309
+ if (cachedWorld) {
310
+ return cachedWorld;
311
+ }
312
+ const world = createWorld();
313
+ worldCache.set(cacheKey, world);
314
+ return world;
12
315
  }
13
316
  /**
14
317
  * Creates a structured error object from a caught error
15
318
  */
16
- function createServerActionError(error, operation, params) {
319
+ function createServerActionError(error, operation, requestParams) {
17
320
  const err = error instanceof Error ? error : new Error(String(error));
18
- // Determine if this is an API layer error (from the World interface)
19
- // or a server layer error (from within the server action)
20
- const isAPIError = err.message?.includes('fetch') ||
21
- err.message?.includes('HTTP') ||
22
- err.message?.includes('network');
23
- const actionError = {
24
- message: getUserFacingMessage(err),
25
- layer: isAPIError ? 'API' : 'server',
26
- cause: err.stack || err.message,
27
- request: params ? { operation, params } : undefined,
321
+ console.error(`[web-api] ${operation} error:`, err);
322
+ let errorResponse;
323
+ if (WorkflowAPIError.is(error)) {
324
+ // If the World threw the error on fetch/fs.read, we add that data
325
+ // to the error object
326
+ errorResponse = {
327
+ message: getUserFacingErrorMessage(err, error.status),
328
+ layer: 'API',
329
+ cause: err.stack || err.message,
330
+ request: {
331
+ operation,
332
+ params: requestParams ?? {},
333
+ status: error.status,
334
+ url: error.url,
335
+ code: error.code ?? undefined,
336
+ },
337
+ };
338
+ }
339
+ else if (WorkflowRunNotFoundError.is(error)) {
340
+ // The World might repackage the error as a WorkflowRunNotFoundError
341
+ errorResponse = {
342
+ message: getUserFacingErrorMessage(error, 404),
343
+ layer: 'API',
344
+ cause: err.stack || err.message,
345
+ request: { operation, status: 404, params: requestParams ?? {} },
346
+ };
347
+ }
348
+ else {
349
+ errorResponse = {
350
+ message: getUserFacingErrorMessage(err),
351
+ layer: 'server',
352
+ cause: err.stack || err.message,
353
+ request: { status: 500, operation, params: requestParams ?? {} },
354
+ };
355
+ }
356
+ return {
357
+ success: false,
358
+ error: errorResponse,
28
359
  };
29
- return actionError;
30
360
  }
31
361
  /**
32
362
  * Converts an error into a user-facing message
33
363
  */
34
- function getUserFacingMessage(error) {
364
+ function getUserFacingErrorMessage(error, status) {
365
+ if (!status) {
366
+ return `Error creating response: ${error.message}`;
367
+ }
35
368
  // Check for common error patterns
36
- if (error.message?.includes('403') || error.message?.includes('Forbidden')) {
369
+ if (status === 403 || status === 401) {
37
370
  return 'Access denied. Please check your credentials and permissions.';
38
371
  }
39
- if (error.message?.includes('404') || error.message?.includes('Not Found')) {
372
+ if (status === 404) {
40
373
  return 'The requested resource was not found.';
41
374
  }
42
- if (error.message?.includes('500') ||
43
- error.message?.includes('Internal Server Error')) {
44
- return 'An internal server error occurred. Please try again later.';
375
+ if (status === 500) {
376
+ return 'Error connecting to World backend, please try again later.';
45
377
  }
46
378
  if (error.message?.includes('Network') || error.message?.includes('fetch')) {
47
379
  return 'Network error. Please check your connection and try again.';
@@ -49,6 +381,12 @@ function getUserFacingMessage(error) {
49
381
  // Return the original message for other errors
50
382
  return error.message || 'An unexpected error occurred';
51
383
  }
384
+ const toJSONCompatible = (data) => {
385
+ if (data && typeof data === 'object') {
386
+ return JSON.parse(JSON.stringify(data));
387
+ }
388
+ return data;
389
+ };
52
390
  const hydrate = (data) => {
53
391
  try {
54
392
  return hydrateResourceIO(data);
@@ -63,6 +401,7 @@ const hydrate = (data) => {
63
401
  * @returns ServerActionResult with success=true and the data
64
402
  */
65
403
  function createResponse(data) {
404
+ data = toJSONCompatible(data);
66
405
  return {
67
406
  success: true,
68
407
  data,
@@ -74,7 +413,7 @@ function createResponse(data) {
74
413
  export async function fetchRuns(worldEnv, params) {
75
414
  const { cursor, sortOrder = 'desc', limit = 10, workflowName, status, } = params;
76
415
  try {
77
- const world = getWorldFromEnv(worldEnv);
416
+ const world = await getWorldFromEnv(worldEnv);
78
417
  const result = await world.runs.list({
79
418
  ...(workflowName ? { workflowName } : {}),
80
419
  ...(status ? { status: status } : {}),
@@ -88,11 +427,7 @@ export async function fetchRuns(worldEnv, params) {
88
427
  });
89
428
  }
90
429
  catch (error) {
91
- console.error('Failed to fetch runs:', error);
92
- return {
93
- success: false,
94
- error: createServerActionError(error, 'world.runs.list', params),
95
- };
430
+ return createServerActionError(error, 'world.runs.list', params);
96
431
  }
97
432
  }
98
433
  /**
@@ -100,20 +435,16 @@ export async function fetchRuns(worldEnv, params) {
100
435
  */
101
436
  export async function fetchRun(worldEnv, runId, resolveData = 'all') {
102
437
  try {
103
- const world = getWorldFromEnv(worldEnv);
438
+ const world = await getWorldFromEnv(worldEnv);
104
439
  const run = await world.runs.get(runId, { resolveData });
105
440
  const hydratedRun = hydrate(run);
106
441
  return createResponse(hydratedRun);
107
442
  }
108
443
  catch (error) {
109
- console.error('Failed to fetch run:', error);
110
- return {
111
- success: false,
112
- error: createServerActionError(error, 'world.runs.get', {
113
- runId,
114
- resolveData,
115
- }),
116
- };
444
+ return createServerActionError(error, 'world.runs.get', {
445
+ runId,
446
+ resolveData,
447
+ });
117
448
  }
118
449
  }
119
450
  /**
@@ -122,27 +453,24 @@ export async function fetchRun(worldEnv, runId, resolveData = 'all') {
122
453
  export async function fetchSteps(worldEnv, runId, params) {
123
454
  const { cursor, sortOrder = 'asc', limit = 100 } = params;
124
455
  try {
125
- const world = getWorldFromEnv(worldEnv);
456
+ const world = await getWorldFromEnv(worldEnv);
126
457
  const result = await world.steps.list({
127
458
  runId,
128
459
  pagination: { cursor, limit, sortOrder },
129
460
  resolveData: 'none',
130
461
  });
131
462
  return createResponse({
463
+ // StepWithoutData has undefined input/output, but after hydration the structure is compatible
132
464
  data: result.data.map(hydrate),
133
465
  cursor: result.cursor ?? undefined,
134
466
  hasMore: result.hasMore,
135
467
  });
136
468
  }
137
469
  catch (error) {
138
- console.error('Failed to fetch steps:', error);
139
- return {
140
- success: false,
141
- error: createServerActionError(error, 'world.steps.list', {
142
- runId,
143
- ...params,
144
- }),
145
- };
470
+ return createServerActionError(error, 'world.steps.list', {
471
+ runId,
472
+ ...params,
473
+ });
146
474
  }
147
475
  }
148
476
  /**
@@ -150,20 +478,17 @@ export async function fetchSteps(worldEnv, runId, params) {
150
478
  */
151
479
  export async function fetchStep(worldEnv, runId, stepId, resolveData = 'all') {
152
480
  try {
153
- const world = getWorldFromEnv(worldEnv);
481
+ const world = await getWorldFromEnv(worldEnv);
154
482
  const step = await world.steps.get(runId, stepId, { resolveData });
155
- return createResponse(hydrate(step));
483
+ const hydratedStep = hydrate(step);
484
+ return createResponse(hydratedStep);
156
485
  }
157
486
  catch (error) {
158
- console.error('Failed to fetch step:', error);
159
- return {
160
- success: false,
161
- error: createServerActionError(error, 'world.steps.get', {
162
- runId,
163
- stepId,
164
- resolveData,
165
- }),
166
- };
487
+ return createServerActionError(error, 'world.steps.get', {
488
+ runId,
489
+ stepId,
490
+ resolveData,
491
+ });
167
492
  }
168
493
  }
169
494
  /**
@@ -172,7 +497,7 @@ export async function fetchStep(worldEnv, runId, stepId, resolveData = 'all') {
172
497
  export async function fetchEvents(worldEnv, runId, params) {
173
498
  const { cursor, sortOrder = 'asc', limit = 1000 } = params;
174
499
  try {
175
- const world = getWorldFromEnv(worldEnv);
500
+ const world = await getWorldFromEnv(worldEnv);
176
501
  const result = await world.events.list({
177
502
  runId,
178
503
  pagination: { cursor, limit, sortOrder },
@@ -185,14 +510,10 @@ export async function fetchEvents(worldEnv, runId, params) {
185
510
  });
186
511
  }
187
512
  catch (error) {
188
- console.error('Failed to fetch events:', error);
189
- return {
190
- success: false,
191
- error: createServerActionError(error, 'world.events.list', {
192
- runId,
193
- ...params,
194
- }),
195
- };
513
+ return createServerActionError(error, 'world.events.list', {
514
+ runId,
515
+ ...params,
516
+ });
196
517
  }
197
518
  }
198
519
  /**
@@ -201,24 +522,23 @@ export async function fetchEvents(worldEnv, runId, params) {
201
522
  export async function fetchEventsByCorrelationId(worldEnv, correlationId, params) {
202
523
  const { cursor, sortOrder = 'asc', limit = 1000, withData = false } = params;
203
524
  try {
204
- const world = getWorldFromEnv(worldEnv);
525
+ const world = await getWorldFromEnv(worldEnv);
205
526
  const result = await world.events.listByCorrelationId({
206
527
  correlationId,
207
528
  pagination: { cursor, limit, sortOrder },
208
529
  resolveData: withData ? 'all' : 'none',
209
530
  });
210
531
  return createResponse({
211
- data: result.data,
532
+ data: result.data.map(hydrate),
212
533
  cursor: result.cursor ?? undefined,
213
534
  hasMore: result.hasMore,
214
535
  });
215
536
  }
216
537
  catch (error) {
217
- console.error('Failed to fetch events by correlation ID:', error);
218
- return {
219
- success: false,
220
- error: createServerActionError(error, 'world.events.listByCorrelationId', { correlationId, ...params }),
221
- };
538
+ return createServerActionError(error, 'world.events.listByCorrelationId', {
539
+ correlationId,
540
+ ...params,
541
+ });
222
542
  }
223
543
  }
224
544
  /**
@@ -227,7 +547,7 @@ export async function fetchEventsByCorrelationId(worldEnv, correlationId, params
227
547
  export async function fetchHooks(worldEnv, params) {
228
548
  const { runId, cursor, sortOrder = 'desc', limit = 10 } = params;
229
549
  try {
230
- const world = getWorldFromEnv(worldEnv);
550
+ const world = await getWorldFromEnv(worldEnv);
231
551
  const result = await world.hooks.list({
232
552
  ...(runId ? { runId } : {}),
233
553
  pagination: { cursor, limit, sortOrder },
@@ -240,11 +560,7 @@ export async function fetchHooks(worldEnv, params) {
240
560
  });
241
561
  }
242
562
  catch (error) {
243
- console.error('Failed to fetch hooks:', error);
244
- return {
245
- success: false,
246
- error: createServerActionError(error, 'world.hooks.list', params),
247
- };
563
+ return createServerActionError(error, 'world.hooks.list', params);
248
564
  }
249
565
  }
250
566
  /**
@@ -252,19 +568,15 @@ export async function fetchHooks(worldEnv, params) {
252
568
  */
253
569
  export async function fetchHook(worldEnv, hookId, resolveData = 'all') {
254
570
  try {
255
- const world = getWorldFromEnv(worldEnv);
571
+ const world = await getWorldFromEnv(worldEnv);
256
572
  const hook = await world.hooks.get(hookId, { resolveData });
257
573
  return createResponse(hydrate(hook));
258
574
  }
259
575
  catch (error) {
260
- console.error('Failed to fetch hook:', error);
261
- return {
262
- success: false,
263
- error: createServerActionError(error, 'world.hooks.get', {
264
- hookId,
265
- resolveData,
266
- }),
267
- };
576
+ return createServerActionError(error, 'world.hooks.get', {
577
+ hookId,
578
+ resolveData,
579
+ });
268
580
  }
269
581
  }
270
582
  /**
@@ -272,16 +584,20 @@ export async function fetchHook(worldEnv, hookId, resolveData = 'all') {
272
584
  */
273
585
  export async function cancelRun(worldEnv, runId) {
274
586
  try {
275
- const world = getWorldFromEnv(worldEnv);
276
- await world.runs.cancel(runId);
587
+ const world = await getWorldFromEnv(worldEnv);
588
+ const run = await world.runs.get(runId, { resolveData: 'none' });
589
+ const compatMode = isLegacySpecVersion(run.specVersion);
590
+ const eventData = {
591
+ eventType: 'run_cancelled',
592
+ specVersion: run.specVersion || 1,
593
+ };
594
+ await world.events.create(runId, eventData, { v1Compat: compatMode });
277
595
  return createResponse(undefined);
278
596
  }
279
597
  catch (error) {
280
- console.error('Failed to cancel run:', error);
281
- return {
282
- success: false,
283
- error: createServerActionError(error, 'world.runs.cancel', { runId }),
284
- };
598
+ return createServerActionError(error, 'world.events.create', {
599
+ runId,
600
+ });
285
601
  }
286
602
  }
287
603
  /**
@@ -289,40 +605,257 @@ export async function cancelRun(worldEnv, runId) {
289
605
  *
290
606
  * This requires the ID of an existing run of which to re-use the deployment ID of.
291
607
  */
292
- export async function recreateRun(worldEnv, runId) {
608
+ export async function recreateRun(worldEnv, runId, deploymentId) {
293
609
  try {
294
- const world = getWorldFromEnv({ ...worldEnv });
610
+ const world = await getWorldFromEnv({ ...worldEnv });
295
611
  const run = await world.runs.get(runId);
612
+ // Get original input/output
296
613
  const hydratedRun = hydrate(run);
297
- const deploymentId = run.deploymentId;
614
+ // Preserve original specVersion - if undefined (legacy v1), use SPEC_VERSION_LEGACY
298
615
  const newRun = await start({ workflowId: run.workflowName }, hydratedRun.input, {
299
- deploymentId,
616
+ deploymentId: deploymentId ?? run.deploymentId,
617
+ world,
618
+ specVersion: run.specVersion ?? SPEC_VERSION_LEGACY,
300
619
  });
301
620
  return createResponse(newRun.runId);
302
621
  }
303
622
  catch (error) {
304
- console.error('Failed to start run:', error);
305
- return {
306
- success: false,
307
- error: createServerActionError(error, 'recreateRun', { runId }),
308
- };
623
+ return createServerActionError(error, 'recreateRun', { runId });
624
+ }
625
+ }
626
+ /**
627
+ * Re-enqueue a workflow run.
628
+ *
629
+ * This re-enqueues the workflow orchestration layer. It's a no-op unless the workflow
630
+ * got stuck due to an implementation issue in the World. Useful for debugging custom Worlds.
631
+ */
632
+ export async function reenqueueRun(worldEnv, runId) {
633
+ try {
634
+ const world = await getWorldFromEnv({ ...worldEnv });
635
+ const run = await world.runs.get(runId);
636
+ const deploymentId = run.deploymentId;
637
+ await world.queue(`__wkf_workflow_${run.workflowName}`, {
638
+ runId,
639
+ }, {
640
+ deploymentId,
641
+ });
642
+ return createResponse(undefined);
643
+ }
644
+ catch (error) {
645
+ return createServerActionError(error, 'reenqueueRun', { runId });
646
+ }
647
+ }
648
+ /**
649
+ * Wake up a workflow run by interrupting pending sleep() calls.
650
+ *
651
+ * This finds wait_created events without matching wait_completed events,
652
+ * creates wait_completed events for them, and then re-enqueues the run.
653
+ *
654
+ * @param worldEnv - Environment configuration for the World
655
+ * @param runId - The run ID to wake up
656
+ * @param options - Optional settings to narrow down targeting (specific correlation IDs)
657
+ */
658
+ export async function wakeUpRun(worldEnv, runId, options) {
659
+ try {
660
+ const world = await getWorldFromEnv({ ...worldEnv });
661
+ const run = await world.runs.get(runId);
662
+ const deploymentId = run.deploymentId;
663
+ const compatMode = isLegacySpecVersion(run.specVersion);
664
+ // Fetch all events for the run
665
+ const eventsResult = await world.events.list({
666
+ runId,
667
+ pagination: { limit: 1000 },
668
+ resolveData: 'none',
669
+ });
670
+ // Find wait_created events without matching wait_completed events
671
+ const waitCreatedEvents = eventsResult.data.filter((e) => e.eventType === 'wait_created');
672
+ const waitCompletedCorrelationIds = new Set(eventsResult.data
673
+ .filter((e) => e.eventType === 'wait_completed')
674
+ .map((e) => e.correlationId));
675
+ let pendingWaits = waitCreatedEvents.filter((e) => !waitCompletedCorrelationIds.has(e.correlationId));
676
+ // If specific correlation IDs are provided, filter to only those
677
+ if (options?.correlationIds && options.correlationIds.length > 0) {
678
+ const targetCorrelationIds = new Set(options.correlationIds);
679
+ pendingWaits = pendingWaits.filter((e) => e.correlationId && targetCorrelationIds.has(e.correlationId));
680
+ }
681
+ // Create wait_completed events for each pending wait
682
+ for (const waitEvent of pendingWaits) {
683
+ if (waitEvent.correlationId) {
684
+ // For v2, include specVersion in event data; for v1Compat, it's not needed
685
+ const eventData = compatMode
686
+ ? {
687
+ eventType: 'wait_completed',
688
+ correlationId: waitEvent.correlationId,
689
+ }
690
+ : {
691
+ eventType: 'wait_completed',
692
+ correlationId: waitEvent.correlationId,
693
+ specVersion: run.specVersion,
694
+ };
695
+ await world.events.create(runId, eventData, { v1Compat: compatMode });
696
+ }
697
+ }
698
+ // Re-enqueue the run to wake it up
699
+ if (pendingWaits.length > 0) {
700
+ await world.queue(`__wkf_workflow_${run.workflowName}`, {
701
+ runId,
702
+ }, {
703
+ deploymentId,
704
+ });
705
+ }
706
+ return createResponse({ stoppedCount: pendingWaits.length });
707
+ }
708
+ catch (error) {
709
+ return createServerActionError(error, 'wakeUpRun', {
710
+ runId,
711
+ correlationIds: options?.correlationIds,
712
+ });
713
+ }
714
+ }
715
+ /**
716
+ * Resume a hook by sending a payload.
717
+ *
718
+ * This sends a payload to a hook identified by its token, which resumes
719
+ * the associated workflow run. The payload will be available as the return
720
+ * value of the `createHook()` call in the workflow.
721
+ *
722
+ * @param worldEnv - Environment configuration for the World
723
+ * @param token - The hook token
724
+ * @param payload - The JSON payload to send to the hook
725
+ */
726
+ export async function resumeHook(worldEnv, token, payload) {
727
+ try {
728
+ // Initialize the world so resumeHookRuntime can access it
729
+ await getWorldFromEnv({ ...worldEnv });
730
+ const hook = await resumeHookRuntime(token, payload);
731
+ return createResponse({
732
+ hookId: hook.hookId,
733
+ runId: hook.runId,
734
+ });
735
+ }
736
+ catch (error) {
737
+ return createServerActionError(error, 'resumeHook', {
738
+ token,
739
+ });
309
740
  }
310
741
  }
311
742
  export async function readStreamServerAction(env, streamId, startIndex) {
312
743
  try {
313
- const world = getWorldFromEnv(env);
744
+ const world = await getWorldFromEnv(env);
745
+ // We should probably use getRun().getReadable() instead, to make the UI
746
+ // more consistent with runtime behavior, and also expose a "replay" and "startIndex",
747
+ // feature, to allow for testing World behavior.
314
748
  const stream = await world.readFromStream(streamId, startIndex);
315
- return createResponse(stream);
749
+ const revivers = getExternalRevivers(globalThis, [], '');
750
+ const transform = getDeserializeStream(revivers);
751
+ return stream.pipeThrough(transform);
316
752
  }
317
753
  catch (error) {
318
- console.error('Failed to read stream:', error);
319
- return {
320
- success: false,
321
- error: createServerActionError(error, 'world.readFromStream', {
322
- streamId,
323
- startIndex,
324
- }),
325
- };
754
+ const actionError = createServerActionError(error, 'world.readFromStream', {
755
+ streamId,
756
+ startIndex,
757
+ });
758
+ if (!actionError.success) {
759
+ return actionError.error;
760
+ }
761
+ // Shouldn't happen, this is just a type guard
762
+ throw new Error();
763
+ }
764
+ }
765
+ /**
766
+ * List all stream IDs for a run
767
+ */
768
+ export async function fetchStreams(env, runId) {
769
+ try {
770
+ const world = await getWorldFromEnv(env);
771
+ const streams = await world.listStreamsByRunId(runId);
772
+ return createResponse(streams);
773
+ }
774
+ catch (error) {
775
+ return createServerActionError(error, 'world.listStreamsByRunId', {
776
+ runId,
777
+ });
778
+ }
779
+ }
780
+ /**
781
+ * Fetch the workflows manifest from the workflow route directory
782
+ * The manifest is generated at build time and contains static structure info about workflows
783
+ *
784
+ * Configuration priority:
785
+ * 1. WORKFLOW_MANIFEST_PATH - explicit path to the manifest file
786
+ * 2. Standard Next.js app router locations (app/.well-known/workflow/v1/manifest.json)
787
+ * 3. WORKFLOW_EMBEDDED_DATA_DIR - legacy data directory
788
+ */
789
+ export async function fetchWorkflowsManifest(_worldEnv) {
790
+ const cwd = getObservabilityCwd();
791
+ // Helper to resolve path (absolute or relative to cwd)
792
+ const resolvePath = (p) => path.isAbsolute(p) ? p : path.join(cwd, p);
793
+ // Build list of paths to try, in priority order
794
+ const manifestPaths = [];
795
+ // 1. Explicit manifest path configuration (highest priority)
796
+ if (process.env.WORKFLOW_MANIFEST_PATH) {
797
+ manifestPaths.push(resolvePath(process.env.WORKFLOW_MANIFEST_PATH));
798
+ }
799
+ // 2. Standard Next.js app router locations
800
+ manifestPaths.push(path.join(cwd, 'app/.well-known/workflow/v1/manifest.json'), path.join(cwd, 'src/app/.well-known/workflow/v1/manifest.json'));
801
+ // 3. Legacy data directory locations
802
+ if (process.env.WORKFLOW_EMBEDDED_DATA_DIR) {
803
+ manifestPaths.push(path.join(resolvePath(process.env.WORKFLOW_EMBEDDED_DATA_DIR), 'manifest.json'));
804
+ }
805
+ // Try each path until we find the manifest
806
+ for (const manifestPath of manifestPaths) {
807
+ try {
808
+ const content = await fs.readFile(manifestPath, 'utf-8');
809
+ const manifest = JSON.parse(content);
810
+ return createResponse(manifest);
811
+ }
812
+ catch (_err) {
813
+ // Continue to next path
814
+ }
815
+ }
816
+ // If no manifest found, return an empty manifest
817
+ // This allows the UI to work without workflows graph data
818
+ return createResponse({
819
+ version: '1.0.0',
820
+ steps: {},
821
+ workflows: {},
822
+ });
823
+ }
824
+ /**
825
+ * Run a queue-based health check on a workflow endpoint.
826
+ *
827
+ * This sends a health check message through the Queue infrastructure,
828
+ * bypassing Vercel Deployment Protection. The endpoint processes the
829
+ * message and writes a response to a stream, which we then read to
830
+ * verify the endpoint is healthy.
831
+ *
832
+ * @param worldEnv - Environment configuration for the World
833
+ * @param endpoint - Which endpoint to check: 'workflow' or 'step'
834
+ * @param options - Optional configuration (timeout in ms)
835
+ */
836
+ export async function runHealthCheck(worldEnv, endpoint, options) {
837
+ const startTime = Date.now();
838
+ try {
839
+ const world = await getWorldFromEnv(worldEnv);
840
+ const result = await healthCheck(world, endpoint, options);
841
+ const latencyMs = Date.now() - startTime;
842
+ return createResponse({
843
+ ...result,
844
+ latencyMs,
845
+ });
846
+ }
847
+ catch (error) {
848
+ const latencyMs = Date.now() - startTime;
849
+ // For health check failures, we want to return success=true with healthy=false
850
+ // so the UI can display the error properly, rather than propagating the server
851
+ // action error. This allows the health check result to be parsed by the UI
852
+ // even when the endpoint is down or unreachable.
853
+ const errorMessage = error instanceof Error ? error.message : String(error);
854
+ return createResponse({
855
+ healthy: false,
856
+ error: errorMessage,
857
+ latencyMs,
858
+ });
326
859
  }
327
860
  }
328
861
  //# sourceMappingURL=workflow-server-actions.js.map