@open-mercato/scheduler 0.6.8-develop.7066.1.6bfa9ff69f → 0.6.8-develop.7068.1.c55b52e1a1

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 (64) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js +2 -2
  3. package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js.map +2 -2
  4. package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js +9 -4
  5. package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js.map +2 -2
  6. package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js +3 -3
  7. package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js.map +2 -2
  8. package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +72 -0
  9. package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +7 -0
  10. package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js +3 -3
  11. package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js.map +1 -1
  12. package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js +107 -44
  13. package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
  14. package/dist/modules/scheduler/__integration__/helpers/scheduler.js +13 -3
  15. package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
  16. package/dist/modules/scheduler/api/targets/route.js +2 -12
  17. package/dist/modules/scheduler/api/targets/route.js.map +2 -2
  18. package/dist/modules/scheduler/cli.js +32 -1
  19. package/dist/modules/scheduler/cli.js.map +2 -2
  20. package/dist/modules/scheduler/commands/jobs.js +84 -2
  21. package/dist/modules/scheduler/commands/jobs.js.map +2 -2
  22. package/dist/modules/scheduler/data/validators.js +27 -3
  23. package/dist/modules/scheduler/data/validators.js.map +2 -2
  24. package/dist/modules/scheduler/i18n/de.json +2 -0
  25. package/dist/modules/scheduler/i18n/en.json +2 -0
  26. package/dist/modules/scheduler/i18n/es.json +2 -0
  27. package/dist/modules/scheduler/i18n/ko.json +2 -0
  28. package/dist/modules/scheduler/i18n/pl.json +2 -0
  29. package/dist/modules/scheduler/lib/safeQueueTargets.js +157 -0
  30. package/dist/modules/scheduler/lib/safeQueueTargets.js.map +7 -0
  31. package/dist/modules/scheduler/services/localSchedulerService.js +31 -6
  32. package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
  33. package/dist/modules/scheduler/workers/execute-schedule.worker.js +64 -6
  34. package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
  35. package/dist/modules/scheduler/workers/test-echo-queue.worker.js +22 -0
  36. package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +7 -0
  37. package/package.json +5 -5
  38. package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
  39. package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +14 -8
  40. package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
  41. package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +90 -0
  42. package/src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts +3 -3
  43. package/src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts +154 -66
  44. package/src/modules/scheduler/__integration__/helpers/scheduler.ts +24 -5
  45. package/src/modules/scheduler/api/targets/__tests__/route.test.ts +3 -4
  46. package/src/modules/scheduler/api/targets/route.ts +6 -17
  47. package/src/modules/scheduler/cli.ts +44 -1
  48. package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +177 -0
  49. package/src/modules/scheduler/commands/jobs.ts +137 -3
  50. package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +110 -0
  51. package/src/modules/scheduler/data/__tests__/validators.test.ts +26 -4
  52. package/src/modules/scheduler/data/validators.ts +37 -2
  53. package/src/modules/scheduler/i18n/de.json +2 -0
  54. package/src/modules/scheduler/i18n/en.json +2 -0
  55. package/src/modules/scheduler/i18n/es.json +2 -0
  56. package/src/modules/scheduler/i18n/ko.json +2 -0
  57. package/src/modules/scheduler/i18n/pl.json +2 -0
  58. package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +311 -0
  59. package/src/modules/scheduler/lib/safeQueueTargets.ts +324 -0
  60. package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +179 -0
  61. package/src/modules/scheduler/services/localSchedulerService.ts +39 -6
  62. package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +25 -3
  63. package/src/modules/scheduler/workers/execute-schedule.worker.ts +76 -7
  64. package/src/modules/scheduler/workers/test-echo-queue.worker.ts +25 -0
@@ -10,6 +10,12 @@ import { assertSchedulerSafeCommandAuthorized } from '../lib/scheduler-safe-comm
10
10
  import { buildScheduledCommandContext } from '../lib/commandContext.js'
11
11
  import { buildQueueTargetPayload, buildSchedulerIdempotencyKey } from '../lib/queueTargetPayload.js'
12
12
  import { createLogger } from '@open-mercato/shared/lib/logger'
13
+ import {
14
+ canDispatchScheduleQueueTarget,
15
+ getSchedulerQueueRequiredFeatures,
16
+ sanitizeSchedulerTargetPayload,
17
+ validateSchedulerTargetPayload,
18
+ } from '../lib/safeQueueTargets'
13
19
 
14
20
  const logger = createLogger('scheduler').child({ component: 'worker' })
15
21
 
