@critical-path/server 0.14.0 → 0.15.0

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/dist/router.js CHANGED
@@ -1,9 +1,6 @@
1
1
  import { CriticalPathEngine } from '@critical-path/core';
2
- const CORS_HEADERS = {
3
- 'Access-Control-Allow-Origin': '*',
4
- 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, OPTIONS',
5
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization'
6
- };
2
+ const CORS_ALLOW_METHODS = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
3
+ const DEFAULT_CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization'];
7
4
  class BadRequestError extends Error {
8
5
  constructor(message) {
9
6
  super(message);
@@ -17,6 +14,10 @@ export class CriticalPathRouter {
17
14
  engine;
18
15
  onError;
19
16
  exposeErrors;
17
+ getContext;
18
+ requireAuth;
19
+ basePath;
20
+ cors;
20
21
  constructor(configOrEngine, options = {}) {
21
22
  if (configOrEngine instanceof CriticalPathEngine) {
22
23
  this.engine = configOrEngine;
@@ -26,49 +27,118 @@ export class CriticalPathRouter {
26
27
  }
27
28
  this.onError = options.onError;
28
29
  this.exposeErrors = options.exposeErrors ?? isDevelopment();
30
+ this.getContext = options.getContext;
31
+ this.requireAuth = options.requireAuth ?? false;
32
+ this.basePath = options.basePath ? '/' + options.basePath.replace(/^\/+|\/+$/g, '') : undefined;
33
+ this.cors = options.cors === undefined ? { origins: '*' } : options.cors;
34
+ if (this.cors && this.cors.origins === '*' && this.cors.credentials) {
35
+ throw new Error('CORS credentials cannot be combined with origins "*"; list the allowed origins instead.');
36
+ }
37
+ }
38
+ /**
39
+ * Handles a request. Adapters that resolve identity from something other than the `Request`
40
+ * (e.g. SvelteKit `locals`) pass `init.getContext`, which replaces the router's `getContext`
41
+ * for this request.
42
+ */
43
+ async handleRequest(request, init = {}) {
44
+ const response = await this.dispatch(request, init);
45
+ return this.applyCors(request, response);
46
+ }
47
+ /** Returns the path after the mount point, or `null` when the request is outside `basePath`. */
48
+ resolveSubpath(pathname) {
49
+ if (this.basePath) {
50
+ if (pathname !== this.basePath && !pathname.startsWith(this.basePath + '/'))
51
+ return null;
52
+ return pathname.slice(this.basePath.length).replace(/^\/+/, '');
53
+ }
54
+ // Extract subpath after /critical-path/ or /api/critical-path/
55
+ return pathname.replace(/^.*?\/critical-path\/?/, '').replace(/^\/+/, '');
56
+ }
57
+ applyCors(request, response) {
58
+ if (!this.cors)
59
+ return response;
60
+ const origin = request.headers.get('Origin');
61
+ let allowOrigin;
62
+ if (this.cors.origins === '*') {
63
+ allowOrigin = '*';
64
+ }
65
+ else if (origin && this.cors.origins.includes(origin)) {
66
+ allowOrigin = origin;
67
+ }
68
+ // Copy so headers are mutable even if a hook returned a Response with immutable headers.
69
+ const result = new Response(response.body, response);
70
+ if (this.cors.origins !== '*')
71
+ result.headers.append('Vary', 'Origin');
72
+ if (!allowOrigin)
73
+ return result;
74
+ result.headers.set('Access-Control-Allow-Origin', allowOrigin);
75
+ result.headers.set('Access-Control-Allow-Methods', CORS_ALLOW_METHODS);
76
+ result.headers.set('Access-Control-Allow-Headers', (this.cors.allowHeaders ?? DEFAULT_CORS_ALLOW_HEADERS).join(', '));
77
+ if (this.cors.credentials)
78
+ result.headers.set('Access-Control-Allow-Credentials', 'true');
79
+ if (request.method.toUpperCase() === 'OPTIONS' && this.cors.maxAge !== undefined) {
80
+ result.headers.set('Access-Control-Max-Age', String(this.cors.maxAge));
81
+ }
82
+ return result;
29
83
  }
30
- async handleRequest(request) {
84
+ async dispatch(request, init) {
31
85
  const url = new URL(request.url);
32
86
  const pathname = url.pathname;
33
87
  const method = request.method.toUpperCase();
34
- // Extract subpath after /critical-path/ or /api/critical-path/
35
- const subpath = pathname.replace(/^.*?\/critical-path\/?/, '').replace(/^\/+/, '');
88
+ const subpath = this.resolveSubpath(pathname);
89
+ if (subpath === null) {
90
+ return this.jsonResponse({ error: `Route not found: ${method} ${pathname}` }, 404);
91
+ }
36
92
  const segments = subpath.split('/').filter(Boolean);
37
- // CORS preflight
93
+ // CORS preflight (browsers send it without credentials, so it is answered before auth)
38
94
  if (method === 'OPTIONS') {
39
- return new Response(null, { status: 204, headers: CORS_HEADERS });
95
+ return new Response(null, { status: 204 });
96
+ }
97
+ let context;
98
+ try {
99
+ const resolve = init.getContext ?? this.getContext;
100
+ context = resolve ? await resolve(request) : undefined;
101
+ }
102
+ catch (err) {
103
+ return this.errorResponse(err, request);
104
+ }
105
+ if (this.requireAuth && !context?.userId) {
106
+ return this.jsonResponse({ error: 'Authentication required' }, 401);
40
107
  }
108
+ const engine = context?.userId
109
+ ? this.engine.withActor({ userId: context.userId, username: context.userName, actorType: context.actorType })
110
+ : this.engine;
41
111
  try {
42
112
  // Workflows API
43
113
  if (segments[0] === 'workflows') {
44
114
  const workflowId = segments[1];
45
115
  if (!workflowId) {
46
116
  if (method === 'GET') {
47
- const workflows = await this.engine.getWorkflows();
117
+ const workflows = await engine.getWorkflows();
48
118
  return this.jsonResponse({ workflows });
49
119
  }
50
120
  if (method === 'POST') {
51
121
  const body = await this.readJson(request);
52
- const workflow = await this.engine.createWorkflow(body);
122
+ const workflow = await engine.createWorkflow(body);
53
123
  return this.jsonResponse({ workflow }, 201);
54
124
  }
55
125
  }
56
126
  else {
57
127
  if (method === 'GET') {
58
- const workflow = await this.engine.getWorkflow(workflowId);
128
+ const workflow = await engine.getWorkflow(workflowId);
59
129
  if (!workflow)
60
130
  return this.jsonResponse({ error: 'Workflow not found' }, 404);
61
131
  return this.jsonResponse({ workflow });
62
132
  }
63
133
  if (method === 'PATCH' || method === 'PUT') {
64
134
  const body = await this.readJson(request);
65
- const updated = await this.engine.updateWorkflow(workflowId, body);
135
+ const updated = await engine.updateWorkflow(workflowId, body);
66
136
  if (!updated)
67
137
  return this.jsonResponse({ error: 'Workflow not found' }, 404);
68
138
  return this.jsonResponse({ workflow: updated });
69
139
  }
70
140
  if (method === 'DELETE') {
71
- const deleted = await this.engine.deleteWorkflow(workflowId);
141
+ const deleted = await engine.deleteWorkflow(workflowId);
72
142
  if (!deleted)
73
143
  return this.jsonResponse({ error: 'Not found' }, 404);
74
144
  return this.jsonResponse({ success: true });
@@ -81,18 +151,18 @@ export class CriticalPathRouter {
81
151
  const subResource = segments[2];
82
152
  if (!projectId) {
83
153
  if (method === 'GET') {
84
- const projects = await this.engine.getProjects();
154
+ const projects = await engine.getProjects();
85
155
  return this.jsonResponse({ projects });
86
156
  }
87
157
  if (method === 'POST') {
88
158
  const body = await this.readJson(request);
89
- const project = await this.engine.createProject(body);
159
+ const project = await engine.createProject(body);
90
160
  return this.jsonResponse({ project }, 201);
91
161
  }
92
162
  }
93
163
  else if (subResource === 'critical-path') {
94
164
  if (method === 'GET') {
95
- const analysis = await this.engine.calculateCriticalPath(projectId);
165
+ const analysis = await engine.calculateCriticalPath(projectId);
96
166
  return this.jsonResponse({ analysis });
97
167
  }
98
168
  }
@@ -101,7 +171,7 @@ export class CriticalPathRouter {
101
171
  const level = (url.searchParams.get('level') || 'all');
102
172
  const containerId = url.searchParams.get('containerId') || undefined;
103
173
  const iterationId = url.searchParams.get('iterationId') || undefined;
104
- const ladder = await this.engine.getTimelineLadder(projectId, { level, containerId, iterationId });
174
+ const ladder = await engine.getTimelineLadder(projectId, { level, containerId, iterationId });
105
175
  return this.jsonResponse({ ladder });
106
176
  }
107
177
  }
@@ -115,7 +185,7 @@ export class CriticalPathRouter {
115
185
  const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
116
186
  ? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours'))
117
187
  : undefined;
118
- const workload = await this.engine.getWorkloadDistribution(projectId, {
188
+ const workload = await engine.getWorkloadDistribution(projectId, {
119
189
  startDate,
120
190
  endDate,
121
191
  interval,
@@ -128,20 +198,20 @@ export class CriticalPathRouter {
128
198
  }
129
199
  else {
130
200
  if (method === 'GET') {
131
- const project = await this.engine.getProject(projectId);
201
+ const project = await engine.getProject(projectId);
132
202
  if (!project)
133
203
  return this.jsonResponse({ error: 'Project not found' }, 404);
134
204
  return this.jsonResponse({ project });
135
205
  }
136
206
  if (method === 'PATCH' || method === 'PUT') {
137
207
  const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, ...updates } = await this.readJson(request);
138
- const project = await this.engine.updateProject(projectId, updates);
208
+ const project = await engine.updateProject(projectId, updates);
139
209
  if (!project)
140
210
  return this.jsonResponse({ error: 'Project not found' }, 404);
141
211
  return this.jsonResponse({ project });
142
212
  }
143
213
  if (method === 'DELETE') {
144
- const deleted = await this.engine.deleteProject(projectId);
214
+ const deleted = await engine.deleteProject(projectId);
145
215
  if (!deleted)
146
216
  return this.jsonResponse({ error: 'Not found' }, 404);
147
217
  return this.jsonResponse({ success: true });
@@ -155,40 +225,40 @@ export class CriticalPathRouter {
155
225
  if (!taskId) {
156
226
  if (method === 'GET') {
157
227
  const projectId = url.searchParams.get('projectId') || undefined;
158
- const tasks = await this.engine.getTasks(projectId);
228
+ const tasks = await engine.getTasks(projectId);
159
229
  return this.jsonResponse({ tasks });
160
230
  }
161
231
  if (method === 'POST') {
162
232
  const body = await this.readJson(request);
163
- const task = await this.engine.createTask(body);
233
+ const task = await engine.createTask(body);
164
234
  return this.jsonResponse({ task }, 201);
165
235
  }
166
236
  }
167
237
  else if (subResource === 'comments') {
168
238
  if (method === 'GET') {
169
- const comments = await this.engine.getComments(taskId);
239
+ const comments = await engine.getComments(taskId);
170
240
  return this.jsonResponse({ comments });
171
241
  }
172
242
  if (method === 'POST') {
173
243
  const body = await this.readJson(request);
174
- const comment = await this.engine.addComment({ ...body, taskId });
244
+ const comment = await engine.addComment({ ...body, taskId });
175
245
  return this.jsonResponse({ comment }, 201);
176
246
  }
177
247
  }
178
248
  else if (subResource === 'attachments') {
179
249
  if (method === 'GET') {
180
- const attachments = await this.engine.getAttachments({ taskId });
250
+ const attachments = await engine.getAttachments({ taskId });
181
251
  return this.jsonResponse({ attachments });
182
252
  }
183
253
  if (method === 'POST') {
184
254
  const body = await this.readJson(request);
185
- const attachment = await this.engine.createAttachment({ ...body, taskId });
255
+ const attachment = await engine.createAttachment({ ...body, taskId });
186
256
  return this.jsonResponse({ attachment }, 201);
187
257
  }
188
258
  }
189
259
  else if (subResource === 'dependencies') {
190
260
  if (method === 'GET') {
191
- const graph = await this.engine.getTaskDependencyGraph(taskId);
261
+ const graph = await engine.getTaskDependencyGraph(taskId);
192
262
  return this.jsonResponse({ graph });
193
263
  }
194
264
  if (method === 'POST') {
@@ -196,7 +266,7 @@ export class CriticalPathRouter {
196
266
  if (typeof body.dependsOnTaskId !== 'string' || !body.dependsOnTaskId) {
197
267
  return this.jsonResponse({ error: 'dependsOnTaskId is required' }, 400);
198
268
  }
199
- const dep = await this.engine.addDependency({
269
+ const dep = await engine.addDependency({
200
270
  taskId,
201
271
  dependsOnTaskId: body.dependsOnTaskId,
202
272
  type: body.type || 'blocking'
@@ -206,7 +276,7 @@ export class CriticalPathRouter {
206
276
  }
207
277
  else if (subResource === 'state' || subResource === 'lifecycle') {
208
278
  if (method === 'GET') {
209
- const state = await this.engine.getTaskLifecycleState(taskId);
279
+ const state = await engine.getTaskLifecycleState(taskId);
210
280
  if (!state)
211
281
  return this.jsonResponse({ error: 'Task not found' }, 404);
212
282
  return this.jsonResponse({ state });
@@ -214,14 +284,14 @@ export class CriticalPathRouter {
214
284
  }
215
285
  else if (subResource === 'transitions' || subResource === 'allowed-transitions') {
216
286
  if (method === 'GET') {
217
- const allowedNextStatuses = await this.engine.getAllowedTaskTransitions(taskId);
218
- const allowedPreviousStatuses = await this.engine.getAllowedPreviousTaskTransitions(taskId);
287
+ const allowedNextStatuses = await engine.getAllowedTaskTransitions(taskId);
288
+ const allowedPreviousStatuses = await engine.getAllowedPreviousTaskTransitions(taskId);
219
289
  return this.jsonResponse({ allowedNextStatuses, allowedPreviousStatuses });
220
290
  }
221
291
  }
222
292
  else if (subResource === 'ladder') {
223
293
  if (method === 'GET') {
224
- const taskLadder = await this.engine.getTaskLadder(taskId);
294
+ const taskLadder = await engine.getTaskLadder(taskId);
225
295
  if (!taskLadder)
226
296
  return this.jsonResponse({ error: 'Task not found' }, 404);
227
297
  return this.jsonResponse({ taskLadder });
@@ -229,7 +299,7 @@ export class CriticalPathRouter {
229
299
  }
230
300
  else if (subResource === 'metrics') {
231
301
  if (method === 'GET') {
232
- const metrics = await this.engine.getTaskMetrics(taskId);
302
+ const metrics = await engine.getTaskMetrics(taskId);
233
303
  if (!metrics)
234
304
  return this.jsonResponse({ error: 'Task not found' }, 404);
235
305
  return this.jsonResponse({ metrics });
@@ -237,7 +307,7 @@ export class CriticalPathRouter {
237
307
  }
238
308
  else if (subResource === 'progress-history') {
239
309
  if (method === 'GET') {
240
- const progressHistory = await this.engine.getTaskProgressHistory(taskId);
310
+ const progressHistory = await engine.getTaskProgressHistory(taskId);
241
311
  if (!progressHistory)
242
312
  return this.jsonResponse({ error: 'Task not found' }, 404);
243
313
  return this.jsonResponse({ progressHistory });
@@ -245,20 +315,20 @@ export class CriticalPathRouter {
245
315
  }
246
316
  else {
247
317
  if (method === 'GET') {
248
- const task = await this.engine.getTask(taskId);
318
+ const task = await engine.getTask(taskId);
249
319
  if (!task)
250
320
  return this.jsonResponse({ error: 'Task not found' }, 404);
251
321
  return this.jsonResponse({ task });
252
322
  }
253
323
  if (method === 'PATCH' || method === 'PUT') {
254
324
  const body = await this.readJson(request);
255
- const updated = await this.engine.updateTask(taskId, body);
325
+ const updated = await engine.updateTask(taskId, body);
256
326
  if (!updated)
257
327
  return this.jsonResponse({ error: 'Task not found' }, 404);
258
328
  return this.jsonResponse({ task: updated });
259
329
  }
260
330
  if (method === 'DELETE') {
261
- const deleted = await this.engine.deleteTask(taskId);
331
+ const deleted = await engine.deleteTask(taskId);
262
332
  if (!deleted)
263
333
  return this.jsonResponse({ error: 'Not found' }, 404);
264
334
  return this.jsonResponse({ success: true });
@@ -270,31 +340,31 @@ export class CriticalPathRouter {
270
340
  const teamId = segments[1];
271
341
  if (!teamId) {
272
342
  if (method === 'GET') {
273
- const teams = await this.engine.getTeams();
343
+ const teams = await engine.getTeams();
274
344
  return this.jsonResponse({ teams });
275
345
  }
276
346
  if (method === 'POST') {
277
347
  const body = await this.readJson(request);
278
- const team = await this.engine.createTeam(body);
348
+ const team = await engine.createTeam(body);
279
349
  return this.jsonResponse({ team }, 201);
280
350
  }
281
351
  }
282
352
  else {
283
353
  if (method === 'GET') {
284
- const team = await this.engine.getTeam(teamId);
354
+ const team = await engine.getTeam(teamId);
285
355
  if (!team)
286
356
  return this.jsonResponse({ error: 'Team not found' }, 404);
287
357
  return this.jsonResponse({ team });
288
358
  }
289
359
  if (method === 'PATCH' || method === 'PUT') {
290
360
  const body = await this.readJson(request);
291
- const updated = await this.engine.updateTeam(teamId, body);
361
+ const updated = await engine.updateTeam(teamId, body);
292
362
  if (!updated)
293
363
  return this.jsonResponse({ error: 'Team not found' }, 404);
294
364
  return this.jsonResponse({ team: updated });
295
365
  }
296
366
  if (method === 'DELETE') {
297
- const deleted = await this.engine.deleteTeam(teamId);
367
+ const deleted = await engine.deleteTeam(teamId);
298
368
  if (!deleted)
299
369
  return this.jsonResponse({ error: 'Not found' }, 404);
300
370
  return this.jsonResponse({ success: true });
@@ -309,31 +379,31 @@ export class CriticalPathRouter {
309
379
  const projectId = url.searchParams.get('projectId');
310
380
  if (!projectId)
311
381
  return this.jsonResponse({ error: 'projectId parameter required' }, 400);
312
- const containers = await this.engine.getContainers(projectId);
382
+ const containers = await engine.getContainers(projectId);
313
383
  return this.jsonResponse({ containers });
314
384
  }
315
385
  if (method === 'POST') {
316
386
  const body = await this.readJson(request);
317
- const container = await this.engine.createContainer(body);
387
+ const container = await engine.createContainer(body);
318
388
  return this.jsonResponse({ container }, 201);
319
389
  }
320
390
  }
321
391
  else {
322
392
  if (method === 'GET') {
323
- const container = await this.engine.getContainer(containerId);
393
+ const container = await engine.getContainer(containerId);
324
394
  if (!container)
325
395
  return this.jsonResponse({ error: 'Container not found' }, 404);
326
396
  return this.jsonResponse({ container });
327
397
  }
328
398
  if (method === 'PATCH' || method === 'PUT') {
329
399
  const body = await this.readJson(request);
330
- const updated = await this.engine.updateContainer(containerId, body);
400
+ const updated = await engine.updateContainer(containerId, body);
331
401
  if (!updated)
332
402
  return this.jsonResponse({ error: 'Container not found' }, 404);
333
403
  return this.jsonResponse({ container: updated });
334
404
  }
335
405
  if (method === 'DELETE') {
336
- const deleted = await this.engine.deleteContainer(containerId);
406
+ const deleted = await engine.deleteContainer(containerId);
337
407
  if (!deleted)
338
408
  return this.jsonResponse({ error: 'Not found' }, 404);
339
409
  return this.jsonResponse({ success: true });
@@ -348,18 +418,18 @@ export class CriticalPathRouter {
348
418
  const projectId = url.searchParams.get('projectId');
349
419
  if (!projectId)
350
420
  return this.jsonResponse({ error: 'projectId parameter required' }, 400);
351
- const deliverables = await this.engine.getDeliverables(projectId);
421
+ const deliverables = await engine.getDeliverables(projectId);
352
422
  return this.jsonResponse({ deliverables });
353
423
  }
354
424
  if (method === 'POST') {
355
425
  const body = await this.readJson(request);
356
- const deliverable = await this.engine.createDeliverable(body);
426
+ const deliverable = await engine.createDeliverable(body);
357
427
  return this.jsonResponse({ deliverable }, 201);
358
428
  }
359
429
  }
360
430
  else if (segments[2] === 'summary') {
361
431
  if (method === 'GET') {
362
- const summary = await this.engine.getDeliverableSummary(deliverableId);
432
+ const summary = await engine.getDeliverableSummary(deliverableId);
363
433
  if (!summary)
364
434
  return this.jsonResponse({ error: 'Deliverable not found' }, 404);
365
435
  return this.jsonResponse({ summary });
@@ -367,20 +437,20 @@ export class CriticalPathRouter {
367
437
  }
368
438
  else {
369
439
  if (method === 'GET') {
370
- const deliverable = await this.engine.getDeliverable(deliverableId);
440
+ const deliverable = await engine.getDeliverable(deliverableId);
371
441
  if (!deliverable)
372
442
  return this.jsonResponse({ error: 'Deliverable not found' }, 404);
373
443
  return this.jsonResponse({ deliverable });
374
444
  }
375
445
  if (method === 'PATCH' || method === 'PUT') {
376
446
  const body = await this.readJson(request);
377
- const updated = await this.engine.updateDeliverable(deliverableId, body);
447
+ const updated = await engine.updateDeliverable(deliverableId, body);
378
448
  if (!updated)
379
449
  return this.jsonResponse({ error: 'Deliverable not found' }, 404);
380
450
  return this.jsonResponse({ deliverable: updated });
381
451
  }
382
452
  if (method === 'DELETE') {
383
- const deleted = await this.engine.deleteDeliverable(deliverableId);
453
+ const deleted = await engine.deleteDeliverable(deliverableId);
384
454
  if (!deleted)
385
455
  return this.jsonResponse({ error: 'Not found' }, 404);
386
456
  return this.jsonResponse({ success: true });
@@ -395,31 +465,31 @@ export class CriticalPathRouter {
395
465
  const projectId = url.searchParams.get('projectId');
396
466
  if (!projectId)
397
467
  return this.jsonResponse({ error: 'projectId parameter required' }, 400);
398
- const iterations = await this.engine.getIterations(projectId);
468
+ const iterations = await engine.getIterations(projectId);
399
469
  return this.jsonResponse({ iterations });
400
470
  }
401
471
  if (method === 'POST') {
402
472
  const body = await this.readJson(request);
403
- const iteration = await this.engine.createIteration(body);
473
+ const iteration = await engine.createIteration(body);
404
474
  return this.jsonResponse({ iteration }, 201);
405
475
  }
406
476
  }
407
477
  else {
408
478
  if (method === 'GET') {
409
- const iteration = await this.engine.getIteration(iterationId);
479
+ const iteration = await engine.getIteration(iterationId);
410
480
  if (!iteration)
411
481
  return this.jsonResponse({ error: 'Iteration not found' }, 404);
412
482
  return this.jsonResponse({ iteration });
413
483
  }
414
484
  if (method === 'PATCH' || method === 'PUT') {
415
485
  const body = await this.readJson(request);
416
- const updated = await this.engine.updateIteration(iterationId, body);
486
+ const updated = await engine.updateIteration(iterationId, body);
417
487
  if (!updated)
418
488
  return this.jsonResponse({ error: 'Iteration not found' }, 404);
419
489
  return this.jsonResponse({ iteration: updated });
420
490
  }
421
491
  if (method === 'DELETE') {
422
- const deleted = await this.engine.deleteIteration(iterationId);
492
+ const deleted = await engine.deleteIteration(iterationId);
423
493
  if (!deleted)
424
494
  return this.jsonResponse({ error: 'Not found' }, 404);
425
495
  return this.jsonResponse({ success: true });
@@ -431,7 +501,7 @@ export class CriticalPathRouter {
431
501
  if (method === 'GET') {
432
502
  const projectId = url.searchParams.get('projectId') || undefined;
433
503
  const taskId = url.searchParams.get('taskId') || undefined;
434
- const activities = await this.engine.store.getActivities({ projectId, taskId });
504
+ const activities = await engine.store.getActivities({ projectId, taskId });
435
505
  return this.jsonResponse({ activities });
436
506
  }
437
507
  }
@@ -442,10 +512,11 @@ export class CriticalPathRouter {
442
512
  if (commentId && subResource === 'reactions') {
443
513
  if (method === 'POST') {
444
514
  const body = await this.readJson(request);
445
- if (!body.emoji || !body.userId) {
515
+ const userId = context?.userId ?? body.userId;
516
+ if (!body.emoji || !userId) {
446
517
  return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
447
518
  }
448
- const comment = await this.engine.addCommentReaction(commentId, body);
519
+ const comment = await engine.addCommentReaction(commentId, { emoji: body.emoji, userId });
449
520
  if (!comment)
450
521
  return this.jsonResponse({ error: 'Comment not found' }, 404);
451
522
  return this.jsonResponse({ comment }, 200);
@@ -459,11 +530,11 @@ export class CriticalPathRouter {
459
530
  // Body may be empty on DELETE, fallback to searchParams
460
531
  }
461
532
  const emoji = body.emoji || url.searchParams.get('emoji');
462
- const userId = body.userId || url.searchParams.get('userId');
533
+ const userId = context?.userId ?? (body.userId || url.searchParams.get('userId'));
463
534
  if (!emoji || !userId) {
464
535
  return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
465
536
  }
466
- const comment = await this.engine.removeCommentReaction(commentId, { emoji, userId });
537
+ const comment = await engine.removeCommentReaction(commentId, { emoji, userId });
467
538
  if (!comment)
468
539
  return this.jsonResponse({ error: 'Comment not found' }, 404);
469
540
  return this.jsonResponse({ comment }, 200);
@@ -474,31 +545,31 @@ export class CriticalPathRouter {
474
545
  const taskId = url.searchParams.get('taskId');
475
546
  if (!taskId)
476
547
  return this.jsonResponse({ error: 'taskId parameter required' }, 400);
477
- const comments = await this.engine.getComments(taskId);
548
+ const comments = await engine.getComments(taskId);
478
549
  return this.jsonResponse({ comments });
479
550
  }
480
551
  if (method === 'POST') {
481
552
  const body = await this.readJson(request);
482
- const comment = await this.engine.addComment(body);
553
+ const comment = await engine.addComment(body);
483
554
  return this.jsonResponse({ comment }, 201);
484
555
  }
485
556
  }
486
557
  else {
487
558
  if (method === 'GET') {
488
- const comment = await this.engine.getComment(commentId);
559
+ const comment = await engine.getComment(commentId);
489
560
  if (!comment)
490
561
  return this.jsonResponse({ error: 'Comment not found' }, 404);
491
562
  return this.jsonResponse({ comment });
492
563
  }
493
564
  if (method === 'PATCH' || method === 'PUT') {
494
565
  const body = await this.readJson(request);
495
- const updated = await this.engine.updateComment(commentId, body);
566
+ const updated = await engine.updateComment(commentId, body);
496
567
  if (!updated)
497
568
  return this.jsonResponse({ error: 'Comment not found' }, 404);
498
569
  return this.jsonResponse({ comment: updated });
499
570
  }
500
571
  if (method === 'DELETE') {
501
- const deleted = await this.engine.deleteComment(commentId);
572
+ const deleted = await engine.deleteComment(commentId);
502
573
  if (!deleted)
503
574
  return this.jsonResponse({ error: 'Not found' }, 404);
504
575
  return this.jsonResponse({ success: true });
@@ -511,14 +582,14 @@ export class CriticalPathRouter {
511
582
  if (attachmentId === 'presign') {
512
583
  if (method === 'POST') {
513
584
  const body = await this.readJson(request);
514
- const presigned = await this.engine.getPresignedAttachmentUploadUrl(body);
585
+ const presigned = await engine.getPresignedAttachmentUploadUrl(body);
515
586
  return this.jsonResponse({ presigned });
516
587
  }
517
588
  }
518
589
  else if (attachmentId === 'upload') {
519
590
  if (method === 'POST') {
520
591
  const body = await this.readJson(request);
521
- const attachment = await this.engine.uploadAttachmentFile(body);
592
+ const attachment = await engine.uploadAttachmentFile(body);
522
593
  return this.jsonResponse({ attachment }, 201);
523
594
  }
524
595
  }
@@ -527,24 +598,24 @@ export class CriticalPathRouter {
527
598
  const taskId = url.searchParams.get('taskId') || undefined;
528
599
  const projectId = url.searchParams.get('projectId') || undefined;
529
600
  const commentId = url.searchParams.get('commentId') || undefined;
530
- const attachments = await this.engine.getAttachments({ taskId, projectId, commentId });
601
+ const attachments = await engine.getAttachments({ taskId, projectId, commentId });
531
602
  return this.jsonResponse({ attachments });
532
603
  }
533
604
  if (method === 'POST') {
534
605
  const body = await this.readJson(request);
535
- const attachment = await this.engine.createAttachment(body);
606
+ const attachment = await engine.createAttachment(body);
536
607
  return this.jsonResponse({ attachment }, 201);
537
608
  }
538
609
  }
539
610
  else {
540
611
  if (method === 'GET') {
541
- const attachment = await this.engine.getAttachment(attachmentId);
612
+ const attachment = await engine.getAttachment(attachmentId);
542
613
  if (!attachment)
543
614
  return this.jsonResponse({ error: 'Attachment not found' }, 404);
544
615
  return this.jsonResponse({ attachment });
545
616
  }
546
617
  if (method === 'DELETE') {
547
- const deleted = await this.engine.deleteAttachment(attachmentId);
618
+ const deleted = await engine.deleteAttachment(attachmentId);
548
619
  if (!deleted)
549
620
  return this.jsonResponse({ error: 'Not found' }, 404);
550
621
  return this.jsonResponse({ success: true });
@@ -557,12 +628,12 @@ export class CriticalPathRouter {
557
628
  const taskId = url.searchParams.get('taskId');
558
629
  if (!taskId)
559
630
  return this.jsonResponse({ error: 'taskId parameter required' }, 400);
560
- const entries = await this.engine.store.getTimeEntries(taskId);
631
+ const entries = await engine.store.getTimeEntries(taskId);
561
632
  return this.jsonResponse({ timeEntries: entries });
562
633
  }
563
634
  if (method === 'POST') {
564
635
  const body = await this.readJson(request);
565
- const entry = await this.engine.logTime(body);
636
+ const entry = await engine.logTime(body);
566
637
  return this.jsonResponse({ timeEntry: entry }, 201);
567
638
  }
568
639
  }
@@ -578,7 +649,7 @@ export class CriticalPathRouter {
578
649
  const defaultWeeklyCapacityHours = url.searchParams.get('defaultWeeklyCapacityHours')
579
650
  ? parseFloat(url.searchParams.get('defaultWeeklyCapacityHours'))
580
651
  : undefined;
581
- const workload = await this.engine.getWorkloadDistribution(projectId, {
652
+ const workload = await engine.getWorkloadDistribution(projectId, {
582
653
  startDate,
583
654
  endDate,
584
655
  interval,
@@ -594,8 +665,8 @@ export class CriticalPathRouter {
594
665
  if (method === 'POST') {
595
666
  const body = await this.readJson(request);
596
667
  const status = typeof body.status === 'string' ? body.status : 'active';
597
- if (this.engine.events) {
598
- this.engine.events.publish({
668
+ if (engine.events) {
669
+ engine.events.publish({
599
670
  type: 'agent.status_updated',
600
671
  aggregateId: body.taskId || body.projectId || 'system',
601
672
  payload: {
@@ -668,8 +739,7 @@ export class CriticalPathRouter {
668
739
  return new Response(JSON.stringify(data), {
669
740
  status,
670
741
  headers: {
671
- 'Content-Type': 'application/json',
672
- ...CORS_HEADERS
742
+ 'Content-Type': 'application/json'
673
743
  }
674
744
  });
675
745
  }