@plandesk/api 1.0.0-beta.8 → 1.0.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.
Files changed (57) hide show
  1. package/dist/agent-keys.d.ts +26 -2
  2. package/dist/agent-keys.js +22 -2
  3. package/dist/auth-context.d.ts +17 -1
  4. package/dist/auth.d.ts +10 -0
  5. package/dist/auth.js +25 -6
  6. package/dist/better-auth.js +4 -3
  7. package/dist/identity.d.ts +34 -0
  8. package/dist/identity.js +182 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/invitations.d.ts +13 -2
  12. package/dist/invitations.js +14 -2
  13. package/dist/organizations.d.ts +3 -0
  14. package/dist/organizations.js +18 -0
  15. package/dist/projection.d.ts +28 -0
  16. package/dist/projection.js +29 -1
  17. package/dist/routes/auth.js +21 -5
  18. package/dist/routes/comments.js +3 -1
  19. package/dist/routes/orgs.js +139 -24
  20. package/dist/routes/projects.js +36 -5
  21. package/dist/routes/shares.js +32 -0
  22. package/dist/routes/tasks.js +3 -0
  23. package/dist/serialize.d.ts +2 -0
  24. package/dist/serialize.js +1 -0
  25. package/dist/server.js +9 -6
  26. package/dist/services/agent-runs.js +18 -0
  27. package/dist/services/artifacts.js +18 -0
  28. package/dist/services/canvas.js +9 -0
  29. package/dist/services/comments.js +63 -13
  30. package/dist/services/documents.js +36 -0
  31. package/dist/services/files.js +19 -1
  32. package/dist/services/folders.js +27 -0
  33. package/dist/services/goals.js +45 -0
  34. package/dist/services/index.d.ts +3 -0
  35. package/dist/services/index.js +1 -1
  36. package/dist/services/notes.js +27 -0
  37. package/dist/services/projects.d.ts +27 -0
  38. package/dist/services/projects.js +113 -4
  39. package/dist/services/scope.d.ts +4 -0
  40. package/dist/services/scope.js +22 -0
  41. package/dist/services/share.d.ts +25 -3
  42. package/dist/services/share.js +160 -9
  43. package/dist/services/sync.d.ts +1 -1
  44. package/dist/services/sync.js +52 -1
  45. package/dist/services/tags.js +18 -0
  46. package/dist/services/tasks.d.ts +1 -1
  47. package/dist/services/tasks.js +22 -0
  48. package/dist/test-helpers.d.ts +1 -0
  49. package/dist/test-helpers.js +14 -0
  50. package/dist/vercel.js +9 -1
  51. package/dist/worker.js +1 -1
  52. package/package.json +3 -3
  53. package/web/assets/index-BpOwO6_E.css +2 -0
  54. package/web/assets/index-CM5NiDOW.js +358 -0
  55. package/web/index.html +2 -2
  56. package/web/assets/index-aQ0kLFAX.js +0 -358
  57. package/web/assets/index-nXT4L9C-.css +0 -2
package/dist/server.js CHANGED
@@ -19,16 +19,13 @@ import { createGoalsRouter } from './routes/goals.js';
19
19
  import { createSubmissionsRouter } from './routes/submissions.js';
20
20
  import { createOrgsRouter } from './routes/orgs.js';
21
21
  import { createServices } from './services/index.js';
22
- import { ProjectNotInOrgError } from './services/scope.js';
22
+ import { ProjectNotInOrgError, WorkspaceNotFoundError } from './services/scope.js';
23
23
  import { ReadOnlyTokenError } from './auth-context.js';
24
24
  import { PermissionDeniedError } from './permissions.js';
