@vertesia/workflow 1.4.0 → 1.5.0-dev.20260713.151751Z
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/activities/detectDocumentLanguage.d.ts +28 -0
- package/lib/activities/detectDocumentLanguage.d.ts.map +1 -0
- package/lib/activities/detectDocumentLanguage.js +58 -0
- package/lib/activities/detectDocumentLanguage.js.map +1 -0
- package/lib/activities/extractDocumentText.js +12 -1
- package/lib/activities/extractDocumentText.js.map +1 -1
- package/lib/activities/generateEmbeddings.d.ts +4 -0
- package/lib/activities/generateEmbeddings.d.ts.map +1 -1
- package/lib/activities/generateEmbeddings.js +43 -39
- package/lib/activities/generateEmbeddings.js.map +1 -1
- package/lib/activities/generateOrAssignContentType.d.ts +10 -0
- package/lib/activities/generateOrAssignContentType.d.ts.map +1 -1
- package/lib/activities/generateOrAssignContentType.js +40 -3
- package/lib/activities/generateOrAssignContentType.js.map +1 -1
- package/lib/activities/handleError.d.ts.map +1 -1
- package/lib/activities/handleError.js +1 -0
- package/lib/activities/handleError.js.map +1 -1
- package/lib/activities/index-dsl.d.ts +1 -0
- package/lib/activities/index-dsl.d.ts.map +1 -1
- package/lib/activities/index-dsl.js +1 -0
- package/lib/activities/index-dsl.js.map +1 -1
- package/lib/activities/media/prepareAudio.d.ts.map +1 -1
- package/lib/activities/media/prepareAudio.js +4 -1
- package/lib/activities/media/prepareAudio.js.map +1 -1
- package/lib/activities/media/prepareVideo.d.ts.map +1 -1
- package/lib/activities/media/prepareVideo.js +4 -1
- package/lib/activities/media/prepareVideo.js.map +1 -1
- package/lib/activities/notifyWebhook.d.ts +2 -0
- package/lib/activities/notifyWebhook.d.ts.map +1 -1
- package/lib/activities/notifyWebhook.js +82 -12
- package/lib/activities/notifyWebhook.js.map +1 -1
- package/lib/bulk-import.d.ts +1 -0
- package/lib/bulk-import.d.ts.map +1 -1
- package/lib/conversion/TextractProcessor.js +4 -4
- package/lib/conversion/TextractProcessor.js.map +1 -1
- package/lib/conversion/image.d.ts.map +1 -1
- package/lib/conversion/image.js +2 -0
- package/lib/conversion/image.js.map +1 -1
- package/lib/dsl/setup/fetch/DataProvider.js +3 -3
- package/lib/dsl/setup/fetch/DataProvider.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/system/notifyWebhookWorkflow.d.ts +10 -3
- package/lib/system/notifyWebhookWorkflow.d.ts.map +1 -1
- package/lib/system/notifyWebhookWorkflow.js +19 -1
- package/lib/system/notifyWebhookWorkflow.js.map +1 -1
- package/lib/system/recalculateEmbeddingsWorkflow.d.ts +4 -0
- package/lib/system/recalculateEmbeddingsWorkflow.d.ts.map +1 -1
- package/lib/workflows-bundle.js +1925 -935
- package/package.json +17 -17
- package/src/activities/detectDocumentLanguage.ts +92 -0
- package/src/activities/extractDocumentText.ts +13 -1
- package/src/activities/generateEmbeddings.ts +44 -42
- package/src/activities/generateOrAssignContentType.ts +56 -3
- package/src/activities/handleError.ts +1 -0
- package/src/activities/index-dsl.ts +1 -0
- package/src/activities/media/prepareAudio.ts +4 -1
- package/src/activities/media/prepareVideo.ts +4 -1
- package/src/activities/media/probeMediaStreams.test.ts +1 -1
- package/src/activities/media/saveGladiaTranscription.test.ts +2 -1
- package/src/activities/media/transcribeMediaWithGladia.test.ts +1 -1
- package/src/activities/notifyWebhook.test.ts +119 -0
- package/src/activities/notifyWebhook.ts +86 -12
- package/src/bulk-import.ts +1 -0
- package/src/conversion/TextractProcessor.ts +4 -4
- package/src/conversion/image.test.ts +24 -0
- package/src/conversion/image.ts +3 -0
- package/src/dsl/setup/fetch/DataProvider.ts +3 -3
- package/src/index.ts +1 -0
- package/src/system/notifyWebhookWorkflow.ts +31 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { log } from '@temporalio/activity';
|
|
1
|
+
import { ApplicationFailure, log } from '@temporalio/activity';
|
|
2
2
|
import { VertesiaClient } from '@vertesia/client';
|
|
3
3
|
import {
|
|
4
4
|
ApiVersions,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '@vertesia/common';
|
|
10
10
|
import { setupActivity } from '../dsl/setup/ActivityContext.js';
|
|
11
11
|
import { WorkflowParamNotFoundError } from '../errors.js';
|
|
12
|
+
import { safeFetch, URLValidationError } from '../security/ssrf.js';
|
|
12
13
|
import { getVertesiaClientOptions } from '../utils/client.js';
|
|
13
14
|
|
|
14
15
|
export interface NotifyWebhookParams {
|
|
@@ -18,9 +19,11 @@ export interface NotifyWebhookParams {
|
|
|
18
19
|
workflow_run_id: string; //The ID of the specific workflow run sending the notification
|
|
19
20
|
event_name: string; //The event that triggered the notification (e.g. "completed", "failed", etc.)
|
|
20
21
|
detail?: Record<string, unknown>; // additional data about the event if any. It will be send to the webhook when using POST
|
|
22
|
+
body?: string; // raw request body. When set, it bypasses workflow notification payload formatting.
|
|
21
23
|
//target_url: string; //URL to send the notification to
|
|
22
24
|
method: 'GET' | 'POST'; //HTTP method to use
|
|
23
25
|
headers?: Record<string, string>; // additional headers to send
|
|
26
|
+
timeout_ms?: number;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface WebhookNotificationPayload {
|
|
@@ -41,10 +44,28 @@ export interface NotifyWebhookResult {
|
|
|
41
44
|
url: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
function validationStatusCode(error: unknown): number | undefined {
|
|
48
|
+
if (typeof error !== 'object' || error === null) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
const record = error as { statusCode?: unknown; status?: unknown; code?: unknown };
|
|
52
|
+
const status = record.statusCode ?? record.status ?? record.code;
|
|
53
|
+
return typeof status === 'number' ? status : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isNonRetryableUrlValidationError(error: unknown): boolean {
|
|
57
|
+
const status = validationStatusCode(error);
|
|
58
|
+
if (status !== undefined) {
|
|
59
|
+
return status >= 400 && status < 500;
|
|
60
|
+
}
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
return /blocked|not allowed|forbidden|invalid url|internal hosts|private network|metadata/i.test(message);
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
export async function notifyWebhook(
|
|
45
66
|
payload: DSLActivityExecutionPayload<NotifyWebhookParams>,
|
|
46
67
|
): Promise<NotifyWebhookResult> {
|
|
47
|
-
const { params } = await setupActivity<NotifyWebhookParams>(payload);
|
|
68
|
+
const { params, client } = await setupActivity<NotifyWebhookParams>(payload);
|
|
48
69
|
const { webhook, method, headers: defaultHeaders } = params;
|
|
49
70
|
// resolve the url and the api version of the webhook
|
|
50
71
|
let target_url: string, version: number | undefined;
|
|
@@ -57,25 +78,73 @@ export async function notifyWebhook(
|
|
|
57
78
|
|
|
58
79
|
if (!target_url) throw new WorkflowParamNotFoundError('target_url');
|
|
59
80
|
|
|
81
|
+
try {
|
|
82
|
+
await client.apps.validateUrl(target_url);
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
if (!isNonRetryableUrlValidationError(err)) {
|
|
86
|
+
log.warn('URL validation preflight failed; retrying webhook notification', {
|
|
87
|
+
url: target_url,
|
|
88
|
+
workflow_id: params.workflow_id,
|
|
89
|
+
workflow_run_id: params.workflow_run_id,
|
|
90
|
+
error: message,
|
|
91
|
+
});
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
log.warn('URL validation blocked webhook endpoint', {
|
|
95
|
+
url: target_url,
|
|
96
|
+
workflow_id: params.workflow_id,
|
|
97
|
+
workflow_run_id: params.workflow_run_id,
|
|
98
|
+
error: message,
|
|
99
|
+
});
|
|
100
|
+
throw ApplicationFailure.create({
|
|
101
|
+
message: `Webhook endpoint blocked: ${message}`,
|
|
102
|
+
nonRetryable: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
const hasBody = method === 'POST'; //body is sent only for POST, always includes workflow info
|
|
61
107
|
|
|
62
108
|
const headers = {
|
|
63
109
|
...defaultHeaders,
|
|
64
110
|
};
|
|
65
|
-
if (hasBody) {
|
|
111
|
+
if (hasBody && !hasHeader(headers, 'content-type')) {
|
|
66
112
|
headers['Content-Type'] = 'application/json';
|
|
67
113
|
}
|
|
68
|
-
const body = hasBody ? await createRequestBody(payload, params, version) : undefined;
|
|
114
|
+
const body = params.body ?? (hasBody ? await createRequestBody(payload, params, version) : undefined);
|
|
69
115
|
|
|
70
116
|
log.info(`Notifying webhook at ${target_url}`);
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
117
|
+
const timeoutMs = typeof params.timeout_ms === 'number' && params.timeout_ms > 0 ? params.timeout_ms : undefined;
|
|
118
|
+
const controller = timeoutMs ? new AbortController() : undefined;
|
|
119
|
+
const timeout = timeoutMs ? setTimeout(() => controller?.abort(), timeoutMs) : undefined;
|
|
120
|
+
let res: Response;
|
|
121
|
+
try {
|
|
122
|
+
res = await safeFetch(target_url, {
|
|
123
|
+
method,
|
|
124
|
+
body,
|
|
125
|
+
headers,
|
|
126
|
+
...(controller ? { signal: controller.signal } : {}),
|
|
127
|
+
}).catch((err: unknown) => {
|
|
128
|
+
if (err instanceof URLValidationError) {
|
|
129
|
+
log.warn('Redirect blocked on webhook endpoint', {
|
|
130
|
+
url: target_url,
|
|
131
|
+
workflow_id: params.workflow_id,
|
|
132
|
+
workflow_run_id: params.workflow_run_id,
|
|
133
|
+
error: err.message,
|
|
134
|
+
});
|
|
135
|
+
throw ApplicationFailure.create({
|
|
136
|
+
message: `Webhook endpoint blocked: ${err.message}`,
|
|
137
|
+
nonRetryable: true,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
log.error(`An error occurred while notifying webhook at ${target_url}`, { err });
|
|
141
|
+
throw err;
|
|
142
|
+
});
|
|
143
|
+
} finally {
|
|
144
|
+
if (timeout) {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
79
148
|
|
|
80
149
|
if (!res.ok) {
|
|
81
150
|
log.warn(`Webhook endpoint ${target_url} returned an error - ${res.status} ${res.statusText}`, {
|
|
@@ -100,6 +169,11 @@ export async function notifyWebhook(
|
|
|
100
169
|
return { status: res.status, message: res.statusText, url: res.url };
|
|
101
170
|
}
|
|
102
171
|
|
|
172
|
+
function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
173
|
+
const lowerName = name.toLowerCase();
|
|
174
|
+
return Object.keys(headers).some((header) => header.toLowerCase() === lowerName);
|
|
175
|
+
}
|
|
176
|
+
|
|
103
177
|
// --------------------------------------
|
|
104
178
|
// Data provider for webhooks
|
|
105
179
|
// this allows to customize the payload sent to the webhook depending on the
|
package/src/bulk-import.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface BulkImportParams {
|
|
|
93
93
|
partitionSize?: number;
|
|
94
94
|
dryRun?: boolean;
|
|
95
95
|
updateByContentSource?: boolean;
|
|
96
|
+
/** @deprecated Events are now always emitted. This suppresses the Temporal-backed delivery targets (workflow, agent, and process) — webhook deliveries still fire. */
|
|
96
97
|
skipWorkflows?: boolean;
|
|
97
98
|
/**
|
|
98
99
|
* Processing priority for the document-processing workflows triggered by created objects.
|
|
@@ -136,7 +136,7 @@ export class TextractProcessor {
|
|
|
136
136
|
for (const cellRel of cell.Relationships) {
|
|
137
137
|
if (
|
|
138
138
|
cellRel.Type === 'CHILD' &&
|
|
139
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
139
|
+
// biome-ignore lint/style/noNonNullAssertion: a Textract word block always has an Id; the SDK type marks Block.Id optional and Array.includes requires a string
|
|
140
140
|
cellRel.Ids?.includes(wordBlock.Id!)
|
|
141
141
|
) {
|
|
142
142
|
return true;
|
|
@@ -256,14 +256,14 @@ export class TextractProcessor {
|
|
|
256
256
|
FeatureTypes: ['TABLES'],
|
|
257
257
|
});
|
|
258
258
|
const response = await this.textractClient.send(command);
|
|
259
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
259
|
+
// biome-ignore lint/style/noNonNullAssertion: a successful StartDocumentAnalysis response always includes JobId; the SDK type marks it optional
|
|
260
260
|
return response.JobId!;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
async checkJobStatus(jobId: string): Promise<string> {
|
|
264
264
|
const command = new GetDocumentAnalysisCommand({ JobId: jobId });
|
|
265
265
|
const response = await this.textractClient.send(command);
|
|
266
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
266
|
+
// biome-ignore lint/style/noNonNullAssertion: a GetDocumentAnalysis response always includes JobStatus; the SDK type marks it optional
|
|
267
267
|
return response.JobStatus!;
|
|
268
268
|
}
|
|
269
269
|
|
|
@@ -342,7 +342,7 @@ export class TextractProcessor {
|
|
|
342
342
|
// Create blocks map
|
|
343
343
|
const blocksMap: BlocksMap = {};
|
|
344
344
|
for (const block of allBlocks) {
|
|
345
|
-
// biome-ignore lint/style/noNonNullAssertion:
|
|
345
|
+
// biome-ignore lint/style/noNonNullAssertion: every Textract block carries an Id used as the map key; the SDK type marks Block.Id optional
|
|
346
346
|
blocksMap[block.Id!] = block;
|
|
347
347
|
}
|
|
348
348
|
|
|
@@ -116,4 +116,28 @@ describe('ImageMagick image resizing', () => {
|
|
|
116
116
|
// Check that interlace is disabled (should be 'none' or empty string)
|
|
117
117
|
expect(['none', '']).to.include(interlaceMode);
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
test('should apply EXIF orientation before resizing and stripping metadata', async () => {
|
|
121
|
+
const max_hw = 100;
|
|
122
|
+
const format = 'jpeg';
|
|
123
|
+
const inputImagePath = path.join(__dirname, '../../fixtures', 'synthetic-exif-righttop.jpg');
|
|
124
|
+
|
|
125
|
+
// Generated from synthetic pixels with only EXIF Orientation=6 added.
|
|
126
|
+
const { stdout: sourceStdout } = await execAsync(`identify -format "%w %h %[orientation]" "${inputImagePath}"`);
|
|
127
|
+
const [sourceWidth, sourceHeight, sourceOrientation] = sourceStdout.trim().split(' ');
|
|
128
|
+
expect(parseInt(sourceWidth, 10)).to.equal(120);
|
|
129
|
+
expect(parseInt(sourceHeight, 10)).to.equal(180);
|
|
130
|
+
expect(sourceOrientation).to.equal('RightTop');
|
|
131
|
+
|
|
132
|
+
const resizedImagePath = await imageResizer(inputImagePath, max_hw, format);
|
|
133
|
+
|
|
134
|
+
expect(fs.existsSync(resizedImagePath)).toBe(true);
|
|
135
|
+
|
|
136
|
+
const { stdout } = await execAsync(`identify -format "%w %h %[orientation]" "${resizedImagePath}"`);
|
|
137
|
+
const [width, height, orientation] = stdout.trim().split(' ');
|
|
138
|
+
|
|
139
|
+
expect(parseInt(width, 10)).to.equal(100);
|
|
140
|
+
expect(parseInt(height, 10)).to.equal(67);
|
|
141
|
+
expect(['Undefined', 'TopLeft']).to.include(orientation);
|
|
142
|
+
});
|
|
119
143
|
});
|
package/src/conversion/image.ts
CHANGED
|
@@ -84,6 +84,9 @@ export async function imageResizer(
|
|
|
84
84
|
// Add input after JPEG shrink-on-load optimization so ImageMagick can apply it while decoding.
|
|
85
85
|
args.push(inputPath);
|
|
86
86
|
|
|
87
|
+
// Apply EXIF orientation to pixels before stripping metadata or resizing.
|
|
88
|
+
args.push('-auto-orient');
|
|
89
|
+
|
|
87
90
|
// Remove metadata
|
|
88
91
|
args.push('-strip');
|
|
89
92
|
|
|
@@ -38,9 +38,9 @@ export abstract class DataProvider {
|
|
|
38
38
|
) {}
|
|
39
39
|
async fetch(payload: FindPayload) {
|
|
40
40
|
let results = await this.doFetch(payload);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
results = results.map((result: Record<string, unknown>) => applyProjection(result,
|
|
41
|
+
const select = payload.select;
|
|
42
|
+
if (select && !this.isProjectionSupported) {
|
|
43
|
+
results = results.map((result: Record<string, unknown>) => applyProjection(result, select));
|
|
44
44
|
}
|
|
45
45
|
return results;
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './activities/advanced/createDocumentTypeFromInteractionRun.js';
|
|
|
10
10
|
export * from './activities/advanced/createOrUpdateDocumentFromInteractionRun.js';
|
|
11
11
|
export * from './activities/advanced/updateDocumentFromInteractionRun.js';
|
|
12
12
|
export * from './activities/chunkDocument.js';
|
|
13
|
+
export * from './activities/detectDocumentLanguage.js';
|
|
13
14
|
export * from './activities/executeInteraction.js';
|
|
14
15
|
export * from './activities/extractDocumentText.js';
|
|
15
16
|
export * from './activities/generateDocumentProperties.js';
|
|
@@ -16,9 +16,17 @@ const { notifyWebhook } = dslProxyActivities<typeof activities>('notifyWebhookWo
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export interface NotifyWebhookWorfklowParams {
|
|
19
|
-
workflow_type
|
|
20
|
-
endpoints
|
|
21
|
-
data
|
|
19
|
+
workflow_type?: string;
|
|
20
|
+
endpoints?: (string | WebHookSpec)[];
|
|
21
|
+
data?: Record<string, unknown>;
|
|
22
|
+
requests?: NotifyWebhookRequest[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NotifyWebhookRequest {
|
|
26
|
+
url: string;
|
|
27
|
+
body: string;
|
|
28
|
+
headers?: Record<string, string>;
|
|
29
|
+
timeout_ms?: number;
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
export async function notifyWebhookWorkflow(
|
|
@@ -32,6 +40,7 @@ export async function notifyWebhookWorkflow(
|
|
|
32
40
|
webhook_data?: Record<string, unknown>;
|
|
33
41
|
};
|
|
34
42
|
const endpoints = legacyVars.endpoints ?? legacyVars.webhooks ?? [];
|
|
43
|
+
const requests = legacyVars.requests ?? [];
|
|
35
44
|
const data = legacyVars.data ?? legacyVars.webhook_data ?? undefined;
|
|
36
45
|
const workflow_type = vars.workflow_type ?? info.workflowType;
|
|
37
46
|
const eventName = payload.event;
|
|
@@ -39,11 +48,29 @@ export async function notifyWebhookWorkflow(
|
|
|
39
48
|
const workflowId = info.parent?.workflowId || info.workflowId;
|
|
40
49
|
const workflowRunId = info.parent?.runId || info.runId;
|
|
41
50
|
|
|
42
|
-
if (!endpoints.length) {
|
|
51
|
+
if (!endpoints.length && !requests.length) {
|
|
43
52
|
log.info(`No webhooks to notify`);
|
|
44
53
|
return { notifications: [], message: 'No webhooks to notify' };
|
|
45
54
|
}
|
|
46
55
|
|
|
56
|
+
for (const request of requests) {
|
|
57
|
+
const n = notifyWebhook(payload, {
|
|
58
|
+
webhook: request.url,
|
|
59
|
+
method: 'POST',
|
|
60
|
+
workflow_type,
|
|
61
|
+
workflow_id: workflowId,
|
|
62
|
+
workflow_run_id: workflowRunId,
|
|
63
|
+
event_name: eventName,
|
|
64
|
+
body: request.body,
|
|
65
|
+
headers: request.headers,
|
|
66
|
+
timeout_ms: request.timeout_ms,
|
|
67
|
+
}).then((res) => {
|
|
68
|
+
log.info(`Webhook notified at ${request.url} with response code: ${res.status}`, { res });
|
|
69
|
+
return res;
|
|
70
|
+
});
|
|
71
|
+
notifications.push(n);
|
|
72
|
+
}
|
|
73
|
+
|
|
47
74
|
for (const ep of endpoints) {
|
|
48
75
|
const n = notifyWebhook(payload, {
|
|
49
76
|
webhook: ep,
|