@critical-path/core 0.26.0 → 0.28.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/CHANGELOG.md +50 -0
- package/README.md +15 -0
- package/dist/domain/custom-fields.d.ts +7 -2
- package/dist/domain/custom-fields.d.ts.map +1 -1
- package/dist/domain/custom-fields.js +23 -1
- package/dist/domain/custom-fields.js.map +1 -1
- package/dist/domain/events.d.ts +5 -0
- package/dist/domain/events.d.ts.map +1 -1
- package/dist/domain/events.js +34 -0
- package/dist/domain/events.js.map +1 -1
- package/dist/engine/index.d.ts +30 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +133 -77
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/index.d.ts +13 -3
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +44 -15
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/plugins.test.d.ts +2 -0
- package/dist/plugins/plugins.test.d.ts.map +1 -0
- package/dist/plugins/plugins.test.js +137 -0
- package/dist/plugins/plugins.test.js.map +1 -0
- package/dist/schemas/index.d.ts +167 -121
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +15 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/schemas.test.js +1 -0
- package/dist/schemas/schemas.test.js.map +1 -1
- package/dist/store/firebase.d.ts +3 -0
- package/dist/store/firebase.d.ts.map +1 -1
- package/dist/store/firebase.js +21 -0
- package/dist/store/firebase.js.map +1 -1
- package/dist/store/firebase.test.js +17 -0
- package/dist/store/firebase.test.js.map +1 -1
- package/dist/store/index.d.ts +6 -0
- package/dist/store/index.d.ts.map +1 -1
- package/dist/store/index.js +14 -0
- package/dist/store/index.js.map +1 -1
- package/dist/store/sqlite.d.ts +4 -0
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +42 -8
- package/dist/store/sqlite.js.map +1 -1
- package/dist/types/index.d.ts +67 -6
- package/dist/types/index.d.ts.map +1 -1
- package/dist/webhooks/dispatcher.d.ts +80 -0
- package/dist/webhooks/dispatcher.d.ts.map +1 -0
- package/dist/webhooks/dispatcher.js +133 -0
- package/dist/webhooks/dispatcher.js.map +1 -0
- package/dist/webhooks/index.d.ts +3 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +3 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/signature.d.ts +28 -0
- package/dist/webhooks/signature.d.ts.map +1 -0
- package/dist/webhooks/signature.js +42 -0
- package/dist/webhooks/signature.js.map +1 -0
- package/dist/webhooks/webhooks.test.d.ts +2 -0
- package/dist/webhooks/webhooks.test.d.ts.map +1 -0
- package/dist/webhooks/webhooks.test.js +153 -0
- package/dist/webhooks/webhooks.test.js.map +1 -0
- package/package.json +1 -1
- package/src/domain/custom-fields.ts +43 -2
- package/src/domain/events.ts +42 -3
- package/src/engine/index.ts +170 -84
- package/src/index.ts +1 -0
- package/src/plugins/index.ts +51 -16
- package/src/plugins/plugins.test.ts +161 -0
- package/src/schemas/index.ts +19 -1
- package/src/schemas/schemas.test.ts +6 -3
- package/src/store/firebase.test.ts +20 -0
- package/src/store/firebase.ts +22 -0
- package/src/store/index.ts +19 -0
- package/src/store/sqlite.ts +54 -7
- package/src/types/index.ts +72 -33
- package/src/webhooks/dispatcher.ts +190 -0
- package/src/webhooks/index.ts +2 -0
- package/src/webhooks/signature.ts +57 -0
- package/src/webhooks/webhooks.test.ts +200 -0
- package/tsconfig.tsbuildinfo +1 -1
package/dist/engine/index.js
CHANGED
|
@@ -5,9 +5,11 @@ import { validateTransition, getAllowedNextStatuses, getAllowedPreviousStatuses,
|
|
|
5
5
|
import { extractMentions } from '../utils/mentions.js';
|
|
6
6
|
import { DomainEventBus } from '../domain/events.js';
|
|
7
7
|
import { validateAttachmentUrl } from '../domain/entities.js';
|
|
8
|
-
import { validateCustomFieldValues } from '../domain/custom-fields.js';
|
|
8
|
+
import { validateCustomFieldValues, validateCustomFieldDefinitions } from '../domain/custom-fields.js';
|
|
9
9
|
import { detectDependencyCycle, CircularDependencyError } from '../domain/graph.js';
|
|
10
10
|
import { ValidationError, NotFoundError, ForbiddenError } from '../domain/errors.js';
|
|
11
|
+
import { WebhookDispatcher, assertWebhookUrl } from '../webhooks/dispatcher.js';
|
|
12
|
+
import { generateWebhookSecret } from '../webhooks/signature.js';
|
|
11
13
|
import { calculateCPM } from '../domain/cpm.js';
|
|
12
14
|
import { buildTimelineLadder, aggregateConcreteEvidenceForTask } from '../domain/ladder.js';
|
|
13
15
|
import { calculateTaskMetrics, reconstructTaskProgressHistory } from '../domain/metrics.js';
|
|
@@ -18,6 +20,8 @@ export class CriticalPathEngine {
|
|
|
18
20
|
fileStorage;
|
|
19
21
|
plugins;
|
|
20
22
|
events;
|
|
23
|
+
/** Delivers domain events to registered webhooks. Exposed for custom queues (`deliver`) and tests (`idle`). */
|
|
24
|
+
webhooks;
|
|
21
25
|
ready = Promise.resolve();
|
|
22
26
|
/** The identity mutations are attributed to. Set only on views returned by `withActor`. */
|
|
23
27
|
actor;
|
|
@@ -31,14 +35,19 @@ export class CriticalPathEngine {
|
|
|
31
35
|
this.fileStorage = config.fileStorage;
|
|
32
36
|
this.plugins = new PluginRegistry();
|
|
33
37
|
this.events = new DomainEventBus();
|
|
38
|
+
this.webhooks = new WebhookDispatcher({
|
|
39
|
+
listWebhooks: () => this.listAllWebhooks(),
|
|
40
|
+
resolveTenant: (event) => this.resolveEventTenant(event)
|
|
41
|
+
}, config.webhookDelivery);
|
|
42
|
+
this.events.subscribe('*', (event) => this.webhooks.handle(event));
|
|
34
43
|
if (config.plugins) {
|
|
35
44
|
for (const plugin of config.plugins) {
|
|
36
45
|
this.plugins.register(plugin);
|
|
37
46
|
}
|
|
38
47
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
48
|
+
this.ready = this.initialize(config);
|
|
49
|
+
// Callers that never await `ready` should not crash on an unhandled rejection; awaiting it still throws.
|
|
50
|
+
this.ready.catch(() => { });
|
|
42
51
|
}
|
|
43
52
|
/**
|
|
44
53
|
* Returns a view of this engine that attributes every mutation to `actor`: activity log
|
|
@@ -151,6 +160,14 @@ export class CriticalPathEngine {
|
|
|
151
160
|
const { tenantId: _ignored, ...rest } = updates;
|
|
152
161
|
return rest;
|
|
153
162
|
}
|
|
163
|
+
async initialize(config) {
|
|
164
|
+
if (config.initialData) {
|
|
165
|
+
await this.seedInitialData(config.initialData);
|
|
166
|
+
}
|
|
167
|
+
for (const plugin of this.plugins.getPlugins()) {
|
|
168
|
+
await plugin.init?.(this);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
154
171
|
async seedInitialData(data) {
|
|
155
172
|
if (data.workflows) {
|
|
156
173
|
for (const wf of data.workflows) {
|
|
@@ -215,7 +232,6 @@ export class CriticalPathEngine {
|
|
|
215
232
|
action: 'workflow.created',
|
|
216
233
|
details: { name: created.name }
|
|
217
234
|
});
|
|
218
|
-
this.dispatchWebhook('workflow.created', { workflow: created });
|
|
219
235
|
return created;
|
|
220
236
|
}
|
|
221
237
|
async updateWorkflow(id, updates) {
|
|
@@ -241,7 +257,6 @@ export class CriticalPathEngine {
|
|
|
241
257
|
action: 'workflow.updated',
|
|
242
258
|
details: { name: updated.name }
|
|
243
259
|
});
|
|
244
|
-
this.dispatchWebhook('workflow.updated', { workflow: updated, previous: existing });
|
|
245
260
|
}
|
|
246
261
|
return updated;
|
|
247
262
|
}
|
|
@@ -259,7 +274,7 @@ export class CriticalPathEngine {
|
|
|
259
274
|
aggregateId: id,
|
|
260
275
|
aggregateType: 'Workflow',
|
|
261
276
|
occurredAt: now,
|
|
262
|
-
payload: { workflowId: id, name: existing.name }
|
|
277
|
+
payload: { workflowId: id, name: existing.name, tenantId: existing.tenantId }
|
|
263
278
|
};
|
|
264
279
|
await this.events.publish(event);
|
|
265
280
|
await this.store.logActivity({
|
|
@@ -267,7 +282,6 @@ export class CriticalPathEngine {
|
|
|
267
282
|
action: 'workflow.deleted',
|
|
268
283
|
details: { name: existing.name }
|
|
269
284
|
});
|
|
270
|
-
this.dispatchWebhook('workflow.deleted', { workflowId: id, name: existing.name });
|
|
271
285
|
}
|
|
272
286
|
return deleted;
|
|
273
287
|
}
|
|
@@ -322,6 +336,7 @@ export class CriticalPathEngine {
|
|
|
322
336
|
}
|
|
323
337
|
async createProject(project) {
|
|
324
338
|
await this.requireWorkspaceAccess('project.create');
|
|
339
|
+
validateCustomFieldDefinitions(project.customFieldDefinitions, this.plugins.getCustomFieldTypes());
|
|
325
340
|
// Views always use the actor's tenant (possibly none); only trusted base-engine calls may set it.
|
|
326
341
|
const input = { ...project, tenantId: this.actor ? this.actor.tenantId : project.tenantId };
|
|
327
342
|
// The creator administers the project they create.
|
|
@@ -345,7 +360,6 @@ export class CriticalPathEngine {
|
|
|
345
360
|
action: 'project.created',
|
|
346
361
|
details: { name: created.name, key: created.key }
|
|
347
362
|
});
|
|
348
|
-
this.dispatchWebhook('project.created', { project: created });
|
|
349
363
|
return created;
|
|
350
364
|
}
|
|
351
365
|
async updateProject(id, updates) {
|
|
@@ -355,6 +369,7 @@ export class CriticalPathEngine {
|
|
|
355
369
|
await this.requireProjectAccess('project.update', id);
|
|
356
370
|
if ('members' in updates)
|
|
357
371
|
await this.requireProjectAccess('project.manage_members', id);
|
|
372
|
+
validateCustomFieldDefinitions(updates.customFieldDefinitions, this.plugins.getCustomFieldTypes());
|
|
358
373
|
updates = this.withoutTenant(updates);
|
|
359
374
|
const updated = await this.store.updateProject(id, updates);
|
|
360
375
|
if (updated) {
|
|
@@ -374,7 +389,6 @@ export class CriticalPathEngine {
|
|
|
374
389
|
action: 'project.updated',
|
|
375
390
|
details: { name: updated.name }
|
|
376
391
|
});
|
|
377
|
-
this.dispatchWebhook('project.updated', { project: updated });
|
|
378
392
|
}
|
|
379
393
|
return updated;
|
|
380
394
|
}
|
|
@@ -405,7 +419,7 @@ export class CriticalPathEngine {
|
|
|
405
419
|
aggregateId: id,
|
|
406
420
|
aggregateType: 'Project',
|
|
407
421
|
occurredAt: now,
|
|
408
|
-
payload: { projectId: id, name: existing.name, deletedTaskIds }
|
|
422
|
+
payload: { projectId: id, name: existing.name, deletedTaskIds, tenantId: existing.tenantId }
|
|
409
423
|
};
|
|
410
424
|
await this.events.publish(event);
|
|
411
425
|
await this.store.logActivity({
|
|
@@ -414,7 +428,6 @@ export class CriticalPathEngine {
|
|
|
414
428
|
action: 'project.deleted',
|
|
415
429
|
details: { name: existing.name, deletedTaskCount: deletedTaskIds.length }
|
|
416
430
|
});
|
|
417
|
-
this.dispatchWebhook('project.deleted', { projectId: id, name: existing.name, deletedTaskIds });
|
|
418
431
|
}
|
|
419
432
|
return deleted;
|
|
420
433
|
}
|
|
@@ -437,13 +450,14 @@ export class CriticalPathEngine {
|
|
|
437
450
|
taskInput = { ...taskInput, reporterId: this.actor.userId };
|
|
438
451
|
}
|
|
439
452
|
const processedInput = await this.plugins.runBeforeTaskCreate(taskInput);
|
|
440
|
-
|
|
453
|
+
if (processedInput.projectId && processedInput.projectId !== taskInput.projectId) {
|
|
454
|
+
throw new ValidationError('Plugins cannot move a task to another project in beforeTaskCreate.');
|
|
455
|
+
}
|
|
456
|
+
const projectId = taskInput.projectId;
|
|
441
457
|
const project = await this.store.getProject(projectId);
|
|
442
458
|
const workflow = await this.resolveProjectWorkflow(projectId);
|
|
443
|
-
// Validate custom
|
|
444
|
-
|
|
445
|
-
validateCustomFieldValues(project.customFieldDefinitions, processedInput.customFields || taskInput.customFields);
|
|
446
|
-
}
|
|
459
|
+
// Validate custom fields on the final (post-plugin) input, including required fields when none are given
|
|
460
|
+
validateCustomFieldValues(project?.customFieldDefinitions, processedInput.customFields ?? taskInput.customFields, this.plugins.getCustomFieldTypes());
|
|
447
461
|
const defaultStatus = workflow?.defaultStatusKey || 'todo';
|
|
448
462
|
const initialStatus = processedInput.status || taskInput.status || defaultStatus;
|
|
449
463
|
const now = new Date().toISOString();
|
|
@@ -514,7 +528,6 @@ export class CriticalPathEngine {
|
|
|
514
528
|
action: 'task.created',
|
|
515
529
|
details: { title: created.title, status: created.status }
|
|
516
530
|
});
|
|
517
|
-
this.dispatchWebhook('task.created', { task: created });
|
|
518
531
|
return created;
|
|
519
532
|
}
|
|
520
533
|
async updateTask(id, updates, options) {
|
|
@@ -531,21 +544,20 @@ export class CriticalPathEngine {
|
|
|
531
544
|
const taskUpdates = updates;
|
|
532
545
|
const project = await this.store.getProject(existing.projectId);
|
|
533
546
|
const workflow = await this.resolveProjectWorkflow(existing.projectId);
|
|
547
|
+
// Plugins run first; their output is then validated like caller input. Identity and
|
|
548
|
+
// ownership fields are never changed by hooks.
|
|
549
|
+
const { id: _id, projectId: _projectId, createdAt: _createdAt, ...processedUpdates } = await this.plugins.runBeforeTaskUpdate(id, taskUpdates);
|
|
534
550
|
// Validate workflow transition invariant
|
|
535
|
-
if (
|
|
536
|
-
const isValid = validateTransition(workflow || undefined, existing.status,
|
|
551
|
+
if (processedUpdates.status && processedUpdates.status !== existing.status) {
|
|
552
|
+
const isValid = validateTransition(workflow || undefined, existing.status, processedUpdates.status);
|
|
537
553
|
if (!isValid) {
|
|
538
|
-
throw new WorkflowValidationError(existing.status,
|
|
554
|
+
throw new WorkflowValidationError(existing.status, processedUpdates.status, workflow?.id);
|
|
539
555
|
}
|
|
540
556
|
}
|
|
541
557
|
// Validate custom field invariants
|
|
542
|
-
if (
|
|
543
|
-
validateCustomFieldValues(project
|
|
544
|
-
...existing.customFields,
|
|
545
|
-
...taskUpdates.customFields
|
|
546
|
-
});
|
|
558
|
+
if (processedUpdates.customFields) {
|
|
559
|
+
validateCustomFieldValues(project?.customFieldDefinitions, { ...existing.customFields, ...processedUpdates.customFields }, this.plugins.getCustomFieldTypes());
|
|
547
560
|
}
|
|
548
|
-
const processedUpdates = await this.plugins.runBeforeTaskUpdate(id, taskUpdates);
|
|
549
561
|
const now = new Date().toISOString();
|
|
550
562
|
const existingStatusDef = resolveStatusDefinition(existing.status, project?.statusDefinitions || workflow?.statuses);
|
|
551
563
|
const newStatusDef = processedUpdates.status
|
|
@@ -759,10 +771,6 @@ export class CriticalPathEngine {
|
|
|
759
771
|
action: 'task.blocked',
|
|
760
772
|
details: { reason: blockedReason, ...(actorName ? { actorName } : {}) }
|
|
761
773
|
});
|
|
762
|
-
this.dispatchWebhook('task.blocked', {
|
|
763
|
-
task: updated,
|
|
764
|
-
reason: blockedReason
|
|
765
|
-
});
|
|
766
774
|
}
|
|
767
775
|
else if (wasBlocked && !isNowBlocked) {
|
|
768
776
|
const unblockedEvent = {
|
|
@@ -786,9 +794,6 @@ export class CriticalPathEngine {
|
|
|
786
794
|
action: 'task.unblocked',
|
|
787
795
|
details: { ...(actorName ? { actorName } : {}) }
|
|
788
796
|
});
|
|
789
|
-
this.dispatchWebhook('task.unblocked', {
|
|
790
|
-
task: updated
|
|
791
|
-
});
|
|
792
797
|
}
|
|
793
798
|
await this.store.logActivity({
|
|
794
799
|
projectId: updated.projectId,
|
|
@@ -801,10 +806,6 @@ export class CriticalPathEngine {
|
|
|
801
806
|
...(actorName ? { actorName } : {})
|
|
802
807
|
}
|
|
803
808
|
});
|
|
804
|
-
this.dispatchWebhook(isStatusChange ? 'task.status_changed' : 'task.updated', {
|
|
805
|
-
task: updated,
|
|
806
|
-
previous: existing
|
|
807
|
-
});
|
|
808
809
|
return updated;
|
|
809
810
|
}
|
|
810
811
|
async checkAndUnblockDownstreamTasks(completedTask) {
|
|
@@ -866,10 +867,6 @@ export class CriticalPathEngine {
|
|
|
866
867
|
action: 'task.unblocked',
|
|
867
868
|
details: { unblockedByTaskId: completedTask.id }
|
|
868
869
|
});
|
|
869
|
-
this.dispatchWebhook('task.unblocked', {
|
|
870
|
-
task: updatedDownstream,
|
|
871
|
-
upstreamTaskId: completedTask.id
|
|
872
|
-
});
|
|
873
870
|
}
|
|
874
871
|
}
|
|
875
872
|
}
|
|
@@ -882,10 +879,10 @@ export class CriticalPathEngine {
|
|
|
882
879
|
if (!existing)
|
|
883
880
|
return false;
|
|
884
881
|
await this.requireProjectAccess('task.delete', existing.projectId);
|
|
885
|
-
await this.plugins.runBeforeTaskDelete(id);
|
|
882
|
+
await this.plugins.runBeforeTaskDelete(id, existing);
|
|
886
883
|
const deleted = await this.store.deleteTask(id);
|
|
887
884
|
if (deleted) {
|
|
888
|
-
await this.plugins.runAfterTaskDelete(id);
|
|
885
|
+
await this.plugins.runAfterTaskDelete(id, existing);
|
|
889
886
|
const now = new Date().toISOString();
|
|
890
887
|
const event = {
|
|
891
888
|
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
@@ -907,7 +904,6 @@ export class CriticalPathEngine {
|
|
|
907
904
|
action: 'task.deleted',
|
|
908
905
|
details: { title: existing.title }
|
|
909
906
|
});
|
|
910
|
-
this.dispatchWebhook('task.deleted', { taskId: id, title: existing.title });
|
|
911
907
|
}
|
|
912
908
|
return deleted;
|
|
913
909
|
}
|
|
@@ -1090,7 +1086,6 @@ export class CriticalPathEngine {
|
|
|
1090
1086
|
payload: { taskId: comment.taskId, comment: created }
|
|
1091
1087
|
};
|
|
1092
1088
|
await this.events.publish(event);
|
|
1093
|
-
this.dispatchWebhook('comment.created', { comment: created });
|
|
1094
1089
|
return created;
|
|
1095
1090
|
}
|
|
1096
1091
|
async updateComment(id, updates) {
|
|
@@ -1116,7 +1111,6 @@ export class CriticalPathEngine {
|
|
|
1116
1111
|
payload: { comment: updated, previous: existing }
|
|
1117
1112
|
};
|
|
1118
1113
|
await this.events.publish(event);
|
|
1119
|
-
this.dispatchWebhook('comment.updated', { comment: updated });
|
|
1120
1114
|
return updated;
|
|
1121
1115
|
}
|
|
1122
1116
|
async deleteComment(id) {
|
|
@@ -1139,7 +1133,6 @@ export class CriticalPathEngine {
|
|
|
1139
1133
|
payload: { taskId: existing.taskId, commentId: id }
|
|
1140
1134
|
};
|
|
1141
1135
|
await this.events.publish(event);
|
|
1142
|
-
this.dispatchWebhook('comment.deleted', { commentId: id, taskId: existing.taskId });
|
|
1143
1136
|
}
|
|
1144
1137
|
return deleted;
|
|
1145
1138
|
}
|
|
@@ -1185,7 +1178,6 @@ export class CriticalPathEngine {
|
|
|
1185
1178
|
}
|
|
1186
1179
|
};
|
|
1187
1180
|
await this.events.publish(event);
|
|
1188
|
-
this.dispatchWebhook('comment.reaction.added', { comment: updated, reaction });
|
|
1189
1181
|
return updated;
|
|
1190
1182
|
}
|
|
1191
1183
|
async removeCommentReaction(commentId, input) {
|
|
@@ -1214,7 +1206,6 @@ export class CriticalPathEngine {
|
|
|
1214
1206
|
payload: { commentId, reaction, comment: updated }
|
|
1215
1207
|
};
|
|
1216
1208
|
await this.events.publish(event);
|
|
1217
|
-
this.dispatchWebhook('comment.reaction.removed', { comment: updated, reaction });
|
|
1218
1209
|
return updated;
|
|
1219
1210
|
}
|
|
1220
1211
|
// --- Attachments ---
|
|
@@ -1256,7 +1247,6 @@ export class CriticalPathEngine {
|
|
|
1256
1247
|
payload: { attachment: created }
|
|
1257
1248
|
};
|
|
1258
1249
|
await this.events.publish(event);
|
|
1259
|
-
this.dispatchWebhook('attachment.created', { attachment: created });
|
|
1260
1250
|
return created;
|
|
1261
1251
|
}
|
|
1262
1252
|
async deleteAttachment(id) {
|
|
@@ -1284,10 +1274,14 @@ export class CriticalPathEngine {
|
|
|
1284
1274
|
aggregateId: id,
|
|
1285
1275
|
aggregateType: 'Attachment',
|
|
1286
1276
|
occurredAt: now,
|
|
1287
|
-
payload: {
|
|
1277
|
+
payload: {
|
|
1278
|
+
attachmentId: id,
|
|
1279
|
+
storageKey: existing.storageKey,
|
|
1280
|
+
url: existing.url,
|
|
1281
|
+
projectId: await this.projectIdOfAttachment(existing)
|
|
1282
|
+
}
|
|
1288
1283
|
};
|
|
1289
1284
|
await this.events.publish(event);
|
|
1290
|
-
this.dispatchWebhook('attachment.deleted', { attachmentId: id, taskId: existing.taskId, projectId: existing.projectId });
|
|
1291
1285
|
}
|
|
1292
1286
|
return deleted;
|
|
1293
1287
|
}
|
|
@@ -1387,7 +1381,6 @@ export class CriticalPathEngine {
|
|
|
1387
1381
|
payload: { team: created }
|
|
1388
1382
|
};
|
|
1389
1383
|
await this.events.publish(event);
|
|
1390
|
-
this.dispatchWebhook('team.created', { team: created });
|
|
1391
1384
|
return created;
|
|
1392
1385
|
}
|
|
1393
1386
|
async updateTeam(id, updates) {
|
|
@@ -1427,7 +1420,6 @@ export class CriticalPathEngine {
|
|
|
1427
1420
|
payload: { container: created }
|
|
1428
1421
|
};
|
|
1429
1422
|
await this.events.publish(event);
|
|
1430
|
-
this.dispatchWebhook('container.created', { container: created });
|
|
1431
1423
|
return created;
|
|
1432
1424
|
}
|
|
1433
1425
|
async updateContainer(id, updates) {
|
|
@@ -1460,7 +1452,7 @@ export class CriticalPathEngine {
|
|
|
1460
1452
|
await this.requireProjectAccess('plan.manage', input.projectId);
|
|
1461
1453
|
const project = await this.store.getProject(input.projectId);
|
|
1462
1454
|
if (project?.customFieldDefinitions && input.customFields) {
|
|
1463
|
-
validateCustomFieldValues(project.customFieldDefinitions, input.customFields);
|
|
1455
|
+
validateCustomFieldValues(project.customFieldDefinitions, input.customFields, this.plugins.getCustomFieldTypes());
|
|
1464
1456
|
}
|
|
1465
1457
|
const created = await this.store.createDeliverable({
|
|
1466
1458
|
...input,
|
|
@@ -1484,7 +1476,6 @@ export class CriticalPathEngine {
|
|
|
1484
1476
|
action: 'deliverable.created',
|
|
1485
1477
|
details: { title: created.title, format: created.format }
|
|
1486
1478
|
});
|
|
1487
|
-
this.dispatchWebhook('deliverable.created', { deliverable: created });
|
|
1488
1479
|
return created;
|
|
1489
1480
|
}
|
|
1490
1481
|
async updateDeliverable(id, updates) {
|
|
@@ -1512,11 +1503,6 @@ export class CriticalPathEngine {
|
|
|
1512
1503
|
}
|
|
1513
1504
|
};
|
|
1514
1505
|
await this.events.publish(statusEvent);
|
|
1515
|
-
this.dispatchWebhook('deliverable.status_changed', {
|
|
1516
|
-
deliverable: updated,
|
|
1517
|
-
previousStatus: existing.status,
|
|
1518
|
-
newStatus: updated.status
|
|
1519
|
-
});
|
|
1520
1506
|
}
|
|
1521
1507
|
const updateEvent = {
|
|
1522
1508
|
id: `evt_${Math.random().toString(36).substring(2, 9)}`,
|
|
@@ -1533,7 +1519,6 @@ export class CriticalPathEngine {
|
|
|
1533
1519
|
action: 'deliverable.updated',
|
|
1534
1520
|
details: { title: updated.title, status: updated.status }
|
|
1535
1521
|
});
|
|
1536
|
-
this.dispatchWebhook('deliverable.updated', { deliverable: updated });
|
|
1537
1522
|
}
|
|
1538
1523
|
return updated;
|
|
1539
1524
|
}
|
|
@@ -1560,7 +1545,6 @@ export class CriticalPathEngine {
|
|
|
1560
1545
|
action: 'deliverable.deleted',
|
|
1561
1546
|
details: { title: existing.title }
|
|
1562
1547
|
});
|
|
1563
|
-
this.dispatchWebhook('deliverable.deleted', { deliverableId: id, projectId: existing.projectId });
|
|
1564
1548
|
}
|
|
1565
1549
|
return deleted;
|
|
1566
1550
|
}
|
|
@@ -1634,7 +1618,6 @@ export class CriticalPathEngine {
|
|
|
1634
1618
|
payload: { iteration: created }
|
|
1635
1619
|
};
|
|
1636
1620
|
await this.events.publish(event);
|
|
1637
|
-
this.dispatchWebhook('iteration.started', { iteration: created });
|
|
1638
1621
|
}
|
|
1639
1622
|
return created;
|
|
1640
1623
|
}
|
|
@@ -1656,7 +1639,6 @@ export class CriticalPathEngine {
|
|
|
1656
1639
|
payload: { iteration: updated }
|
|
1657
1640
|
};
|
|
1658
1641
|
await this.events.publish(event);
|
|
1659
|
-
this.dispatchWebhook('iteration.started', { iteration: updated });
|
|
1660
1642
|
}
|
|
1661
1643
|
else if (existing.status !== 'completed' && updated.status === 'completed') {
|
|
1662
1644
|
const event = {
|
|
@@ -1668,7 +1650,6 @@ export class CriticalPathEngine {
|
|
|
1668
1650
|
payload: { iteration: updated }
|
|
1669
1651
|
};
|
|
1670
1652
|
await this.events.publish(event);
|
|
1671
|
-
this.dispatchWebhook('iteration.completed', { iteration: updated });
|
|
1672
1653
|
}
|
|
1673
1654
|
}
|
|
1674
1655
|
return updated;
|
|
@@ -1819,18 +1800,93 @@ export class CriticalPathEngine {
|
|
|
1819
1800
|
schedule
|
|
1820
1801
|
});
|
|
1821
1802
|
}
|
|
1822
|
-
|
|
1803
|
+
// --- Webhooks ---
|
|
1804
|
+
/** Webhooks from `config.webhooks` (static, not editable) plus those in the store. */
|
|
1805
|
+
async listAllWebhooks() {
|
|
1806
|
+
const fromConfig = (this.config.webhooks ?? []).map((w, i) => ({
|
|
1807
|
+
...w,
|
|
1808
|
+
id: `config_${i}`,
|
|
1809
|
+
createdAt: new Date(0).toISOString()
|
|
1810
|
+
}));
|
|
1811
|
+
return [...fromConfig, ...(await this.store.getWebhooks())];
|
|
1812
|
+
}
|
|
1813
|
+
toPublicWebhook({ secret, ...webhook }) {
|
|
1814
|
+
return { ...webhook, hasSecret: !!secret };
|
|
1815
|
+
}
|
|
1816
|
+
/** Webhooks in the actor's tenant, with secrets redacted. Requires `workspace.manage` on views. */
|
|
1817
|
+
async getWebhooks() {
|
|
1818
|
+
await this.requireWorkspaceAccess('workspace.manage');
|
|
1823
1819
|
const webhooks = await this.store.getWebhooks();
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1820
|
+
return webhooks.filter((w) => this.inActorTenant(w)).map((w) => this.toPublicWebhook(w));
|
|
1821
|
+
}
|
|
1822
|
+
async getWebhook(id) {
|
|
1823
|
+
await this.requireWorkspaceAccess('workspace.manage');
|
|
1824
|
+
const webhook = await this.store.getWebhook(id);
|
|
1825
|
+
return webhook && this.inActorTenant(webhook) ? this.toPublicWebhook(webhook) : null;
|
|
1826
|
+
}
|
|
1827
|
+
/**
|
|
1828
|
+
* Registers a webhook. The signing secret is returned only here; generate one by omitting
|
|
1829
|
+
* `secret`. Deliveries carry `X-CriticalPath-Signature` (see `verifyWebhookSignature`).
|
|
1830
|
+
*/
|
|
1831
|
+
async createWebhook(input) {
|
|
1832
|
+
await this.requireWorkspaceAccess('workspace.manage');
|
|
1833
|
+
this.validateWebhookUrl(input.url);
|
|
1834
|
+
const secret = input.secret ?? generateWebhookSecret();
|
|
1835
|
+
const created = await this.store.addWebhook({
|
|
1836
|
+
name: input.name,
|
|
1837
|
+
url: input.url,
|
|
1838
|
+
events: input.events,
|
|
1839
|
+
secret,
|
|
1840
|
+
active: input.active ?? true,
|
|
1841
|
+
tenantId: this.actor?.tenantId
|
|
1842
|
+
});
|
|
1843
|
+
this.webhooks.invalidate();
|
|
1844
|
+
return { webhook: this.toPublicWebhook(created), secret };
|
|
1845
|
+
}
|
|
1846
|
+
async updateWebhook(id, updates) {
|
|
1847
|
+
if (!(await this.getWebhook(id)))
|
|
1848
|
+
return null;
|
|
1849
|
+
if (updates.url)
|
|
1850
|
+
this.validateWebhookUrl(updates.url);
|
|
1851
|
+
const { name, url, events, active, secret } = updates;
|
|
1852
|
+
const updated = await this.store.updateWebhook(id, Object.fromEntries(Object.entries({ name, url, events, active, secret }).filter(([, v]) => v !== undefined)));
|
|
1853
|
+
this.webhooks.invalidate();
|
|
1854
|
+
return updated ? this.toPublicWebhook(updated) : null;
|
|
1855
|
+
}
|
|
1856
|
+
async deleteWebhook(id) {
|
|
1857
|
+
if (!(await this.getWebhook(id)))
|
|
1858
|
+
return false;
|
|
1859
|
+
const deleted = await this.store.deleteWebhook(id);
|
|
1860
|
+
this.webhooks.invalidate();
|
|
1861
|
+
return deleted;
|
|
1862
|
+
}
|
|
1863
|
+
validateWebhookUrl(url) {
|
|
1864
|
+
try {
|
|
1865
|
+
assertWebhookUrl(url, this.config.webhookDelivery?.allowPrivateUrls);
|
|
1833
1866
|
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
throw new ValidationError(err.message);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
/** Finds the tenant an event belongs to, from the entity in its payload or its project. */
|
|
1872
|
+
async resolveEventTenant(event) {
|
|
1873
|
+
const p = event.payload;
|
|
1874
|
+
const owner = p.project ?? p.workflow ?? p.team;
|
|
1875
|
+
if (owner)
|
|
1876
|
+
return owner.tenantId ?? undefined;
|
|
1877
|
+
// Deleted projects and workflows carry their tenant in the payload.
|
|
1878
|
+
if ('tenantId' in p)
|
|
1879
|
+
return p.tenantId ?? undefined;
|
|
1880
|
+
const projectId = p.projectId ??
|
|
1881
|
+
p.task?.projectId ??
|
|
1882
|
+
p.deliverable?.projectId ??
|
|
1883
|
+
p.iteration?.projectId ??
|
|
1884
|
+
p.container?.projectId ??
|
|
1885
|
+
p.attachment?.projectId ??
|
|
1886
|
+
(await this.projectIdOfTask(p.taskId ?? p.comment?.taskId ?? p.attachment?.taskId ?? p.dependency?.taskId ?? p.timeEntry?.taskId));
|
|
1887
|
+
if (!projectId)
|
|
1888
|
+
return undefined;
|
|
1889
|
+
return (await this.store.getProject(projectId))?.tenantId ?? undefined;
|
|
1834
1890
|
}
|
|
1835
1891
|
}
|
|
1836
1892
|
//# sourceMappingURL=index.js.map
|