@plandesk/api 0.20.0 → 1.0.0-beta.2

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 (101) hide show
  1. package/dist/access-control.d.ts +150 -0
  2. package/dist/access-control.js +63 -0
  3. package/dist/agent-keys.d.ts +81 -0
  4. package/dist/agent-keys.js +181 -0
  5. package/dist/auth-context.d.ts +60 -0
  6. package/dist/auth-context.js +41 -0
  7. package/dist/auth.d.ts +33 -0
  8. package/dist/auth.js +357 -0
  9. package/dist/better-auth.d.ts +66 -0
  10. package/dist/better-auth.js +106 -0
  11. package/dist/github.d.ts +57 -0
  12. package/dist/github.js +115 -0
  13. package/dist/hosted-auth.d.ts +23 -0
  14. package/dist/hosted-auth.js +49 -0
  15. package/dist/identity.d.ts +38 -0
  16. package/dist/identity.js +163 -0
  17. package/dist/index.d.ts +13 -2
  18. package/dist/index.js +15 -3
  19. package/dist/invitations.d.ts +94 -0
  20. package/dist/invitations.js +211 -0
  21. package/dist/organizations.d.ts +32 -0
  22. package/dist/organizations.js +63 -0
  23. package/dist/permissions.d.ts +19 -0
  24. package/dist/permissions.js +42 -0
  25. package/dist/projection.d.ts +1 -1
  26. package/dist/projection.js +5 -5
  27. package/dist/routes/agent-runs.js +3 -3
  28. package/dist/routes/artifacts.js +6 -6
  29. package/dist/routes/auth.d.ts +13 -0
  30. package/dist/routes/auth.js +83 -0
  31. package/dist/routes/canvas.js +5 -5
  32. package/dist/routes/comments.js +11 -11
  33. package/dist/routes/documents.js +10 -10
  34. package/dist/routes/folders.js +8 -8
  35. package/dist/routes/goals.js +11 -11
  36. package/dist/routes/notes.js +8 -8
  37. package/dist/routes/orgs.d.ts +17 -0
  38. package/dist/routes/orgs.js +241 -0
  39. package/dist/routes/projects.js +13 -13
  40. package/dist/routes/shares.js +92 -3
  41. package/dist/routes/submissions.js +7 -8
  42. package/dist/routes/tags.js +6 -6
  43. package/dist/routes/tasks.js +14 -3
  44. package/dist/routes/tokens.js +4 -4
  45. package/dist/serialize.d.ts +3 -3
  46. package/dist/server.d.ts +20 -2
  47. package/dist/server.js +57 -9
  48. package/dist/services/agent-runs.d.ts +13 -14
  49. package/dist/services/agent-runs.js +39 -41
  50. package/dist/services/artifacts.d.ts +6 -7
  51. package/dist/services/artifacts.js +30 -26
  52. package/dist/services/canvas.d.ts +11 -12
  53. package/dist/services/canvas.js +58 -41
  54. package/dist/services/comments.d.ts +11 -12
  55. package/dist/services/comments.js +44 -64
  56. package/dist/services/documents.d.ts +10 -11
  57. package/dist/services/documents.js +74 -55
  58. package/dist/services/files.d.ts +2 -1
  59. package/dist/services/files.js +13 -5
  60. package/dist/services/folders.d.ts +7 -8
  61. package/dist/services/folders.js +45 -33
  62. package/dist/services/goals.d.ts +24 -25
  63. package/dist/services/goals.js +60 -53
  64. package/dist/services/index.d.ts +2 -5
  65. package/dist/services/index.js +15 -20
  66. package/dist/services/notes.d.ts +7 -8
  67. package/dist/services/notes.js +35 -23
  68. package/dist/services/org-scope.d.ts +22 -0
  69. package/dist/services/org-scope.js +38 -0
  70. package/dist/services/projects.d.ts +12 -13
  71. package/dist/services/projects.js +90 -58
  72. package/dist/services/scope.d.ts +12 -0
  73. package/dist/services/scope.js +29 -0
  74. package/dist/services/share.d.ts +74 -10
  75. package/dist/services/share.js +213 -23
  76. package/dist/services/sync.d.ts +7 -29
  77. package/dist/services/sync.js +37 -142
  78. package/dist/services/tags.d.ts +6 -7
  79. package/dist/services/tags.js +34 -22
  80. package/dist/services/tasks.d.ts +24 -17
  81. package/dist/services/tasks.js +97 -58
  82. package/dist/services/tokens.d.ts +6 -5
  83. package/dist/services/tokens.js +13 -6
  84. package/dist/session.d.ts +5 -0
  85. package/dist/session.js +8 -0
  86. package/dist/storage/local.js +16 -10
  87. package/dist/storage/s3.js +11 -6
  88. package/dist/test-ba-org.d.ts +16 -0
  89. package/dist/test-ba-org.js +35 -0
  90. package/dist/test-helpers.d.ts +21 -5
  91. package/dist/test-helpers.js +45 -8
  92. package/dist/vercel.d.ts +3 -0
  93. package/dist/vercel.js +62 -0
  94. package/dist/worker.d.ts +44 -0
  95. package/dist/worker.js +106 -0
  96. package/package.json +8 -5
  97. package/web/assets/index-D29SyYFS.css +2 -0
  98. package/web/assets/index-tBJ_K3ur.js +358 -0
  99. package/web/index.html +2 -2
  100. package/web/assets/index-CeBvZ-aq.css +0 -2
  101. package/web/assets/index-CuRAxImC.js +0 -358