25
25
  export function createApp(deps) {
26
- const services = deps.services ?? createServices({ db: deps.db });
27
- const { projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, syncService, fileService, artifactService, shareService, } = services;
28
26
  const bindHost = deps.bindHost ?? '127.0.0.1';
29
- const app = new Hono();
30
- // Create better-auth before org middleware so the same instance is recognized
31
- // by AuthContext and mounted at /api/auth/* (BA4a session recognition).
27
+ // Create better-auth before services so workspace resolution is available
28
+ // to the project service (REQ-6).
32
29
  const betterAuthInstance = deps.betterAuthInstance ??
33
30
  (deps.betterAuth !== undefined
34
31
  ? createBetterAuth({
@@ -39,6 +36,9 @@ export function createApp(deps) {
39
36
  github: deps.github,
40
37
  })
41
38
  : undefined);
39
+ const services = deps.services ?? createServices({ db: deps.db, auth: betterAuthInstance });
40
+ const { projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, syncService, fileService, artifactService, shareService, } = services;
41
+ const app = new Hono();
42
42
  // Always-on org resolution (better-auth apiKey/session, loopback, or guest).
43
43
  app.use('*', createOrgAuthMiddleware({
44
44
  db: deps.db,
@@ -53,6 +53,9 @@ export function createApp(deps) {
53
53
  if (err instanceof ProjectNotInOrgError) {
54
54
  return c.json({ error: 'not_found' }, 404);
55
55
  }
56
+ if (err instanceof WorkspaceNotFoundError) {
57
+ return c.json({ error: 'not_found' }, 404);
58
+ }
56
59
  if (err instanceof ReadOnlyTokenError || err instanceof PermissionDeniedError) {
57
60
  return c.json({ error: 'forbidden' }, 403);
58
61
  }
@@ -62,6 +62,15 @@ export function createAgentRunService(deps) {
62
62
  if (!run) {
63
63
  return undefined;
64
64
  }
65
+ try {
66
+ await assertProjectInOrg(db, run.projectId, resolveOrgId(deps));
67
+ }
68
+ catch (error) {
69
+ if (error instanceof ProjectNotInOrgError) {
70
+ return undefined;
71
+ }
72
+ throw error;
73
+ }
65
74
  if (terminalStatuses.has(run.status)) {
66
75
  throw new InvalidAgentRunError('Agent run is already complete');
67
76
  }
@@ -74,6 +83,15 @@ export function createAgentRunService(deps) {
74
83
  if (!run) {
75
84
  return undefined;
76
85
  }
86
+ try {
87
+ await assertProjectInOrg(db, run.projectId, resolveOrgId(deps));
88
+ }
89
+ catch (error) {
90
+ if (error instanceof ProjectNotInOrgError) {
91
+ return undefined;
92
+ }
93
+ throw error;
94
+ }
77
95
  if (terminalStatuses.has(run.status)) {
78
96
  throw new InvalidAgentRunError('Agent run is already complete');
79
97
  }
@@ -53,6 +53,15 @@ export function createArtifactService(deps) {
53
53
  if (!artifact) {
54
54
  return undefined;
55
55
  }
56
+ try {
57
+ await assertProjectInOrg(db, artifact.projectId, resolveOrgId(deps));
58
+ }
59
+ catch (error) {
60
+ if (error instanceof ProjectNotInOrgError) {
61
+ return undefined;
62
+ }
63
+ throw error;
64
+ }
56
65
  return serializeArtifact(artifact);
57
66
  },
58
67
  async update(id, input) {
@@ -61,6 +70,15 @@ export function createArtifactService(deps) {
61
70
  if (!existing) {
62
71
  return undefined;
63
72
  }
73
+ try {
74
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
75
+ }
76
+ catch (error) {
77
+ if (error instanceof ProjectNotInOrgError) {
78
+ return undefined;
79
+ }
80
+ throw error;
81
+ }
64
82
  if (input.title !== undefined) {
65
83
  assertNonEmptyTitle(input.title);
66
84
  }
@@ -167,6 +167,15 @@ export function createCanvasService(deps) {
167
167
  },
168
168
  async deleteEdge(projectId, edgeId) {
169
169
  assertPermission(deps, 'edge', 'delete');
170
+ try {
171
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
172
+ }
173
+ catch (error) {
174
+ if (error instanceof ProjectNotInOrgError) {
175
+ return false;
176
+ }
177
+ throw error;
178
+ }
170
179
  const edge = await getEdgeByProjectAndId(db, projectId, edgeId);
171
180
  if (!edge) {
172
181
  return false;
@@ -1,4 +1,4 @@
1
- import { createComment, deleteComment as dbDeleteComment, getComment as dbGetComment, getDocument as dbGetDocument, getNote as dbGetNote, getProject, getSubmission, getTask, listCommentsByProject as dbListCommentsByProject, listCommentsByTarget as dbListCommentsByTarget, updateComment as dbUpdateComment, } from '@plandesk/db';
1
+ import { createComment, deleteComment as dbDeleteComment, getComment as dbGetComment, getDocument as dbGetDocument, getNote as dbGetNote, getSubmission, getTask, listCommentsByProject as dbListCommentsByProject, listCommentsByTarget as dbListCommentsByTarget, listCommentsByTargetInProject as dbListCommentsByTargetInProject, updateComment as dbUpdateComment, } from '@plandesk/db';
2
2
  import { serializeComment } from '../serialize.js';
3
3
  import { assertPermission, resolveOrgId } from './org-scope.js';
4
4
  import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
@@ -39,6 +39,15 @@ export function createCommentService(deps) {
39
39
  if (!projectId) {
40
40
  return undefined;
41
41
  }
42
+ try {
43
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
44
+ }
45
+ catch (error) {
46
+ if (error instanceof ProjectNotInOrgError) {
47
+ return undefined;
48
+ }
49
+ throw error;
50
+ }
42
51
  assertNonEmptyBody(input.body);
43
52
  const comment = await createComment(db, {
44
53
  projectId,
@@ -55,8 +64,14 @@ export function createCommentService(deps) {
55
64
  // in the file identity mean it travels in the body/query, never a path seg.
56
65
  async createForArtifact(projectId, artifactId, input) {
57
66
  assertPermission(deps, 'comment', 'create');
58
- if (!(await getProject(db, projectId))) {
59
- return undefined;
67
+ try {
68
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
69
+ }
70
+ catch (error) {
71
+ if (error instanceof ProjectNotInOrgError) {
72
+ return undefined;
73
+ }
74
+ throw error;
60
75
  }
61
76
  if (artifactId.trim() === '') {
62
77
  throw new InvalidCommentError('Artifact id must not be empty');
@@ -73,16 +88,33 @@ export function createCommentService(deps) {
73
88
  return serializeComment(comment);
74
89
  },
75
90
  async listForArtifact(projectId, artifactId, options) {
76
- if (!(await getProject(db, projectId))) {
77
- return undefined;
91
+ try {
92
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
93
+ }
94
+ catch (error) {
95
+ if (error instanceof ProjectNotInOrgError) {
96
+ return undefined;
97
+ }
98
+ throw error;
78
99
  }
79
- return (await dbListCommentsByTarget(db, 'artifact', artifactId, options)).map(serializeComment);
100
+ // Artifact ids (file paths) are not globally unique — scope by projectId
101
+ // so project A's list never returns project B's same-path comment.
102
+ return (await dbListCommentsByTargetInProject(db, 'artifact', artifactId, projectId, options)).map(serializeComment);
80
103
  },
81
104
  async listByTarget(target, options) {
82
105
  const projectId = await targetProjectId(db, target);
83
106
  if (!projectId) {
84
107
  return undefined;
85
108
  }
109
+ try {
110
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
111
+ }
112
+ catch (error) {
113
+ if (error instanceof ProjectNotInOrgError) {
114
+ return undefined;
115
+ }
116
+ throw error;
117
+ }
86
118
  return (await dbListCommentsByTarget(db, target.type, target.id, options)).map(serializeComment);
87
119
  },
88
120
  async resolveTargetProjectId(target) {
@@ -109,6 +141,22 @@ export function createCommentService(deps) {
109
141
  if (!existing) {
110
142
  return undefined;
111
143
  }
144
+ const projectId = await targetProjectId(db, {
145
+ type: existing.targetType,
146
+ id: existing.targetId,
147
+ });
148
+ if (!projectId) {
149
+ return undefined;
150
+ }
151
+ try {
152
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
153
+ }
154
+ catch (error) {
155
+ if (error instanceof ProjectNotInOrgError) {
156
+ return undefined;
157
+ }
158
+ throw error;
159
+ }
112
160
  if (input.body !== undefined) {
113
161
  assertNonEmptyBody(input.body);
114
162
  }
@@ -116,13 +164,6 @@ export function createCommentService(deps) {
116
164
  if (!comment) {
117
165
  return undefined;
118
166
  }
119
- const projectId = await targetProjectId(db, {
120
- type: comment.targetType,
121
- id: comment.targetId,
122
- });
123
- if (!projectId) {
124
- return undefined;
125
- }
126
167
  return serializeComment(comment);
127
168
  },
128
169
  async delete(id) {
@@ -138,6 +179,15 @@ export function createCommentService(deps) {
138
179
  if (!projectId) {
139
180
  return false;
140
181
  }
182
+ try {
183
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
184
+ }
185
+ catch (error) {
186
+ if (error instanceof ProjectNotInOrgError) {
187
+ return false;
188
+ }
189
+ throw error;
190
+ }
141
191
  const deleted = await dbDeleteComment(db, id);
142
192
  if (!deleted) {
143
193
  return false;
@@ -103,6 +103,15 @@ export function createDocumentService(deps) {
103
103
  if (!document) {
104
104
  return undefined;
105
105
  }
106
+ try {
107
+ await assertProjectInOrg(db, document.projectId, resolveOrgId(deps));
108
+ }
109
+ catch (error) {
110
+ if (error instanceof ProjectNotInOrgError) {
111
+ return undefined;
112
+ }
113
+ throw error;
114
+ }
106
115
  return serializeDocument(document);
107
116
  },
108
117
  async update(id, input) {
@@ -111,6 +120,15 @@ export function createDocumentService(deps) {
111
120
  if (!existing) {
112
121
  return undefined;
113
122
  }
123
+ try {
124
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
125
+ }
126
+ catch (error) {
127
+ if (error instanceof ProjectNotInOrgError) {
128
+ return undefined;
129
+ }
130
+ throw error;
131
+ }
114
132
  if (input.linkedTaskId !== undefined && input.linkedTaskId !== null) {
115
133
  await assertTaskInProject(db, existing.projectId, input.linkedTaskId);
116
134
  }
@@ -138,6 +156,15 @@ export function createDocumentService(deps) {
138
156
  if (!document) {
139
157
  return undefined;
140
158
  }
159
+ try {
160
+ await assertProjectInOrg(db, document.projectId, resolveOrgId(deps));
161
+ }
162
+ catch (error) {
163
+ if (error instanceof ProjectNotInOrgError) {
164
+ return undefined;
165
+ }
166
+ throw error;
167
+ }
141
168
  return serializeDocument(document);
142
169
  },
143
170
  async delete(id) {
@@ -146,6 +173,15 @@ export function createDocumentService(deps) {
146
173
  if (!existing) {
147
174
  return false;
148
175
  }
176
+ try {
177
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
178
+ }
179
+ catch (error) {
180
+ if (error instanceof ProjectNotInOrgError) {
181
+ return false;
182
+ }
183
+ throw error;
184
+ }
149
185
  await withTransaction(db, async (tx) => {
150
186
  await detachDocumentChildren(tx, id);
151
187
  await deleteCommentsByTarget(tx, 'document', id);
@@ -1,4 +1,5 @@
1
- import { getFile } from '@plandesk/db';
1
+ import { getFile, getFileInOrg } from '@plandesk/db';
2
+ import { tryGetAuthContext } from '../auth-context.js';
2
3
  import { assertPermission, resolveOrgId } from './org-scope.js';
3
4
  import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
4
5
  export function createFileService(deps) {
@@ -31,6 +32,23 @@ export function createFileService(deps) {
31
32
  return { id, url, filename: file.filename, mime: file.mime, size: file.size };
32
33
  },
33
34
  async get(id) {
35
+ const auth = tryGetAuthContext();
36
+ if (auth === undefined || auth.kind === 'guest') {
37
+ return undefined;
38
+ }
39
+ const file = await getFileInOrg(db, id, auth.orgId);
40
+ if (!file) {
41
+ return undefined;
42
+ }
43
+ try {
44
+ await assertProjectInOrg(db, file.projectId, resolveOrgId(deps));
45
+ }
46
+ catch (error) {
47
+ if (error instanceof ProjectNotInOrgError) {
48
+ return undefined;
49
+ }
50
+ throw error;
51
+ }
34
52
  const resolved = await storage.resolve(id);
35
53
  return resolved ?? undefined;
36
54
  },
@@ -75,6 +75,15 @@ export function createFolderService(deps) {
75
75
  if (!folder) {
76
76
  return undefined;
77
77
  }
78
+ try {
79
+ await assertProjectInOrg(db, folder.projectId, resolveOrgId(deps));
80
+ }
81
+ catch (error) {
82
+ if (error instanceof ProjectNotInOrgError) {
83
+ return undefined;
84
+ }
85
+ throw error;
86
+ }
78
87
  return serializeFolder(folder);
79
88
  },
80
89
  async update(id, input) {
@@ -83,6 +92,15 @@ export function createFolderService(deps) {
83
92
  if (!existing) {
84
93
  return undefined;
85
94
  }
95
+ try {
96
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
97
+ }
98
+ catch (error) {
99
+ if (error instanceof ProjectNotInOrgError) {
100
+ return undefined;
101
+ }
102
+ throw error;
103
+ }
86
104
  if (input.name !== undefined) {
87
105
  assertNonEmptyName(input.name);
88
106
  }
@@ -105,6 +123,15 @@ export function createFolderService(deps) {
105
123
  if (!existing) {
106
124
  return false;
107
125
  }
126
+ try {
127
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
128
+ }
129
+ catch (error) {
130
+ if (error instanceof ProjectNotInOrgError) {
131
+ return false;
132
+ }
133
+ throw error;
134
+ }
108
135
  // Never orphan: children folders and contained documents move to the
109
136
  // deleted folder's parent (or root when the folder was at root).
110
137
  await withTransaction(db, async (tx) => {
@@ -190,6 +190,15 @@ export function createGoalService(deps) {
190
190
  if (!goal) {
191
191
  return undefined;
192
192
  }
193
+ try {
194
+ await assertProjectInOrg(db, goal.projectId, resolveOrgId(deps));
195
+ }
196
+ catch (error) {
197
+ if (error instanceof ProjectNotInOrgError) {
198
+ return undefined;
199
+ }
200
+ throw error;
201
+ }
193
202
  return {
194
203
  ...serializeGoal(goal),
195
204
  cycle_tasks: await cycleTasksForGoal(db, goal.projectId, goalId),
@@ -213,6 +222,15 @@ export function createGoalService(deps) {
213
222
  if (!existing) {
214
223
  return undefined;
215
224
  }
225
+ try {
226
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
227
+ }
228
+ catch (error) {
229
+ if (error instanceof ProjectNotInOrgError) {
230
+ return undefined;
231
+ }
232
+ throw error;
233
+ }
216
234
  validateVerificationSurfaceInput(input.verificationSurface);
217
235
  const goal = await withTransaction(db, async (tx) => updateGoal(tx, goalId, input));
218
236
  if (!goal) {
@@ -226,6 +244,15 @@ export function createGoalService(deps) {
226
244
  if (!existing) {
227
245
  return undefined;
228
246
  }
247
+ try {
248
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
249
+ }
250
+ catch (error) {
251
+ if (error instanceof ProjectNotInOrgError) {
252
+ return undefined;
253
+ }
254
+ throw error;
255
+ }
229
256
  if (existing.status !== 'active') {
230
257
  throw new InvalidGoalTransitionError('Goal can only be paused from active status');
231
258
  }
@@ -241,6 +268,15 @@ export function createGoalService(deps) {
241
268
  if (!existing) {
242
269
  return undefined;
243
270
  }
271
+ try {
272
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
273
+ }
274
+ catch (error) {
275
+ if (error instanceof ProjectNotInOrgError) {
276
+ return undefined;
277
+ }
278
+ throw error;
279
+ }
244
280
  if (existing.status !== 'paused') {
245
281
  throw new InvalidGoalTransitionError('Goal can only be resumed from paused status');
246
282
  }
@@ -256,6 +292,15 @@ export function createGoalService(deps) {
256
292
  if (!existing) {
257
293
  return undefined;
258
294
  }
295
+ try {
296
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
297
+ }
298
+ catch (error) {
299
+ if (error instanceof ProjectNotInOrgError) {
300
+ return undefined;
301
+ }
302
+ throw error;
303
+ }
259
304
  if (existing.status === 'complete') {
260
305
  throw new InvalidGoalTransitionError('Goal is already complete');
261
306
  }
@@ -1,4 +1,5 @@
1
1
  import type { Db } from '@plandesk/db';
2
+ import type { BetterAuthInstance } from '../better-auth.js';
2
3
  import { type StorageAdapter } from '../storage/index.js';
3
4
  import { type CanvasService } from './canvas.js';
4
5
  import { type CommentService } from './comments.js';
@@ -19,6 +20,8 @@ export type ServicesDeps = {
19
20
  storage?: StorageAdapter;
20
21
  /** Fixed org scope for unit tests; production request path uses auth context. */
21
22
  orgId?: string;
23
+ /** better-auth instance for workspace resolution (project creation). */
24
+ auth?: BetterAuthInstance;
22
25
  };
23
26
  export type Services = {
24
27
  projectService: ProjectService;
@@ -14,7 +14,7 @@ import { createTaskService } from './tasks.js';
14
14
  import { createShareService } from './share.js';
15
15
  import { createSyncService } from './sync.js';
16
16
  export function createServices(deps) {
17
- const scoped = { db: deps.db, orgId: deps.orgId };
17
+ const scoped = { db: deps.db, orgId: deps.orgId, auth: deps.auth };
18
18
  const storage = deps.storage ?? createStorageAdapter({ db: deps.db });
19
19
  const projectService = createProjectService(scoped);
20
20
  const goalService = createGoalService(scoped);
@@ -52,6 +52,15 @@ export function createNoteService(deps) {
52
52
  if (!note) {
53
53
  return undefined;
54
54
  }
55
+ try {
56
+ await assertProjectInOrg(db, note.projectId, resolveOrgId(deps));
57
+ }
58
+ catch (error) {
59
+ if (error instanceof ProjectNotInOrgError) {
60
+ return undefined;
61
+ }
62
+ throw error;
63
+ }
55
64
  return serializeNote(note);
56
65
  },
57
66
  async update(id, input) {
@@ -60,6 +69,15 @@ export function createNoteService(deps) {
60
69
  if (!existing) {
61
70
  return undefined;
62
71
  }
72
+ try {
73
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
74
+ }
75
+ catch (error) {
76
+ if (error instanceof ProjectNotInOrgError) {
77
+ return undefined;
78
+ }
79
+ throw error;
80
+ }
63
81
  if (input.title !== undefined) {
64
82
  assertNonEmptyTitle(input.title);
65
83
  }
@@ -75,6 +93,15 @@ export function createNoteService(deps) {
75
93
  if (!existing) {
76
94
  return false;
77
95
  }
96
+ try {
97
+ await assertProjectInOrg(db, existing.projectId, resolveOrgId(deps));
98
+ }
99
+ catch (error) {
100
+ if (error instanceof ProjectNotInOrgError) {
101
+ return false;
102
+ }
103
+ throw error;
104
+ }
78
105
  const deleted = await dbDeleteNote(db, id);
79
106
  if (!deleted) {
80
107
  return false;
@@ -1,4 +1,5 @@
1
1
  import { type Db, type TaskStatus } from '@plandesk/db';
2
+ import type { BetterAuthInstance } from '../better-auth.js';
2
3
  import { serializeEdge, serializeProject, serializeTask, type PaginationParams, type SerializedDocument, type TaskStatusSummary } from '../serialize.js';
3
4
  import { type OrgScopedDeps } from './org-scope.js';
4
5
  type SerializedProject = ReturnType<typeof serializeProject>;
@@ -33,6 +34,10 @@ export type ScaffoldPlanInput = {
33
34
  /** Name for a new project. Required when projectId is omitted; ignored when it is set. */
34
35
  name?: string;
35
36
  description?: string | null;
37
+ /** Target workspace (team) for a new project. A workspace/project-scoped agent
38
+ * key must target its own scope (or omit this to default to it); a mismatch is
39
+ * a 404. Ignored when projectId is set. */
40
+ workspaceId?: string;
36
41
  tasks: ScaffoldTaskInput[];
37
42
  edges?: ScaffoldEdgeInput[];
38
43
  documents?: ScaffoldDocumentInput[];
@@ -51,10 +56,12 @@ export type ScaffoldPlanResult = {
51
56
  };
52
57
  export type ProjectServiceDeps = OrgScopedDeps & {
53
58
  db: Db;
59
+ auth?: BetterAuthInstance;
54
60
  };
55
61
  export type CreateProjectInput = {
56
62
  name: string;
57
63
  description?: string | null;
64
+ workspaceId?: string;
58
65
  };
59
66
  export type UpdateProjectInput = {
60
67
  name?: string;
@@ -65,6 +72,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
65
72
  id: string;
66
73
  name: string;
67
74
  description: string | null;
75
+ workspace_id: string;
68
76
  created_at: string;
69
77
  updated_at: string;
70
78
  }>;
@@ -72,6 +80,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
72
80
  id: string;
73
81
  name: string;
74
82
  description: string | null;
83
+ workspace_id: string;
75
84
  created_at: string;
76
85
  updated_at: string;
77
86
  }[]>;
@@ -80,6 +89,7 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
80
89
  id: string;
81
90
  name: string;
82
91
  description: string | null;
92
+ workspace_id: string;
83
93
  created_at: string;
84
94
  updated_at: string;
85
95
  } | undefined>;
@@ -87,6 +97,23 @@ export declare function createProjectService(deps: ProjectServiceDeps): {
87
97
  id: string;
88
98
  name: string;
89
99
  description: string | null;
100
+ workspace_id: string;
101
+ created_at: string;
102
+ updated_at: string;
103
+ } | undefined>;
104
+ /**
105
+ * Move a project to another workspace (team) in the caller's org.
106
+ *
107
+ * Owner-gated: a workspace/project-scoped agent key must not move projects
108
+ * (it would drag a project out of its scope), so scoped callers are rejected
109
+ * before any existence check. Requires project:create authority (owner/admin).
110
+ * The target team must exist in the caller's org; otherwise 404.
111
+ */
112
+ moveProjectToWorkspace(id: string, workspaceId: string): Promise<{
113
+ id: string;
114
+ name: string;
115
+ description: string | null;
116
+ workspace_id: string;
90
117
  created_at: string;
91
118
  updated_at: string;
92
119
  } | undefined>;