@open-mercato/core 0.6.6-develop.6429.1.2fba7258d1 → 0.6.6-develop.6450.1.b493fb430c

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 (87) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/api/people/[id]/route.js +73 -2
  3. package/dist/modules/customers/api/people/[id]/route.js.map +2 -2
  4. package/dist/modules/planner/commands/shared.js +1 -8
  5. package/dist/modules/planner/commands/shared.js.map +2 -2
  6. package/dist/modules/resources/commands/shared.js +1 -7
  7. package/dist/modules/resources/commands/shared.js.map +2 -2
  8. package/dist/modules/shipping_carriers/api/cancel/route.js +2 -2
  9. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  10. package/dist/modules/shipping_carriers/api/points/route.js +2 -2
  11. package/dist/modules/shipping_carriers/api/points/route.js.map +2 -2
  12. package/dist/modules/shipping_carriers/api/rates/route.js +2 -2
  13. package/dist/modules/shipping_carriers/api/rates/route.js.map +2 -2
  14. package/dist/modules/shipping_carriers/api/shipments/route.js +2 -2
  15. package/dist/modules/shipping_carriers/api/shipments/route.js.map +2 -2
  16. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js +2 -2
  17. package/dist/modules/shipping_carriers/api/tracking/refresh/route.js.map +2 -2
  18. package/dist/modules/shipping_carriers/api/tracking/route.js +2 -2
  19. package/dist/modules/shipping_carriers/api/tracking/route.js.map +2 -2
  20. package/dist/modules/shipping_carriers/lib/upstream-error-response.js +13 -0
  21. package/dist/modules/shipping_carriers/lib/upstream-error-response.js.map +7 -0
  22. package/dist/modules/staff/commands/activities.js +46 -8
  23. package/dist/modules/staff/commands/activities.js.map +2 -2
  24. package/dist/modules/staff/commands/addresses.js +40 -7
  25. package/dist/modules/staff/commands/addresses.js.map +2 -2
  26. package/dist/modules/staff/commands/comments.js +46 -8
  27. package/dist/modules/staff/commands/comments.js.map +2 -2
  28. package/dist/modules/staff/commands/job-histories.js +40 -7
  29. package/dist/modules/staff/commands/job-histories.js.map +2 -2
  30. package/dist/modules/staff/commands/leave-requests.js +31 -9
  31. package/dist/modules/staff/commands/leave-requests.js.map +2 -2
  32. package/dist/modules/staff/commands/shared.js +56 -7
  33. package/dist/modules/staff/commands/shared.js.map +2 -2
  34. package/dist/modules/staff/commands/tag-assignments.js +18 -7
  35. package/dist/modules/staff/commands/tag-assignments.js.map +2 -2
  36. package/dist/modules/staff/commands/team-members.js +16 -5
  37. package/dist/modules/staff/commands/team-members.js.map +2 -2
  38. package/dist/modules/staff/commands/team-roles.js +16 -5
  39. package/dist/modules/staff/commands/team-roles.js.map +2 -2
  40. package/dist/modules/staff/commands/teams.js +16 -5
  41. package/dist/modules/staff/commands/teams.js.map +2 -2
  42. package/dist/modules/staff/commands/timesheets-entries.js +17 -5
  43. package/dist/modules/staff/commands/timesheets-entries.js.map +2 -2
  44. package/dist/modules/staff/commands/timesheets-projects.js +22 -9
  45. package/dist/modules/staff/commands/timesheets-projects.js.map +2 -2
  46. package/dist/modules/sync_excel/lib/upload-storage.js +9 -6
  47. package/dist/modules/sync_excel/lib/upload-storage.js.map +2 -2
  48. package/dist/modules/translations/api/get/locales.js +2 -1
  49. package/dist/modules/translations/api/get/locales.js.map +2 -2
  50. package/dist/modules/translations/api/put/locales.js +1 -1
  51. package/dist/modules/translations/api/put/locales.js.map +2 -2
  52. package/dist/modules/workflows/examples/sales-pipeline-definition.json +8 -8
  53. package/dist/modules/workflows/lib/activity-executor.js +39 -3
  54. package/dist/modules/workflows/lib/activity-executor.js.map +2 -2
  55. package/package.json +7 -7
  56. package/src/modules/customers/api/people/[id]/route.ts +107 -2
  57. package/src/modules/planner/commands/shared.ts +2 -11
  58. package/src/modules/resources/commands/shared.ts +2 -10
  59. package/src/modules/shipping_carriers/api/cancel/route.ts +2 -2
  60. package/src/modules/shipping_carriers/api/points/route.ts +2 -2
  61. package/src/modules/shipping_carriers/api/rates/route.ts +2 -2
  62. package/src/modules/shipping_carriers/api/shipments/route.ts +2 -2
  63. package/src/modules/shipping_carriers/api/tracking/refresh/route.ts +2 -2
  64. package/src/modules/shipping_carriers/api/tracking/route.ts +2 -2
  65. package/src/modules/shipping_carriers/i18n/de.json +1 -0
  66. package/src/modules/shipping_carriers/i18n/en.json +1 -0
  67. package/src/modules/shipping_carriers/i18n/es.json +1 -0
  68. package/src/modules/shipping_carriers/i18n/pl.json +1 -0
  69. package/src/modules/shipping_carriers/lib/upstream-error-response.ts +11 -0
  70. package/src/modules/staff/commands/activities.ts +46 -8
  71. package/src/modules/staff/commands/addresses.ts +40 -7
  72. package/src/modules/staff/commands/comments.ts +46 -8
  73. package/src/modules/staff/commands/job-histories.ts +40 -7
  74. package/src/modules/staff/commands/leave-requests.ts +36 -9
  75. package/src/modules/staff/commands/shared.ts +71 -8
  76. package/src/modules/staff/commands/tag-assignments.ts +18 -7
  77. package/src/modules/staff/commands/team-members.ts +16 -5
  78. package/src/modules/staff/commands/team-roles.ts +16 -5
  79. package/src/modules/staff/commands/teams.ts +16 -5
  80. package/src/modules/staff/commands/timesheets-entries.ts +17 -5
  81. package/src/modules/staff/commands/timesheets-projects.ts +22 -9
  82. package/src/modules/sync_excel/lib/upload-storage.ts +8 -6
  83. package/src/modules/translations/api/get/locales.ts +1 -0
  84. package/src/modules/translations/api/put/locales.ts +1 -1
  85. package/src/modules/workflows/AGENTS.md +2 -1
  86. package/src/modules/workflows/examples/sales-pipeline-definition.json +8 -8
  87. package/src/modules/workflows/lib/activity-executor.ts +49 -5
