@open-mercato/core 0.10.1-develop.7707.1.61845a3ef7 → 0.10.1-develop.7709.1.046124599c

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.
@@ -382,7 +382,7 @@ export async function enqueueActivity(
382
382
  workflowContext,
383
383
  stepContext,
384
384
  retryPolicy: activity.retryPolicy,
385
- timeoutMs: activity.timeoutMs,
385
+ timeoutMs: resolveActivityTimeoutMs(activity),
386
386
  tenantId: workflowInstance.tenantId,
387
387
  organizationId: workflowInstance.organizationId,
388
388
  userId: context.userId,
@@ -851,6 +851,7 @@ async function executeActivityByType(
851
851
  em: em as PostgreSqlEntityManager,
852
852
  container,
853
853
  signal,
854
+ timeoutMs: resolveActivityTimeoutMs(activity),
854
855
  })
855
856
  }
856
857
 
@@ -1215,6 +1216,38 @@ export type CallWebhookDeps = {
1215
1216
  allowPrivate?: boolean
1216
1217
  fetchImpl?: typeof fetch
1217
1218
  signal?: AbortSignal
1219
+ timeoutMs?: number
1220
+ }
1221
+
1222
+ const WEBHOOK_TIMEOUT_MS = 30_000
1223
+ const WEBHOOK_MAX_RESPONSE_BYTES = 1024 * 1024
1224
+
1225
+ async function readWebhookResponse(response: Response, signal: AbortSignal): Promise<string> {
1226
+ const declaredLength = Number(response.headers.get('content-length'))
1227
+ if (declaredLength > WEBHOOK_MAX_RESPONSE_BYTES) {
1228
+ throw new Error(`[internal] CALL_WEBHOOK response exceeds ${WEBHOOK_MAX_RESPONSE_BYTES} bytes`)
1229
+ }
1230
+ if (!response.body) return ''
1231
+
1232
+ const reader = response.body.getReader()
1233
+ const chunks: Uint8Array[] = []
1234
+ let totalBytes = 0
1235
+ try {
1236
+ for (;;) {
1237
+ throwIfAborted(signal)
1238
+ const { done, value } = await raceAbortable(reader.read(), signal)
1239
+ if (done) break
1240
+ totalBytes += value.byteLength
1241
+ if (totalBytes > WEBHOOK_MAX_RESPONSE_BYTES) {
1242
+ throw new Error(`[internal] CALL_WEBHOOK response exceeds ${WEBHOOK_MAX_RESPONSE_BYTES} bytes`)
1243
+ }
1244
+ chunks.push(value)
1245
+ }
1246
+ return new TextDecoder().decode(Buffer.concat(chunks, totalBytes))
1247
+ } finally {
1248
+ void reader.cancel().catch(() => undefined)
1249
+ reader.releaseLock()
1250
+ }
1218
1251
  }
1219
1252
 
