@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 { clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteGoalsByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getOrCreateDefaultGoal, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
1
+ import { withTransaction, clearDocumentParentRefsByProject, clearFolderParentRefsByProject, createDocument, createEdge, createProject as dbCreateProject, createTask, deleteAgentRun, deleteAgentRunEventsByRunId, deleteCommentsByProjectId, deleteDocumentsByProjectId, deleteFoldersByProjectId, deleteNotesByProjectId, deleteEdgesByProjectId, deleteGoalsByProjectId, deleteShareSubmissionsByProjectId, deleteSharesByProjectId, deleteTagsByProjectId, deleteSyncRemoteByProjectId, deleteSyncStateByProjectId, deleteProject as dbDeleteProject, deleteTasksByProjectId, getOrCreateDefaultGoal, getProject as dbGetProject, InvalidTaskStatusError, isTaskStatus, listAgentRuns, listProjects as dbListProjects, listTasks, updateProject as dbUpdateProject, } from '@plandesk/db';
2
2
  import { emptyTaskStatusSummary, serializeDocument, serializeEdge, serializeProject, serializeProjectDetail, serializeTask, } from '../serialize.js';
3
+ import { assertPermission, resolveOrgId } from './org-scope.js';
4
+ import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
3
5
  export class InvalidScaffoldError extends Error {
4
6
  constructor(message) {
5
7
  super(message);
@@ -48,80 +50,113 @@ function summarizeTasks(tasks) {
48
50
  return summary;
49
51
  }
50
52
  export function createProjectService(deps) {
51
- const { db, eventBus } = deps;
53
+ const { db } = deps;
52
54
  return {
53
- create(input) {
54
- const project = dbCreateProject(db, input);
55
+ async create(input) {
56
+ assertPermission(deps, 'project', 'create');
57
+ const orgId = resolveOrgId(deps);
58
+ const project = await dbCreateProject(db, { ...input, orgId });
55
59
  return serializeProject(project);
56
60
  },
57
- list(pagination = {}) {
58
- return dbListProjects(db, pagination).map(serializeProject);
61
+ async list(pagination = {}) {
62
+ const orgId = resolveOrgId(deps);
63
+ return (await dbListProjects(db, orgId, pagination)).map(serializeProject);
59
64
  },
60
- get(id) {
61
- const project = dbGetProject(db, id);
62
- if (!project) {
63
- return undefined;
65
+ async get(id) {
66
+ const orgId = resolveOrgId(deps);
67
+ try {
68
+ const project = await assertProjectInOrg(db, id, orgId);
69
+ const summary = summarizeTasks(await listTasks(db, id));
70
+ return serializeProjectDetail(project, summary);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ProjectNotInOrgError) {
74
+ return undefined;
75
+ }
76
+ throw error;
64
77
  }
65
- const summary = summarizeTasks(listTasks(db, id));
66
- return serializeProjectDetail(project, summary);
67
78
  },
68
- update(id, input) {
69
- const project = dbUpdateProject(db, id, input);
79
+ async update(id, input) {
80
+ assertPermission(deps, 'task', 'update');
81
+ const orgId = resolveOrgId(deps);
82
+ try {
83
+ await assertProjectInOrg(db, id, orgId);
84
+ }
85
+ catch (error) {
86
+ if (error instanceof ProjectNotInOrgError) {
87
+ return undefined;
88
+ }
89
+ throw error;
90
+ }
91
+ const project = await dbUpdateProject(db, id, input);
70
92
  if (!project) {
71
93
  return undefined;
72
94
  }
73
95
  return serializeProject(project);
74
96
  },
75
- delete(id) {
76
- const project = dbGetProject(db, id);
77
- if (!project) {
78
- return false;
97
+ async delete(id) {
98
+ assertPermission(deps, 'project', 'delete');
99
+ const orgId = resolveOrgId(deps);
100
+ try {
101
+ await assertProjectInOrg(db, id, orgId);
102
+ }
103
+ catch (error) {
104
+ if (error instanceof ProjectNotInOrgError) {
105
+ return false;
106
+ }
107
+ throw error;
79
108
  }
80
- db.transaction((tx) => {
81
- const runs = listAgentRuns(tx, id);
109
+ await withTransaction(db, async (tx) => {
110
+ const runs = await listAgentRuns(tx, id);
82
111
  for (const run of runs) {
83
- deleteAgentRunEventsByRunId(tx, run.id);
112
+ await deleteAgentRunEventsByRunId(tx, run.id);
84
113
  }
85
114
  for (const run of runs) {
86
- deleteAgentRun(tx, run.id);
115
+ await deleteAgentRun(tx, run.id);
87
116
  }
88
- deleteEdgesByProjectId(tx, id);
89
- clearDocumentParentRefsByProject(tx, id);
90
- deleteCommentsByProjectId(tx, id);
91
- deleteDocumentsByProjectId(tx, id);
92
- clearFolderParentRefsByProject(tx, id);
93
- deleteFoldersByProjectId(tx, id);
94
- deleteNotesByProjectId(tx, id);
95
- deleteTagsByProjectId(tx, id);
96
- deleteTasksByProjectId(tx, id);
97
- deleteGoalsByProjectId(tx, id);
98
- deleteShareSubmissionsByProjectId(tx, id);
99
- deleteSyncStateByProjectId(tx, id);
100
- deleteSyncRemoteByProjectId(tx, id);
101
- deleteSharesByProjectId(tx, id);
102
- dbDeleteProject(tx, id);
117
+ await deleteEdgesByProjectId(tx, id);
118
+ await clearDocumentParentRefsByProject(tx, id);
119
+ await deleteCommentsByProjectId(tx, id);
120
+ await deleteDocumentsByProjectId(tx, id);
121
+ await clearFolderParentRefsByProject(tx, id);
122
+ await deleteFoldersByProjectId(tx, id);
123
+ await deleteNotesByProjectId(tx, id);
124
+ await deleteTagsByProjectId(tx, id);
125
+ await deleteTasksByProjectId(tx, id);
126
+ await deleteGoalsByProjectId(tx, id);
127
+ await deleteShareSubmissionsByProjectId(tx, id);
128
+ await deleteSyncStateByProjectId(tx, id);
129
+ await deleteSyncRemoteByProjectId(tx, id);
130
+ await deleteSharesByProjectId(tx, id);
131
+ await dbDeleteProject(tx, id);
103
132
  });
104
- eventBus.emit({ type: 'canvas_updated', projectId: id });
105
133
  return true;
106
134
  },
107
- scaffoldFromPlan(input) {
135
+ async scaffoldFromPlan(input) {
136
+ // New project needs manager; adding plan content to an existing project needs editor.
137
+ assertPermission(deps, input.projectId === undefined ? 'project' : 'task', input.projectId === undefined ? 'create' : 'create');
108
138
  validateScaffoldInput(input);
139
+ const orgId = resolveOrgId(deps);
109
140
  const taskRows = [];
110
141
  const edgeRows = [];
111
142
  const documentRows = [];
112
143
  const keyToId = new Map();
113
144
  let projectId = '';
114
- db.transaction((tx) => {
115
- // Row offset so tasks scaffolded INTO a non-empty project are laid out
116
- // below its existing nodes instead of stacking on top of them.
145
+ await withTransaction(db, async (tx) => {
117
146
  let startRow = 0;
118
147
  if (input.projectId !== undefined) {
119
- const existing = dbGetProject(tx, input.projectId);
120
- if (!existing) {
121
- throw new InvalidScaffoldError(`project not found: ${input.projectId}`);
148
+ let existing;
149
+ try {
150
+ existing = await assertProjectInOrg(tx, input.projectId, orgId);
151
+ }
152
+ catch (error) {
153
+ if (error instanceof ProjectNotInOrgError) {
154
+ throw new InvalidScaffoldError(`project not found: ${input.projectId}`);
155
+ }
156
+ throw error;
122
157
  }
123
158
  projectId = existing.id;
124
- const existingTasks = listTasks(tx, existing.id);
159
+ const existingTasks = await listTasks(tx, existing.id);
125
160
  if (existingTasks.length > 0) {
126
161
  const maxY = existingTasks.reduce((m, t) => Math.max(m, t.y ?? 0), 0);
127
162
  startRow = Math.floor(maxY / 160) + 1;
@@ -131,17 +166,18 @@ export function createProjectService(deps) {
131
166
  if (input.name === undefined || input.name.trim() === '') {
132
167
  throw new InvalidScaffoldError('name is required to create a new project (or pass projectId to add to an existing one)');
133
168
  }
134
- const project = dbCreateProject(tx, {
169
+ const project = await dbCreateProject(tx, {
135
170
  name: input.name,
136
171
  description: input.description,
172
+ orgId,
137
173
  });
138
174
  projectId = project.id;
139
175
  }
140
- const defaultGoal = getOrCreateDefaultGoal(tx, projectId);
141
- input.tasks.forEach((taskInput, i) => {
176
+ const defaultGoal = await getOrCreateDefaultGoal(tx, projectId);
177
+ for (const [i, taskInput] of input.tasks.entries()) {
142
178
  const x = taskInput.x ?? (i % 4) * 240;
143
179
  const y = taskInput.y ?? (startRow + Math.floor(i / 4)) * 160;
144
- const task = createTask(tx, {
180
+ const task = await createTask(tx, {
145
181
  projectId,
146
182
  goalId: defaultGoal.id,
147
183
  label: taskInput.label,
@@ -152,14 +188,14 @@ export function createProjectService(deps) {
152
188
  });
153
189
  keyToId.set(taskInput.key, task.id);
154
190
  taskRows.push(task);
155
- });
191
+ }
156
192
  for (const edgeInput of input.edges ?? []) {
157
193
  const fromTaskId = keyToId.get(edgeInput.from);
158
194
  const toTaskId = keyToId.get(edgeInput.to);
159
195
  if (fromTaskId === undefined || toTaskId === undefined) {
160
196
  throw new InvalidScaffoldError('edge references unknown task key');
161
197
  }
162
- const edge = createEdge(tx, {
198
+ const edge = await createEdge(tx, {
163
199
  projectId,
164
200
  fromTaskId,
165
201
  toTaskId,
@@ -170,7 +206,7 @@ export function createProjectService(deps) {
170
206
  }
171
207
  for (const docInput of input.documents ?? []) {
172
208
  const linkedTaskId = docInput.linkTo !== undefined ? keyToId.get(docInput.linkTo) : undefined;
173
- const document = createDocument(tx, {
209
+ const document = await createDocument(tx, {
174
210
  projectId,
175
211
  title: docInput.title,
176
212
  body: docInput.body,
@@ -180,11 +216,7 @@ export function createProjectService(deps) {
180
216
  documentRows.push(document);
181
217
  }
182
218
  });
183
- eventBus.emit({ type: 'canvas_updated', projectId });
184
- for (const doc of documentRows) {
185
- eventBus.emit({ type: 'document_created', documentId: doc.id, projectId });
186
- }
187
- const project = dbGetProject(db, projectId);
219
+ const project = await dbGetProject(db, projectId);
188
220
  if (!project) {
189
221
  throw new Error('scaffolded project missing after transaction');
190
222
  }
@@ -0,0 +1,12 @@
1
+ import { type DbClient, type Project } from '@plandesk/db';
2
+ /** Thrown when a project is missing or belongs to another org. Maps to HTTP 404. */
3
+ export declare class ProjectNotInOrgError extends Error {
4
+ constructor(projectId: string);
5
+ }
6
+ /**
7
+ * Fail-closed tenant boundary: project must exist and belong to orgId.
8
+ * Returns 404-shaped error (not 403) so existence is not leaked across orgs.
9
+ * BA5: agent keys may also carry projectId — wrong project is the same 404.
10
+ */
11
+ export declare function assertProjectInOrg(db: DbClient, projectId: string, orgId: string): Promise<Project>;
12
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { getProjectInOrg } from '@plandesk/db';
2
+ import { tryGetAuthContext } from '../auth-context.js';
3
+ /** Thrown when a project is missing or belongs to another org. Maps to HTTP 404. */
4
+ export class ProjectNotInOrgError extends Error {
5
+ constructor(projectId) {
6
+ super(`Project not found: ${projectId}`);
7
+ this.name = 'ProjectNotInOrgError';
8
+ }
9
+ }
10
+ /**
11
+ * Fail-closed tenant boundary: project must exist and belong to orgId.
12
+ * Returns 404-shaped error (not 403) so existence is not leaked across orgs.
13
+ * BA5: agent keys may also carry projectId — wrong project is the same 404.
14
+ */
15
+ export async function assertProjectInOrg(db, projectId, orgId) {
16
+ const project = await getProjectInOrg(db, projectId, orgId);
17
+ if (!project) {
18
+ throw new ProjectNotInOrgError(projectId);
19
+ }
20
+ const ctx = tryGetAuthContext();
21
+ if (ctx !== undefined &&
22
+ ctx.kind === 'apikey' &&
23
+ ctx.projectId !== undefined &&
24
+ ctx.projectId !== projectId) {
25
+ throw new ProjectNotInOrgError(projectId);
26
+ }
27
+ return project;
28
+ }
29
+ //# sourceMappingURL=scope.js.map
@@ -1,12 +1,11 @@
1
1
  import { type Db, type Share, type ShareMode, type SharePermissions } from '@plandesk/db';
2
- import type { EventBus } from '../events.js';
3
2
  import { type ClientView, type SharePolicy } from '../projection.js';
3
+ import { type OrgScopedDeps } from './org-scope.js';
4
4
  export declare class InvalidShareError extends Error {
5
5
  constructor(message: string);
6
6
  }
7
- export type ShareServiceDeps = {
7
+ export type ShareServiceDeps = OrgScopedDeps & {
8
8
  db: Db;
9
- eventBus: EventBus;
10
9
  };
11
10
  export type SerializedShare = {
12
11
  id: string;
@@ -53,16 +52,81 @@ export type ResourceMarkdownResult = {
53
52
  } | {
54
53
  status: 'gone';
55
54
  };
55
+ export type ShareMetaResult = {
56
+ status: 'ok';
57
+ audienceName: string;
58
+ mode: ShareMode;
59
+ } | {
60
+ status: 'not_found';
61
+ };
62
+ export type JoinShareInput = {
63
+ name: string;
64
+ email?: string;
65
+ };
66
+ export type JoinShareResult = {
67
+ status: 'ok';
68
+ sessionToken: string;
69
+ participant: {
70
+ id: string;
71
+ name: string;
72
+ };
73
+ share: {
74
+ audienceName: string;
75
+ permissions: SharePermissions;
76
+ };
77
+ } | {
78
+ status: 'unauthorized';
79
+ } | {
80
+ status: 'name_required';
81
+ } | {
82
+ status: 'email_not_invited';
83
+ };
84
+ export type GuestSubmission = {
85
+ id: string;
86
+ title: string;
87
+ severity: string | null;
88
+ status: string;
89
+ created_at: string;
90
+ };
91
+ export type SubmitIssueInput = {
92
+ title: string;
93
+ body?: string;
94
+ severity?: string;
95
+ task_ref?: string;
96
+ };
97
+ export type SubmitIssueResult = {
98
+ status: 'ok';
99
+ submission: GuestSubmission;
100
+ } | {
101
+ status: 'unauthorized';
102
+ } | {
103
+ status: 'submit_not_permitted';
104
+ } | {
105
+ status: 'title_required';
106
+ } | {
107
+ status: 'rate_limited';
108
+ };
109
+ export type ListMySubmissionsResult = {
110
+ status: 'ok';
111
+ submissions: GuestSubmission[];
112
+ } | {
113
+ status: 'unauthorized';
114
+ };
56
115
  export declare function createShareService(deps: ShareServiceDeps): {
57
- createShare(projectId: string, input: CreateShareInput): {
116
+ createShare(projectId: string, input: CreateShareInput): Promise<{
58
117
  share: SerializedShare;
59
118
  token: string;
60
- } | undefined;
61
- listShares(projectId: string): SerializedShare[] | undefined;
62
- revokeShare(id: string): boolean;
63
- buildClientView(projectId: string, shareId: string): ClientView | undefined;
64
- createResourceShare(input: CreateResourceShareInput, origin: string): ResourceShareResult | undefined;
65
- getResourceMarkdown(token: string, origin: string): ResourceMarkdownResult;
119
+ } | undefined>;
120
+ listShares(projectId: string): Promise<SerializedShare[] | undefined>;
121
+ revokeShare(id: string): Promise<boolean>;
122
+ buildClientView(projectId: string, shareId: string): Promise<ClientView | undefined>;
123
+ createResourceShare(input: CreateResourceShareInput, origin: string): Promise<ResourceShareResult | undefined>;
124
+ getResourceMarkdown(token: string, origin: string): Promise<ResourceMarkdownResult>;
125
+ getShareMeta(token: string): Promise<ShareMetaResult>;
126
+ joinShare(token: string, input: JoinShareInput): Promise<JoinShareResult>;
127
+ getClientView(token: string): Promise<ClientView | undefined>;
128
+ submitIssue(token: string, input: SubmitIssueInput): Promise<SubmitIssueResult>;
129
+ listMySubmissions(token: string): Promise<ListMySubmissionsResult>;
66
130
  };
67
131
  export type ShareService = ReturnType<typeof createShareService>;
68
132
  //# sourceMappingURL=share.d.ts.map