@plandesk/api 1.0.0-beta.8 → 1.0.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.
- package/dist/agent-keys.d.ts +26 -2
- package/dist/agent-keys.js +22 -2
- package/dist/auth-context.d.ts +17 -1
- package/dist/auth.d.ts +10 -0
- package/dist/auth.js +25 -6
- package/dist/better-auth.js +4 -3
- package/dist/identity.d.ts +34 -0
- package/dist/identity.js +190 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/invitations.d.ts +13 -2
- package/dist/invitations.js +14 -2
- package/dist/organizations.d.ts +3 -0
- package/dist/organizations.js +18 -0
- package/dist/projection.d.ts +28 -0
- package/dist/projection.js +29 -1
- package/dist/routes/auth.js +21 -5
- package/dist/routes/comments.js +3 -1
- package/dist/routes/orgs.js +139 -24
- package/dist/routes/projects.js +36 -5
- package/dist/routes/shares.js +32 -0
- package/dist/routes/tasks.js +3 -0
- package/dist/serialize.d.ts +2 -0
- package/dist/serialize.js +1 -0
- package/dist/server.js +9 -6
- package/dist/services/agent-runs.js +18 -0
- package/dist/services/artifacts.js +18 -0
- package/dist/services/canvas.js +9 -0
- package/dist/services/comments.js +63 -13
- package/dist/services/documents.js +36 -0
- package/dist/services/files.js +19 -1
- package/dist/services/folders.js +27 -0
- package/dist/services/goals.js +45 -0
- package/dist/services/index.d.ts +3 -0
- package/dist/services/index.js +1 -1
- package/dist/services/notes.js +27 -0
- package/dist/services/projects.d.ts +27 -0
- package/dist/services/projects.js +113 -4
- package/dist/services/scope.d.ts +4 -0
- package/dist/services/scope.js +22 -0
- package/dist/services/share.d.ts +25 -3
- package/dist/services/share.js +160 -9
- package/dist/services/sync.d.ts +1 -1
- package/dist/services/sync.js +52 -1
- package/dist/services/tags.js +18 -0
- package/dist/services/tasks.d.ts +1 -1
- package/dist/services/tasks.js +22 -0
- package/dist/test-helpers.d.ts +1 -0
- package/dist/test-helpers.js +14 -0
- package/dist/vercel.js +9 -1
- package/dist/worker.js +1 -1
- package/package.json +3 -3
- package/web/assets/index-BBNvafSX.js +358 -0
- package/web/assets/index-BqoMRSUH.css +2 -0
- package/web/index.html +2 -2
- package/web/assets/index-aQ0kLFAX.js +0 -358
- package/web/assets/index-nXT4L9C-.css +0 -2
package/dist/invitations.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* call via runtime-validated Reflect, same pattern as agent-keys.ts.
|
|
7
7
|
*/
|
|
8
8
|
import { makeSignature } from 'better-auth/crypto';
|
|
9
|
+
import { ensureDefaultTeamForOrg } from './identity.js';
|
|
9
10
|
export const INVITATION_ROLES = ['owner', 'admin', 'member'];
|
|
10
11
|
export function isInvitationRole(value) {
|
|
11
12
|
return INVITATION_ROLES.includes(value);
|
|
@@ -46,6 +47,7 @@ function parseInvitation(raw) {
|
|
|
46
47
|
email: typeof raw.email === 'string' ? raw.email : '',
|
|
47
48
|
role: typeof raw.role === 'string' ? raw.role : '',
|
|
48
49
|
organizationId: typeof raw.organizationId === 'string' ? raw.organizationId : '',
|
|
50
|
+
teamId: typeof raw.teamId === 'string' && raw.teamId.length > 0 ? raw.teamId : null,
|
|
49
51
|
inviterId: typeof raw.inviterId === 'string' ? raw.inviterId : '',
|
|
50
52
|
status: typeof raw.status === 'string' ? raw.status : '',
|
|
51
53
|
expiresAt: raw.expiresAt instanceof Date ? raw.expiresAt : new Date(String(raw.expiresAt ?? '')),
|
|
@@ -53,8 +55,10 @@ function parseInvitation(raw) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
/**
|
|
56
|
-
* Create
|
|
57
|
-
*
|
|
58
|
+
* Create a workspace-scoped invitation using the caller's better-auth session
|
|
59
|
+
* (Cookie headers). The invitee joins the org AND the team on accept
|
|
60
|
+
* (better-auth does both when teamId is set). No mailer: caller delivers
|
|
61
|
+
* claimUrl by hand.
|
|
58
62
|
*/
|
|
59
63
|
export async function createOrganizationInvitation(auth, opts) {
|
|
60
64
|
const raw = await callPluginApi(auth, 'createInvitation', {
|
|
@@ -62,6 +66,7 @@ export async function createOrganizationInvitation(auth, opts) {
|
|
|
62
66
|
email: opts.email.trim().toLowerCase(),
|
|
63
67
|
role: opts.role,
|
|
64
68
|
organizationId: opts.organizationId,
|
|
69
|
+
teamId: opts.teamId,
|
|
65
70
|
},
|
|
66
71
|
headers: opts.headers,
|
|
67
72
|
});
|
|
@@ -69,6 +74,8 @@ export async function createOrganizationInvitation(auth, opts) {
|
|
|
69
74
|
return {
|
|
70
75
|
invitationId: invitation.id,
|
|
71
76
|
claimUrl: invitationClaimUrl(opts.baseURL, invitation.id),
|
|
77
|
+
organizationId: opts.organizationId,
|
|
78
|
+
teamId: opts.teamId,
|
|
72
79
|
invitation,
|
|
73
80
|
};
|
|
74
81
|
}
|
|
@@ -196,13 +203,18 @@ export async function ensureShellOwner(auth, organizationId) {
|
|
|
196
203
|
}
|
|
197
204
|
/**
|
|
198
205
|
* Shell-side owner invitation for `plandesk admin invite-owner` (no GitHub, no mailer).
|
|
206
|
+
* The owner role is org-level; teamId targets the org's default workspace so
|
|
207
|
+
* the bootstrap satisfies the workspace-scoped invite contract (the owner
|
|
208
|
+
* reaches every workspace via the owner role regardless).
|
|
199
209
|
*/
|
|
200
210
|
export async function mintOwnerInvitation(auth, opts) {
|
|
201
211
|
const { headers } = await ensureShellOwner(auth, opts.organizationId);
|
|
212
|
+
const teamId = await ensureDefaultTeamForOrg(auth, opts.organizationId);
|
|
202
213
|
const created = await createOrganizationInvitation(auth, {
|
|
203
214
|
email: opts.email,
|
|
204
215
|
role: 'owner',
|
|
205
216
|
organizationId: opts.organizationId,
|
|
217
|
+
teamId,
|
|
206
218
|
headers,
|
|
207
219
|
baseURL: opts.baseURL,
|
|
208
220
|
});
|
package/dist/organizations.d.ts
CHANGED
|
@@ -33,6 +33,9 @@ export declare function listOrganizationMembers(auth: BetterAuthInstance, organi
|
|
|
33
33
|
export type InvitationPreview = {
|
|
34
34
|
organizationId: string;
|
|
35
35
|
organizationName: string;
|
|
36
|
+
/** Workspace (team) the invitee will join; null for legacy team-less rows. */
|
|
37
|
+
workspaceId: string | null;
|
|
38
|
+
workspaceName: string;
|
|
36
39
|
role: string;
|
|
37
40
|
email: string;
|
|
38
41
|
status: string;
|
package/dist/organizations.js
CHANGED
|
@@ -87,9 +87,27 @@ export async function getInvitationPreview(auth, invitationId) {
|
|
|
87
87
|
model: 'organization',
|
|
88
88
|
where: [{ field: 'id', value: invitation.organizationId }],
|
|
89
89
|
});
|
|
90
|
+
// better-auth stores team ids comma-joined on the invitation row; take the
|
|
91
|
+
// first. Scoped by org so a stale/foreign id cannot leak a name.
|
|
92
|
+
const workspaceId = typeof invitation.teamId === 'string' && invitation.teamId.length > 0
|
|
93
|
+
? invitation.teamId.split(',')[0]
|
|
94
|
+
: null;
|
|
95
|
+
let workspaceName = '';
|
|
96
|
+
if (workspaceId !== null) {
|
|
97
|
+
const team = await adapter.findOne({
|
|
98
|
+
model: 'team',
|
|
99
|
+
where: [
|
|
100
|
+
{ field: 'id', value: workspaceId },
|
|
101
|
+
{ field: 'organizationId', value: invitation.organizationId },
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
workspaceName = team?.name ?? '';
|
|
105
|
+
}
|
|
90
106
|
return {
|
|
91
107
|
organizationId: invitation.organizationId,
|
|
92
108
|
organizationName: org?.name ?? '',
|
|
109
|
+
workspaceId,
|
|
110
|
+
workspaceName,
|
|
93
111
|
role: invitation.role,
|
|
94
112
|
email: invitation.email,
|
|
95
113
|
status: invitation.status,
|
package/dist/projection.d.ts
CHANGED
|
@@ -47,5 +47,33 @@ export type ClientView = {
|
|
|
47
47
|
expires_at: string | null;
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
|
+
export type WorkspaceClientView = {
|
|
51
|
+
kind: 'workspace';
|
|
52
|
+
workspace: {
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
};
|
|
56
|
+
projects: Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
view: ClientView;
|
|
60
|
+
}>;
|
|
61
|
+
share: {
|
|
62
|
+
audience_name: string;
|
|
63
|
+
permissions: {
|
|
64
|
+
read: boolean;
|
|
65
|
+
submit: boolean;
|
|
66
|
+
};
|
|
67
|
+
expires_at: string | null;
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
export type AnyClientView = ClientView | WorkspaceClientView;
|
|
50
71
|
export declare function buildClientView(db: Db, projectId: string, share: Share): Promise<ClientView | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Workspace projection: the existing per-project ClientView for every project in
|
|
74
|
+
* the shared workspace. Reuses buildClientView per project (no reimplementation).
|
|
75
|
+
* The workspace name is unknown to the projects table — the caller may pass it;
|
|
76
|
+
* otherwise the workspace id stands in (the share was validated at creation).
|
|
77
|
+
*/
|
|
78
|
+
export declare function buildWorkspaceClientView(db: Db, workspaceId: string, share: Share, workspaceName?: string): Promise<WorkspaceClientView | undefined>;
|
|
51
79
|
//# sourceMappingURL=projection.d.ts.map
|
package/dist/projection.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getProject, listDocuments, listEdges, listTasks, parseSharePermissions, parseSharePolicy, } from '@plandesk/db';
|
|
1
|
+
import { getProject, listDocuments, listEdges, listProjectsByWorkspace, listTasks, parseSharePermissions, parseSharePolicy, } from '@plandesk/db';
|
|
2
2
|
function buildProgress(tasks) {
|
|
3
3
|
const progress = {};
|
|
4
4
|
for (const task of tasks) {
|
|
@@ -72,4 +72,32 @@ export async function buildClientView(db, projectId, share) {
|
|
|
72
72
|
},
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Workspace projection: the existing per-project ClientView for every project in
|
|
77
|
+
* the shared workspace. Reuses buildClientView per project (no reimplementation).
|
|
78
|
+
* The workspace name is unknown to the projects table — the caller may pass it;
|
|
79
|
+
* otherwise the workspace id stands in (the share was validated at creation).
|
|
80
|
+
*/
|
|
81
|
+
export async function buildWorkspaceClientView(db, workspaceId, share, workspaceName) {
|
|
82
|
+
const workspaceProjects = await listProjectsByWorkspace(db, workspaceId);
|
|
83
|
+
const permissions = parseSharePermissions(share);
|
|
84
|
+
const projects = [];
|
|
85
|
+
for (const project of workspaceProjects) {
|
|
86
|
+
const view = await buildClientView(db, project.id, share);
|
|
87
|
+
if (view === undefined) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
projects.push({ id: project.id, name: project.name, view });
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
kind: 'workspace',
|
|
94
|
+
workspace: { id: workspaceId, name: workspaceName ?? workspaceId },
|
|
95
|
+
projects,
|
|
96
|
+
share: {
|
|
97
|
+
audience_name: share.audienceName,
|
|
98
|
+
permissions,
|
|
99
|
+
expires_at: share.expiresAt?.toISOString() ?? null,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
75
103
|
//# sourceMappingURL=projection.js.map
|
package/dist/routes/auth.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { createOrgOwnerKey } from '../agent-keys.js';
|
|
3
3
|
import { getAuthContext } from '../auth-context.js';
|
|
4
|
+
import { getActiveTeamForSession, listTeamsForOrg, } from '../identity.js';
|
|
4
5
|
import { listOrganizationsForUser, resolveOrganizationName } from '../organizations.js';
|
|
5
6
|
import { requirePermission } from '../permissions.js';
|
|
6
7
|
export function createAuthRouter(deps) {
|
|
@@ -27,12 +28,25 @@ export function createAuthRouter(deps) {
|
|
|
27
28
|
let orgs = [
|
|
28
29
|
{ id: org.id, name: org.name, role: ctx.role ?? 'member' },
|
|
29
30
|
];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
let activeWorkspace = null;
|
|
32
|
+
let workspaces = [];
|
|
33
|
+
if (betterAuth !== undefined) {
|
|
34
|
+
workspaces = await listTeamsForOrg(betterAuth, ctx.orgId);
|
|
35
|
+
if (ctx.kind === 'session') {
|
|
36
|
+
const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
|
|
37
|
+
if (session === null) {
|
|
38
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
39
|
+
}
|
|
40
|
+
orgs = await listOrganizationsForUser(betterAuth, session.user.id);
|
|
41
|
+
const active = await getActiveTeamForSession(betterAuth, session.session.token, ctx.orgId);
|
|
42
|
+
activeWorkspace =
|
|
43
|
+
active ?? (workspaces[0] !== undefined ? workspaces[0] : null);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
// Loopback: no session row to carry an active team — default to the
|
|
47
|
+
// first workspace (the org's default team) so the switcher still renders.
|
|
48
|
+
activeWorkspace = workspaces[0] !== undefined ? workspaces[0] : null;
|
|
34
49
|
}
|
|
35
|
-
orgs = await listOrganizationsForUser(betterAuth, session.user.id);
|
|
36
50
|
}
|
|
37
51
|
return c.json({
|
|
38
52
|
kind: ctx.kind,
|
|
@@ -40,6 +54,8 @@ export function createAuthRouter(deps) {
|
|
|
40
54
|
role: ctx.role,
|
|
41
55
|
org,
|
|
42
56
|
orgs,
|
|
57
|
+
active_workspace: activeWorkspace,
|
|
58
|
+
workspaces,
|
|
43
59
|
});
|
|
44
60
|
});
|
|
45
61
|
/**
|
package/dist/routes/comments.js
CHANGED
|
@@ -30,7 +30,9 @@ async function handleListComments(c, commentService, target) {
|
|
|
30
30
|
const includeResolved = parseIncludeResolved(c.req.query('include_resolved'));
|
|
31
31
|
const comments = await commentService.listByTarget(target, { includeResolved });
|
|
32
32
|
if (!comments) {
|
|
33
|
-
|
|
33
|
+
// Fail-closed: out-of-scope target is a 404 with an empty collection body,
|
|
34
|
+
// leaking neither existence nor rows.
|
|
35
|
+
return c.json([], 404);
|
|
34
36
|
}
|
|
35
37
|
return c.json(comments);
|
|
36
38
|
}
|
package/dist/routes/orgs.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { getProjectInOrg, importProject, InvalidExportVersionError, PLANDESK_EXPORT_VERSION, } from '@plandesk/db';
|
|
3
|
-
import { createScopedAgentKey } from '../agent-keys.js';
|
|
3
|
+
import { createScopedAgentKey, createWorkspaceScopedAgentKey, } from '../agent-keys.js';
|
|
4
4
|
import { getAuthContext, getOrgAuthContext } from '../auth-context.js';
|
|
5
5
|
import { acceptOrganizationInvitation, createOrganizationInvitation, isAuthApiError, isInvitationRole, } from '../invitations.js';
|
|
6
6
|
import { getInvitationPreview, getOrganizationById, listOrganizationMembers, } from '../organizations.js';
|
|
7
|
+
import { createTeamForOrg, getTeamInOrg } from '../identity.js';
|
|
7
8
|
import { requirePermission } from '../permissions.js';
|
|
8
9
|
function isPermissionSet(value) {
|
|
9
10
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -56,11 +57,11 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
56
57
|
return (await getOrganizationById(betterAuth, orgId)) !== undefined;
|
|
57
58
|
}
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* REQ-A1: list teams (workspaces) in an org. Any org member may read.
|
|
61
|
+
* A workspace/project-scoped agent key sees only its own workspace (never
|
|
62
|
+
* sibling workspaces); owner keys and sessions keep full org reach.
|
|
62
63
|
*/
|
|
63
|
-
router.
|
|
64
|
+
router.get('/orgs/:orgId/workspaces', async (c) => {
|
|
64
65
|
if (betterAuth === undefined) {
|
|
65
66
|
return c.json({ error: 'unavailable' }, 503);
|
|
66
67
|
}
|
|
@@ -68,16 +69,67 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
68
69
|
if (!(await requireKnownOrg(orgId))) {
|
|
69
70
|
return c.json({ error: 'not_found' }, 404);
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
const adapter = (await betterAuth.$context).adapter;
|
|
73
|
+
let teams = await adapter.findMany({
|
|
74
|
+
model: 'team',
|
|
75
|
+
where: [{ field: 'organizationId', value: orgId }],
|
|
76
|
+
});
|
|
77
|
+
const authCtx = getOrgAuthContext();
|
|
78
|
+
if (authCtx.kind === 'apikey' && authCtx.profile === 'agent') {
|
|
79
|
+
const allowed = new Set();
|
|
80
|
+
if (authCtx.workspaceId !== undefined) {
|
|
81
|
+
allowed.add(authCtx.workspaceId);
|
|
82
|
+
}
|
|
83
|
+
else if (authCtx.projectId !== undefined) {
|
|
84
|
+
const project = await getProjectInOrg(db, authCtx.projectId, orgId);
|
|
85
|
+
if (project !== undefined) {
|
|
86
|
+
allowed.add(project.workspaceId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
teams = teams.filter((t) => allowed.has(t.id));
|
|
90
|
+
}
|
|
91
|
+
return c.json({ workspaces: teams.map((t) => ({ id: t.id, name: t.name })) }, 200);
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* REQ-A2: create a team (workspace) in an org. Owner only — agent apikeys are
|
|
95
|
+
* rejected outright (403) so even a stray custom team:create permission (which
|
|
96
|
+
* AGENT_FORBIDDEN_RESOURCES strips) can never create a workspace.
|
|
97
|
+
*/
|
|
98
|
+
router.post('/orgs/:orgId/workspaces', async (c) => {
|
|
99
|
+
if (betterAuth === undefined) {
|
|
100
|
+
return c.json({ error: 'unavailable' }, 503);
|
|
101
|
+
}
|
|
102
|
+
const orgId = c.req.param('orgId');
|
|
103
|
+
if (!(await requireKnownOrg(orgId))) {
|
|
104
|
+
return c.json({ error: 'not_found' }, 404);
|
|
105
|
+
}
|
|
106
|
+
const authCtx = getOrgAuthContext();
|
|
107
|
+
if (authCtx.kind === 'apikey' && authCtx.profile === 'agent') {
|
|
108
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
109
|
+
}
|
|
110
|
+
requirePermission(authCtx, 'team', 'create');
|
|
72
111
|
const body = await c.req.json();
|
|
73
|
-
if (typeof body.
|
|
112
|
+
if (typeof body.name !== 'string' || body.name.trim() === '') {
|
|
74
113
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
75
114
|
}
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
115
|
+
const name = body.name.trim();
|
|
116
|
+
const team = await createTeamForOrg(betterAuth, orgId, name);
|
|
117
|
+
return c.json({ id: team.id, name: team.name }, 201);
|
|
118
|
+
});
|
|
119
|
+
/**
|
|
120
|
+
* BA4b-3 + REQ-A3: mint a project-scoped or workspace-scoped agent key.
|
|
121
|
+
* Caller must hold apiKey:create (owner key or session owner) — agent keys cannot.
|
|
122
|
+
*/
|
|
123
|
+
router.post('/orgs/:orgId/agent-keys', async (c) => {
|
|
124
|
+
if (betterAuth === undefined) {
|
|
125
|
+
return c.json({ error: 'unavailable' }, 503);
|
|
126
|
+
}
|
|
127
|
+
const orgId = c.req.param('orgId');
|
|
128
|
+
if (!(await requireKnownOrg(orgId))) {
|
|
79
129
|
return c.json({ error: 'not_found' }, 404);
|
|
80
130
|
}
|
|
131
|
+
requirePermission(getOrgAuthContext(), 'apiKey', 'create');
|
|
132
|
+
const body = await c.req.json();
|
|
81
133
|
let permissions;
|
|
82
134
|
if (body.permissions !== undefined) {
|
|
83
135
|
if (!isPermissionSet(body.permissions)) {
|
|
@@ -96,25 +148,66 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
96
148
|
if (userId === undefined) {
|
|
97
149
|
return c.json({ error: 'unauthorized' }, 401);
|
|
98
150
|
}
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
151
|
+
const hasProjectId = typeof body.project_id === 'string' && body.project_id.trim() !== '';
|
|
152
|
+
const hasTeamId = typeof body.team_id === 'string' && body.team_id.trim() !== '';
|
|
153
|
+
if (hasProjectId && hasTeamId) {
|
|
154
|
+
return c.json({ error: 'invalid_argument' }, 400);
|
|
155
|
+
}
|
|
156
|
+
if (hasProjectId) {
|
|
157
|
+
const projectId = body.project_id.trim();
|
|
158
|
+
const project = await getProjectInOrg(db, projectId, orgId);
|
|
159
|
+
if (project === undefined) {
|
|
160
|
+
return c.json({ error: 'not_found' }, 404);
|
|
161
|
+
}
|
|
162
|
+
const mintInput = {
|
|
163
|
+
auth: betterAuth,
|
|
164
|
+
userId,
|
|
165
|
+
orgId,
|
|
166
|
+
projectId,
|
|
167
|
+
...(permissions !== undefined ? { permissions } : {}),
|
|
168
|
+
...(name !== undefined ? { name } : {}),
|
|
169
|
+
};
|
|
170
|
+
const minted = await createScopedAgentKey(mintInput);
|
|
171
|
+
return c.json({ token: minted.key, project_id: projectId }, 200);
|
|
172
|
+
}
|
|
173
|
+
if (hasTeamId) {
|
|
174
|
+
const teamId = body.team_id.trim();
|
|
175
|
+
const adapter = (await betterAuth.$context).adapter;
|
|
176
|
+
const team = await adapter.findOne({
|
|
177
|
+
model: 'team',
|
|
178
|
+
where: [
|
|
179
|
+
{ field: 'id', value: teamId },
|
|
180
|
+
{ field: 'organizationId', value: orgId },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
if (team === null) {
|
|
184
|
+
return c.json({ error: 'not_found' }, 404);
|
|
185
|
+
}
|
|
186
|
+
const minted = await createWorkspaceScopedAgentKey({
|
|
187
|
+
auth: betterAuth,
|
|
188
|
+
userId,
|
|
189
|
+
orgId,
|
|
190
|
+
teamId,
|
|
191
|
+
...(permissions !== undefined ? { permissions } : {}),
|
|
192
|
+
...(name !== undefined ? { name } : {}),
|
|
193
|
+
});
|
|
194
|
+
return c.json({ token: minted.key, team_id: teamId }, 200);
|
|
195
|
+
}
|
|
196
|
+
return c.json({ error: 'invalid_argument' }, 400);
|
|
109
197
|
});
|
|
110
198
|
/**
|
|
111
|
-
* List better-auth org members (role + email). Any org member may read
|
|
199
|
+
* List better-auth org members (role + email). Any org member may read; agent
|
|
200
|
+
* apikeys never receive member PII (403). Owner keys and sessions unchanged.
|
|
112
201
|
*/
|
|
113
202
|
router.get('/orgs/:id/members', async (c) => {
|
|
114
203
|
const orgId = c.req.param('id');
|
|
115
204
|
if (!(await requireKnownOrg(orgId))) {
|
|
116
205
|
return c.json({ error: 'not_found' }, 404);
|
|
117
206
|
}
|
|
207
|
+
const authCtx = getOrgAuthContext();
|
|
208
|
+
if (authCtx.kind === 'apikey' && authCtx.profile === 'agent') {
|
|
209
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
210
|
+
}
|
|
118
211
|
if (betterAuth === undefined) {
|
|
119
212
|
return c.json({ error: 'unavailable' }, 503);
|
|
120
213
|
}
|
|
@@ -122,9 +215,11 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
122
215
|
return c.json({ members }, 200);
|
|
123
216
|
});
|
|
124
217
|
/**
|
|
125
|
-
* BA3c: invite by email (link-only, no mailer).
|
|
126
|
-
*
|
|
127
|
-
*
|
|
218
|
+
* BA3c / RFC§5: invite by email to a workspace (link-only, no mailer). The
|
|
219
|
+
* team_id is required — invites are workspace-scoped; accepting joins the
|
|
220
|
+
* team, not just the org. Owners and admins may invite (invitation:create);
|
|
221
|
+
* better-auth still blocks a non-owner inviting an owner. Returns claimUrl
|
|
222
|
+
* for the inviter to deliver by hand.
|
|
128
223
|
*/
|
|
129
224
|
router.post('/orgs/:id/invitations', async (c) => {
|
|
130
225
|
const orgId = c.req.param('id');
|
|
@@ -147,17 +242,27 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
147
242
|
if (typeof body.role !== 'string' || !isInvitationRole(body.role)) {
|
|
148
243
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
149
244
|
}
|
|
245
|
+
if (typeof body.team_id !== 'string' || body.team_id.trim() === '') {
|
|
246
|
+
return c.json({ error: 'invalid_argument', message: 'team_id is required' }, 400);
|
|
247
|
+
}
|
|
248
|
+
const teamId = body.team_id.trim();
|
|
249
|
+
const team = await getTeamInOrg(betterAuth, teamId, orgId);
|
|
250
|
+
if (team === undefined) {
|
|
251
|
+
return c.json({ error: 'not_found' }, 404);
|
|
252
|
+
}
|
|
150
253
|
try {
|
|
151
254
|
const created = await createOrganizationInvitation(betterAuth, {
|
|
152
255
|
email: body.email,
|
|
153
256
|
role: body.role,
|
|
154
257
|
organizationId: orgId,
|
|
258
|
+
teamId,
|
|
155
259
|
headers: c.req.raw.headers,
|
|
156
260
|
baseURL,
|
|
157
261
|
});
|
|
158
262
|
return c.json({
|
|
159
263
|
invitationId: created.invitationId,
|
|
160
264
|
claimUrl: created.claimUrl,
|
|
265
|
+
teamId: created.teamId,
|
|
161
266
|
}, 201);
|
|
162
267
|
}
|
|
163
268
|
catch (err) {
|
|
@@ -215,6 +320,7 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
215
320
|
organizationId: result.member.organizationId,
|
|
216
321
|
role: result.member.role,
|
|
217
322
|
userId: result.member.userId,
|
|
323
|
+
teamId: result.invitation.teamId,
|
|
218
324
|
}, 200);
|
|
219
325
|
}
|
|
220
326
|
catch (err) {
|
|
@@ -241,6 +347,15 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
241
347
|
return c.json({ error: 'not_found' }, 404);
|
|
242
348
|
}
|
|
243
349
|
requirePermission(getOrgAuthContext(), 'organization', 'update');
|
|
350
|
+
// Import lands the project in the org-default workspace (the portable
|
|
351
|
+
// format carries no workspace), so a workspace/project-scoped agent key
|
|
352
|
+
// would escape its scope. Reject scoped callers before reading the body.
|
|
353
|
+
const importAuthCtx = getOrgAuthContext();
|
|
354
|
+
if ((importAuthCtx.kind === 'apikey' &&
|
|
355
|
+
(importAuthCtx.workspaceId !== undefined || importAuthCtx.projectId !== undefined)) ||
|
|
356
|
+
(importAuthCtx.kind === 'loopback' && importAuthCtx.workspaceId !== undefined)) {
|
|
357
|
+
return c.json({ error: 'forbidden' }, 403);
|
|
358
|
+
}
|
|
244
359
|
let body;
|
|
245
360
|
try {
|
|
246
361
|
body = await c.req.json();
|
package/dist/routes/projects.js
CHANGED
|
@@ -4,6 +4,7 @@ import { InvalidGoalReferenceError } from '../services/tasks.js';
|
|
|
4
4
|
import { InvalidTagError } from '../services/tags.js';
|
|
5
5
|
import { isStringArray } from './tasks.js';
|
|
6
6
|
import { parsePaginationParams } from '../serialize.js';
|
|
7
|
+
import { WorkspaceNotFoundError } from '../services/scope.js';
|
|
7
8
|
export function createProjectsRouter(projectService, taskService) {
|
|
8
9
|
const router = new Hono();
|
|
9
10
|
router.get('/projects', async (c) => {
|
|
@@ -18,11 +19,23 @@ export function createProjectsRouter(projectService, taskService) {
|
|
|
18
19
|
if (typeof body.name !== 'string' || body.name.trim() === '') {
|
|
19
20
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
20
21
|
}
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const workspaceId = typeof body.workspace_id === 'string' && body.workspace_id.length > 0
|
|
23
|
+
? body.workspace_id
|
|
24
|
+
: undefined;
|
|
25
|
+
try {
|
|
26
|
+
const project = await projectService.create({
|
|
27
|
+
name: body.name,
|
|
28
|
+
description: body.description,
|
|
29
|
+
...(workspaceId !== undefined ? { workspaceId } : {}),
|
|
30
|
+
});
|
|
31
|
+
return c.json(project, 201);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
if (error instanceof WorkspaceNotFoundError) {
|
|
35
|
+
return c.json({ error: 'not_found' }, 404);
|
|
36
|
+
}
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
26
39
|
});
|
|
27
40
|
router.get('/projects/:id', async (c) => {
|
|
28
41
|
const project = await projectService.get(c.req.param('id'));
|
|
@@ -36,6 +49,24 @@ export function createProjectsRouter(projectService, taskService) {
|
|
|
36
49
|
if (body.name !== undefined && (typeof body.name !== 'string' || body.name.trim() === '')) {
|
|
37
50
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
38
51
|
}
|
|
52
|
+
if (body.workspace_id !== undefined && body.workspace_id !== null) {
|
|
53
|
+
if (typeof body.workspace_id !== 'string' || body.workspace_id.trim() === '') {
|
|
54
|
+
return c.json({ error: 'invalid_argument' }, 400);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const moved = await projectService.moveProjectToWorkspace(c.req.param('id'), body.workspace_id);
|
|
58
|
+
if (!moved) {
|
|
59
|
+
return c.json({ error: 'not_found' }, 404);
|
|
60
|
+
}
|
|
61
|
+
return c.json(moved);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof WorkspaceNotFoundError) {
|
|
65
|
+
return c.json({ error: 'not_found' }, 404);
|
|
66
|
+
}
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
39
70
|
const project = await projectService.update(c.req.param('id'), {
|
|
40
71
|
...(body.name !== undefined ? { name: body.name } : {}),
|
|
41
72
|
...(body.description !== undefined ? { description: body.description } : {}),
|
package/dist/routes/shares.js
CHANGED
|
@@ -34,6 +34,34 @@ export function createSharesRouter(shareService) {
|
|
|
34
34
|
};
|
|
35
35
|
router.post('/tasks/:id/share', createShareHandler('task'));
|
|
36
36
|
router.post('/documents/:id/share', createShareHandler('document'));
|
|
37
|
+
// Share an entire workspace (all its projects) with a client. Owner-gated in
|
|
38
|
+
// the service via getTeamInOrg (workspace must be in the caller's org).
|
|
39
|
+
router.post('/workspaces/:workspaceId/share', async (c) => {
|
|
40
|
+
const workspaceId = c.req.param('workspaceId');
|
|
41
|
+
let body;
|
|
42
|
+
try {
|
|
43
|
+
body = await c.req.json();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return c.json({ error: 'invalid_json' }, 400);
|
|
47
|
+
}
|
|
48
|
+
const audienceName = (body.audience_name ?? '').trim();
|
|
49
|
+
if (audienceName === '') {
|
|
50
|
+
return c.json({ error: 'invalid_argument' }, 400);
|
|
51
|
+
}
|
|
52
|
+
const mode = body.mode === 'public' ? 'public' : 'invite';
|
|
53
|
+
const origin = new URL(c.req.url).origin;
|
|
54
|
+
const result = await shareService.createWorkspaceShare(workspaceId, {
|
|
55
|
+
audienceName,
|
|
56
|
+
mode,
|
|
57
|
+
permissions: { read: true, submit: body.submit === true },
|
|
58
|
+
...(body.invited_emails !== undefined ? { invitedEmails: body.invited_emails } : {}),
|
|
59
|
+
}, origin);
|
|
60
|
+
if (result === undefined) {
|
|
61
|
+
return c.json({ error: 'not_found' }, 404);
|
|
62
|
+
}
|
|
63
|
+
return c.json({ url: result.url, token: result.token }, 201);
|
|
64
|
+
});
|
|
37
65
|
// Hono doesn't match a literal `.md` suffix inside a param, so the route
|
|
38
66
|
// takes the raw segment and the handler enforces + strips the extension.
|
|
39
67
|
router.get('/share/:tokenWithExt', async (c) => {
|
|
@@ -118,6 +146,7 @@ export function createSharesRouter(shareService) {
|
|
|
118
146
|
body: body.body,
|
|
119
147
|
severity: body.severity,
|
|
120
148
|
task_ref: body.task_ref,
|
|
149
|
+
project_id: body.project_id,
|
|
121
150
|
});
|
|
122
151
|
if (result.status === 'unauthorized') {
|
|
123
152
|
return c.json({ error: 'unauthorized' }, 401);
|
|
@@ -128,6 +157,9 @@ export function createSharesRouter(shareService) {
|
|
|
128
157
|
if (result.status === 'title_required') {
|
|
129
158
|
return c.json({ error: 'title_required' }, 400);
|
|
130
159
|
}
|
|
160
|
+
if (result.status === 'project_required') {
|
|
161
|
+
return c.json({ error: 'project_required' }, 400);
|
|
162
|
+
}
|
|
131
163
|
if (result.status === 'rate_limited') {
|
|
132
164
|
return c.json({ error: 'rate_limited' }, 429);
|
|
133
165
|
}
|
package/dist/routes/tasks.js
CHANGED
|
@@ -48,6 +48,9 @@ export function createTasksRouter(taskService) {
|
|
|
48
48
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
49
49
|
}
|
|
50
50
|
const result = await taskService.claim(c.req.param('id'), body.agent_ref);
|
|
51
|
+
if (result === undefined) {
|
|
52
|
+
return c.json({ error: 'not_found' }, 404);
|
|
53
|
+
}
|
|
51
54
|
if (!result.claimed) {
|
|
52
55
|
return c.json(result, 409);
|
|
53
56
|
}
|
package/dist/serialize.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function serializeProject(project: Project): {
|
|
|
10
10
|
id: string;
|
|
11
11
|
name: string;
|
|
12
12
|
description: string | null;
|
|
13
|
+
workspace_id: string;
|
|
13
14
|
created_at: string;
|
|
14
15
|
updated_at: string;
|
|
15
16
|
};
|
|
@@ -18,6 +19,7 @@ export declare function serializeProjectDetail(project: Project, summary: TaskSt
|
|
|
18
19
|
id: string;
|
|
19
20
|
name: string;
|
|
20
21
|
description: string | null;
|
|
22
|
+
workspace_id: string;
|
|
21
23
|
created_at: string;
|
|
22
24
|
updated_at: string;
|
|
23
25
|
};
|
package/dist/serialize.js
CHANGED
|
@@ -26,6 +26,7 @@ export function serializeProject(project) {
|
|
|
26
26
|
id: project.id,
|
|
27
27
|
name: project.name,
|
|
28
28
|
description: project.description,
|
|
29
|
+
workspace_id: project.workspaceId,
|
|
29
30
|
created_at: project.createdAt.toISOString(),
|
|
30
31
|
updated_at: project.updatedAt.toISOString(),
|
|
31
32
|
};
|