1220
1253
  export async function executeCallWebhook(
@@ -1234,67 +1267,65 @@ export async function executeCallWebhook(
1234
1267
 
1235
1268
  const allowPrivate = deps.allowPrivate ?? isAllowPrivateWorkflowWebhookUrlsEnabled()
1236
1269
 
1237
- let response: Response
1238
- try {
1239
- response = await safeOutboundFetch(
1240
- url,
1241
- {
1242
- method,
1243
- headers: {
1244
- 'Content-Type': 'application/json',
1245
- ...headers,
1246
- },
1247
- body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
1248
- redirect: 'manual',
1249
- signal: deps.signal,
1250
- },
1251
- {
1252
- subject: 'Workflow webhook URL',
1253
- allowPrivate,
1254
- lookupHost: deps.lookupHost,
1255
- fetchImpl: deps.fetchImpl,
1256
- },
1257
- )
1258
- } catch (error) {
1259
- if (error instanceof UnsafeOutboundUrlError) {
1260
- throw new Error(
1261
- `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`
1262
- )
1263
- }
1264
- throw error
1265
- }
1266
-
1267
- if (response.status >= 300 && response.status < 400) {
1268
- const location = response.headers.get('location')
1269
- throw new Error(
1270
- `CALL_WEBHOOK refused to follow redirect ${response.status} to ${
1271
- location ?? '(no Location header)'
1272
- }`
1273
- )
1274
- }
1270
+ return executeWithTimeout(async (deadlineSignal) => {
1271
+ const signal = deps.signal
1272
+ ? AbortSignal.any([deadlineSignal, deps.signal])
1273
+ : deadlineSignal
1274
+ throwIfAborted(signal)
1275
1275
 
1276
- // Parse response
1277
- let result: any
1278
- const contentType = response.headers.get('content-type')
1276
+ let response: Response | undefined
1277
+ try {
1278
+ response = await raceAbortable(safeOutboundFetch(
1279
+ url,
1280
+ {
1281
+ method,
1282
+ headers: {
1283
+ 'Content-Type': 'application/json',
1284
+ ...headers,
1285
+ },
1286
+ body: body !== undefined && body !== null ? JSON.stringify(body) : undefined,
1287
+ redirect: 'manual',
1288
+ signal,
1289
+ },
1290
+ {
1291
+ subject: 'Workflow webhook URL',
1292
+ allowPrivate,
1293
+ lookupHost: deps.lookupHost,
1294
+ fetchImpl: deps.fetchImpl,
1295
+ },
1296
+ ), signal)
1297
+
1298
+ if (response.status >= 300 && response.status < 400) {
1299
+ const location = response.headers.get('location')
1300
+ throw new Error(
1301
+ `CALL_WEBHOOK refused to follow redirect ${response.status} to ${
1302
+ location ?? '(no Location header)'
1303
+ }`
1304
+ )
1305
+ }
1279
1306
 
1280
- if (contentType && contentType.includes('application/json')) {
1281
- result = await response.json()
1282
- } else {
1283
- result = await response.text()
1284
- }
1307
+ const text = await readWebhookResponse(response, signal)
1308
+ const contentType = response.headers.get('content-type')
1309
+ const result: unknown = contentType?.includes('application/json') ? JSON.parse(text) : text
1285
1310
 
1286
- // Check for HTTP errors
1287
- if (!response.ok) {
1288
- throw new Error(
1289
- `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`
1290
- )
1291
- }
1311
+ if (!response.ok) {
1312
+ throw new Error(
1313
+ `Webhook request failed with status ${response.status}: ${JSON.stringify(result)}`
1314
+ )
1315
+ }
1292
1316
 
1293
- return {
1294
- status: response.status,
1295
- statusText: response.statusText,
1296
- result,
1297
- }
1317
+ return { status: response.status, statusText: response.statusText, result }
1318
+ } catch (error) {
1319
+ if (error instanceof UnsafeOutboundUrlError) {
1320
+ throw new Error(
1321
+ `CALL_WEBHOOK rejected unsafe URL (reason=${error.reason}): ${error.message}`
1322
+ )
1323
+ }
1324
+ throw error
1325
+ } finally {
1326
+ void response?.body?.cancel().catch(() => undefined)
1327
+ }
1328
+ }, deps.timeoutMs && deps.timeoutMs > 0 ? deps.timeoutMs : WEBHOOK_TIMEOUT_MS)
1298
1329
  }
1299
1330
 
1300
1331
  /**
@@ -38,6 +38,7 @@ export interface ActivityExecuteDeps {
38
38
  em: EntityManager
39
39
  container: AwilixContainer
40
40
  signal?: AbortSignal
41
+ timeoutMs?: number
41
42
  }
42
43
 
43
44
  export interface ActivityTypeEntry<TConfig = unknown> {
@@ -301,7 +301,10 @@ export function registerBuiltinActivityTypes(): void {
301
301
  { id: 'headers', component: 'keyValue' },
302
302
  { id: 'body', component: 'json' },
303
303
  ],
304
- execute: async (config, ctx, deps) => (await loadExecutor()).executeCallWebhook(config, ctx, { signal: deps.signal }),
304
+ execute: async (config, ctx, deps) => (await loadExecutor()).executeCallWebhook(config, ctx, {
305
+ signal: deps.signal,
306
+ timeoutMs: deps.timeoutMs,
307
+ }),
305
308
  async: { capable: true },
306
309
  mock: (config) => {
307
310
  const record = asConfigRecord(config)
@@ -47,7 +47,7 @@ const INVOKE_AGENT_QUEUE_MAX_ATTEMPTS = 3
47
47
  * place, so callers never need their own side-effect imports.
48
48
  */
49
49
  export async function executeRegistryActivity(
50
- payload: Pick<WorkflowActivityJobActivity, 'activityType' | 'activityConfig'>,
50
+ payload: Pick<WorkflowActivityJobActivity, 'activityType' | 'activityConfig' | 'timeoutMs'>,
51
51
  activityContext: ActivityContext,
52
52
  deps: ActivityExecuteDeps
53
53
  ): Promise<unknown> {
@@ -61,7 +61,7 @@ export async function executeRegistryActivity(
61
61
  )
62
62
  }
63
63
  const runActivity = entry.executeAsync ?? entry.execute
64
- return await runActivity(payload.activityConfig, activityContext, deps)
64
+ return await runActivity(payload.activityConfig, activityContext, { ...deps, timeoutMs: payload.timeoutMs })
65
65
  }
66
66
 
67
67
  /**