@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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-001.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js +9 -4
- package/dist/modules/scheduler/__integration__/TC-SCHED-004.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-007.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js +72 -0
- package/dist/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.js.map +7 -0
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js +3 -3
- package/dist/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.js.map +1 -1
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js +107 -44
- package/dist/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.js.map +2 -2
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js +13 -3
- package/dist/modules/scheduler/__integration__/helpers/scheduler.js.map +2 -2
- package/dist/modules/scheduler/api/targets/route.js +2 -12
- package/dist/modules/scheduler/api/targets/route.js.map +2 -2
- package/dist/modules/scheduler/cli.js +32 -1
- package/dist/modules/scheduler/cli.js.map +2 -2
- package/dist/modules/scheduler/commands/jobs.js +84 -2
- package/dist/modules/scheduler/commands/jobs.js.map +2 -2
- package/dist/modules/scheduler/data/validators.js +27 -3
- package/dist/modules/scheduler/data/validators.js.map +2 -2
- package/dist/modules/scheduler/i18n/de.json +2 -0
- package/dist/modules/scheduler/i18n/en.json +2 -0
- package/dist/modules/scheduler/i18n/es.json +2 -0
- package/dist/modules/scheduler/i18n/ko.json +2 -0
- package/dist/modules/scheduler/i18n/pl.json +2 -0
- package/dist/modules/scheduler/lib/safeQueueTargets.js +157 -0
- package/dist/modules/scheduler/lib/safeQueueTargets.js.map +7 -0
- package/dist/modules/scheduler/services/localSchedulerService.js +31 -6
- package/dist/modules/scheduler/services/localSchedulerService.js.map +2 -2
- package/dist/modules/scheduler/workers/execute-schedule.worker.js +64 -6
- package/dist/modules/scheduler/workers/execute-schedule.worker.js.map +2 -2
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js +22 -0
- package/dist/modules/scheduler/workers/test-echo-queue.worker.js.map +7 -0
- package/package.json +5 -5
- package/src/modules/scheduler/__integration__/TC-SCHED-001.spec.ts +2 -2
- package/src/modules/scheduler/__integration__/TC-SCHED-004.spec.ts +14 -8
- package/src/modules/scheduler/__integration__/TC-SCHED-007.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-SCHED-008-safe-targets.spec.ts +90 -0
- package/src/modules/scheduler/__integration__/TC-SCHED-CRUDFORM-001.spec.ts +3 -3
- package/src/modules/scheduler/__integration__/TC-UNDO-001-scheduler-jobs.spec.ts +154 -66
- package/src/modules/scheduler/__integration__/helpers/scheduler.ts +24 -5
- package/src/modules/scheduler/api/targets/__tests__/route.test.ts +3 -4
- package/src/modules/scheduler/api/targets/route.ts +6 -17
- package/src/modules/scheduler/cli.ts +44 -1
- package/src/modules/scheduler/commands/__tests__/jobs.target-guard.test.ts +177 -0
- package/src/modules/scheduler/commands/jobs.ts +137 -3
- package/src/modules/scheduler/data/__tests__/safe-targets.test.ts +110 -0
- package/src/modules/scheduler/data/__tests__/validators.test.ts +26 -4
- package/src/modules/scheduler/data/validators.ts +37 -2
- package/src/modules/scheduler/i18n/de.json +2 -0
- package/src/modules/scheduler/i18n/en.json +2 -0
- package/src/modules/scheduler/i18n/es.json +2 -0
- package/src/modules/scheduler/i18n/ko.json +2 -0
- package/src/modules/scheduler/i18n/pl.json +2 -0
- package/src/modules/scheduler/lib/__tests__/safeQueueTargets.test.ts +311 -0
- package/src/modules/scheduler/lib/safeQueueTargets.ts +324 -0
- package/src/modules/scheduler/services/__tests__/localSchedulerService.test.ts +179 -0
- package/src/modules/scheduler/services/localSchedulerService.ts +39 -6
- package/src/modules/scheduler/workers/__tests__/execute-schedule.worker.test.ts +25 -3
- package/src/modules/scheduler/workers/execute-schedule.worker.ts +76 -7
- 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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|