@critical-path/core 0.25.0 → 0.26.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 +24 -0
- package/README.md +12 -0
- package/dist/domain/authorization.d.ts +23 -0
- package/dist/domain/authorization.d.ts.map +1 -0
- package/dist/domain/authorization.js +62 -0
- package/dist/domain/authorization.js.map +1 -0
- package/dist/domain/entities.d.ts +2 -2
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/errors.d.ts +6 -0
- package/dist/domain/errors.d.ts.map +1 -1
- package/dist/domain/errors.js +9 -0
- package/dist/domain/errors.js.map +1 -1
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.d.ts.map +1 -1
- package/dist/domain/index.js +1 -0
- package/dist/domain/index.js.map +1 -1
- package/dist/engine/authorization.test.d.ts +2 -0
- package/dist/engine/authorization.test.d.ts.map +1 -0
- package/dist/engine/authorization.test.js +150 -0
- package/dist/engine/authorization.test.js.map +1 -0
- package/dist/engine/index.d.ts +37 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +302 -48
- package/dist/engine/index.js.map +1 -1
- package/dist/schemas/index.d.ts +40 -6
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +5 -1
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/schemas.test.js.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/sqlite.js +20 -9
- package/dist/store/sqlite.js.map +1 -1
- package/dist/types/index.d.ts +45 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/domain/authorization.ts +82 -0
- package/src/domain/entities.ts +2 -1
- package/src/domain/errors.ts +10 -0
- package/src/domain/index.ts +1 -0
- package/src/engine/authorization.test.ts +191 -0
- package/src/engine/index.ts +295 -49
- package/src/schemas/index.ts +5 -1
- package/src/schemas/schemas.test.ts +3 -2
- package/src/store/sqlite.ts +20 -6
- package/src/types/index.ts +66 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/schemas/index.ts
CHANGED
|
@@ -111,7 +111,9 @@ export const CreateProjectSchema = strictObject({
|
|
|
111
111
|
name: nonEmpty,
|
|
112
112
|
description: z.string().optional(),
|
|
113
113
|
ownerId: z.string().optional(),
|
|
114
|
-
members: z
|
|
114
|
+
members: z
|
|
115
|
+
.array(strictObject({ userId: nonEmpty, role: z.enum(['admin', 'project_manager', 'contributor', 'viewer']) }))
|
|
116
|
+
.optional(),
|
|
115
117
|
teamIds: z.array(z.string()).optional(),
|
|
116
118
|
workflowId: z.string().optional(),
|
|
117
119
|
taskTypes: z.array(TaskTypeDefinitionSchema).optional(),
|
|
@@ -303,6 +305,8 @@ export const UploadAttachmentSchema = strictObject({
|
|
|
303
305
|
});
|
|
304
306
|
|
|
305
307
|
export const PresignAttachmentSchema = strictObject({
|
|
308
|
+
/** Project the upload belongs to; scoped callers may only write under `projects/<projectId>/`. */
|
|
309
|
+
projectId: nonEmpty,
|
|
306
310
|
storageKey: nonEmpty,
|
|
307
311
|
expiresInSeconds: z.number().int().positive().optional(),
|
|
308
312
|
contentType: z.string().optional()
|
|
@@ -46,7 +46,8 @@ type SameKeys<Schema, Expected> = [Exclude<keyof Schema, keyof Expected>, Exclud
|
|
|
46
46
|
: { extraInSchema: Exclude<keyof Schema, keyof Expected>; missingFromSchema: Exclude<keyof Expected, keyof Schema> };
|
|
47
47
|
|
|
48
48
|
type Infer<S extends z.ZodTypeAny> = z.infer<S>;
|
|
49
|
-
|
|
49
|
+
// tenantId is stamped from the actor by the engine, never accepted from payloads.
|
|
50
|
+
type ServerAssigned = 'id' | 'createdAt' | 'updatedAt' | 'tenantId';
|
|
50
51
|
// Identity is resolved by the server (withActor), never accepted in payloads.
|
|
51
52
|
type CommentIdentity = 'authorId' | 'authorType';
|
|
52
53
|
type UploaderIdentity = 'uploaderId' | 'uploaderType';
|
|
@@ -55,7 +56,7 @@ const keyChecks: true[] = [
|
|
|
55
56
|
true as SameKeys<Infer<typeof CreateTaskSchema>, Omit<CreateTaskInput, 'key'>>,
|
|
56
57
|
true as SameKeys<Infer<typeof UpdateTaskSchema>, Omit<Task, ServerAssigned | 'key' | 'projectId'>>,
|
|
57
58
|
// `workflow` is a denormalised copy resolved from `workflowId`, so it is not writable.
|
|
58
|
-
true as SameKeys<Infer<typeof CreateProjectSchema>, Omit<CreateProjectInput, 'workflow'>>,
|
|
59
|
+
true as SameKeys<Infer<typeof CreateProjectSchema>, Omit<CreateProjectInput, 'workflow' | 'tenantId'>>,
|
|
59
60
|
true as SameKeys<Infer<typeof UpdateProjectSchema>, Omit<Project, ServerAssigned | 'workflow'>>,
|
|
60
61
|
true as SameKeys<Infer<typeof CreateWorkflowSchema>, Omit<Workflow, ServerAssigned>>,
|
|
61
62
|
true as SameKeys<Infer<typeof CreateDeliverableSchema>, CreateDeliverableInput>,
|
package/src/store/sqlite.ts
CHANGED
|
@@ -279,6 +279,14 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
279
279
|
} catch {
|
|
280
280
|
// Column may already exist
|
|
281
281
|
}
|
|
282
|
+
|
|
283
|
+
for (const table of ['projects', 'workflows', 'teams']) {
|
|
284
|
+
try {
|
|
285
|
+
this.db.exec(`ALTER TABLE ${table} ADD COLUMN tenantId TEXT`);
|
|
286
|
+
} catch {
|
|
287
|
+
// Column may already exist
|
|
288
|
+
}
|
|
289
|
+
}
|
|
282
290
|
}
|
|
283
291
|
|
|
284
292
|
// --- Projects ---
|
|
@@ -301,8 +309,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
301
309
|
const newProj: Project = { ...project, key, id, createdAt: now, updatedAt: now };
|
|
302
310
|
|
|
303
311
|
const stmt = this.db.prepare(`
|
|
304
|
-
INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, workflowId, taskTypes, statusDefinitions, priorityDefinitions, customFieldDefinitions, createdAt, updatedAt)
|
|
305
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
312
|
+
INSERT INTO projects (id, key, name, description, ownerId, members, teamIds, workflowId, taskTypes, statusDefinitions, priorityDefinitions, customFieldDefinitions, tenantId, createdAt, updatedAt)
|
|
313
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
306
314
|
`);
|
|
307
315
|
stmt.run(
|
|
308
316
|
newProj.id,
|
|
@@ -317,6 +325,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
317
325
|
JSON.stringify(newProj.statusDefinitions || []),
|
|
318
326
|
JSON.stringify(newProj.priorityDefinitions || []),
|
|
319
327
|
JSON.stringify(newProj.customFieldDefinitions || []),
|
|
328
|
+
newProj.tenantId ?? null,
|
|
320
329
|
newProj.createdAt,
|
|
321
330
|
newProj.updatedAt
|
|
322
331
|
);
|
|
@@ -375,8 +384,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
375
384
|
const newWf: Workflow = { ...workflow, id, createdAt: now, updatedAt: now };
|
|
376
385
|
|
|
377
386
|
const stmt = this.db.prepare(`
|
|
378
|
-
INSERT INTO workflows (id, name, description, statuses, transitions, taskTypes, defaultStatusKey, isDefault, createdAt, updatedAt)
|
|
379
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
387
|
+
INSERT INTO workflows (id, name, description, statuses, transitions, taskTypes, defaultStatusKey, isDefault, tenantId, createdAt, updatedAt)
|
|
388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
380
389
|
`);
|
|
381
390
|
stmt.run(
|
|
382
391
|
newWf.id,
|
|
@@ -387,6 +396,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
387
396
|
newWf.taskTypes ? JSON.stringify(newWf.taskTypes) : null,
|
|
388
397
|
newWf.defaultStatusKey || null,
|
|
389
398
|
newWf.isDefault ? 1 : 0,
|
|
399
|
+
newWf.tenantId ?? null,
|
|
390
400
|
newWf.createdAt,
|
|
391
401
|
newWf.updatedAt
|
|
392
402
|
);
|
|
@@ -452,8 +462,8 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
452
462
|
const newTeam: Team = { ...team, id, createdAt: now, updatedAt: now };
|
|
453
463
|
|
|
454
464
|
const stmt = this.db.prepare(`
|
|
455
|
-
INSERT INTO teams (id, name, description, leaderId, memberIds, createdAt, updatedAt)
|
|
456
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
465
|
+
INSERT INTO teams (id, name, description, leaderId, memberIds, tenantId, createdAt, updatedAt)
|
|
466
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
457
467
|
`);
|
|
458
468
|
stmt.run(
|
|
459
469
|
newTeam.id,
|
|
@@ -461,6 +471,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
461
471
|
newTeam.description || null,
|
|
462
472
|
newTeam.leaderId || null,
|
|
463
473
|
JSON.stringify(newTeam.memberIds || []),
|
|
474
|
+
newTeam.tenantId ?? null,
|
|
464
475
|
newTeam.createdAt,
|
|
465
476
|
newTeam.updatedAt
|
|
466
477
|
);
|
|
@@ -1195,6 +1206,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1195
1206
|
private mapProject(row: any): Project {
|
|
1196
1207
|
return {
|
|
1197
1208
|
...row,
|
|
1209
|
+
tenantId: row.tenantId ?? undefined,
|
|
1198
1210
|
workflowId: row.workflowId || undefined,
|
|
1199
1211
|
taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
|
|
1200
1212
|
members: row.members ? JSON.parse(row.members) : [],
|
|
@@ -1208,6 +1220,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1208
1220
|
private mapWorkflow(row: any): Workflow {
|
|
1209
1221
|
return {
|
|
1210
1222
|
...row,
|
|
1223
|
+
tenantId: row.tenantId ?? undefined,
|
|
1211
1224
|
statuses: row.statuses ? JSON.parse(row.statuses) : [],
|
|
1212
1225
|
transitions: row.transitions ? JSON.parse(row.transitions) : [],
|
|
1213
1226
|
taskTypes: row.taskTypes ? JSON.parse(row.taskTypes) : undefined,
|
|
@@ -1218,6 +1231,7 @@ export class SQLiteStore implements StorageAdapter {
|
|
|
1218
1231
|
private mapTeam(row: any): Team {
|
|
1219
1232
|
return {
|
|
1220
1233
|
...row,
|
|
1234
|
+
tenantId: row.tenantId ?? undefined,
|
|
1221
1235
|
memberIds: row.memberIds ? JSON.parse(row.memberIds) : []
|
|
1222
1236
|
};
|
|
1223
1237
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -78,6 +78,8 @@ export interface Team {
|
|
|
78
78
|
memberIds: string[];
|
|
79
79
|
weeklyCapacityHours?: number;
|
|
80
80
|
schedule?: WorkSchedule;
|
|
81
|
+
/** Tenant that owns the team. Set by the engine from the creating actor. */
|
|
82
|
+
tenantId?: string;
|
|
81
83
|
createdAt: string;
|
|
82
84
|
updatedAt: string;
|
|
83
85
|
}
|
|
@@ -117,6 +119,8 @@ export interface Workflow {
|
|
|
117
119
|
taskTypes?: TaskTypeDefinition[];
|
|
118
120
|
defaultStatusKey?: string;
|
|
119
121
|
isDefault?: boolean;
|
|
122
|
+
/** Tenant that owns the workflow. Set by the engine from the creating actor. */
|
|
123
|
+
tenantId?: string;
|
|
120
124
|
createdAt: string;
|
|
121
125
|
updatedAt: string;
|
|
122
126
|
}
|
|
@@ -127,7 +131,10 @@ export interface Project {
|
|
|
127
131
|
name: string;
|
|
128
132
|
description?: string;
|
|
129
133
|
ownerId?: string;
|
|
130
|
-
|
|
134
|
+
/** Project membership and roles, used by role-based authorization policies. */
|
|
135
|
+
members?: ProjectMember[];
|
|
136
|
+
/** Tenant that owns the project. Set by the engine from the creating actor; never from payloads. */
|
|
137
|
+
tenantId?: string;
|
|
131
138
|
teamIds?: string[]; // team IDs
|
|
132
139
|
workflowId?: string;
|
|
133
140
|
workflow?: Workflow;
|
|
@@ -305,6 +312,21 @@ export interface Actor {
|
|
|
305
312
|
userId: string;
|
|
306
313
|
username?: string;
|
|
307
314
|
actorType?: AuthorType;
|
|
315
|
+
/**
|
|
316
|
+
* Tenant the actor belongs to. When set, the actor only sees projects, workflows and teams
|
|
317
|
+
* with the same `tenantId`, and everything it creates is stamped with it.
|
|
318
|
+
*/
|
|
319
|
+
tenantId?: string;
|
|
320
|
+
/** Workspace-wide roles, e.g. `['admin']`. Interpreted by the authorization policy. */
|
|
321
|
+
roles?: string[];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** A user's role on a project. */
|
|
325
|
+
export type ProjectRole = Role;
|
|
326
|
+
|
|
327
|
+
export interface ProjectMember {
|
|
328
|
+
userId: string;
|
|
329
|
+
role: ProjectRole;
|
|
308
330
|
}
|
|
309
331
|
|
|
310
332
|
export interface CommentReaction {
|
|
@@ -462,6 +484,13 @@ export interface CriticalPathPlugin {
|
|
|
462
484
|
}
|
|
463
485
|
|
|
464
486
|
export interface CriticalPathConfig {
|
|
487
|
+
/**
|
|
488
|
+
* Authorization policy applied to every call made through a `withActor` view (which is how
|
|
489
|
+
* `@critical-path/server` and the MCP server call the engine). Calls on the base engine are
|
|
490
|
+
* trusted. Use `createRolePolicy()` for project-membership roles, or supply your own.
|
|
491
|
+
* When omitted, actors may do anything within their tenant.
|
|
492
|
+
*/
|
|
493
|
+
authorize?: AuthorizationPolicy;
|
|
465
494
|
store?: 'memory' | 'sqlite' | unknown;
|
|
466
495
|
fileStorage?: FileStorageAdapter;
|
|
467
496
|
plugins?: CriticalPathPlugin[];
|
|
@@ -742,3 +771,39 @@ export interface WorkloadDistributionOptions {
|
|
|
742
771
|
teamSchedules?: Record<string, WorkSchedule>;
|
|
743
772
|
}
|
|
744
773
|
|
|
774
|
+
// ==========================================
|
|
775
|
+
// Authorization
|
|
776
|
+
// ==========================================
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Operations the engine authorizes. Project-scoped actions are checked against a project;
|
|
780
|
+
* `project.create` and `workspace.manage` (workflows, teams, webhooks) are workspace-level.
|
|
781
|
+
*/
|
|
782
|
+
export type AuthorizationAction =
|
|
783
|
+
| 'project.read'
|
|
784
|
+
| 'project.create'
|
|
785
|
+
| 'project.update'
|
|
786
|
+
| 'project.delete'
|
|
787
|
+
| 'project.manage_members'
|
|
788
|
+
| 'task.create'
|
|
789
|
+
| 'task.update'
|
|
790
|
+
| 'task.delete'
|
|
791
|
+
| 'comment.create'
|
|
792
|
+
| 'comment.moderate'
|
|
793
|
+
| 'attachment.create'
|
|
794
|
+
| 'attachment.delete'
|
|
795
|
+
| 'time.log'
|
|
796
|
+
| 'plan.manage'
|
|
797
|
+
| 'workspace.manage';
|
|
798
|
+
|
|
799
|
+
export interface AuthorizationRequest {
|
|
800
|
+
actor: Actor;
|
|
801
|
+
action: AuthorizationAction;
|
|
802
|
+
/** The project the action targets, for project-scoped actions. */
|
|
803
|
+
project?: Project;
|
|
804
|
+
/** The specific record, when relevant (e.g. the comment being edited). */
|
|
805
|
+
resource?: { type: 'task' | 'comment' | 'attachment'; ownerId?: string };
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export type AuthorizationPolicy = (request: AuthorizationRequest) => boolean | Promise<boolean>;
|
|
809
|
+
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/errors.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/actor.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/schemas/index.ts","./src/schemas/schemas.test.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|
|
1
|
+
{"root":["./src/deliverables.test.ts","./src/index.test.ts","./src/index.ts","./src/status.test.ts","./src/task-state.test.ts","./src/use-case-validation.test.ts","./src/domain/authorization.ts","./src/domain/calendar.test.ts","./src/domain/calendar.ts","./src/domain/cpm.test.ts","./src/domain/cpm.ts","./src/domain/custom-fields.ts","./src/domain/ddd.test.ts","./src/domain/entities.ts","./src/domain/errors.ts","./src/domain/events.ts","./src/domain/graph.ts","./src/domain/index.ts","./src/domain/ladder.test.ts","./src/domain/ladder.ts","./src/domain/metrics.test.ts","./src/domain/metrics.ts","./src/domain/task-duration.test.ts","./src/domain/workload.test.ts","./src/domain/workload.ts","./src/engine/actor.test.ts","./src/engine/authorization.test.ts","./src/engine/index.ts","./src/plugins/index.ts","./src/schemas/index.ts","./src/schemas/schemas.test.ts","./src/storage/file-storage.ts","./src/storage/firebase-storage.ts","./src/storage/index.ts","./src/storage/s3.ts","./src/storage/storage.test.ts","./src/store/firebase.test.ts","./src/store/firebase.ts","./src/store/index.ts","./src/store/sqlite.test.ts","./src/store/sqlite.ts","./src/types/index.ts","./src/utils/fractional-index.test.ts","./src/utils/fractional-index.ts","./src/utils/index.ts","./src/utils/key.test.ts","./src/utils/key.ts","./src/utils/mentions.test.ts","./src/utils/mentions.ts","./src/utils/status.ts","./src/utils/workflow.ts"],"version":"5.9.3"}
|