@vertesia/workflow 1.0.0 → 1.1.0-dev.20260427.060440Z
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/lib/cjs/activities/executeRemoteActivity.js +21 -2
- package/lib/cjs/activities/executeRemoteActivity.js.map +1 -1
- package/lib/cjs/activities/generateEmbeddings.js +5 -3
- package/lib/cjs/activities/generateEmbeddings.js.map +1 -1
- package/lib/cjs/activities/handleError.js +8 -1
- package/lib/cjs/activities/handleError.js.map +1 -1
- package/lib/cjs/activities/index-dsl.js +5 -1
- package/lib/cjs/activities/index-dsl.js.map +1 -1
- package/lib/cjs/activities/loadChildWorkflowSpec.js +15 -0
- package/lib/cjs/activities/loadChildWorkflowSpec.js.map +1 -0
- package/lib/cjs/activities/media/prepareAudio.js +3 -2
- package/lib/cjs/activities/media/prepareAudio.js.map +1 -1
- package/lib/cjs/activities/media/prepareVideo.js +4 -1
- package/lib/cjs/activities/media/prepareVideo.js.map +1 -1
- package/lib/cjs/activities/media/probeMediaStreams.js +49 -0
- package/lib/cjs/activities/media/probeMediaStreams.js.map +1 -0
- package/lib/cjs/activities/resolveRemoteActivities.js +29 -16
- package/lib/cjs/activities/resolveRemoteActivities.js.map +1 -1
- package/lib/cjs/dsl/dsl-workflow.js +22 -4
- package/lib/cjs/dsl/dsl-workflow.js.map +1 -1
- package/lib/cjs/dsl/setup/ActivityContext.js +12 -2
- package/lib/cjs/dsl/setup/ActivityContext.js.map +1 -1
- package/lib/cjs/security/ssrf.js +34 -0
- package/lib/cjs/security/ssrf.js.map +1 -0
- package/lib/esm/activities/executeRemoteActivity.js +21 -2
- package/lib/esm/activities/executeRemoteActivity.js.map +1 -1
- package/lib/esm/activities/generateEmbeddings.js +5 -3
- package/lib/esm/activities/generateEmbeddings.js.map +1 -1
- package/lib/esm/activities/handleError.js +8 -1
- package/lib/esm/activities/handleError.js.map +1 -1
- package/lib/esm/activities/index-dsl.js +2 -0
- package/lib/esm/activities/index-dsl.js.map +1 -1
- package/lib/esm/activities/loadChildWorkflowSpec.js +12 -0
- package/lib/esm/activities/loadChildWorkflowSpec.js.map +1 -0
- package/lib/esm/activities/media/prepareAudio.js +3 -2
- package/lib/esm/activities/media/prepareAudio.js.map +1 -1
- package/lib/esm/activities/media/prepareVideo.js +5 -2
- package/lib/esm/activities/media/prepareVideo.js.map +1 -1
- package/lib/esm/activities/media/probeMediaStreams.js +46 -0
- package/lib/esm/activities/media/probeMediaStreams.js.map +1 -0
- package/lib/esm/activities/resolveRemoteActivities.js +29 -16
- package/lib/esm/activities/resolveRemoteActivities.js.map +1 -1
- package/lib/esm/dsl/dsl-workflow.js +22 -4
- package/lib/esm/dsl/dsl-workflow.js.map +1 -1
- package/lib/esm/dsl/setup/ActivityContext.js +12 -2
- package/lib/esm/dsl/setup/ActivityContext.js.map +1 -1
- package/lib/esm/security/ssrf.js +29 -0
- package/lib/esm/security/ssrf.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/types/activities/executeRemoteActivity.d.ts.map +1 -1
- package/lib/types/activities/generateEmbeddings.d.ts +6 -4
- package/lib/types/activities/generateEmbeddings.d.ts.map +1 -1
- package/lib/types/activities/handleError.d.ts.map +1 -1
- package/lib/types/activities/index-dsl.d.ts +3 -0
- package/lib/types/activities/index-dsl.d.ts.map +1 -1
- package/lib/types/activities/loadChildWorkflowSpec.d.ts +6 -0
- package/lib/types/activities/loadChildWorkflowSpec.d.ts.map +1 -0
- package/lib/types/activities/media/prepareAudio.d.ts.map +1 -1
- package/lib/types/activities/media/prepareVideo.d.ts.map +1 -1
- package/lib/types/activities/media/probeMediaStreams.d.ts +12 -0
- package/lib/types/activities/media/probeMediaStreams.d.ts.map +1 -0
- package/lib/types/activities/resolveRemoteActivities.d.ts.map +1 -1
- package/lib/types/dsl/dsl-workflow.d.ts.map +1 -1
- package/lib/types/dsl/setup/ActivityContext.d.ts.map +1 -1
- package/lib/types/security/ssrf.d.ts +18 -0
- package/lib/types/security/ssrf.d.ts.map +1 -0
- package/lib/types/system/recalculateEmbeddingsWorkflow.d.ts +6 -4
- package/lib/types/system/recalculateEmbeddingsWorkflow.d.ts.map +1 -1
- package/lib/workflows-bundle.js +352 -158
- package/package.json +7 -7
- package/src/activities/executeRemoteActivity.test.ts +8 -0
- package/src/activities/executeRemoteActivity.ts +21 -2
- package/src/activities/generateEmbeddings.ts +6 -3
- package/src/activities/handleError.ts +9 -1
- package/src/activities/index-dsl.ts +3 -0
- package/src/activities/loadChildWorkflowSpec.ts +21 -0
- package/src/activities/media/prepareAudio.ts +3 -2
- package/src/activities/media/prepareVideo.ts +5 -2
- package/src/activities/media/probeMediaStreams.test.ts +126 -0
- package/src/activities/media/probeMediaStreams.ts +81 -0
- package/src/activities/resolveRemoteActivities.test.ts +11 -10
- package/src/activities/resolveRemoteActivities.ts +31 -16
- package/src/dsl/dsl-workflow.ts +22 -4
- package/src/dsl/setup/ActivityContext.test.ts +57 -0
- package/src/dsl/setup/ActivityContext.ts +16 -2
- package/src/security/ssrf.ts +32 -0
package/src/dsl/dsl-workflow.ts
CHANGED
|
@@ -183,9 +183,9 @@ async function executeSteps(definition: DSLWorkflowSpec, payload: DSLWorkflowExe
|
|
|
183
183
|
if (stepType === 'workflow') {
|
|
184
184
|
const childWorkflowStep = step as DSLChildWorkflowStep;
|
|
185
185
|
if (childWorkflowStep.async) {
|
|
186
|
-
await startChildWorkflow(childWorkflowStep, payload, vars, basePayload.debug_mode);
|
|
186
|
+
await startChildWorkflow(childWorkflowStep, payload, vars, basePayload.debug_mode, defaultProxy, basePayload);
|
|
187
187
|
} else {
|
|
188
|
-
await executeChildWorkflow(childWorkflowStep, payload, vars, basePayload.debug_mode);
|
|
188
|
+
await executeChildWorkflow(childWorkflowStep, payload, vars, basePayload.debug_mode, defaultProxy, basePayload);
|
|
189
189
|
}
|
|
190
190
|
} else { // activity
|
|
191
191
|
await runActivity(step as DSLActivitySpec, basePayload, vars, defaultProxy, defaultOptions, remoteActivities);
|
|
@@ -236,11 +236,20 @@ async function handleError(originalError: any, basePayload: BaseActivityPayload,
|
|
|
236
236
|
throw originalError;
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
async function startChildWorkflow(step: DSLChildWorkflowStep, payload: DSLWorkflowExecutionPayload, vars: Vars, debug_mode?: boolean) {
|
|
239
|
+
async function startChildWorkflow(step: DSLChildWorkflowStep, payload: DSLWorkflowExecutionPayload, vars: Vars, debug_mode?: boolean, proxy?: ActivityInterfaceFor<UntypedActivities>, basePayload?: BaseActivityPayload) {
|
|
240
240
|
if (step.condition && !vars.match(step.condition)) {
|
|
241
241
|
log.info("Child workflow skipped: condition not satisfied", { workflow: step.name, condition: step.condition });
|
|
242
242
|
return;
|
|
243
243
|
}
|
|
244
|
+
if (step.name.startsWith('dsl:') && !step.spec && proxy && basePayload) {
|
|
245
|
+
const workflowName = step.name.slice(4);
|
|
246
|
+
const specPayload = dslActivityPayload(basePayload, { name: 'loadChildWorkflowSpec' } as DSLActivitySpec, { workflowName });
|
|
247
|
+
const spec = await proxy.loadChildWorkflowSpec(specPayload) as DSLWorkflowSpec;
|
|
248
|
+
const humanName = spec.name.replace(/([A-Z])/g, ' $1').trim();
|
|
249
|
+
const objectIds = getDocumentIds(payload);
|
|
250
|
+
const workflowId = objectIds.length > 0 ? `${humanName}:${objectIds[0]}` : humanName;
|
|
251
|
+
step = { ...step, name: 'dslWorkflow', spec, options: { ...step.options, workflowId } };
|
|
252
|
+
}
|
|
244
253
|
const resolvedVars = vars.resolve();
|
|
245
254
|
if (step.vars) {
|
|
246
255
|
// copy user vars (from step definition) to the resolved vars, resolving any expressions
|
|
@@ -274,11 +283,20 @@ async function startChildWorkflow(step: DSLChildWorkflowStep, payload: DSLWorkfl
|
|
|
274
283
|
}
|
|
275
284
|
}
|
|
276
285
|
|
|
277
|
-
async function executeChildWorkflow(step: DSLChildWorkflowStep, payload: DSLWorkflowExecutionPayload, vars: Vars, debug_mode?: boolean) {
|
|
286
|
+
async function executeChildWorkflow(step: DSLChildWorkflowStep, payload: DSLWorkflowExecutionPayload, vars: Vars, debug_mode?: boolean, proxy?: ActivityInterfaceFor<UntypedActivities>, basePayload?: BaseActivityPayload) {
|
|
278
287
|
if (step.condition && !vars.match(step.condition)) {
|
|
279
288
|
log.info("Child workflow skipped: condition not satisfied", { workflow: step.name, condition: step.condition });
|
|
280
289
|
return;
|
|
281
290
|
}
|
|
291
|
+
if (step.name.startsWith('dsl:') && !step.spec && proxy && basePayload) {
|
|
292
|
+
const workflowName = step.name.slice(4);
|
|
293
|
+
const specPayload = dslActivityPayload(basePayload, { name: 'loadChildWorkflowSpec' } as DSLActivitySpec, { workflowName });
|
|
294
|
+
const spec = await proxy.loadChildWorkflowSpec(specPayload) as DSLWorkflowSpec;
|
|
295
|
+
const humanName = spec.name.replace(/([A-Z])/g, ' $1').trim();
|
|
296
|
+
const objectIds = getDocumentIds(payload);
|
|
297
|
+
const workflowId = objectIds.length > 0 ? `${humanName}:${objectIds[0]}` : humanName;
|
|
298
|
+
step = { ...step, name: 'dslWorkflow', spec, options: { ...step.options, workflowId } };
|
|
299
|
+
}
|
|
282
300
|
const resolvedVars = vars.resolve();
|
|
283
301
|
if (step.vars) {
|
|
284
302
|
// copy user vars (from step definition) to the resolved vars, resolving any expressions
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { ContentEventName, DSLActivityExecutionPayload } from "@vertesia/common";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { setupActivity } from "./ActivityContext.js";
|
|
4
|
+
|
|
5
|
+
const MOCK_AUTH_TOKEN =
|
|
6
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwOi8vbW9jay10b2tlbi1zZXJ2ZXIiLCJzdWIiOiJ0ZXN0In0.signature";
|
|
7
|
+
|
|
8
|
+
function basePayload<T extends Record<string, any>>(
|
|
9
|
+
activity: DSLActivityExecutionPayload<T>["activity"],
|
|
10
|
+
params: T,
|
|
11
|
+
): DSLActivityExecutionPayload<T> {
|
|
12
|
+
return {
|
|
13
|
+
event: ContentEventName.create,
|
|
14
|
+
vars: {},
|
|
15
|
+
account_id: "acc",
|
|
16
|
+
project_id: "prj",
|
|
17
|
+
auth_token: MOCK_AUTH_TOKEN,
|
|
18
|
+
config: { studio_url: "http://localhost", store_url: "http://localhost" },
|
|
19
|
+
activity,
|
|
20
|
+
params,
|
|
21
|
+
workflow_name: "test",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("setupActivity", () => {
|
|
26
|
+
// Regression guard: user-supplied strings containing "${...}" must be preserved
|
|
27
|
+
// verbatim when the activity is dispatched from TypeScript code (no activity.params,
|
|
28
|
+
// no activity.fetch). Previously Vars.parse/resolve would treat any "${...}" as a
|
|
29
|
+
// template ref and silently drop the key when it failed to resolve.
|
|
30
|
+
it("preserves user data containing literal ${} in code-dispatched activities", async () => {
|
|
31
|
+
const data = {
|
|
32
|
+
task: 'preserve "${}" literal',
|
|
33
|
+
other: "also has ${undefined_ref}",
|
|
34
|
+
};
|
|
35
|
+
const payload = basePayload({ name: "testActivity" }, { data });
|
|
36
|
+
const ctx = await setupActivity(payload);
|
|
37
|
+
expect(ctx.params.data.task).toBe('preserve "${}" literal');
|
|
38
|
+
expect(ctx.params.data.other).toBe("also has ${undefined_ref}");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("returns empty params when code-dispatched activity has no params", async () => {
|
|
42
|
+
const payload = basePayload({ name: "testActivity" }, undefined as any);
|
|
43
|
+
const ctx = await setupActivity(payload);
|
|
44
|
+
expect(ctx.params).toEqual({});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Counter-test: DSL-defined activities (those carrying activity.params) must
|
|
48
|
+
// still have their ${refs} resolved against payload.params (imported vars).
|
|
49
|
+
it("still resolves ${refs} in activity.params for DSL-dispatched activities", async () => {
|
|
50
|
+
const payload = basePayload(
|
|
51
|
+
{ name: "dslActivity", params: { message: "Hello ${name}" } },
|
|
52
|
+
{ name: "World" } as any,
|
|
53
|
+
);
|
|
54
|
+
const ctx = await setupActivity(payload);
|
|
55
|
+
expect((ctx.params as any).message).toBe("Hello World");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -183,6 +183,22 @@ export class ActivityContext<ParamsT extends Record<string, any>> {
|
|
|
183
183
|
export async function setupActivity<ParamsT extends Record<string, any>>(
|
|
184
184
|
payload: DSLActivityExecutionPayload<ParamsT>,
|
|
185
185
|
) {
|
|
186
|
+
const client = await getVertesiaClient(payload);
|
|
187
|
+
|
|
188
|
+
// Activities dispatched from TypeScript code (via dslProxyActivities) set
|
|
189
|
+
// only `activity.name`; their params are a plain TS object that must not
|
|
190
|
+
// be run through Vars template expansion — doing so would interpret any
|
|
191
|
+
// `${...}` in user-supplied strings as a variable reference and silently
|
|
192
|
+
// drop the key when it fails to resolve. Only DSL-defined activities
|
|
193
|
+
// (which carry `activity.params` and/or `activity.fetch`) need Vars.
|
|
194
|
+
const isDslDispatched =
|
|
195
|
+
payload.activity.params !== undefined || payload.activity.fetch !== undefined;
|
|
196
|
+
|
|
197
|
+
if (!isDslDispatched) {
|
|
198
|
+
const params = (payload.params ?? {}) as ParamsT;
|
|
199
|
+
return new ActivityContext<ParamsT>(payload, client, params);
|
|
200
|
+
}
|
|
201
|
+
|
|
186
202
|
const isDebugMode = !!payload.debug_mode;
|
|
187
203
|
|
|
188
204
|
const vars = new Vars({
|
|
@@ -190,7 +206,6 @@ export async function setupActivity<ParamsT extends Record<string, any>>(
|
|
|
190
206
|
...payload.activity.params, // activity params (may contain expressions)
|
|
191
207
|
});
|
|
192
208
|
|
|
193
|
-
//}
|
|
194
209
|
if (isDebugMode) {
|
|
195
210
|
log.info(`Setting up activity ${payload.activity.name}`, {
|
|
196
211
|
config: payload.config,
|
|
@@ -200,7 +215,6 @@ export async function setupActivity<ParamsT extends Record<string, any>>(
|
|
|
200
215
|
});
|
|
201
216
|
}
|
|
202
217
|
|
|
203
|
-
const client = await getVertesiaClient(payload);
|
|
204
218
|
const fetchSpecs = payload.activity.fetch;
|
|
205
219
|
if (fetchSpecs) {
|
|
206
220
|
const keys = Object.keys(fetchSpecs);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSRF protection shim for @vertesia/workflow.
|
|
3
|
+
*
|
|
4
|
+
* URL validation is delegated via client.apps.validateUrl() so that security
|
|
5
|
+
* policy details (blocked IP ranges, ports, hostnames) are not exposed in this
|
|
6
|
+
* public package.
|
|
7
|
+
*
|
|
8
|
+
* This file only provides redirect-safe fetch and the error class.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export class URLValidationError extends Error {
|
|
12
|
+
constructor(message: string) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'URLValidationError';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Redirect-safe fetch wrapper. Does not validate the URL — call client.apps.validateUrl() first.
|
|
20
|
+
* @throws {URLValidationError} if the server redirects (potential redirect-based SSRF)
|
|
21
|
+
*/
|
|
22
|
+
export async function safeFetch(url: string, init: RequestInit = {}): Promise<Response> {
|
|
23
|
+
const response = await fetch(url, { ...init, redirect: 'manual' });
|
|
24
|
+
// Only block actual redirects (those with a Location header). 304 Not Modified is a 3xx
|
|
25
|
+
// but carries no Location and must not be treated as a redirect.
|
|
26
|
+
if (response.headers.has('location')) {
|
|
27
|
+
throw new URLValidationError(
|
|
28
|
+
`Request to ${url} returned a redirect (${response.status}), which is not allowed`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return response;
|
|
32
|
+
}
|