@@ -1,5 +1,7 @@
1
- import { createGoal, createTask, getGoal, getProject, InvalidGoalStatusError, isGoalStatus, listGoals, listTagsByTaskForProject, listTasks, updateGoal, updateGoalStatus, updateTask, } from '@plandesk/db';
1
+ import { withTransaction, createGoal, createTask, getGoal, getProject, InvalidGoalStatusError, isGoalStatus, listGoals, listTagsByTaskForProject, listTasks, updateGoal, updateGoalStatus, updateTask, } from '@plandesk/db';
2
2
  import { serializeGoal, serializeTask } from '../serialize.js';
3
+ import { assertPermission, resolveOrgId } from './org-scope.js';
4
+ import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
3
5
  export class InvalidGoalTransitionError extends Error {
4
6
  constructor(message) {
5
7
  super(message);
@@ -112,17 +114,17 @@ function validateVerificationSurfaceInput(raw) {
112
114
  function acceptanceBlockMarker(goalId) {
113
115
  return `Acceptance-block-for: ${goalId}`;
114
116
  }
115
- function hasUnresolvedBlockingTask(db, projectId, goalId) {
117
+ async function hasUnresolvedBlockingTask(db, projectId, goalId) {
116
118
  const marker = acceptanceBlockMarker(goalId);
117
- return listTasks(db, projectId).some((task) => task.goalId === goalId && task.status === 'scope' && task.description?.includes(marker));
119
+ return (await listTasks(db, projectId)).some((task) => task.goalId === goalId && task.status === 'scope' && task.description?.includes(marker));
118
120
  }
119
121
  // When acceptance finally passes, the remediation task filed on the earlier red
120
122
  // result is obsolete — resolve it so a completed goal never shows open work.
121
- function resolveBlockingTasks(db, projectId, goalId) {
123
+ async function resolveBlockingTasks(db, projectId, goalId) {
122
124
  const marker = acceptanceBlockMarker(goalId);
123
- for (const task of listTasks(db, projectId)) {
125
+ for (const task of await listTasks(db, projectId)) {
124
126
  if (task.goalId === goalId && task.status !== 'done' && task.description?.includes(marker)) {
125
- updateTask(db, task.id, { status: 'done' });
127
+ await updateTask(db, task.id, { status: 'done' });
126
128
  }
127
129
  }
128
130
  }
@@ -139,15 +141,12 @@ function buildBlockingTaskDescription(goalId, surfaceKind, detail) {
139
141
  }
140
142
  return lines.join('\n');
141
143
  }
142
- function cycleTasksForGoal(db, projectId, goalId) {
143
- const tagsByTask = listTagsByTaskForProject(db, projectId);
144
- return listTasks(db, projectId)
144
+ async function cycleTasksForGoal(db, projectId, goalId) {
145
+ const tagsByTask = await listTagsByTaskForProject(db, projectId);
146
+ return (await listTasks(db, projectId))
145
147
  .filter((task) => task.goalId === goalId)
146
148
  .map((task) => serializeTask(task, tagsByTask.get(task.id) ?? []));
147
149
  }
148
- function emitGoalUpdated(eventBus, goalId, projectId) {
149
- eventBus.emit({ type: 'goal_updated', goalId, projectId });
150
- }
151
150
  function recordLastVerification(at, green, kind, detail) {
152
151
  const record = { at, green, kind };
153
152
  if (detail) {
@@ -156,18 +155,24 @@ function recordLastVerification(at, green, kind, detail) {
156
155
  return JSON.stringify(record);
157
156
  }
158
157
  export function createGoalService(deps) {
159
- const { db, eventBus } = deps;
158
+ const { db } = deps;
160
159
  return {
161
- create(projectId, input) {
160
+ async create(projectId, input) {
161
+ assertPermission(deps, 'goal', 'create');
162
162
  if (input.status !== undefined && !isGoalStatus(input.status)) {
163
163
  throw new InvalidGoalStatusError(input.status);
164
164
  }
165
165
  validateVerificationSurfaceInput(input.verificationSurface);
166
- const project = getProject(db, projectId);
167
- if (!project) {
168
- return undefined;
166
+ try {
167
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
168
+ }
169
+ catch (error) {
170
+ if (error instanceof ProjectNotInOrgError) {
171
+ return undefined;
172
+ }
173
+ throw error;
169
174
  }
170
- const goal = db.transaction((tx) => createGoal(tx, {
175
+ const goal = await withTransaction(db, async (tx) => createGoal(tx, {
171
176
  projectId,
172
177
  objective: input.objective,
173
178
  status: input.status,
@@ -178,71 +183,76 @@ export function createGoalService(deps) {
178
183
  stopCondition: input.stopCondition,
179
184
  budget: input.budget,
180
185
  }));
181
- emitGoalUpdated(eventBus, goal.id, projectId);
182
186
  return serializeGoal(goal);
183
187
  },
184
- get(goalId) {
185
- const goal = getGoal(db, goalId);
188
+ async get(goalId) {
189
+ const goal = await getGoal(db, goalId);
186
190
  if (!goal) {
187
191
  return undefined;
188
192
  }
189
193
  return {
190
194
  ...serializeGoal(goal),
191
- cycle_tasks: cycleTasksForGoal(db, goal.projectId, goalId),
195
+ cycle_tasks: await cycleTasksForGoal(db, goal.projectId, goalId),
192
196
  };
193
197
  },
194
- listByProject(projectId) {
195
- const project = getProject(db, projectId);
196
- if (!project) {
197
- return undefined;
198
+ async listByProject(projectId) {
199
+ try {
200
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
201
+ }
202
+ catch (error) {
203
+ if (error instanceof ProjectNotInOrgError) {
204
+ return undefined;
205
+ }
206
+ throw error;
198
207
  }
199
- return listGoals(db, projectId).map(serializeGoal);
208
+ return (await listGoals(db, projectId)).map(serializeGoal);
200
209
  },
201
- update(goalId, input) {
202
- const existing = getGoal(db, goalId);
210
+ async update(goalId, input) {
211
+ assertPermission(deps, 'goal', 'update');
212
+ const existing = await getGoal(db, goalId);
203
213
  if (!existing) {
204
214
  return undefined;
205
215
  }
206
216
  validateVerificationSurfaceInput(input.verificationSurface);
207
- const goal = db.transaction((tx) => updateGoal(tx, goalId, input));
217
+ const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, input));
208
218
  if (!goal) {
209
219
  return undefined;
210
220
  }
211
- emitGoalUpdated(eventBus, goalId, existing.projectId);
212
221
  return serializeGoal(goal);
213
222
  },
214
- pause(goalId) {
215
- const existing = getGoal(db, goalId);
223
+ async pause(goalId) {
224
+ assertPermission(deps, 'goal', 'update');
225
+ const existing = await getGoal(db, goalId);
216
226
  if (!existing) {
217
227
  return undefined;
218
228
  }
219
229
  if (existing.status !== 'active') {
220
230
  throw new InvalidGoalTransitionError('Goal can only be paused from active status');
221
231
  }
222
- const goal = db.transaction((tx) => updateGoalStatus(tx, goalId, 'paused'));
232
+ const goal = await withTransaction(db, async (tx) => updateGoalStatus(tx, goalId, 'paused'));
223
233
  if (!goal) {
224
234
  return undefined;
225
235
  }
226
- emitGoalUpdated(eventBus, goalId, existing.projectId);
227
236
  return serializeGoal(goal);
228
237
  },
229
- resume(goalId) {
230
- const existing = getGoal(db, goalId);
238
+ async resume(goalId) {
239
+ assertPermission(deps, 'goal', 'update');
240
+ const existing = await getGoal(db, goalId);
231
241
  if (!existing) {
232
242
  return undefined;
233
243
  }
234
244
  if (existing.status !== 'paused') {
235
245
  throw new InvalidGoalTransitionError('Goal can only be resumed from paused status');
236
246
  }
237
- const goal = db.transaction((tx) => updateGoalStatus(tx, goalId, 'active'));
247
+ const goal = await withTransaction(db, async (tx) => updateGoalStatus(tx, goalId, 'active'));
238
248
  if (!goal) {
239
249
  return undefined;
240
250
  }
241
- emitGoalUpdated(eventBus, goalId, existing.projectId);
242
251
  return serializeGoal(goal);
243
252
  },
244
- complete(goalId, evidence) {
245
- const existing = getGoal(db, goalId);
253
+ async complete(goalId, evidence) {
254
+ assertPermission(deps, 'goal', 'update');
255
+ const existing = await getGoal(db, goalId);
246
256
  if (!existing) {
247
257
  return undefined;
248
258
  }
@@ -250,7 +260,7 @@ export function createGoalService(deps) {
250
260
  throw new InvalidGoalTransitionError('Goal is already complete');
251
261
  }
252
262
  if (existing.status !== 'blocked') {
253
- const incomplete = listTasks(db, existing.projectId)
263
+ const incomplete = (await listTasks(db, existing.projectId))
254
264
  .filter((task) => task.goalId === goalId && task.status !== 'done')
255
265
  .map((task) => task.id);
256
266
  if (incomplete.length > 0) {
@@ -260,14 +270,13 @@ export function createGoalService(deps) {
260
270
  const surface = parseVerificationSurface(existing.verificationSurface);
261
271
  const at = new Date().toISOString();
262
272
  if (!surface) {
263
- const goal = db.transaction((tx) => updateGoal(tx, goalId, {
273
+ const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, {
264
274
  status: 'complete',
265
275
  lastVerification: recordLastVerification(at, true, null),
266
276
  }));
267
277
  if (!goal) {
268
278
  return undefined;
269
279
  }
270
- emitGoalUpdated(eventBus, goalId, existing.projectId);
271
280
  return serializeGoal(goal);
272
281
  }
273
282
  if (!evidence || evidence.kind !== surface.kind) {
@@ -276,33 +285,32 @@ export function createGoalService(deps) {
276
285
  const evaluation = evaluateEvidence(surface, evidence);
277
286
  const lastVerification = recordLastVerification(at, evaluation.green, surface.kind, evaluation.detail);
278
287
  if (evaluation.green) {
279
- const goal = db.transaction((tx) => {
280
- const updated = updateGoal(tx, goalId, {
288
+ const goal = await withTransaction(db, async (tx) => {
289
+ const updated = await updateGoal(tx, goalId, {
281
290
  status: 'complete',
282
291
  lastVerification,
283
292
  });
284
293
  if (!updated) {
285
294
  return undefined;
286
295
  }
287
- resolveBlockingTasks(tx, existing.projectId, goalId);
296
+ await resolveBlockingTasks(tx, existing.projectId, goalId);
288
297
  return updated;
289
298
  });
290
299
  if (!goal) {
291
300
  return undefined;
292
301
  }
293
- emitGoalUpdated(eventBus, goalId, existing.projectId);
294
302
  return serializeGoal(goal);
295
303
  }
296
- const goal = db.transaction((tx) => {
297
- const updated = updateGoal(tx, goalId, {
304
+ const goal = await withTransaction(db, async (tx) => {
305
+ const updated = await updateGoal(tx, goalId, {
298
306
  status: 'blocked',
299
307
  lastVerification,
300
308
  });
301
309
  if (!updated) {
302
310
  return undefined;
303
311
  }
304
- if (!hasUnresolvedBlockingTask(tx, existing.projectId, goalId)) {
305
- createTask(tx, {
312
+ if (!(await hasUnresolvedBlockingTask(tx, existing.projectId, goalId))) {
313
+ await createTask(tx, {
306
314
  projectId: existing.projectId,
307
315
  goalId,
308
316
  label: `Fix acceptance failure: ${truncateObjective(existing.objective)}`,
@@ -315,7 +323,6 @@ export function createGoalService(deps) {
315
323
  if (!goal) {
316
324
  return undefined;
317
325
  }
318
- emitGoalUpdated(eventBus, goalId, existing.projectId);
319
326
  return serializeGoal(goal);
320
327
  },
321
328
  };
@@ -1,5 +1,4 @@
1
1
  import type { Db } from '@plandesk/db';
2
- import { type EventBus } from '../events.js';
3
2
  import { type StorageAdapter } from '../storage/index.js';
4
3
  import { type CanvasService } from './canvas.js';
5
4
  import { type CommentService } from './comments.js';
@@ -13,16 +12,15 @@ import { type AgentRunService } from './agent-runs.js';
13
12
  import { type TagService } from './tags.js';
14
13
  import { type GoalService } from './goals.js';
15
14
  import { type TaskService } from './tasks.js';
16
- import { type TokenService } from './tokens.js';
17
15
  import { type ShareService } from './share.js';
18
16
  import { type SyncService } from './sync.js';
19
17
  export type ServicesDeps = {
20
18
  db: Db;
21
- eventBus?: EventBus;
22
19
  storage?: StorageAdapter;
20
+ /** Fixed org scope for unit tests; production request path uses auth context. */
21
+ orgId?: string;
23
22
  };
24
23
  export type Services = {
25
- eventBus: EventBus;
26
24
  projectService: ProjectService;
27
25
  goalService: GoalService;
28
26
  taskService: TaskService;
@@ -33,7 +31,6 @@ export type Services = {
33
31
  noteService: NoteService;
34
32
  commentService: CommentService;
35
33
  agentRunService: AgentRunService;
36
- tokenService: TokenService;
37
34
  shareService: ShareService;
38
35
  syncService: SyncService;
39
36
  fileService: FileService;
@@ -1,4 +1,3 @@
1
- import { createEventBus } from '../events.js';
2
1
  import { createStorageAdapter } from '../storage/index.js';
3
2
  import { createCanvasService } from './canvas.js';
4
3
  import { createCommentService } from './comments.js';
@@ -12,29 +11,26 @@ import { createAgentRunService } from './agent-runs.js';
12
11
  import { createTagService } from './tags.js';
13
12
  import { createGoalService } from './goals.js';
14
13
  import { createTaskService } from './tasks.js';
15
- import { createTokenService } from './tokens.js';
16
14
  import { createShareService } from './share.js';
17
15
  import { createSyncService } from './sync.js';
18
16
  export function createServices(deps) {
19
- const eventBus = deps.eventBus ?? createEventBus();
17
+ const scoped = { db: deps.db, orgId: deps.orgId };
20
18
  const storage = deps.storage ?? createStorageAdapter({ db: deps.db });
21
- const projectService = createProjectService({ db: deps.db, eventBus });
22
- const goalService = createGoalService({ db: deps.db, eventBus });
23
- const taskService = createTaskService({ db: deps.db, eventBus });
24
- const tagService = createTagService({ db: deps.db, eventBus });
25
- const canvasService = createCanvasService({ db: deps.db, eventBus });
26
- const documentService = createDocumentService({ db: deps.db, eventBus });
27
- const folderService = createFolderService({ db: deps.db, eventBus });
28
- const noteService = createNoteService({ db: deps.db, eventBus });
29
- const commentService = createCommentService({ db: deps.db, eventBus });
30
- const agentRunService = createAgentRunService({ db: deps.db, eventBus });
31
- const tokenService = createTokenService({ db: deps.db });
32
- const shareService = createShareService({ db: deps.db, eventBus });
33
- const syncService = createSyncService({ db: deps.db, eventBus, taskService, shareService });
34
- const fileService = createFileService({ db: deps.db, storage });
35
- const artifactService = createArtifactService({ db: deps.db, eventBus });
19
+ const projectService = createProjectService(scoped);
20
+ const goalService = createGoalService(scoped);
21
+ const taskService = createTaskService(scoped);
22
+ const tagService = createTagService(scoped);
23
+ const canvasService = createCanvasService(scoped);
24
+ const documentService = createDocumentService(scoped);
25
+ const folderService = createFolderService(scoped);
26
+ const noteService = createNoteService(scoped);
27
+ const commentService = createCommentService(scoped);
28
+ const agentRunService = createAgentRunService(scoped);
29
+ const shareService = createShareService(scoped);
30
+ const syncService = createSyncService({ ...scoped, taskService });
31
+ const fileService = createFileService({ ...scoped, storage });
32
+ const artifactService = createArtifactService(scoped);
36
33
  return {
37
- eventBus,
38
34
  projectService,
39
35
  goalService,
40
36
  taskService,
@@ -45,7 +41,6 @@ export function createServices(deps) {
45
41
  noteService,
46
42
  commentService,
47
43
  agentRunService,
48
- tokenService,
49
44
  shareService,
50
45
  syncService,
51
46
  fileService,
@@ -1,9 +1,8 @@
1
1
  import { type Db } from '@plandesk/db';
2
2
  import { type PaginationParams, type SerializedNote } from '../serialize.js';
3
- import type { EventBus } from '../events.js';
4
- export type NoteServiceDeps = {
3
+ import { type OrgScopedDeps } from './org-scope.js';
4
+ export type NoteServiceDeps = OrgScopedDeps & {
5
5
  db: Db;
6
- eventBus: EventBus;
7
6
  };
8
7
  export type CreateNoteInput = {
9
8
  title: string;
@@ -17,11 +16,11 @@ export declare class InvalidNoteError extends Error {
17
16
  constructor(message: string);
18
17
  }
19
18
  export declare function createNoteService(deps: NoteServiceDeps): {
20
- list(projectId: string, pagination?: PaginationParams): SerializedNote[] | undefined;
21
- create(projectId: string, input: CreateNoteInput): SerializedNote | undefined;
22
- get(id: string): SerializedNote | undefined;
23
- update(id: string, input: UpdateNoteInput): SerializedNote | undefined;
24
- delete(id: string): boolean;
19
+ list(projectId: string, pagination?: PaginationParams): Promise<SerializedNote[] | undefined>;
20
+ create(projectId: string, input: CreateNoteInput): Promise<SerializedNote | undefined>;
21
+ get(id: string): Promise<SerializedNote | undefined>;
22
+ update(id: string, input: UpdateNoteInput): Promise<SerializedNote | undefined>;
23
+ delete(id: string): Promise<boolean>;
25
24
  };
26
25
  export type NoteService = ReturnType<typeof createNoteService>;
27
26
  //# sourceMappingURL=notes.d.ts.map
@@ -1,5 +1,7 @@
1
1
  import { createNote as dbCreateNote, deleteCommentsByTarget, deleteNote as dbDeleteNote, getNote as dbGetNote, getProject, listNotes as dbListNotes, updateNote as dbUpdateNote, } from '@plandesk/db';
2
2
  import { serializeNote } from '../serialize.js';
3
+ import { assertPermission, resolveOrgId } from './org-scope.js';
4
+ import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
3
5
  export class InvalidNoteError extends Error {
4
6
  constructor(message) {
5
7
  super(message);
@@ -12,62 +14,72 @@ function assertNonEmptyTitle(title) {
12
14
  }
13
15
  }
14
16
  export function createNoteService(deps) {
15
- const { db, eventBus } = deps;
17
+ const { db } = deps;
16
18
  return {
17
- list(projectId, pagination = {}) {
18
- const project = getProject(db, projectId);
19
- if (!project) {
20
- return undefined;
19
+ async list(projectId, pagination = {}) {
20
+ try {
21
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
22
+ }
23
+ catch (error) {
24
+ if (error instanceof ProjectNotInOrgError) {
25
+ return undefined;
26
+ }
27
+ throw error;
21
28
  }
22
- return dbListNotes(db, projectId, pagination).map(serializeNote);
29
+ return (await dbListNotes(db, projectId, pagination)).map(serializeNote);
23
30
  },
24
- create(projectId, input) {
25
- const project = getProject(db, projectId);
26
- if (!project) {
27
- return undefined;
31
+ async create(projectId, input) {
32
+ assertPermission(deps, 'document', 'create');
33
+ try {
34
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
35
+ }
36
+ catch (error) {
37
+ if (error instanceof ProjectNotInOrgError) {
38
+ return undefined;
39
+ }
40
+ throw error;
28
41
  }
29
42
  assertNonEmptyTitle(input.title);
30
- const note = dbCreateNote(db, {
43
+ const note = await dbCreateNote(db, {
31
44
  projectId,
32
45
  title: input.title,
33
46
  body: input.body,
34
47
  });
35
- eventBus.emit({ type: 'note_created', noteId: note.id, projectId });
36
48
  return serializeNote(note);
37
49
  },
38
- get(id) {
39
- const note = dbGetNote(db, id);
50
+ async get(id) {
51
+ const note = await dbGetNote(db, id);
40
52
  if (!note) {
41
53
  return undefined;
42
54
  }
43
55
  return serializeNote(note);
44
56
  },
45
- update(id, input) {
46
- const existing = dbGetNote(db, id);
57
+ async update(id, input) {
58
+ assertPermission(deps, 'document', 'update');
59
+ const existing = await dbGetNote(db, id);
47
60
  if (!existing) {
48
61
  return undefined;
49
62
  }
50
63
  if (input.title !== undefined) {
51
64
  assertNonEmptyTitle(input.title);
52
65
  }
53
- const note = dbUpdateNote(db, id, input);
66
+ const note = await dbUpdateNote(db, id, input);
54
67
  if (!note) {
55
68
  return undefined;
56
69
  }
57
- eventBus.emit({ type: 'note_updated', noteId: note.id, projectId: note.projectId });
58
70
  return serializeNote(note);
59
71
  },
60
- delete(id) {
61
- const existing = dbGetNote(db, id);
72
+ async delete(id) {
73
+ assertPermission(deps, 'document', 'delete');
74
+ const existing = await dbGetNote(db, id);
62
75
  if (!existing) {
63
76
  return false;
64
77
  }
65
- const deleted = dbDeleteNote(db, id);
78
+ const deleted = await dbDeleteNote(db, id);
66
79
  if (!deleted) {
67
80
  return false;
68
81
  }
69
- deleteCommentsByTarget(db, 'note', id);
70
- eventBus.emit({ type: 'note_updated', noteId: id, projectId: existing.projectId });
82
+ await deleteCommentsByTarget(db, 'note', id);
71
83
  return true;
72
84
  },
73
85
  };
@@ -0,0 +1,22 @@
1
+ import type { OrgRole } from '@plandesk/db';
2
+ import { type PermissionSet } from '../permissions.js';
3
+ export type OrgScopedDeps = {
4
+ /** Fixed org scope for unit tests; production uses request auth context. */
5
+ orgId?: string;
6
+ /**
7
+ * Fixed permission for unit tests. Production uses AuthContext.permission.
8
+ * When orgId is injected without permission (legacy unit-test path), defaults
9
+ * to owner so role checks do not require a full request context.
10
+ */
11
+ permission?: OrgRole | PermissionSet;
12
+ };
13
+ export declare function resolveOrgId(deps: OrgScopedDeps): string;
14
+ /**
15
+ * Effective permission set for this call.
16
+ * Priority: deps.permission → AuthContext.permission → owner (service unit tests
17
+ * that invoke methods without request middleware; production always has context).
18
+ */
19
+ export declare function resolvePermissionSet(deps: OrgScopedDeps): PermissionSet;
20
+ /** requirePermission against the caller's effective permission set (deps or AuthContext). */
21
+ export declare function assertPermission(deps: OrgScopedDeps, resource: string, action: string): void;
22
+ //# sourceMappingURL=org-scope.d.ts.map
@@ -0,0 +1,38 @@
1
+ import { getAuthContext, tryGetAuthContext } from '../auth-context.js';
2
+ import { orgRoleToPermissionSet, requirePermission, } from '../permissions.js';
3
+ export function resolveOrgId(deps) {
4
+ if (deps.orgId !== undefined) {
5
+ return deps.orgId;
6
+ }
7
+ const ctx = getAuthContext();
8
+ if (ctx.kind === 'guest') {
9
+ throw new Error('Guest context has no orgId');
10
+ }
11
+ return ctx.orgId;
12
+ }
13
+ function isOrgRole(value) {
14
+ return typeof value === 'string';
15
+ }
16
+ /**
17
+ * Effective permission set for this call.
18
+ * Priority: deps.permission → AuthContext.permission → owner (service unit tests
19
+ * that invoke methods without request middleware; production always has context).
20
+ */
21
+ export function resolvePermissionSet(deps) {
22
+ if (deps.permission !== undefined) {
23
+ return isOrgRole(deps.permission) ? orgRoleToPermissionSet(deps.permission) : deps.permission;
24
+ }
25
+ const ctx = tryGetAuthContext();
26
+ if (ctx !== undefined && ctx.kind !== 'guest') {
27
+ return ctx.permission;
28
+ }
29
+ if (ctx?.kind === 'guest') {
30
+ return {};
31
+ }
32
+ return orgRoleToPermissionSet('owner');
33
+ }
34
+ /** requirePermission against the caller's effective permission set (deps or AuthContext). */
35
+ export function assertPermission(deps, resource, action) {
36
+ requirePermission({ permission: resolvePermissionSet(deps) }, resource, action);
37
+ }
38
+ //# sourceMappingURL=org-scope.js.map
@@ -1,6 +1,6 @@
1
1
  import { type Db, type TaskStatus } from '@plandesk/db';
2
- import type { EventBus } from '../events.js';
3
2
  import { serializeEdge, serializeProject, serializeTask, type PaginationParams, type SerializedDocument, type TaskStatusSummary } from '../serialize.js';
3
+ import { type OrgScopedDeps } from './org-scope.js';
4
4
  type SerializedProject = ReturnType<typeof serializeProject>;
5
5
  type SerializedTask = ReturnType<typeof serializeTask>;
6
6
  type SerializedEdge = ReturnType<typeof serializeEdge>;
@@ -49,9 +49,8 @@ export type ScaffoldPlanResult = {
49
49
  documents: number;
50
50
  };
51
51
  };
52
- export type ProjectServiceDeps = {
52
+ export type ProjectServiceDeps = OrgScopedDeps & {
53
53
  db: Db;
54
- eventBus: EventBus;
55
54
  };
56
55
  export type CreateProjectInput = {
57
56
  name: string;
@@ -62,37 +61,37 @@ export type UpdateProjectInput = {
62
61
  description?: string | null;
63
62
  };
64
63
  export declare function createProjectService(deps: ProjectServiceDeps): {
65
- create(input: CreateProjectInput): {
64
+ create(input: CreateProjectInput): Promise<{
66
65
  id: string;
67
66
  name: string;
68
67
  description: string | null;
69
68
  created_at: string;
70
69
  updated_at: string;
71
- };
72
- list(pagination?: PaginationParams): {
70
+ }>;
71
+ list(pagination?: PaginationParams): Promise<{
73
72
  id: string;
74
73
  name: string;
75
74
  description: string | null;
76
75
  created_at: string;
77
76
  updated_at: string;
78
- }[];
79
- get(id: string): {
77
+ }[]>;
78
+ get(id: string): Promise<{
80
79
  summary: TaskStatusSummary;
81
80
  id: string;
82
81
  name: string;
83
82
  description: string | null;
84
83
  created_at: string;
85
84
  updated_at: string;
86
- } | undefined;
87
- update(id: string, input: UpdateProjectInput): {
85
+ } | undefined>;
86
+ update(id: string, input: UpdateProjectInput): Promise<{
88
87
  id: string;
89
88
  name: string;
90
89
  description: string | null;
91
90
  created_at: string;
92
91
  updated_at: string;
93
- } | undefined;
94
- delete(id: string): boolean;
95
- scaffoldFromPlan(input: ScaffoldPlanInput): ScaffoldPlanResult;
92
+ } | undefined>;
93
+ delete(id: string): Promise<boolean>;
94
+ scaffoldFromPlan(input: ScaffoldPlanInput): Promise<ScaffoldPlanResult>;
96
95
  };
97
96
  export type ProjectService = ReturnType<typeof createProjectService>;
98
97
  export {};