@@ -22,7 +22,15 @@ import {
22
22
  type StaffTimeEntryUpdateInput,
23
23
  } from '../data/validators'
24
24
  import { staffTimeEntryCrudEvents } from '../lib/crud'
25
- import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
25
+ import {
26
+ applyScopeToWhere,
27
+ commandActorScope,
28
+ commandInputScope,
29
+ ensureOrganizationScope,
30
+ ensureTenantScope,
31
+ extractUndoPayload,
32
+ scopeForDecryption,
33
+ } from './shared'
26
34
  import { getStaffMemberByUserId } from '../lib/staffMemberResolver'
27
35
 
28
36
  type RbacServiceLike = {
@@ -174,6 +182,7 @@ const createTimeEntryCommand: CommandHandler<StaffTimeEntryCreateInput, { timeEn
174
182
  const parsed = staffTimeEntryCreateSchema.parse(rawInput)
175
183
  ensureTenantScope(ctx, parsed.tenantId)
176
184
  ensureOrganizationScope(ctx, parsed.organizationId)
185
+ commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
177
186
 
178
187
  const em = (ctx.container.resolve('em') as EntityManager).fork()
179
188
 
@@ -290,6 +299,7 @@ const startTimerCommand: CommandHandler<StaffTimeEntryStartTimerInput, { timeEnt
290
299
  const parsed = staffTimeEntryStartTimerSchema.parse(rawInput)
291
300
  ensureTenantScope(ctx, parsed.tenantId)
292
301
  ensureOrganizationScope(ctx, parsed.organizationId)
302
+ commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
293
303
 
294
304
  const em = (ctx.container.resolve('em') as EntityManager).fork()
295
305
 
@@ -457,12 +467,13 @@ const updateTimeEntryCommand: CommandHandler<StaffTimeEntryUpdateInput, { timeEn
457
467
  async execute(rawInput, ctx) {
458
468
  const parsed = staffTimeEntryUpdateSchema.parse(rawInput)
459
469
  const em = (ctx.container.resolve('em') as EntityManager).fork()
470
+ const scope = commandActorScope(ctx)
460
471
  const entry = await findOneWithDecryption(
461
472
  em,
462
473
  StaffTimeEntry,
463
- { id: parsed.id, deletedAt: null },
474
+ applyScopeToWhere<StaffTimeEntry>({ id: parsed.id, deletedAt: null }, scope),
464
475
  undefined,
465
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
476
+ scopeForDecryption(scope),
466
477
  )
467
478
  if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })
468
479
  ensureTenantScope(ctx, entry.tenantId)
@@ -583,12 +594,13 @@ const deleteTimeEntryCommand: CommandHandler<{ id?: string }, { timeEntryId: str
583
594
  const id = input?.id
584
595
  if (!id) throw new CrudHttpError(400, { error: 'Time entry id is required.' })
585
596
  const em = (ctx.container.resolve('em') as EntityManager).fork()
597
+ const scope = commandActorScope(ctx)
586
598
  const entry = await findOneWithDecryption(
587
599
  em,
588
600
  StaffTimeEntry,
589
- { id, deletedAt: null },
601
+ applyScopeToWhere<StaffTimeEntry>({ id, deletedAt: null }, scope),
590
602
  undefined,
591
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
603
+ scopeForDecryption(scope),
592
604
  )
593
605
  if (!entry) throw new CrudHttpError(404, { error: 'Time entry not found.' })
594
606
  ensureTenantScope(ctx, entry.tenantId)
@@ -27,7 +27,15 @@ import {
27
27
  type StaffTimeProjectMemberUpdateInput,
28
28
  } from '../data/validators'
29
29
  import { staffTimeProjectCrudEvents, staffTimeProjectMemberCrudEvents } from '../lib/crud'
30
- import { ensureOrganizationScope, ensureTenantScope, extractUndoPayload } from './shared'
30
+ import {
31
+ applyScopeToWhere,
32
+ commandActorScope,
33
+ commandInputScope,
34
+ ensureOrganizationScope,
35
+ ensureTenantScope,
36
+ extractUndoPayload,
37
+ scopeForDecryption,
38
+ } from './shared'
31
39
 
32
40
  function isUniqueViolation(error: unknown): boolean {
33
41
  if (error instanceof UniqueConstraintViolationException) return true
@@ -163,6 +171,7 @@ const createTimeProjectCommand: CommandHandler<StaffTimeProjectCreateInput, { ti
163
171
  const parsed = staffTimeProjectCreateSchema.parse(rawInput)
164
172
  ensureTenantScope(ctx, parsed.tenantId)
165
173
  ensureOrganizationScope(ctx, parsed.organizationId)
174
+ commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
166
175
 
167
176
  const em = (ctx.container.resolve('em') as EntityManager).fork()
168
177
  const now = new Date()
@@ -282,12 +291,13 @@ const updateTimeProjectCommand: CommandHandler<StaffTimeProjectUpdateInput, { ti
282
291
  async execute(rawInput, ctx) {
283
292
  const parsed = staffTimeProjectUpdateSchema.parse(rawInput)
284
293
  const em = (ctx.container.resolve('em') as EntityManager).fork()
294
+ const scope = commandActorScope(ctx)
285
295
  const project = await findOneWithDecryption(
286
296
  em,
287
297
  StaffTimeProject,
288
- { id: parsed.id, deletedAt: null },
298
+ applyScopeToWhere<StaffTimeProject>({ id: parsed.id, deletedAt: null }, scope),
289
299
  undefined,
290
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
300
+ scopeForDecryption(scope),
291
301
  )
292
302
  if (!project) throw new CrudHttpError(404, { error: 'Time project not found.' })
293
303
  ensureTenantScope(ctx, project.tenantId)
@@ -419,12 +429,13 @@ const deleteTimeProjectCommand: CommandHandler<{ id?: string }, { timeProjectId:
419
429
  const id = input?.id
420
430
  if (!id) throw new CrudHttpError(400, { error: 'Time project id is required.' })
421
431
  const em = (ctx.container.resolve('em') as EntityManager).fork()
432
+ const scope = commandActorScope(ctx)
422
433
  const project = await findOneWithDecryption(
423
434
  em,
424
435
  StaffTimeProject,
425
- { id, deletedAt: null },
436
+ applyScopeToWhere<StaffTimeProject>({ id, deletedAt: null }, scope),
426
437
  undefined,
427
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
438
+ scopeForDecryption(scope),
428
439
  )
429
440
  if (!project) throw new CrudHttpError(404, { error: 'Time project not found.' })
430
441
  ensureTenantScope(ctx, project.tenantId)
@@ -527,6 +538,7 @@ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssig
527
538
  const parsed = staffTimeProjectMemberAssignSchema.parse(rawInput)
528
539
  ensureTenantScope(ctx, parsed.tenantId)
529
540
  ensureOrganizationScope(ctx, parsed.organizationId)
541
+ const scope = commandInputScope(ctx, parsed.tenantId, parsed.organizationId)
530
542
 
531
543
  const em = (ctx.container.resolve('em') as EntityManager).fork()
532
544
 
@@ -534,7 +546,7 @@ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssig
534
546
  // Without this check a foreign or stale UUID would produce a dangling reference.
535
547
  const projectExists = await em.findOne(
536
548
  StaffTimeProject,
537
- { id: parsed.timeProjectId, tenantId: parsed.tenantId, organizationId: parsed.organizationId, deletedAt: null },
549
+ applyScopeToWhere<StaffTimeProject>({ id: parsed.timeProjectId, deletedAt: null }, scope),
538
550
  { fields: ['id'] },
539
551
  )
540
552
  if (!projectExists) {
@@ -548,7 +560,7 @@ const assignTimeProjectMemberCommand: CommandHandler<StaffTimeProjectMemberAssig
548
560
  }
549
561
  const memberExists = await em.findOne(
550
562
  StaffTeamMember,
551
- { id: parsed.staffMemberId, tenantId: parsed.tenantId, organizationId: parsed.organizationId, deletedAt: null },
563
+ applyScopeToWhere<StaffTeamMember>({ id: parsed.staffMemberId, deletedAt: null }, scope),
552
564
  { fields: ['id'] },
553
565
  )
554
566
  if (!memberExists) {
@@ -658,12 +670,13 @@ const unassignTimeProjectMemberCommand: CommandHandler<{ id?: string }, { timePr
658
670
  const id = input?.id
659
671
  if (!id) throw new CrudHttpError(400, { error: 'Time project member id is required.' })
660
672
  const em = (ctx.container.resolve('em') as EntityManager).fork()
673
+ const scope = commandActorScope(ctx)
661
674
  const member = await findOneWithDecryption(
662
675
  em,
663
676
  StaffTimeProjectMember,
664
- { id, deletedAt: null },
677
+ applyScopeToWhere<StaffTimeProjectMember>({ id, deletedAt: null }, scope),
665
678
  undefined,
666
- { tenantId: ctx.auth?.tenantId ?? null, organizationId: ctx.auth?.orgId ?? null },
679
+ scopeForDecryption(scope),
667
680
  )
668
681
  if (!member) throw new CrudHttpError(404, { error: 'Time project member not found.' })
669
682
  ensureTenantScope(ctx, member.tenantId)
@@ -52,7 +52,6 @@ export async function createSyncExcelUploadAttachment(input: {
52
52
  module: 'sync_excel',
53
53
  temporary: true,
54
54
  uploadId: input.uploadId,
55
- inlineCsvBase64: input.buffer.toString('base64'),
56
55
  },
57
56
  })
58
57
 
@@ -66,14 +65,17 @@ export async function readSyncExcelUploadBuffer(
66
65
  attachment: Pick<Attachment, 'partitionCode' | 'storagePath' | 'storageDriver' | 'storageMetadata'>,
67
66
  ): Promise<Buffer> {
68
67
  const inlineCsvBase64 = attachment.storageMetadata?.inlineCsvBase64
69
- if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {
70
- return Buffer.from(inlineCsvBase64, 'base64')
71
- }
72
-
73
68
  const absolutePath = resolveAttachmentAbsolutePath(
74
69
  attachment.partitionCode,
75
70
  attachment.storagePath,
76
71
  attachment.storageDriver,
77
72
  )
78
- return fs.readFile(absolutePath)
73
+ try {
74
+ return await fs.readFile(absolutePath)
75
+ } catch (error) {
76
+ if (typeof inlineCsvBase64 === 'string' && inlineCsvBase64.length > 0) {
77
+ return Buffer.from(inlineCsvBase64, 'base64')
78
+ }
79
+ throw error
80
+ }
79
81
  }
@@ -18,6 +18,7 @@ async function GET(req: Request) {
18
18
  const configService = context.container.resolve('moduleConfigService') as ModuleConfigService
19
19
  const locales = await configService.getValue<string[]>('translations', 'supported_locales', {
20
20
  defaultValue: [...defaultLocales],
21
+ scope: { tenantId: context.tenantId },
21
22
  })
22
23
 
23
24
  return NextResponse.json({ locales: Array.isArray(locales) ? locales : [...defaultLocales] })
@@ -44,7 +44,7 @@ async function PUT(req: Request) {
44
44
  }
45
45
 
46
46
  const configService = context.container.resolve('moduleConfigService') as ModuleConfigService
47
- await configService.setValue('translations', 'supported_locales', uniqueLocales)
47
+ await configService.setValue('translations', 'supported_locales', uniqueLocales, { tenantId: context.tenantId })
48
48
 
49
49
  if (guardResult?.ok && guardResult.shouldRunAfterSuccess) {
50
50
  await runCrudMutationGuardAfterSuccess(context.container, {
@@ -10,7 +10,7 @@ Use the workflows module for business process automation: defining step-based wo
10
10
  4. **MUST follow the instance state machine** — instances transition `RUNNING → COMPLETED|FAILED|CANCELLED`; intermediate states include `PAUSED`, `WAITING_FOR_ACTIVITIES`, `COMPENSATING`
11
11
  5. **MUST keep activity handlers idempotent** — check state before mutating; activities may be retried on failure
12
12
  6. **MUST use event sourcing** — log all workflow events via `eventLogger.logWorkflowEvent()`; never mutate instance state without a corresponding event
13
- 7. **MUST use variable interpolation** for dynamic activity config — use `{{context.*}}`, `{{workflow.*}}`, `{{env.*}}`, `{{now}}`; never hardcode values
13
+ 7. **MUST use variable interpolation** for dynamic activity config — use `{{context.*}}`, `{{workflow.*}}`, server-allowlisted non-secret `{{env.*}}` keys such as `{{env.APP_URL}}`, and `{{now}}`; never hardcode values or read secrets from `{{env.*}}`
14
14
  8. **MUST use event triggers, signals, and widget injection** for cross-module integration.
15
15
  9. **MUST declare new events in `events.ts`** with `as const` — undeclared events trigger TypeScript errors and runtime warnings
16
16
  10. **MUST scope all queries by `organization_id`** — workflow data is tenant-scoped; never expose cross-tenant instances or tasks
@@ -99,6 +99,7 @@ Definition → startWorkflow() → Instance → executeWorkflow() loop
99
99
  | Variable | Effect | Default |
100
100
  |----------|--------|---------|
101
101
  | `OM_WORKFLOWS_ALLOW_PRIVATE_URLS` | When `1`/`true`/`yes`, bypasses the SSRF guard in `CALL_WEBHOOK` so workflow authors can hit `localhost`, RFC1918, and `.internal` targets. For dev only — MUST remain unset in production. | unset (guard enforced) |
102
+ | `OM_WORKFLOWS_ENV_INTERPOLATION_ALLOWLIST` | Comma-separated non-secret process env keys allowed for `{{env.*}}` interpolation in workflow activity config. `APP_URL` is always allowed. Never include secrets. | unset (`APP_URL` only) |
102
103
 
103
104
  ## DI Services
104
105
 
@@ -262,10 +262,10 @@
262
262
  "activityType": "CALL_API",
263
263
  "config": {
264
264
  "method": "POST",
265
- "url": "{{env.CONTRACT_SERVICE_URL}}/api/contracts/generate",
265
+ "url": "{{context.integration.contractServiceUrl}}/api/contracts/generate",
266
266
  "headers": {
267
267
  "Content-Type": "application/json",
268
- "Authorization": "Bearer {{env.SERVICE_TOKEN}}"
268
+ "Authorization": "Bearer {{context.integration.serviceToken}}"
269
269
  },
270
270
  "body": {
271
271
  "orderId": "{{context.order.id}}",
@@ -331,7 +331,7 @@
331
331
  "activityType": "CALL_API",
332
332
  "config": {
333
333
  "method": "POST",
334
- "url": "{{env.CONTRACT_SERVICE_URL}}/api/contracts/verify",
334
+ "url": "{{context.integration.contractServiceUrl}}/api/contracts/verify",
335
335
  "body": {
336
336
  "contractId": "{{context.contract.id}}",
337
337
  "signatureId": "{{task.result.signatureId}}"
@@ -393,10 +393,10 @@
393
393
  "activityType": "CALL_API",
394
394
  "config": {
395
395
  "method": "POST",
396
- "url": "{{env.BILLING_SERVICE_URL}}/api/invoices",
396
+ "url": "{{context.integration.billingServiceUrl}}/api/invoices",
397
397
  "headers": {
398
398
  "Content-Type": "application/json",
399
- "Authorization": "Bearer {{env.SERVICE_TOKEN}}"
399
+ "Authorization": "Bearer {{context.integration.serviceToken}}"
400
400
  },
401
401
  "body": {
402
402
  "orderId": "{{context.order.id}}",
@@ -486,10 +486,10 @@
486
486
  "activityType": "CALL_API",
487
487
  "config": {
488
488
  "method": "POST",
489
- "url": "{{env.WAREHOUSE_SERVICE_URL}}/api/fulfillment/orders",
489
+ "url": "{{context.integration.warehouseServiceUrl}}/api/fulfillment/orders",
490
490
  "headers": {
491
491
  "Content-Type": "application/json",
492
- "Authorization": "Bearer {{env.SERVICE_TOKEN}}"
492
+ "Authorization": "Bearer {{context.integration.serviceToken}}"
493
493
  },
494
494
  "body": {
495
495
  "orderId": "{{context.order.id}}",
@@ -607,4 +607,4 @@
607
607
  }
608
608
  ]
609
609
  }
610
- }
610
+ }
@@ -31,10 +31,27 @@ export { isPrivateUrl } from '@open-mercato/shared/lib/network'
31
31
 
32
32
  function isAllowPrivateWorkflowWebhookUrlsEnabled(): boolean {
33
33
  if (parseBooleanWithDefault(process.env.OM_WORKFLOWS_ALLOW_PRIVATE_URLS, false)) {
34
+ if (process.env.NODE_ENV === 'production') {
35
+ console.warn(
36
+ '[CALL_WEBHOOK] OM_WORKFLOWS_ALLOW_PRIVATE_URLS is set but ignored in production. SSRF protection remains enabled.'
37
+ )
38
+ return false
39
+ }
40
+
41
+ console.warn(
42
+ '[CALL_WEBHOOK] OM_WORKFLOWS_ALLOW_PRIVATE_URLS is enabled. SSRF protection is bypassed for workflow webhooks; use only in development.'
43
+ )
34
44
  return true
35
45
  }
36
46
 
37
47
  if (parseBooleanWithDefault(process.env.WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS, false)) {
48
+ if (process.env.NODE_ENV === 'production') {
49
+ console.warn(
50
+ '[CALL_WEBHOOK] WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated and ignored in production. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS for development only. SSRF protection remains enabled.'
51
+ )
52
+ return false
53
+ }
54
+
38
55
  console.warn(
39
56
  '[CALL_WEBHOOK] WORKFLOW_WEBHOOK_ALLOW_PRIVATE_URLS is deprecated. Use OM_WORKFLOWS_ALLOW_PRIVATE_URLS instead. SSRF protection is bypassed.'
40
57
  )
@@ -44,6 +61,34 @@ function isAllowPrivateWorkflowWebhookUrlsEnabled(): boolean {
44
61
  return false
45
62
  }
46
63
 
64
+ const DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST = new Set(['APP_URL'])
65
+ const WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY = 'OM_WORKFLOWS_ENV_INTERPOLATION_ALLOWLIST'
66
+
67
+ function getWorkflowEnvInterpolationAllowlist(): Set<string> {
68
+ const allowlist = new Set(DEFAULT_WORKFLOW_ENV_INTERPOLATION_ALLOWLIST)
69
+ const configuredKeys = process.env[WORKFLOW_ENV_INTERPOLATION_ALLOWLIST_KEY]
70
+ if (!configuredKeys) {
71
+ return allowlist
72
+ }
73
+
74
+ for (const key of configuredKeys.split(',')) {
75
+ const trimmedKey = key.trim()
76
+ if (trimmedKey) {
77
+ allowlist.add(trimmedKey)
78
+ }
79
+ }
80
+
81
+ return allowlist
82
+ }
83
+
84
+ function resolveWorkflowEnvInterpolation(envKey: string): string {
85
+ if (!getWorkflowEnvInterpolationAllowlist().has(envKey)) {
86
+ return ''
87
+ }
88
+
89
+ return process.env[envKey] ?? ''
90
+ }
91
+
47
92
  // ============================================================================
48
93
  // Types and Interfaces
49
94
  // ============================================================================
@@ -857,7 +902,7 @@ export async function executeCallApi(
857
902
  container: AwilixContainer,
858
903
  signal?: AbortSignal
859
904
  ): Promise<any> {
860
- // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, {{env.*}}, {{now}})
905
+ // 1. Interpolate variables in config (including {{workflow.*}}, {{context.*}}, allowlisted {{env.*}}, {{now}})
861
906
  const interpolatedConfig = interpolateVariables(config, context.workflowContext, context.workflowInstance)
862
907
 
863
908
  const {
@@ -1145,7 +1190,7 @@ function classifyAndThrowError(status: number, body: any, url: string): never {
1145
1190
  * - {{workflow.tenantId}} - tenant ID
1146
1191
  * - {{workflow.organizationId}} - organization ID
1147
1192
  * - {{workflow.currentStepId}} - current step ID
1148
- * - {{env.VAR_NAME}} - environment variables
1193
+ * - {{env.VAR_NAME}} - server-allowlisted environment variables
1149
1194
  * - {{now}} - current ISO timestamp
1150
1195
  */
1151
1196
  function interpolateVariables(
@@ -1185,7 +1230,7 @@ function interpolateVariables(
1185
1230
  // Handle {{env.*}} variables
1186
1231
  if (trimmedPath.startsWith('env.')) {
1187
1232
  const envKey = trimmedPath.substring('env.'.length)
1188
- return process.env[envKey] ?? config
1233
+ return resolveWorkflowEnvInterpolation(envKey)
1189
1234
  }
1190
1235
 
1191
1236
  // Handle {{now}} - current timestamp
@@ -1230,8 +1275,7 @@ function interpolateVariables(
1230
1275
  // Handle {{env.*}} variables
1231
1276
  if (trimmedPath.startsWith('env.')) {
1232
1277
  const envKey = trimmedPath.substring('env.'.length)
1233
- const envValue = process.env[envKey]
1234
- return envValue !== undefined ? envValue : match
1278
+ return resolveWorkflowEnvInterpolation(envKey)
1235
1279
  }
1236
1280
 
1237
1281
  // Handle {{now}} - current timestamp