@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.
- package/dist/access-control.d.ts +150 -0
- package/dist/access-control.js +63 -0
- package/dist/agent-keys.d.ts +81 -0
- package/dist/agent-keys.js +181 -0
- package/dist/auth-context.d.ts +60 -0
- package/dist/auth-context.js +41 -0
- package/dist/auth.d.ts +33 -0
- package/dist/auth.js +357 -0
- package/dist/better-auth.d.ts +66 -0
- package/dist/better-auth.js +106 -0
- package/dist/github.d.ts +57 -0
- package/dist/github.js +115 -0
- package/dist/hosted-auth.d.ts +23 -0
- package/dist/hosted-auth.js +49 -0
- package/dist/identity.d.ts +38 -0
- package/dist/identity.js +163 -0
- package/dist/index.d.ts +13 -2
- package/dist/index.js +15 -3
- package/dist/invitations.d.ts +94 -0
- package/dist/invitations.js +211 -0
- package/dist/organizations.d.ts +32 -0
- package/dist/organizations.js +63 -0
- package/dist/permissions.d.ts +19 -0
- package/dist/permissions.js +42 -0
- package/dist/projection.d.ts +1 -1
- package/dist/projection.js +5 -5
- package/dist/routes/agent-runs.js +3 -3
- package/dist/routes/artifacts.js +6 -6
- package/dist/routes/auth.d.ts +13 -0
- package/dist/routes/auth.js +83 -0
- package/dist/routes/canvas.js +5 -5
- package/dist/routes/comments.js +11 -11
- package/dist/routes/documents.js +10 -10
- package/dist/routes/folders.js +8 -8
- package/dist/routes/goals.js +11 -11
- package/dist/routes/notes.js +8 -8
- package/dist/routes/orgs.d.ts +17 -0
- package/dist/routes/orgs.js +241 -0
- package/dist/routes/projects.js +13 -13
- package/dist/routes/shares.js +92 -3
- package/dist/routes/submissions.js +7 -8
- package/dist/routes/tags.js +6 -6
- package/dist/routes/tasks.js +14 -3
- package/dist/routes/tokens.js +4 -4
- package/dist/serialize.d.ts +3 -3
- package/dist/server.d.ts +20 -2
- package/dist/server.js +57 -9
- package/dist/services/agent-runs.d.ts +13 -14
- package/dist/services/agent-runs.js +39 -41
- package/dist/services/artifacts.d.ts +6 -7
- package/dist/services/artifacts.js +30 -26
- package/dist/services/canvas.d.ts +11 -12
- package/dist/services/canvas.js +58 -41
- package/dist/services/comments.d.ts +11 -12
- package/dist/services/comments.js +44 -64
- package/dist/services/documents.d.ts +10 -11
- package/dist/services/documents.js +74 -55
- package/dist/services/files.d.ts +2 -1
- package/dist/services/files.js +13 -5
- package/dist/services/folders.d.ts +7 -8
- package/dist/services/folders.js +45 -33
- package/dist/services/goals.d.ts +24 -25
- package/dist/services/goals.js +60 -53
- package/dist/services/index.d.ts +2 -5
- package/dist/services/index.js +15 -20
- package/dist/services/notes.d.ts +7 -8
- package/dist/services/notes.js +35 -23
- package/dist/services/org-scope.d.ts +22 -0
- package/dist/services/org-scope.js +38 -0
- package/dist/services/projects.d.ts +12 -13
- package/dist/services/projects.js +90 -58
- package/dist/services/scope.d.ts +12 -0
- package/dist/services/scope.js +29 -0
- package/dist/services/share.d.ts +74 -10
- package/dist/services/share.js +213 -23
- package/dist/services/sync.d.ts +7 -29
- package/dist/services/sync.js +37 -142
- package/dist/services/tags.d.ts +6 -7
- package/dist/services/tags.js +34 -22
- package/dist/services/tasks.d.ts +24 -17
- package/dist/services/tasks.js +97 -58
- package/dist/services/tokens.d.ts +6 -5
- package/dist/services/tokens.js +13 -6
- package/dist/session.d.ts +5 -0
- package/dist/session.js +8 -0
- package/dist/storage/local.js +16 -10
- package/dist/storage/s3.js +11 -6
- package/dist/test-ba-org.d.ts +16 -0
- package/dist/test-ba-org.js +35 -0
- package/dist/test-helpers.d.ts +21 -5
- package/dist/test-helpers.js +45 -8
- package/dist/vercel.d.ts +3 -0
- package/dist/vercel.js +62 -0
- package/dist/worker.d.ts +44 -0
- package/dist/worker.js +106 -0
- package/package.json +8 -5
- package/web/assets/index-D29SyYFS.css +2 -0
- package/web/assets/index-tBJ_K3ur.js +358 -0
- package/web/index.html +2 -2
- package/web/assets/index-CeBvZ-aq.css +0 -2
- package/web/assets/index-CuRAxImC.js +0 -358
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* better-auth organization invitations (BA3c).
|
|
3
|
+
*
|
|
4
|
+
* createInvitation / acceptInvitation / removeMember require session headers
|
|
5
|
+
* (orgSessionMiddleware). Plugin methods are not on the erased Auth type —
|
|
6
|
+
* call via runtime-validated Reflect, same pattern as agent-keys.ts.
|
|
7
|
+
*/
|
|
8
|
+
import { makeSignature } from 'better-auth/crypto';
|
|
9
|
+
export const INVITATION_ROLES = ['owner', 'admin', 'member'];
|
|
10
|
+
export function isInvitationRole(value) {
|
|
11
|
+
return INVITATION_ROLES.includes(value);
|
|
12
|
+
}
|
|
13
|
+
export function invitationClaimUrl(baseURL, invitationId) {
|
|
14
|
+
const base = baseURL.replace(/\/$/, '');
|
|
15
|
+
return `${base}/invite/${invitationId}`;
|
|
16
|
+
}
|
|
17
|
+
const SHELL_EMAIL = 'shell@plandesk.local';
|
|
18
|
+
const SHELL_NAME = 'Plan Desk shell';
|
|
19
|
+
function isRecord(value) {
|
|
20
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
export function isAuthApiError(err) {
|
|
23
|
+
if (!isRecord(err))
|
|
24
|
+
return false;
|
|
25
|
+
return (typeof err.statusCode === 'number' &&
|
|
26
|
+
typeof err.status === 'string' &&
|
|
27
|
+
typeof err.message === 'string');
|
|
28
|
+
}
|
|
29
|
+
async function callPluginApi(auth, method, args) {
|
|
30
|
+
const api = auth.api;
|
|
31
|
+
if (!(method in api)) {
|
|
32
|
+
throw new Error(`better-auth ${method} is not available`);
|
|
33
|
+
}
|
|
34
|
+
const fn = Reflect.get(api, method);
|
|
35
|
+
if (typeof fn !== 'function') {
|
|
36
|
+
throw new Error(`better-auth ${method} is not a function`);
|
|
37
|
+
}
|
|
38
|
+
return fn.call(api, args);
|
|
39
|
+
}
|
|
40
|
+
function parseInvitation(raw) {
|
|
41
|
+
if (!isRecord(raw) || typeof raw.id !== 'string') {
|
|
42
|
+
throw new Error('createInvitation did not return an invitation id');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
id: raw.id,
|
|
46
|
+
email: typeof raw.email === 'string' ? raw.email : '',
|
|
47
|
+
role: typeof raw.role === 'string' ? raw.role : '',
|
|
48
|
+
organizationId: typeof raw.organizationId === 'string' ? raw.organizationId : '',
|
|
49
|
+
inviterId: typeof raw.inviterId === 'string' ? raw.inviterId : '',
|
|
50
|
+
status: typeof raw.status === 'string' ? raw.status : '',
|
|
51
|
+
expiresAt: raw.expiresAt instanceof Date ? raw.expiresAt : new Date(String(raw.expiresAt ?? '')),
|
|
52
|
+
createdAt: raw.createdAt instanceof Date ? raw.createdAt : new Date(String(raw.createdAt ?? '')),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an invitation using the caller's better-auth session (Cookie headers).
|
|
57
|
+
* No mailer: sendInvitationEmail is unset; caller delivers claimUrl by hand.
|
|
58
|
+
*/
|
|
59
|
+
export async function createOrganizationInvitation(auth, opts) {
|
|
60
|
+
const raw = await callPluginApi(auth, 'createInvitation', {
|
|
61
|
+
body: {
|
|
62
|
+
email: opts.email.trim().toLowerCase(),
|
|
63
|
+
role: opts.role,
|
|
64
|
+
organizationId: opts.organizationId,
|
|
65
|
+
},
|
|
66
|
+
headers: opts.headers,
|
|
67
|
+
});
|
|
68
|
+
const invitation = parseInvitation(raw);
|
|
69
|
+
return {
|
|
70
|
+
invitationId: invitation.id,
|
|
71
|
+
claimUrl: invitationClaimUrl(opts.baseURL, invitation.id),
|
|
72
|
+
invitation,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Accept a pending invitation as the signed-in better-auth user (email must match).
|
|
77
|
+
* Single-use CAS: a second accept fails (maps to 410 at the route).
|
|
78
|
+
*/
|
|
79
|
+
export async function acceptOrganizationInvitation(auth, opts) {
|
|
80
|
+
const raw = await callPluginApi(auth, 'acceptInvitation', {
|
|
81
|
+
body: { invitationId: opts.invitationId },
|
|
82
|
+
headers: opts.headers,
|
|
83
|
+
});
|
|
84
|
+
if (!isRecord(raw) || !isRecord(raw.invitation) || !isRecord(raw.member)) {
|
|
85
|
+
throw new Error('acceptInvitation did not return invitation + member');
|
|
86
|
+
}
|
|
87
|
+
const invitation = parseInvitation(raw.invitation);
|
|
88
|
+
const member = raw.member;
|
|
89
|
+
if (typeof member.id !== 'string' ||
|
|
90
|
+
typeof member.organizationId !== 'string' ||
|
|
91
|
+
typeof member.userId !== 'string' ||
|
|
92
|
+
typeof member.role !== 'string') {
|
|
93
|
+
throw new Error('acceptInvitation member shape invalid');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
invitation,
|
|
97
|
+
member: {
|
|
98
|
+
id: member.id,
|
|
99
|
+
organizationId: member.organizationId,
|
|
100
|
+
userId: member.userId,
|
|
101
|
+
role: member.role,
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Remove a member via better-auth (enforces ≥1 owner). Used by tests / future routes.
|
|
107
|
+
*/
|
|
108
|
+
export async function removeOrganizationMember(auth, opts) {
|
|
109
|
+
return callPluginApi(auth, 'removeMember', {
|
|
110
|
+
body: {
|
|
111
|
+
memberIdOrEmail: opts.memberIdOrEmail,
|
|
112
|
+
organizationId: opts.organizationId,
|
|
113
|
+
},
|
|
114
|
+
headers: opts.headers,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Update a member role via better-auth (enforces ≥1 owner on demote).
|
|
119
|
+
*/
|
|
120
|
+
export async function updateOrganizationMemberRole(auth, opts) {
|
|
121
|
+
return callPluginApi(auth, 'updateMemberRole', {
|
|
122
|
+
body: {
|
|
123
|
+
memberId: opts.memberId,
|
|
124
|
+
role: opts.role,
|
|
125
|
+
organizationId: opts.organizationId,
|
|
126
|
+
},
|
|
127
|
+
headers: opts.headers,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/** Mint a signed better-auth session cookie header for an existing user. */
|
|
131
|
+
export async function mintSessionCookieHeader(auth, userId) {
|
|
132
|
+
const adapter = (await auth.$context).adapter;
|
|
133
|
+
const ctx = await auth.$context;
|
|
134
|
+
const now = new Date();
|
|
135
|
+
const token = `ba-sess-shell-${userId}-${Math.random().toString(36).slice(2)}`;
|
|
136
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
137
|
+
await adapter.create({
|
|
138
|
+
model: 'session',
|
|
139
|
+
data: {
|
|
140
|
+
userId,
|
|
141
|
+
token,
|
|
142
|
+
expiresAt,
|
|
143
|
+
createdAt: now,
|
|
144
|
+
updatedAt: now,
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
const signed = `${token}.${await makeSignature(token, ctx.secret)}`;
|
|
148
|
+
const headers = new Headers();
|
|
149
|
+
headers.set('cookie', `${ctx.authCookies.sessionToken.name}=${signed}`);
|
|
150
|
+
return headers;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Ensure a shell inviter user exists as owner of organizationId (REQ-3 bootstrap).
|
|
154
|
+
* Shell authority owns the DB — no interactive session required from the operator.
|
|
155
|
+
*/
|
|
156
|
+
export async function ensureShellOwner(auth, organizationId) {
|
|
157
|
+
const adapter = (await auth.$context).adapter;
|
|
158
|
+
const now = new Date();
|
|
159
|
+
let user = await adapter.findOne({
|
|
160
|
+
model: 'user',
|
|
161
|
+
where: [{ field: 'email', value: SHELL_EMAIL }],
|
|
162
|
+
});
|
|
163
|
+
if (user === null) {
|
|
164
|
+
user = await adapter.create({
|
|
165
|
+
model: 'user',
|
|
166
|
+
data: {
|
|
167
|
+
name: SHELL_NAME,
|
|
168
|
+
email: SHELL_EMAIL,
|
|
169
|
+
emailVerified: true,
|
|
170
|
+
image: null,
|
|
171
|
+
createdAt: now,
|
|
172
|
+
updatedAt: now,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const existing = await adapter.findMany({
|
|
177
|
+
model: 'member',
|
|
178
|
+
where: [
|
|
179
|
+
{ field: 'userId', value: user.id },
|
|
180
|
+
{ field: 'organizationId', value: organizationId },
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
if (existing.length === 0) {
|
|
184
|
+
await adapter.create({
|
|
185
|
+
model: 'member',
|
|
186
|
+
data: {
|
|
187
|
+
organizationId,
|
|
188
|
+
userId: user.id,
|
|
189
|
+
role: 'owner',
|
|
190
|
+
createdAt: now,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const headers = await mintSessionCookieHeader(auth, user.id);
|
|
195
|
+
return { userId: user.id, headers };
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Shell-side owner invitation for `plandesk admin invite-owner` (no GitHub, no mailer).
|
|
199
|
+
*/
|
|
200
|
+
export async function mintOwnerInvitation(auth, opts) {
|
|
201
|
+
const { headers } = await ensureShellOwner(auth, opts.organizationId);
|
|
202
|
+
const created = await createOrganizationInvitation(auth, {
|
|
203
|
+
email: opts.email,
|
|
204
|
+
role: 'owner',
|
|
205
|
+
organizationId: opts.organizationId,
|
|
206
|
+
headers,
|
|
207
|
+
baseURL: opts.baseURL,
|
|
208
|
+
});
|
|
209
|
+
return { invitationId: created.invitationId, claimUrl: created.claimUrl };
|
|
210
|
+
}
|
|
211
|
+
//# sourceMappingURL=invitations.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { BetterAuthInstance } from './better-auth.js';
|
|
2
|
+
export type OrganizationSummary = {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
slug: string;
|
|
6
|
+
createdAt: Date;
|
|
7
|
+
};
|
|
8
|
+
export type UserOrganizationSummary = {
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
role: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Look up a better-auth organization by id (user-less / server path).
|
|
15
|
+
* Prefer this over auth.api.getFullOrganization when no session headers exist.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getOrganizationById(auth: BetterAuthInstance, organizationId: string): Promise<OrganizationSummary | undefined>;
|
|
18
|
+
/**
|
|
19
|
+
* Loopback default org: DEFAULT_ORG_ID if present, else the sole organization.
|
|
20
|
+
*/
|
|
21
|
+
export declare function resolveDefaultOrganization(auth: BetterAuthInstance): Promise<OrganizationSummary | undefined>;
|
|
22
|
+
export declare function listOrganizationsForUser(auth: BetterAuthInstance, userId: string): Promise<UserOrganizationSummary[]>;
|
|
23
|
+
/**
|
|
24
|
+
* Display-friendly org for /auth/session and similar.
|
|
25
|
+
* Falls back to DEFAULT_ORG_ID → "Personal" when better-auth is not configured
|
|
26
|
+
* (loopback offline).
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveOrganizationName(auth: BetterAuthInstance | undefined, orgId: string): Promise<{
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
}>;
|
|
32
|
+
//# sourceMappingURL=organizations.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { DEFAULT_ORG_ID } from '@plandesk/db';
|
|
2
|
+
/**
|
|
3
|
+
* Look up a better-auth organization by id (user-less / server path).
|
|
4
|
+
* Prefer this over auth.api.getFullOrganization when no session headers exist.
|
|
5
|
+
*/
|
|
6
|
+
export async function getOrganizationById(auth, organizationId) {
|
|
7
|
+
const adapter = (await auth.$context).adapter;
|
|
8
|
+
const org = await adapter.findOne({
|
|
9
|
+
model: 'organization',
|
|
10
|
+
where: [{ field: 'id', value: organizationId }],
|
|
11
|
+
});
|
|
12
|
+
return org ?? undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Loopback default org: DEFAULT_ORG_ID if present, else the sole organization.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveDefaultOrganization(auth) {
|
|
18
|
+
const byId = await getOrganizationById(auth, DEFAULT_ORG_ID);
|
|
19
|
+
if (byId !== undefined) {
|
|
20
|
+
return byId;
|
|
21
|
+
}
|
|
22
|
+
const adapter = (await auth.$context).adapter;
|
|
23
|
+
const all = await adapter.findMany({
|
|
24
|
+
model: 'organization',
|
|
25
|
+
});
|
|
26
|
+
if (all.length === 1) {
|
|
27
|
+
return all[0];
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
export async function listOrganizationsForUser(auth, userId) {
|
|
32
|
+
const adapter = (await auth.$context).adapter;
|
|
33
|
+
const members = await adapter.findMany({
|
|
34
|
+
model: 'member',
|
|
35
|
+
where: [{ field: 'userId', value: userId }],
|
|
36
|
+
sortBy: { field: 'createdAt', direction: 'asc' },
|
|
37
|
+
});
|
|
38
|
+
return Promise.all(members.map(async (member) => {
|
|
39
|
+
const organization = await getOrganizationById(auth, member.organizationId);
|
|
40
|
+
if (organization === undefined) {
|
|
41
|
+
throw new Error('Membership points to a missing organization');
|
|
42
|
+
}
|
|
43
|
+
return { id: organization.id, name: organization.name, role: member.role };
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Display-friendly org for /auth/session and similar.
|
|
48
|
+
* Falls back to DEFAULT_ORG_ID → "Personal" when better-auth is not configured
|
|
49
|
+
* (loopback offline).
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveOrganizationName(auth, orgId) {
|
|
52
|
+
if (auth !== undefined) {
|
|
53
|
+
const org = await getOrganizationById(auth, orgId);
|
|
54
|
+
if (org !== undefined) {
|
|
55
|
+
return { id: org.id, name: org.name };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (orgId === DEFAULT_ORG_ID) {
|
|
59
|
+
return { id: DEFAULT_ORG_ID, name: 'Personal' };
|
|
60
|
+
}
|
|
61
|
+
return { id: orgId, name: orgId };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=organizations.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OrgRole } from '@plandesk/db';
|
|
2
|
+
import { type statement } from './access-control.js';
|
|
3
|
+
export type PermissionSet = Record<string, readonly string[]>;
|
|
4
|
+
export type PermissionResource = keyof typeof statement;
|
|
5
|
+
export type WorkAction = (typeof statement)['task'][number];
|
|
6
|
+
/** Map better-auth org role directly onto the access-control statement sets. */
|
|
7
|
+
export declare function orgRoleToPermissionSet(role: OrgRole): PermissionSet;
|
|
8
|
+
export declare function hasPermission(permissions: PermissionSet, resource: string, action: string): boolean;
|
|
9
|
+
export declare function hasAnyWritePermission(permissions: PermissionSet): boolean;
|
|
10
|
+
/** Thrown when the caller lacks a specific resource:action → HTTP 403. */
|
|
11
|
+
export declare class PermissionDeniedError extends Error {
|
|
12
|
+
readonly resource: string;
|
|
13
|
+
readonly action: string;
|
|
14
|
+
constructor(resource: string, action: string);
|
|
15
|
+
}
|
|
16
|
+
export declare function requirePermission(ctx: {
|
|
17
|
+
permission: PermissionSet;
|
|
18
|
+
}, resource: string, action: string): void;
|
|
19
|
+
//# sourceMappingURL=permissions.d.ts.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { admin, member, owner, } from './access-control.js';
|
|
2
|
+
const writeActions = new Set(['create', 'update', 'delete']);
|
|
3
|
+
/** Map better-auth org role directly onto the access-control statement sets. */
|
|
4
|
+
export function orgRoleToPermissionSet(role) {
|
|
5
|
+
switch (role) {
|
|
6
|
+
case 'member':
|
|
7
|
+
return member.statements;
|
|
8
|
+
case 'admin':
|
|
9
|
+
return admin.statements;
|
|
10
|
+
case 'owner':
|
|
11
|
+
return owner.statements;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function hasPermission(permissions, resource, action) {
|
|
15
|
+
const actions = permissions[resource];
|
|
16
|
+
return actions !== undefined && actions.includes(action);
|
|
17
|
+
}
|
|
18
|
+
export function hasAnyWritePermission(permissions) {
|
|
19
|
+
for (const actions of Object.values(permissions)) {
|
|
20
|
+
if (actions.some((action) => writeActions.has(action))) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
/** Thrown when the caller lacks a specific resource:action → HTTP 403. */
|
|
27
|
+
export class PermissionDeniedError extends Error {
|
|
28
|
+
resource;
|
|
29
|
+
action;
|
|
30
|
+
constructor(resource, action) {
|
|
31
|
+
super(`requires ${resource}:${action}`);
|
|
32
|
+
this.name = 'PermissionDeniedError';
|
|
33
|
+
this.resource = resource;
|
|
34
|
+
this.action = action;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function requirePermission(ctx, resource, action) {
|
|
38
|
+
if (!hasPermission(ctx.permission, resource, action)) {
|
|
39
|
+
throw new PermissionDeniedError(resource, action);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=permissions.js.map
|
package/dist/projection.d.ts
CHANGED
|
@@ -47,5 +47,5 @@ export type ClientView = {
|
|
|
47
47
|
expires_at: string | null;
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
|
-
export declare function buildClientView(db: Db, projectId: string, share: Share): ClientView | undefined
|
|
50
|
+
export declare function buildClientView(db: Db, projectId: string, share: Share): Promise<ClientView | undefined>;
|
|
51
51
|
//# sourceMappingURL=projection.d.ts.map
|
package/dist/projection.js
CHANGED
|
@@ -6,14 +6,14 @@ function buildProgress(tasks) {
|
|
|
6
6
|
}
|
|
7
7
|
return progress;
|
|
8
8
|
}
|
|
9
|
-
export function buildClientView(db, projectId, share) {
|
|
10
|
-
const project = getProject(db, projectId);
|
|
9
|
+
export async function buildClientView(db, projectId, share) {
|
|
10
|
+
const project = await getProject(db, projectId);
|
|
11
11
|
if (!project) {
|
|
12
12
|
return undefined;
|
|
13
13
|
}
|
|
14
14
|
const policy = parseSharePolicy(share);
|
|
15
15
|
const permissions = parseSharePermissions(share);
|
|
16
|
-
const allTasks = listTasks(db, projectId);
|
|
16
|
+
const allTasks = await listTasks(db, projectId);
|
|
17
17
|
const sharedTasks = [];
|
|
18
18
|
for (const task of allTasks) {
|
|
19
19
|
const included = policy.tasks === 'all' || (Array.isArray(policy.tasks) && policy.tasks.includes(task.id));
|
|
@@ -37,7 +37,7 @@ export function buildClientView(db, projectId, share) {
|
|
|
37
37
|
sharedTasks.push(viewTask);
|
|
38
38
|
}
|
|
39
39
|
const sharedTaskIds = new Set(sharedTasks.map((task) => task.id));
|
|
40
|
-
const sharedEdges = listEdges(db, projectId)
|
|
40
|
+
const sharedEdges = (await listEdges(db, projectId))
|
|
41
41
|
.filter((edge) => sharedTaskIds.has(edge.fromTaskId) && sharedTaskIds.has(edge.toTaskId))
|
|
42
42
|
.map((edge) => ({
|
|
43
43
|
id: edge.id,
|
|
@@ -46,7 +46,7 @@ export function buildClientView(db, projectId, share) {
|
|
|
46
46
|
label: edge.label,
|
|
47
47
|
}));
|
|
48
48
|
const documentIdSet = new Set(policy.documentIds);
|
|
49
|
-
const sharedDocuments = listDocuments(db, projectId)
|
|
49
|
+
const sharedDocuments = (await listDocuments(db, projectId))
|
|
50
50
|
.filter((doc) => documentIdSet.has(doc.id))
|
|
51
51
|
.map((doc) => ({
|
|
52
52
|
id: doc.id,
|
|
@@ -3,12 +3,12 @@ import { InvalidAgentRunError } from '../services/agent-runs.js';
|
|
|
3
3
|
import { parsePaginationParams } from '../serialize.js';
|
|
4
4
|
export function createAgentRunsRouter(agentRunService) {
|
|
5
5
|
const router = new Hono();
|
|
6
|
-
router.get('/projects/:id/agent-runs', (c) => {
|
|
6
|
+
router.get('/projects/:id/agent-runs', async (c) => {
|
|
7
7
|
const pagination = parsePaginationParams(c.req.query('limit'), c.req.query('offset'));
|
|
8
8
|
if (pagination === 'invalid') {
|
|
9
9
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
10
10
|
}
|
|
11
|
-
const runs = agentRunService.listForProject(c.req.param('id'), pagination);
|
|
11
|
+
const runs = await agentRunService.listForProject(c.req.param('id'), pagination);
|
|
12
12
|
if (!runs) {
|
|
13
13
|
return c.json({ error: 'not_found' }, 404);
|
|
14
14
|
}
|
|
@@ -20,7 +20,7 @@ export function createAgentRunsRouter(agentRunService) {
|
|
|
20
20
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
21
21
|
}
|
|
22
22
|
try {
|
|
23
|
-
const event = agentRunService.recordProgress(c.req.param('id'), body.message);
|
|
23
|
+
const event = await agentRunService.recordProgress(c.req.param('id'), body.message);
|
|
24
24
|
if (!event) {
|
|
25
25
|
return c.json({ error: 'not_found' }, 404);
|
|
26
26
|
}
|
package/dist/routes/artifacts.js
CHANGED
|
@@ -6,8 +6,8 @@ function isValidKind(kind) {
|
|
|
6
6
|
}
|
|
7
7
|
export function createArtifactsRouter(artifactService) {
|
|
8
8
|
const router = new Hono();
|
|
9
|
-
router.get('/projects/:id/artifacts', (c) => {
|
|
10
|
-
const artifacts = artifactService.listByProject(c.req.param('id'));
|
|
9
|
+
router.get('/projects/:id/artifacts', async (c) => {
|
|
10
|
+
const artifacts = await artifactService.listByProject(c.req.param('id'));
|
|
11
11
|
if (!artifacts) {
|
|
12
12
|
return c.json({ error: 'not_found' }, 404);
|
|
13
13
|
}
|
|
@@ -22,7 +22,7 @@ export function createArtifactsRouter(artifactService) {
|
|
|
22
22
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
23
23
|
}
|
|
24
24
|
try {
|
|
25
|
-
const artifact = artifactService.create(c.req.param('id'), {
|
|
25
|
+
const artifact = await artifactService.create(c.req.param('id'), {
|
|
26
26
|
title: body.title,
|
|
27
27
|
kind: body.kind,
|
|
28
28
|
content: body.content,
|
|
@@ -39,8 +39,8 @@ export function createArtifactsRouter(artifactService) {
|
|
|
39
39
|
throw error;
|
|
40
40
|
}
|
|
41
41
|
});
|
|
42
|
-
router.get('/artifacts/:id', (c) => {
|
|
43
|
-
const artifact = artifactService.get(c.req.param('id'));
|
|
42
|
+
router.get('/artifacts/:id', async (c) => {
|
|
43
|
+
const artifact = await artifactService.get(c.req.param('id'));
|
|
44
44
|
if (!artifact) {
|
|
45
45
|
return c.json({ error: 'not_found' }, 404);
|
|
46
46
|
}
|
|
@@ -52,7 +52,7 @@ export function createArtifactsRouter(artifactService) {
|
|
|
52
52
|
return c.json({ error: 'invalid_argument' }, 400);
|
|
53
53
|
}
|
|
54
54
|
try {
|
|
55
|
-
const artifact = artifactService.update(c.req.param('id'), {
|
|
55
|
+
const artifact = await artifactService.update(c.req.param('id'), {
|
|
56
56
|
...(body.title !== undefined ? { title: body.title } : {}),
|
|
57
57
|
...(body.kind !== undefined ? { kind: body.kind } : {}),
|
|
58
58
|
...(body.content !== undefined ? { content: body.content } : {}),
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import type { Db } from '@plandesk/db';
|
|
3
|
+
import type { BetterAuthInstance } from '../better-auth.js';
|
|
4
|
+
import type { GithubConfig } from '../github.js';
|
|
5
|
+
export type AuthRouterDeps = {
|
|
6
|
+
db: Db;
|
|
7
|
+
/** Absent on a self-hosted instance with no GitHub app registered (REQ-20). */
|
|
8
|
+
github?: GithubConfig;
|
|
9
|
+
/** better-auth instance for session-gated CLI owner-key mint (BA4b-2). */
|
|
10
|
+
betterAuth?: BetterAuthInstance;
|
|
11
|
+
};
|
|
12
|
+
export declare function createAuthRouter(deps: AuthRouterDeps): Hono;
|
|
13
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { createOrgOwnerKey } from '../agent-keys.js';
|
|
3
|
+
import { getAuthContext } from '../auth-context.js';
|
|
4
|
+
import { listOrganizationsForUser, resolveOrganizationName } from '../organizations.js';
|
|
5
|
+
import { requirePermission } from '../permissions.js';
|
|
6
|
+
export function createAuthRouter(deps) {
|
|
7
|
+
const router = new Hono();
|
|
8
|
+
const { github, betterAuth } = deps;
|
|
9
|
+
// What sign-in this instance offers.
|
|
10
|
+
//
|
|
11
|
+
// `githubEnabled` is for the dashboard: it decides between "Sign in with
|
|
12
|
+
// GitHub" and token entry. A self-hoster who never registered a GitHub app
|
|
13
|
+
// gets the token path (REQ-20). Device flow is gone — paste-a-token only.
|
|
14
|
+
router.get('/auth/methods', (c) => c.json({
|
|
15
|
+
method: 'token',
|
|
16
|
+
githubEnabled: github !== undefined,
|
|
17
|
+
}));
|
|
18
|
+
// Who the caller is right now, for the org/role badge and the sign-out
|
|
19
|
+
// control. Unauthenticated callers never reach this — org auth answers 401
|
|
20
|
+
// first, which is the dashboard's cue to show sign-in.
|
|
21
|
+
router.get('/auth/session', async (c) => {
|
|
22
|
+
const ctx = getAuthContext();
|
|
23
|
+
if (ctx.kind === 'guest') {
|
|
24
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
25
|
+
}
|
|
26
|
+
const org = await resolveOrganizationName(betterAuth, ctx.orgId);
|
|
27
|
+
let orgs = [
|
|
28
|
+
{ id: org.id, name: org.name, role: ctx.role ?? 'member' },
|
|
29
|
+
];
|
|
30
|
+
if (ctx.kind === 'session' && betterAuth !== undefined) {
|
|
31
|
+
const session = await betterAuth.api.getSession({ headers: c.req.raw.headers });
|
|
32
|
+
if (session === null) {
|
|
33
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
34
|
+
}
|
|
35
|
+
orgs = await listOrganizationsForUser(betterAuth, session.user.id);
|
|
36
|
+
}
|
|
37
|
+
return c.json({
|
|
38
|
+
kind: ctx.kind,
|
|
39
|
+
user_ref: ctx.kind === 'session' ? ctx.userRef : null,
|
|
40
|
+
role: ctx.role,
|
|
41
|
+
org,
|
|
42
|
+
orgs,
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* BA4b-2: mint an org-wide owner API key for CLI paste (`plandesk login`).
|
|
47
|
+
* Session-only — apikey/token/loopback must not mint owner keys here.
|
|
48
|
+
* Raw key returned once; never stored retrievable.
|
|
49
|
+
*/
|
|
50
|
+
router.post('/auth/cli-token', async (c) => {
|
|
51
|
+
if (betterAuth === undefined) {
|
|
52
|
+
return c.json({ error: 'unavailable' }, 503);
|
|
53
|
+
}
|
|
54
|
+
const ctx = getAuthContext();
|
|
55
|
+
if (ctx.kind !== 'session') {
|
|
56
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
57
|
+
}
|
|
58
|
+
requirePermission(ctx, 'apiKey', 'create');
|
|
59
|
+
const baSession = await betterAuth.api.getSession({ headers: c.req.raw.headers });
|
|
60
|
+
if (baSession === null) {
|
|
61
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
62
|
+
}
|
|
63
|
+
let name = 'CLI token';
|
|
64
|
+
const body = (await c.req.json().catch(() => ({})));
|
|
65
|
+
if (typeof body.name === 'string' && body.name.trim() !== '') {
|
|
66
|
+
name = body.name.trim();
|
|
67
|
+
}
|
|
68
|
+
const minted = await createOrgOwnerKey({
|
|
69
|
+
auth: betterAuth,
|
|
70
|
+
userId: baSession.user.id,
|
|
71
|
+
orgId: ctx.orgId,
|
|
72
|
+
name,
|
|
73
|
+
});
|
|
74
|
+
const org = await resolveOrganizationName(betterAuth, ctx.orgId);
|
|
75
|
+
return c.json({
|
|
76
|
+
token: minted.key,
|
|
77
|
+
org_id: ctx.orgId,
|
|
78
|
+
org_name: org.name,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return router;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/routes/canvas.js
CHANGED
|
@@ -2,8 +2,8 @@ import { Hono } from 'hono';
|
|
|
2
2
|
import { InvalidCanvasError } from '../services/canvas.js';
|
|
3
3
|
export function createCanvasRouter(canvasService) {
|
|
4
4
|
const router = new Hono();
|
|
5
|
-
router.get('/projects/:id/canvas', (c) => {
|
|
6
|
-
const canvas = canvasService.get(c.req.param('id'));
|
|
5
|
+
router.get('/projects/:id/canvas', async (c) => {
|
|
6
|
+
const canvas = await canvasService.get(c.req.param('id'));
|
|
7
7
|
if (!canvas) {
|
|
8
8
|
return c.json({ error: 'not_found' }, 404);
|
|
9
9
|
}
|
|
@@ -31,7 +31,7 @@ export function createCanvasRouter(canvasService) {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
try {
|
|
34
|
-
const canvas = canvasService.putLayout(c.req.param('id'), {
|
|
34
|
+
const canvas = await canvasService.putLayout(c.req.param('id'), {
|
|
35
35
|
nodes: body.nodes,
|
|
36
36
|
edges: body.edges,
|
|
37
37
|
layout: body.layout,
|
|
@@ -48,8 +48,8 @@ export function createCanvasRouter(canvasService) {
|
|
|
48
48
|
throw error;
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
|
-
router.delete('/projects/:id/edges/:edgeId', (c) => {
|
|
52
|
-
const deleted = canvasService.deleteEdge(c.req.param('id'), c.req.param('edgeId'));
|
|
51
|
+
router.delete('/projects/:id/edges/:edgeId', async (c) => {
|
|
52
|
+
const deleted = await canvasService.deleteEdge(c.req.param('id'), c.req.param('edgeId'));
|
|
53
53
|
if (!deleted) {
|
|
54
54
|
return c.json({ error: 'not_found' }, 404);
|
|
55
55
|
}
|