@@ -167,12 +173,69 @@ export default async function executeScheduleWorker(
167
173
 
168
174
  // Enqueue target job or execute command
169
175
  if (schedule.targetType === 'queue' && schedule.targetQueue) {
176
+ // Dispatch-time reauthorization (#5213): provenance is verified, never
177
+ // trusted — module-authored rows must still be owned by their recorded
178
+ // sourceModule and carry no acting-user stamp; API-authored rows may only
179
+ // target workers that opted into scheduling.
180
+ if (!canDispatchScheduleQueueTarget(schedule)) {
181
+ logger.error('Refusing non-safe queue target for schedule', {
182
+ scheduleId,
183
+ targetQueue: schedule.targetQueue,
184
+ sourceType: schedule.sourceType,
185
+ sourceModule: schedule.sourceType === 'module' ? schedule.sourceModule : undefined,
186
+ })
187
+ await emitSchedulerEvent('scheduler.job.skipped', {
188
+ id: schedule.id,
189
+ tenantId: schedule.tenantId,
190
+ organizationId: schedule.organizationId,
191
+ reason: `Queue is not an approved scheduler target: ${schedule.targetQueue}`,
192
+ })
193
+ return
194
+ }
195
+
196
+ // Re-validate the stored payload against the target's registered schema and
197
+ // the tenant-level features the target requires (#5213, defense in depth).
198
+ const payloadIssue = validateSchedulerTargetPayload(schedule.targetQueue, schedule.targetPayload)
199
+ if (payloadIssue) {
200
+ logger.error('Refusing schedule with invalid payload for queue target', {
201
+ scheduleId,
202
+ targetQueue: schedule.targetQueue,
203
+ issue: payloadIssue,
204
+ })
205
+ await emitSchedulerEvent('scheduler.job.skipped', {
206
+ id: schedule.id,
207
+ tenantId: schedule.tenantId,
208
+ organizationId: schedule.organizationId,
209
+ reason: `Invalid payload for scheduler queue ${schedule.targetQueue}`,
210
+ })
211
+ return
212
+ }
213
+
214
+ const requiredQueueFeatures = getSchedulerQueueRequiredFeatures(schedule.targetQueue)
215
+ if (schedule.scopeType !== 'system' && requiredQueueFeatures.length > 0) {
216
+ for (const feature of requiredQueueFeatures) {
217
+ if (await rbacService.tenantHasFeature(schedule.tenantId, feature, { organizationId: schedule.organizationId })) continue
218
+ logger.error('Refusing schedule whose tenant lacks a required feature of the queue target', {
219
+ scheduleId,
220
+ targetQueue: schedule.targetQueue,
221
+ feature,
222
+ })
223
+ await emitSchedulerEvent('scheduler.job.skipped', {
224
+ id: schedule.id,
225
+ tenantId: schedule.tenantId,
226
+ organizationId: schedule.organizationId,
227
+ reason: `Tenant lacks feature required by scheduler queue ${schedule.targetQueue}: ${feature}`,
228
+ })
229
+ return
230
+ }
231
+ }
232
+
170
233
  // Determine queue strategy from environment
171
234
  const queueStrategy = (process.env.QUEUE_STRATEGY || 'local') as 'local' | 'async'
172
235
  const targetQueue = createQueue(schedule.targetQueue, queueStrategy, {
173
236
  connection: { url: getRedisUrlOrThrow('QUEUE') },
174
237
  })
175
-
238
+
176
239
  let targetJobId: string | undefined
177
240
  try {
178
241
  // The execute-schedule job id is stable across BullMQ retries, so if
@@ -180,12 +243,18 @@ export default async function executeScheduleWorker(
180
243
  // reuses the same idempotency key and downstream workers can dedupe.
181
244
  const idempotencyKey = buildSchedulerIdempotencyKey(schedule.id, ctx.jobId ?? Date.now())
182
245
 
183
- targetJobId = await targetQueue.enqueue(buildQueueTargetPayload({
184
- targetPayload: schedule.targetPayload,
185
- tenantId: schedule.tenantId,
186
- organizationId: schedule.organizationId,
187
- idempotencyKey,
188
- }))
246
+ // Rebuild tenant/org authority context and the trusted dispatch origin
247
+ // server-side; author-supplied scope/envelope keys never survive (#5213).
248
+ const sanitizedPayload = sanitizeSchedulerTargetPayload(schedule.targetPayload, schedule)
249
+ targetJobId = await targetQueue.enqueue({
250
+ ...buildQueueTargetPayload({
251
+ targetPayload: sanitizedPayload,
252
+ tenantId: schedule.tenantId,
253
+ organizationId: schedule.organizationId,
254
+ idempotencyKey,
255
+ }),
256
+ _jobOrigin: 'scheduler' as const,
257
+ })
189
258
  } finally {
190
259
  // Always close the queue instance to free Redis connections
191
260
  await targetQueue.close()
@@ -0,0 +1,25 @@
1
+ import type { QueuedJob, JobContext, WorkerMeta } from '@open-mercato/queue'
2
+ import { createLogger } from '@open-mercato/shared/lib/logger'
3
+
4
+ const logger = createLogger('scheduler').child({ component: 'test-echo-queue' })
5
+
6
+ export const metadata: WorkerMeta = {
7
+ queue: 'scheduler-test',
8
+ id: 'scheduler:test-echo-queue',
9
+ concurrency: 1,
10
+ /**
11
+ * Deliberate scheduler-safe target for QA fixtures and manual experiments,
12
+ * mirroring the `scheduler.test.echo` command. The handler is side-effect
13
+ * free: it only logs that a delivery arrived.
14
+ */
15
+ schedulerSafe: true,
16
+ }
17
+
18
+ type TestEchoPayload = {
19
+ message?: unknown
20
+ }
21
+
22
+ export default async function handle(job: QueuedJob<TestEchoPayload>, _ctx: JobContext): Promise<void> {
23
+ const message = typeof job.payload?.message === 'string' ? job.payload.message : ''
24
+ logger.info('Test queue target received a scheduled delivery', { messageId: message.slice(0, 80) })
25
+ }