@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
@@ -26,7 +26,7 @@ export function createSharesRouter(shareService) {
26
26
  return c.json({ error: 'invalid_expires' }, 400);
27
27
  }
28
28
  const origin = new URL(c.req.url).origin;
29
- const result = shareService.createResourceShare({ resource: { kind, id }, expiresAt: resolveExpiresAt(body.expires) }, origin);
29
+ const result = await shareService.createResourceShare({ resource: { kind, id }, expiresAt: resolveExpiresAt(body.expires) }, origin);
30
30
  if (!result) {
31
31
  return c.json({ error: 'not_found' }, 404);
32
32
  }
@@ -36,14 +36,14 @@ export function createSharesRouter(shareService) {
36
36
  router.post('/documents/:id/share', createShareHandler('document'));
37
37
  // Hono doesn't match a literal `.md` suffix inside a param, so the route
38
38
  // takes the raw segment and the handler enforces + strips the extension.
39
- router.get('/share/:tokenWithExt', (c) => {
39
+ router.get('/share/:tokenWithExt', async (c) => {
40
40
  const raw = c.req.param('tokenWithExt');
41
41
  if (!raw.endsWith('.md')) {
42
42
  return c.notFound();
43
43
  }
44
44
  const token = raw.slice(0, -'.md'.length);
45
45
  const origin = new URL(c.req.url).origin;
46
- const result = shareService.getResourceMarkdown(token, origin);
46
+ const result = await shareService.getResourceMarkdown(token, origin);
47
47
  if (result.status === 'not_found') {
48
48
  return c.json({ error: 'not_found' }, 404);
49
49
  }
@@ -52,6 +52,95 @@ export function createSharesRouter(shareService) {
52
52
  }
53
53
  return c.body(result.markdown, 200, { 'Content-Type': 'text/markdown; charset=utf-8' });
54
54
  });
55
+ // Pre-join: audience name + mode so the portal can render the join gate.
56
+ router.get('/share/:token/meta', async (c) => {
57
+ const token = c.req.param('token');
58
+ const result = await shareService.getShareMeta(token);
59
+ if (result.status === 'not_found') {
60
+ return c.json({ error: 'not_found' }, 404);
61
+ }
62
+ return c.json({ audience_name: result.audienceName, mode: result.mode });
63
+ });
64
+ // Named join: gate by invite allow-list when mode=invite; mint guest session.
65
+ router.post('/share/:token/join', async (c) => {
66
+ const token = c.req.param('token');
67
+ let body;
68
+ try {
69
+ body = await c.req.json();
70
+ }
71
+ catch {
72
+ return c.json({ error: 'invalid_json' }, 400);
73
+ }
74
+ const result = await shareService.joinShare(token, {
75
+ name: body.name ?? '',
76
+ email: body.email,
77
+ });
78
+ if (result.status === 'unauthorized') {
79
+ return c.json({ error: 'unauthorized' }, 401);
80
+ }
81
+ if (result.status === 'name_required') {
82
+ return c.json({ error: 'name_required' }, 400);
83
+ }
84
+ if (result.status === 'email_not_invited') {
85
+ return c.json({ error: 'email_not_invited' }, 403);
86
+ }
87
+ return c.json({
88
+ session_token: result.sessionToken,
89
+ participant: result.participant,
90
+ share: {
91
+ audience_name: result.share.audienceName,
92
+ permissions: result.share.permissions,
93
+ },
94
+ });
95
+ });
96
+ // Guest-session-gated portal view (middleware sets AuthContext kind guest).
97
+ // Live projection per request; uniform 404 on any failure shape.
98
+ router.get('/share/:token/view', async (c) => {
99
+ const token = c.req.param('token');
100
+ const view = await shareService.getClientView(token);
101
+ if (view === undefined) {
102
+ return c.json({ error: 'not_found' }, 404);
103
+ }
104
+ return c.json(view);
105
+ });
106
+ // Guest moderated inbox — same guest session as view (BA6b single-server).
107
+ router.post('/share/:token/submissions', async (c) => {
108
+ const token = c.req.param('token');
109
+ let body;
110
+ try {
111
+ body = await c.req.json();
112
+ }
113
+ catch {
114
+ return c.json({ error: 'invalid_json' }, 400);
115
+ }
116
+ const result = await shareService.submitIssue(token, {
117
+ title: body.title ?? '',
118
+ body: body.body,
119
+ severity: body.severity,
120
+ task_ref: body.task_ref,
121
+ });
122
+ if (result.status === 'unauthorized') {
123
+ return c.json({ error: 'unauthorized' }, 401);
124
+ }
125
+ if (result.status === 'submit_not_permitted') {
126
+ return c.json({ error: 'submit_not_permitted' }, 403);
127
+ }
128
+ if (result.status === 'title_required') {
129
+ return c.json({ error: 'title_required' }, 400);
130
+ }
131
+ if (result.status === 'rate_limited') {
132
+ return c.json({ error: 'rate_limited' }, 429);
133
+ }
134
+ return c.json({ submission: result.submission }, 201);
135
+ });
136
+ router.get('/share/:token/submissions', async (c) => {
137
+ const token = c.req.param('token');
138
+ const result = await shareService.listMySubmissions(token);
139
+ if (result.status === 'unauthorized') {
140
+ return c.json({ error: 'unauthorized' }, 401);
141
+ }
142
+ return c.json(result.submissions);
143
+ });
55
144
  return router;
56
145
  }
57
146
  //# sourceMappingURL=shares.js.map
@@ -6,16 +6,16 @@ function isSubmissionStatus(value) {
6
6
  }
7
7
  export function createSubmissionsRouter(syncService, projectService) {
8
8
  const router = new Hono();
9
- router.get('/projects/:id/submissions', (c) => {
9
+ router.get('/projects/:id/submissions', async (c) => {
10
10
  const projectId = c.req.param('id');
11
- if (!projectService.get(projectId)) {
11
+ if (!(await projectService.get(projectId))) {
12
12
  return c.json({ error: 'not_found' }, 404);
13
13
  }
14
14
  const statusParam = c.req.query('status') ?? 'pending';
15
15
  if (!isSubmissionStatus(statusParam)) {
16
16
  return c.json({ error: 'invalid_argument' }, 400);
17
17
  }
18
- return c.json(syncService.listTriage(projectId, statusParam));
18
+ return c.json(await syncService.listTriage(projectId, statusParam));
19
19
  });
20
20
  router.post('/submissions/:id/triage', async (c) => {
21
21
  const submissionId = c.req.param('id');
@@ -23,14 +23,13 @@ export function createSubmissionsRouter(syncService, projectService) {
23
23
  if (body.action !== 'accept' && body.action !== 'reject') {
24
24
  return c.json({ error: 'invalid_argument' }, 400);
25
25
  }
26
- const submission = syncService.getSubmission(submissionId);
26
+ const submission = await syncService.getSubmission(submissionId);
27
27
  if (submission === undefined) {
28
28
  return c.json({ error: 'not_found' }, 404);
29
29
  }
30
- const remote = syncService.getRemote(submission.project_id);
31
- if (remote === undefined) {
32
- return c.json({ error: 'not_published' }, 400);
33
- }
30
+ // Optional remote: single-server guest submit has no cross-server ack target.
31
+ // Legacy local→remote pull still stores a remote when present.
32
+ const remote = await syncService.getRemote(submission.project_id);
34
33
  try {
35
34
  // Triage never creates a `todo` task — the scope->todo release is the human's own
36
35
  // board action, enforced in syncService.triage(). A merge (link_task_id) links to
@@ -2,8 +2,8 @@ import { Hono } from 'hono';
2
2
  import { InvalidTagError } from '../services/tags.js';
3
3
  export function createTagsRouter(tagService) {
4
4
  const router = new Hono();
5
- router.get('/projects/:id/tags', (c) => {
6
- const tags = tagService.list(c.req.param('id'));
5
+ router.get('/projects/:id/tags', async (c) => {
6
+ const tags = await tagService.list(c.req.param('id'));
7
7
  if (!tags) {
8
8
  return c.json({ error: 'not_found' }, 404);
9
9
  }
@@ -15,7 +15,7 @@ export function createTagsRouter(tagService) {
15
15
  return c.json({ error: 'invalid_argument' }, 400);
16
16
  }
17
17
  try {
18
- const tag = tagService.create(c.req.param('id'), {
18
+ const tag = await tagService.create(c.req.param('id'), {
19
19
  name: body.name,
20
20
  color: body.color,
21
21
  });
@@ -34,7 +34,7 @@ export function createTagsRouter(tagService) {
34
34
  router.patch('/tags/:id', async (c) => {
35
35
  const body = await c.req.json();
36
36
  try {
37
- const tag = tagService.update(c.req.param('id'), {
37
+ const tag = await tagService.update(c.req.param('id'), {
38
38
  ...(body.name !== undefined ? { name: body.name } : {}),
39
39
  ...(body.color !== undefined ? { color: body.color } : {}),
40
40
  });
@@ -50,8 +50,8 @@ export function createTagsRouter(tagService) {
50
50
  throw error;
51
51
  }
52
52
  });
53
- router.delete('/tags/:id', (c) => {
54
- const deleted = tagService.delete(c.req.param('id'));
53
+ router.delete('/tags/:id', async (c) => {
54
+ const deleted = await tagService.delete(c.req.param('id'));
55
55
  if (!deleted) {
56
56
  return c.json({ error: 'not_found' }, 404);
57
57
  }
@@ -15,7 +15,7 @@ export function createTasksRouter(taskService) {
15
15
  return c.json({ error: 'invalid_argument' }, 400);
16
16
  }
17
17
  try {
18
- const task = taskService.update(c.req.param('id'), {
18
+ const task = await taskService.update(c.req.param('id'), {
19
19
  ...(body.label !== undefined ? { label: body.label } : {}),
20
20
  ...(body.status !== undefined ? { status: body.status } : {}),
21
21
  ...(body.description !== undefined ? { description: body.description } : {}),
@@ -35,13 +35,24 @@ export function createTasksRouter(taskService) {
35
35
  throw error;
36
36
  }
37
37
  });
38
- router.delete('/tasks/:id', (c) => {
39
- const deleted = taskService.delete(c.req.param('id'));
38
+ router.delete('/tasks/:id', async (c) => {
39
+ const deleted = await taskService.delete(c.req.param('id'));
40
40
  if (!deleted) {
41
41
  return c.json({ error: 'not_found' }, 404);
42
42
  }
43
43
  return c.body(null, 204);
44
44
  });
45
+ router.post('/tasks/:id/claim', async (c) => {
46
+ const body = await c.req.json();
47
+ if (typeof body.agent_ref !== 'string' || body.agent_ref.length === 0) {
48
+ return c.json({ error: 'invalid_argument' }, 400);
49
+ }
50
+ const result = await taskService.claim(c.req.param('id'), body.agent_ref);
51
+ if (!result.claimed) {
52
+ return c.json(result, 409);
53
+ }
54
+ return c.json(result);
55
+ });
45
56
  return router;
46
57
  }
47
58
  //# sourceMappingURL=tasks.js.map
@@ -6,12 +6,12 @@ export function createTokensRouter(tokenService) {
6
6
  if (typeof body.name !== 'string' || body.name.trim() === '') {
7
7
  return c.json({ error: 'invalid_argument' }, 400);
8
8
  }
9
- const created = tokenService.create(body.name.trim());
9
+ const created = await tokenService.create(body.name.trim());
10
10
  return c.json(created, 201);
11
11
  });
12
- router.get('/mcp-tokens', (c) => c.json(tokenService.list()));
13
- router.delete('/mcp-tokens/:id', (c) => {
14
- const revoked = tokenService.revoke(c.req.param('id'));
12
+ router.get('/mcp-tokens', async (c) => c.json(await tokenService.list()));
13
+ router.delete('/mcp-tokens/:id', async (c) => {
14
+ const revoked = await tokenService.revoke(c.req.param('id'));
15
15
  if (!revoked) {
16
16
  return c.json({ error: 'not_found' }, 404);
17
17
  }
@@ -33,7 +33,7 @@ export declare function serializeGoal(goal: Goal): {
33
33
  id: string;
34
34
  project_id: string;
35
35
  objective: string;
36
- status: "active" | "paused" | "complete" | "blocked";
36
+ status: "paused" | "active" | "complete" | "blocked";
37
37
  verification_surface: string | null;
38
38
  constraints: string | null;
39
39
  boundaries: string | null;
@@ -55,7 +55,7 @@ export declare function serializeTask(task: Task, tags?: Tag[]): {
55
55
  project_id: string;
56
56
  goal_id: string;
57
57
  label: string;
58
- status: "scope" | "todo" | "in_progress" | "done" | "backlog";
58
+ status: "in_progress" | "done" | "todo" | "scope" | "backlog";
59
59
  description: string | null;
60
60
  x: number;
61
61
  y: number;
@@ -150,7 +150,7 @@ export declare function serializeEdge(edge: Edge): {
150
150
  export declare function serializeAgentRun(run: AgentRun): {
151
151
  id: string;
152
152
  project_id: string;
153
- status: "running" | "completed" | "failed";
153
+ status: "completed" | "running" | "failed";
154
154
  label: string | null;
155
155
  started_at: string;
156
156
  completed_at: string | null;
package/dist/server.d.ts CHANGED
@@ -1,13 +1,31 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { Db } from '@plandesk/db';
3
- import type { EventBus } from './events.js';
3
+ import type { GithubConfig } from './github.js';
4
+ import type { BetterAuthInstance } from './better-auth.js';
4
5
  import { type Services } from './services/index.js';
5
6
  export type AppDeps = {
6
7
  db: Db;
7
- eventBus?: EventBus;
8
8
  services?: Services;
9
9
  mcp?: Hono;
10
10
  authPassword?: string;
11
+ /** Server bind host — loopback default-org only when this is loopback. Default 127.0.0.1. */
12
+ bindHost?: string;
13
+ /**
14
+ * GitHub app for better-auth social sign-in. Omit it and the instance simply
15
+ * has no GitHub sign-in; /auth/methods reports githubEnabled:false.
16
+ * Self-hosting must never require registering an app (REQ-20).
17
+ */
18
+ github?: GithubConfig;
19
+ /**
20
+ * better-auth (GitHub sign-in + session + apiKey). Omit and /api/auth/* 404s;
21
+ * when set, org middleware recognizes better-auth session cookies and API keys.
22
+ */
23
+ betterAuth?: {
24
+ secret: string;
25
+ baseURL: string;
26
+ };
27
+ /** Reuse a better-auth instance owned by an edge entry across requests. */
28
+ betterAuthInstance?: BetterAuthInstance;
11
29
  };
12
30
  export declare function createApp(deps: AppDeps): Hono;
13
31
  //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Hono } from 'hono';
2
- import { createAuthMiddleware } from './auth.js';
2
+ import { createAuthMiddleware, createOrgAuthMiddleware, createWriteGuardMiddleware, } from './auth.js';
3
3
  import { healthRouter } from './routes/health.js';
4
+ import { createAuthRouter } from './routes/auth.js';
5
+ import { createBetterAuth } from './better-auth.js';
4
6
  import { createProjectsRouter } from './routes/projects.js';
5
7
  import { createTasksRouter } from './routes/tasks.js';
6
8
  import { createTagsRouter } from './routes/tags.js';
@@ -12,21 +14,60 @@ import { createFilesRouter } from './routes/files.js';
12
14
  import { createFoldersRouter } from './routes/folders.js';
13
15
  import { createNotesRouter } from './routes/notes.js';
14
16
  import { createSharesRouter } from './routes/shares.js';
15
- import { createEventsRouter } from './routes/events.js';
16
- import { createTokensRouter } from './routes/tokens.js';
17
17
  import { createAgentRunsRouter } from './routes/agent-runs.js';
18
18
  import { createGoalsRouter } from './routes/goals.js';
19
19
  import { createSubmissionsRouter } from './routes/submissions.js';
20
- import { mountStatic } from './static.js';
20
+ import { createOrgsRouter } from './routes/orgs.js';
21
21
  import { createServices } from './services/index.js';
22
+ import { ProjectNotInOrgError } from './services/scope.js';
23
+ import { ReadOnlyTokenError } from './auth-context.js';
24
+ import { PermissionDeniedError } from './permissions.js';
22
25
  export function createApp(deps) {
23
- const services = deps.services ?? createServices({ db: deps.db, eventBus: deps.eventBus });
24
- const { eventBus, projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, syncService, fileService, artifactService, shareService, } = services;
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
+ const bindHost = deps.bindHost ?? '127.0.0.1';
25
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).
32
+ const betterAuthInstance = deps.betterAuthInstance ??
33
+ (deps.betterAuth !== undefined
34
+ ? createBetterAuth({
35
+ client: deps.db.$client,
36
+ db: deps.db,
37
+ secret: deps.betterAuth.secret,
38
+ baseURL: deps.betterAuth.baseURL,
39
+ github: deps.github,
40
+ })
41
+ : undefined);
42
+ // Always-on org resolution (better-auth apiKey/session, loopback, or guest).
43
+ app.use('*', createOrgAuthMiddleware({
44
+ db: deps.db,
45
+ bindHost,
46
+ betterAuth: betterAuthInstance,
47
+ }));
48
+ app.use('*', createWriteGuardMiddleware());
26
49
  if (deps.authPassword !== undefined && deps.authPassword.length > 0) {
27
50
  app.use('*', createAuthMiddleware(deps.authPassword));
28
51
  }
52
+ app.onError((err, c) => {
53
+ if (err instanceof ProjectNotInOrgError) {
54
+ return c.json({ error: 'not_found' }, 404);
55
+ }
56
+ if (err instanceof ReadOnlyTokenError || err instanceof PermissionDeniedError) {
57
+ return c.json({ error: 'forbidden' }, 403);
58
+ }
59
+ throw err;
60
+ });
29
61
  app.route('/api/v1', healthRouter);
62
+ app.route('/api/v1', createAuthRouter({
63
+ db: deps.db,
64
+ github: deps.github,
65
+ betterAuth: betterAuthInstance,
66
+ }));
67
+ app.route('/api/v1', createOrgsRouter(deps.db, {
68
+ betterAuth: betterAuthInstance,
69
+ baseURL: deps.betterAuth?.baseURL,
70
+ }));
30
71
  app.route('/api/v1', createProjectsRouter(projectService, taskService));
31
72
  app.route('/api/v1', createGoalsRouter(goalService));
32
73
  app.route('/api/v1', createTasksRouter(taskService));
@@ -39,10 +80,8 @@ export function createApp(deps) {
39
80
  app.route('/api/v1', createNotesRouter(noteService));
40
81
  app.route('/api/v1', createSharesRouter(shareService));
41
82
  app.route('/api/v1', createCommentsRouter(commentService));
42
- app.route('/api/v1', createTokensRouter(tokenService));
43
83
  app.route('/api/v1', createAgentRunsRouter(agentRunService));
44
84
  app.route('/api/v1', createSubmissionsRouter(syncService, projectService));
45
- app.route('/api/v1', createEventsRouter(eventBus));
46
85
  // Mount the MCP router BEFORE the static/SPA handler. The MCP transport uses
47
86
  // GET /mcp/ for its server->client SSE stream; if the SPA catch-all (app.get('*'))
48
87
  // is registered first it shadows that GET and the stream never reaches the
@@ -50,7 +89,16 @@ export function createApp(deps) {
50
89
  if (deps.mcp) {
51
90
  app.route('/mcp', deps.mcp);
52
91
  }
53
- mountStatic(app);
92
+ // Mounted at /api/auth/*, one segment away from the existing /api/v1/auth/*
93
+ // (routes/auth.ts) so it can never shadow it. /api/auth/* is public so a
94
+ // stranger can reach sign-in without a plandesk credential first (BA4a).
95
+ if (betterAuthInstance) {
96
+ app.on(['GET', 'POST'], '/api/auth/*', (c) => betterAuthInstance.handler(c.req.raw));
97
+ }
98
+ // SPA static files are NOT mounted here. Node mounts them via `mountStatic`
99
+ // in the serve entry; Workers/Vercel serve the SPA through platform assets.
100
+ // Keeping static out of createApp prevents node:fs and @hono/node-server
101
+ // from entering the edge bundle graph.
54
102
  app.notFound((c) => c.json({ error: 'not_found' }, 404));
55
103
  return app;
56
104
  }
@@ -1,15 +1,14 @@
1
1
  import { type Db } from '@plandesk/db';
2
- import type { EventBus } from '../events.js';
3
2
  import { type PaginationParams } from '../serialize.js';
4
- export type AgentRunServiceDeps = {
3
+ import { type OrgScopedDeps } from './org-scope.js';
4
+ export type AgentRunServiceDeps = OrgScopedDeps & {
5
5
  db: Db;
6
- eventBus: EventBus;
7
6
  };
8
7
  export declare class InvalidAgentRunError extends Error {
9
8
  constructor(message: string);
10
9
  }
11
10
  export declare function createAgentRunService(deps: AgentRunServiceDeps): {
12
- listForProject(projectId: string, pagination?: PaginationParams): {
11
+ listForProject(projectId: string, pagination?: PaginationParams): Promise<{
13
12
  events: {
14
13
  id: string;
15
14
  message: string;
@@ -17,33 +16,33 @@ export declare function createAgentRunService(deps: AgentRunServiceDeps): {
17
16
  }[];
18
17
  id: string;
19
18
  project_id: string;
20
- status: "running" | "completed" | "failed";
19
+ status: "completed" | "running" | "failed";
21
20
  label: string | null;
22
21
  started_at: string;
23
22
  completed_at: string | null;
24
- }[] | undefined;
25
- start(projectId: string, label?: string | null): {
23
+ }[] | undefined>;
24
+ start(projectId: string, label?: string | null): Promise<{
26
25
  id: string;
27
26
  project_id: string;
28
- status: "running" | "completed" | "failed";
27
+ status: "completed" | "running" | "failed";
29
28
  label: string | null;
30
29
  started_at: string;
31
30
  completed_at: string | null;
32
- } | undefined;
33
- recordProgress(runId: string, message: string): {
31
+ } | undefined>;
32
+ recordProgress(runId: string, message: string): Promise<{
34
33
  id: string;
35
34
  run_id: string;
36
35
  message: string;
37
36
  created_at: string;
38
- } | undefined;
39
- complete(runId: string, status: "completed" | "failed"): {
37
+ } | undefined>;
38
+ complete(runId: string, status: "completed" | "failed"): Promise<{
40
39
  id: string;
41
40
  project_id: string;
42
- status: "running" | "completed" | "failed";
41
+ status: "completed" | "running" | "failed";
43
42
  label: string | null;
44
43
  started_at: string;
45
44
  completed_at: string | null;
46
- } | undefined;
45
+ } | undefined>;
47
46
  };
48
47
  export type AgentRunService = ReturnType<typeof createAgentRunService>;
49
48
  //# sourceMappingURL=agent-runs.d.ts.map
@@ -1,5 +1,7 @@
1
1
  import { createAgentRun, createAgentRunEvent, getAgentRun, getProject, listAgentRunEvents, listAgentRuns, updateAgentRunStatus, } from '@plandesk/db';
2
2
  import { serializeAgentRun, serializeAgentRunEvent } from '../serialize.js';
3
+ import { assertPermission, resolveOrgId } from './org-scope.js';
4
+ import { assertProjectInOrg, ProjectNotInOrgError } from './scope.js';
3
5
  export class InvalidAgentRunError extends Error {
4
6
  constructor(message) {
5
7
  super(message);
@@ -8,84 +10,80 @@ export class InvalidAgentRunError extends Error {
8
10
  }
9
11
  const terminalStatuses = new Set(['completed', 'failed']);
10
12
  export function createAgentRunService(deps) {
11
- const { db, eventBus } = deps;
13
+ const { db } = deps;
12
14
  return {
13
- listForProject(projectId, pagination = {}) {
14
- const project = getProject(db, projectId);
15
- if (!project) {
16
- return undefined;
15
+ async listForProject(projectId, pagination = {}) {
16
+ try {
17
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
18
+ }
19
+ catch (error) {
20
+ if (error instanceof ProjectNotInOrgError) {
21
+ return undefined;
22
+ }
23
+ throw error;
17
24
  }
18
- const runs = listAgentRuns(db, projectId, pagination)
19
- .slice()
20
- .sort((a, b) => {
25
+ const runs = (await listAgentRuns(db, projectId, pagination)).slice().sort((a, b) => {
21
26
  const timeDiff = b.startedAt.getTime() - a.startedAt.getTime();
22
27
  if (timeDiff !== 0) {
23
28
  return timeDiff;
24
29
  }
25
30
  return b.id.localeCompare(a.id);
26
31
  });
27
- return runs.map((run) => {
32
+ return Promise.all(runs.map(async (run) => {
28
33
  const serialized = serializeAgentRun(run);
29
- const events = listAgentRunEvents(db, run.id).map((event) => {
30
- const serialized = serializeAgentRunEvent(event);
34
+ const events = (await listAgentRunEvents(db, run.id)).map((event) => {
35
+ const serializedEvent = serializeAgentRunEvent(event);
31
36
  return {
32
- id: serialized.id,
33
- message: serialized.message,
34
- created_at: serialized.created_at,
37
+ id: serializedEvent.id,
38
+ message: serializedEvent.message,
39
+ created_at: serializedEvent.created_at,
35
40
  };
36
41
  });
37
42
  return { ...serialized, events };
38
- });
43
+ }));
39
44
  },
40
- start(projectId, label) {
41
- const project = getProject(db, projectId);
42
- if (!project) {
43
- return undefined;
45
+ async start(projectId, label) {
46
+ assertPermission(deps, 'agent_run', 'create');
47
+ try {
48
+ await assertProjectInOrg(db, projectId, resolveOrgId(deps));
44
49
  }
45
- const run = createAgentRun(db, { projectId, label });
46
- eventBus.emit({
47
- type: 'agent_run_started',
48
- runId: run.id,
49
- projectId,
50
- });
50
+ catch (error) {
51
+ if (error instanceof ProjectNotInOrgError) {
52
+ return undefined;
53
+ }
54
+ throw error;
55
+ }
56
+ const run = await createAgentRun(db, { projectId, label });
51
57
  return serializeAgentRun(run);
52
58
  },
53
- recordProgress(runId, message) {
54
- const run = getAgentRun(db, runId);
59
+ async recordProgress(runId, message) {
60
+ assertPermission(deps, 'agent_run', 'update');
61
+ const run = await getAgentRun(db, runId);
55
62
  if (!run) {
56
63
  return undefined;
57
64
  }
58
65
  if (terminalStatuses.has(run.status)) {
59
66
  throw new InvalidAgentRunError('Agent run is already complete');
60
67
  }
61
- const event = createAgentRunEvent(db, { runId, message });
62
- eventBus.emit({
63
- type: 'agent_run_progress',
64
- runId,
65
- projectId: run.projectId,
66
- });
68
+ const event = await createAgentRunEvent(db, { runId, message });
67
69
  return serializeAgentRunEvent(event);
68
70
  },
69
- complete(runId, status) {
70
- const run = getAgentRun(db, runId);
71
+ async complete(runId, status) {
72
+ assertPermission(deps, 'agent_run', 'update');
73
+ const run = await getAgentRun(db, runId);
71
74
  if (!run) {
72
75
  return undefined;
73
76
  }
74
77
  if (terminalStatuses.has(run.status)) {
75
78
  throw new InvalidAgentRunError('Agent run is already complete');
76
79
  }
77
- const updated = updateAgentRunStatus(db, runId, {
80
+ const updated = await updateAgentRunStatus(db, runId, {
78
81
  status,
79
82
  completedAt: new Date(),
80
83
  });
81
84
  if (!updated) {
82
85
  return undefined;
83
86
  }
84
- eventBus.emit({
85
- type: 'agent_run_completed',
86
- runId,
87
- projectId: run.projectId,
88
- });
89
87
  return serializeAgentRun(updated);
90
88
  },
91
89
  };
@@ -1,9 +1,8 @@
1
1
  import { type ArtifactKind, type Db } from '@plandesk/db';
2
2
  import { type SerializedArtifact, type SerializedArtifactSummary } from '../serialize.js';
3
- import type { EventBus } from '../events.js';
4
- export type ArtifactServiceDeps = {
3
+ import { type OrgScopedDeps } from './org-scope.js';
4
+ export type ArtifactServiceDeps = OrgScopedDeps & {
5
5
  db: Db;
6
- eventBus: EventBus;
7
6
  };
8
7
  export type CreateArtifactInput = {
9
8
  title: string;
@@ -19,10 +18,10 @@ export declare class InvalidArtifactError extends Error {
19
18
  constructor(message: string);
20
19
  }
21
20
  export declare function createArtifactService(deps: ArtifactServiceDeps): {
22
- listByProject(projectId: string): SerializedArtifactSummary[] | undefined;
23
- create(projectId: string, input: CreateArtifactInput): SerializedArtifact | undefined;
24
- get(id: string): SerializedArtifact | undefined;
25
- update(id: string, input: UpdateArtifactInput): SerializedArtifact | undefined;
21
+ listByProject(projectId: string): Promise<SerializedArtifactSummary[] | undefined>;
22
+ create(projectId: string, input: CreateArtifactInput): Promise<SerializedArtifact | undefined>;
23
+ get(id: string): Promise<SerializedArtifact | undefined>;
24
+ update(id: string, input: UpdateArtifactInput): Promise<SerializedArtifact | undefined>;
26
25
  };
27
26
  export type ArtifactService = ReturnType<typeof createArtifactService>;
28
27
  //# sourceMappingURL=artifacts.d.ts.map