@plandesk/api 1.0.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +182 -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-BpOwO6_E.css +2 -0
- package/web/assets/index-CM5NiDOW.js +358 -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/agent-keys.d.ts
CHANGED
|
@@ -10,8 +10,9 @@ export type ApiKeyKind = 'agent' | 'owner';
|
|
|
10
10
|
export declare const DEFAULT_AGENT_KEY_PERMISSIONS: PermissionSet;
|
|
11
11
|
/** Full owner permission set — default grant for org-wide owner keys (BA4b-1). */
|
|
12
12
|
export declare const DEFAULT_OWNER_KEY_PERMISSIONS: PermissionSet;
|
|
13
|
-
/** Always stripped from agent-key effective perms — a key must never mint keys
|
|
14
|
-
|
|
13
|
+
/** Always stripped from agent-key effective perms — a key must never mint keys
|
|
14
|
+
* or create/rename workspaces (team), regardless of custom permissions. */
|
|
15
|
+
export declare const AGENT_FORBIDDEN_RESOURCES: readonly ["apiKey", "team"];
|
|
15
16
|
/**
|
|
16
17
|
* Live-role ceiling at verify time (BA5 + BA4b-1):
|
|
17
18
|
* effective = intersect(keyPermissions, liveMemberRole);
|
|
@@ -55,6 +56,29 @@ export type CreatedScopedAgentKey = {
|
|
|
55
56
|
* Metadata has no `kind` (or may omit it) — resolver treats absent as agent.
|
|
56
57
|
*/
|
|
57
58
|
export declare function createScopedAgentKey(input: CreateScopedAgentKeyInput): Promise<CreatedScopedAgentKey>;
|
|
59
|
+
export type CreateWorkspaceScopedAgentKeyInput = {
|
|
60
|
+
auth: BetterAuthInstance;
|
|
61
|
+
userId: string;
|
|
62
|
+
orgId: string;
|
|
63
|
+
teamId: string;
|
|
64
|
+
permissions?: PermissionSet;
|
|
65
|
+
name?: string;
|
|
66
|
+
};
|
|
67
|
+
export type CreatedWorkspaceScopedAgentKey = {
|
|
68
|
+
id: string;
|
|
69
|
+
key: string;
|
|
70
|
+
name: string | null;
|
|
71
|
+
permissions: Record<string, string[]> | null;
|
|
72
|
+
metadata: {
|
|
73
|
+
orgId: string;
|
|
74
|
+
teamId: string;
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Server-side mint for a workspace-scoped agent key.
|
|
79
|
+
* Metadata is `{ orgId, teamId }` — no `kind` so resolver treats absent as agent.
|
|
80
|
+
*/
|
|
81
|
+
export declare function createWorkspaceScopedAgentKey(input: CreateWorkspaceScopedAgentKeyInput): Promise<CreatedWorkspaceScopedAgentKey>;
|
|
58
82
|
export type CreateOrgOwnerKeyInput = {
|
|
59
83
|
auth: BetterAuthInstance;
|
|
60
84
|
userId: string;
|
package/dist/agent-keys.js
CHANGED
|
@@ -19,8 +19,9 @@ export const DEFAULT_AGENT_KEY_PERMISSIONS = {
|
|
|
19
19
|
};
|
|
20
20
|
/** Full owner permission set — default grant for org-wide owner keys (BA4b-1). */
|
|
21
21
|
export const DEFAULT_OWNER_KEY_PERMISSIONS = orgRoleToPermissionSet('owner');
|
|
22
|
-
/** Always stripped from agent-key effective perms — a key must never mint keys
|
|
23
|
-
|
|
22
|
+
/** Always stripped from agent-key effective perms — a key must never mint keys
|
|
23
|
+
* or create/rename workspaces (team), regardless of custom permissions. */
|
|
24
|
+
export const AGENT_FORBIDDEN_RESOURCES = ['apiKey', 'team'];
|
|
24
25
|
/**
|
|
25
26
|
* Live-role ceiling at verify time (BA5 + BA4b-1):
|
|
26
27
|
* effective = intersect(keyPermissions, liveMemberRole);
|
|
@@ -158,6 +159,25 @@ export async function createScopedAgentKey(input) {
|
|
|
158
159
|
metadata,
|
|
159
160
|
};
|
|
160
161
|
}
|
|
162
|
+
/**
|
|
163
|
+
* Server-side mint for a workspace-scoped agent key.
|
|
164
|
+
* Metadata is `{ orgId, teamId }` — no `kind` so resolver treats absent as agent.
|
|
165
|
+
*/
|
|
166
|
+
export async function createWorkspaceScopedAgentKey(input) {
|
|
167
|
+
const permissions = compactPermissions(input.permissions ?? DEFAULT_AGENT_KEY_PERMISSIONS);
|
|
168
|
+
const metadata = { orgId: input.orgId, teamId: input.teamId };
|
|
169
|
+
const minted = await mintBetterAuthApiKey({
|
|
170
|
+
auth: input.auth,
|
|
171
|
+
userId: input.userId,
|
|
172
|
+
name: input.name ?? 'agent',
|
|
173
|
+
permissions,
|
|
174
|
+
metadata,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
...minted,
|
|
178
|
+
metadata,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
161
181
|
/**
|
|
162
182
|
* Server-side mint for an org-wide owner key (BA4b-1).
|
|
163
183
|
* No projectId in metadata → org-wide reach. kind: 'owner' retains apiKey
|
package/dist/auth-context.d.ts
CHANGED
|
@@ -11,23 +11,36 @@ export type AuthContext = {
|
|
|
11
11
|
orgId: string;
|
|
12
12
|
/** Stable identity, e.g. `github:<numeric id>` — never the login. */
|
|
13
13
|
userRef: string;
|
|
14
|
+
/** better-auth user id owning this session. */
|
|
15
|
+
userId: string;
|
|
14
16
|
/** better-auth org role (owner/admin/member) for display. */
|
|
15
17
|
role: OrgRole;
|
|
16
18
|
/** Resolved permission set for resource:action checks. */
|
|
17
19
|
permission: PermissionSet;
|
|
20
|
+
/**
|
|
21
|
+
* Workspace (team) ids in this org the user is a `teamMember` of. Gates a
|
|
22
|
+
* member to their own workspaces; owner/admin bypass by role (see scope.ts).
|
|
23
|
+
*/
|
|
24
|
+
memberWorkspaceIds: string[];
|
|
18
25
|
} | {
|
|
19
26
|
kind: 'loopback';
|
|
20
27
|
orgId: string;
|
|
21
28
|
/** REQ-21: local loopback single-org is always owner — no login. */
|
|
22
29
|
role: 'owner';
|
|
23
30
|
permission: PermissionSet;
|
|
31
|
+
/** Optional local workspace scope from the x-plandesk-workspace-id header; not a security boundary. */
|
|
32
|
+
workspaceId?: string;
|
|
24
33
|
} | {
|
|
25
34
|
kind: 'apikey';
|
|
26
35
|
orgId: string;
|
|
27
36
|
/** better-auth user id that owns the key (referenceId). */
|
|
28
37
|
userId: string;
|
|
38
|
+
/** Key profile from metadata: agent (scoped/ceilinged) vs owner (org-wide). */
|
|
39
|
+
profile: 'agent' | 'owner';
|
|
29
40
|
/** Optional project scope from key metadata; cross-project → 404. */
|
|
30
41
|
projectId?: string;
|
|
42
|
+
/** Optional workspace scope from key metadata; cross-workspace → 404. */
|
|
43
|
+
workspaceId?: string;
|
|
31
44
|
/**
|
|
32
45
|
* Live member role when present. Effective authority is always
|
|
33
46
|
* `permission` (key ∩ live role; agent profile also strips apiKey —
|
|
@@ -39,7 +52,10 @@ export type AuthContext = {
|
|
|
39
52
|
/** Portal participant after join — no org membership, one share only. */
|
|
40
53
|
kind: 'guest';
|
|
41
54
|
shareId: string;
|
|
42
|
-
|
|
55
|
+
/** Set for a project share; absent for a workspace share. */
|
|
56
|
+
projectId?: string;
|
|
57
|
+
/** Set for a workspace share; absent for a project share. */
|
|
58
|
+
workspaceId?: string;
|
|
43
59
|
guestSessionId: string;
|
|
44
60
|
};
|
|
45
61
|
/** Org-bearing contexts (everything except portal guests). */
|
package/dist/auth.d.ts
CHANGED
|
@@ -13,6 +13,16 @@ export type OrgAuthOptions = {
|
|
|
13
13
|
*/
|
|
14
14
|
betterAuth?: BetterAuthInstance;
|
|
15
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Read better-auth key metadata. Absent/unknown `kind` → `'agent'` (BA4b-1
|
|
18
|
+
* back-compat: every key minted without kind keeps agent ceiling).
|
|
19
|
+
*/
|
|
20
|
+
export declare function readApiKeyMetadata(metadata: unknown): {
|
|
21
|
+
orgId: string | undefined;
|
|
22
|
+
projectId: string | undefined;
|
|
23
|
+
workspaceId: string | undefined;
|
|
24
|
+
kind: 'agent' | 'owner';
|
|
25
|
+
};
|
|
16
26
|
export declare function isInvitationAcceptPath(path: string): boolean;
|
|
17
27
|
export declare function isPublicAuthPath(path: string): boolean;
|
|
18
28
|
export declare function isPublicShareReadPath(path: string): boolean;
|
package/dist/auth.js
CHANGED
|
@@ -2,7 +2,7 @@ import { timingSafeEqual } from 'node:crypto';
|
|
|
2
2
|
import { DEFAULT_ORG_ID, hashShareToken, orgRoles, verifyGuestSession, } from '@plandesk/db';
|
|
3
3
|
import { applyAgentKeyPermissionCeiling, verifyBetterAuthApiKey, } from './agent-keys.js';
|
|
4
4
|
import { runWithAuthContext, tryGetAuthContext } from './auth-context.js';
|
|
5
|
-
import { userRefFromGithubAccountId } from './identity.js';
|
|
5
|
+
import { userRefFromGithubAccountId, listMemberWorkspaceIds } from './identity.js';
|
|
6
6
|
import { resolveDefaultOrganization } from './organizations.js';
|
|
7
7
|
import { hasAnyWritePermission, orgRoleToPermissionSet, } from './permissions.js';
|
|
8
8
|
import { readGuestSessionCookie } from './session.js';
|
|
@@ -115,22 +115,25 @@ async function resolveBetterAuthSessionContext(auth, headers) {
|
|
|
115
115
|
kind: 'session',
|
|
116
116
|
orgId: active.organizationId,
|
|
117
117
|
userRef,
|
|
118
|
+
userId,
|
|
118
119
|
role,
|
|
119
120
|
permission: orgRoleToPermissionSet(role),
|
|
121
|
+
memberWorkspaceIds: await listMemberWorkspaceIds(auth, userId, active.organizationId),
|
|
120
122
|
};
|
|
121
123
|
}
|
|
122
124
|
/**
|
|
123
125
|
* Read better-auth key metadata. Absent/unknown `kind` → `'agent'` (BA4b-1
|
|
124
126
|
* back-compat: every key minted without kind keeps agent ceiling).
|
|
125
127
|
*/
|
|
126
|
-
function readApiKeyMetadata(metadata) {
|
|
128
|
+
export function readApiKeyMetadata(metadata) {
|
|
127
129
|
if (metadata === null || metadata === undefined || typeof metadata !== 'object') {
|
|
128
|
-
return { orgId: undefined, projectId: undefined, kind: 'agent' };
|
|
130
|
+
return { orgId: undefined, projectId: undefined, workspaceId: undefined, kind: 'agent' };
|
|
129
131
|
}
|
|
130
132
|
const m = metadata;
|
|
131
133
|
return {
|
|
132
134
|
orgId: typeof m.orgId === 'string' && m.orgId.length > 0 ? m.orgId : undefined,
|
|
133
135
|
projectId: typeof m.projectId === 'string' && m.projectId.length > 0 ? m.projectId : undefined,
|
|
136
|
+
workspaceId: typeof m.teamId === 'string' && m.teamId.length > 0 ? m.teamId : undefined,
|
|
134
137
|
kind: m.kind === 'owner' ? 'owner' : 'agent',
|
|
135
138
|
};
|
|
136
139
|
}
|
|
@@ -165,7 +168,7 @@ async function resolveBetterAuthApiKeyContext(auth, bearer) {
|
|
|
165
168
|
return undefined;
|
|
166
169
|
}
|
|
167
170
|
const userId = verified.referenceId;
|
|
168
|
-
const { orgId, projectId, kind } = readApiKeyMetadata(verified.metadata);
|
|
171
|
+
const { orgId, projectId, workspaceId, kind } = readApiKeyMetadata(verified.metadata);
|
|
169
172
|
if (orgId === undefined) {
|
|
170
173
|
return 'unauthorized';
|
|
171
174
|
}
|
|
@@ -175,7 +178,9 @@ async function resolveBetterAuthApiKeyContext(auth, bearer) {
|
|
|
175
178
|
kind: 'apikey',
|
|
176
179
|
orgId,
|
|
177
180
|
userId,
|
|
181
|
+
profile: kind,
|
|
178
182
|
...(projectId !== undefined ? { projectId } : {}),
|
|
183
|
+
...(workspaceId !== undefined ? { workspaceId } : {}),
|
|
179
184
|
role: liveRole,
|
|
180
185
|
permission,
|
|
181
186
|
};
|
|
@@ -273,7 +278,8 @@ export function createOrgAuthMiddleware(options) {
|
|
|
273
278
|
const ctx = {
|
|
274
279
|
kind: 'guest',
|
|
275
280
|
shareId: guest.shareId,
|
|
276
|
-
projectId: guest.projectId,
|
|
281
|
+
...(guest.projectId !== null ? { projectId: guest.projectId } : {}),
|
|
282
|
+
...(guest.workspaceId !== null ? { workspaceId: guest.workspaceId } : {}),
|
|
277
283
|
guestSessionId: guest.id,
|
|
278
284
|
};
|
|
279
285
|
await runWithAuthContext(ctx, async () => {
|
|
@@ -326,18 +332,31 @@ export function createOrgAuthMiddleware(options) {
|
|
|
326
332
|
// organization table missing / adapter error — keep DEFAULT_ORG_ID
|
|
327
333
|
}
|
|
328
334
|
}
|
|
335
|
+
const headerWorkspaceId = c.req.header('x-plandesk-workspace-id');
|
|
329
336
|
const ctx = {
|
|
330
337
|
kind: 'loopback',
|
|
331
338
|
orgId,
|
|
332
339
|
role: 'owner',
|
|
333
340
|
permission: orgRoleToPermissionSet('owner'),
|
|
341
|
+
...(headerWorkspaceId !== undefined && headerWorkspaceId.trim() !== ''
|
|
342
|
+
? { workspaceId: headerWorkspaceId.trim() }
|
|
343
|
+
: {}),
|
|
334
344
|
};
|
|
335
345
|
await runWithAuthContext(ctx, async () => {
|
|
336
346
|
await next();
|
|
337
347
|
});
|
|
338
348
|
return;
|
|
339
349
|
}
|
|
340
|
-
|
|
350
|
+
// No credential resolved. API/auth/mcp namespaces require a credential →
|
|
351
|
+
// 401 challenge. Any other path is not an API route, so defer to routing
|
|
352
|
+
// (SPA handler or Hono 404): an unauthenticated probe of an unknown path
|
|
353
|
+
// gets a 404, not a 401 that implies a real route exists there.
|
|
354
|
+
if (c.req.path.startsWith('/api/v1') ||
|
|
355
|
+
c.req.path.startsWith('/api/auth') ||
|
|
356
|
+
c.req.path.startsWith('/mcp')) {
|
|
357
|
+
return c.json({ error: 'unauthorized' }, 401);
|
|
358
|
+
}
|
|
359
|
+
await next();
|
|
341
360
|
};
|
|
342
361
|
}
|
|
343
362
|
/** Optional Basic auth gate (PLANDESK_AUTH_PASSWORD). Skips MCP paths. */
|
package/dist/better-auth.js
CHANGED
|
@@ -35,7 +35,7 @@ import { organization } from 'better-auth/plugins';
|
|
|
35
35
|
import { apiKey } from '@better-auth/api-key';
|
|
36
36
|
import { LibsqlDialect } from '@libsql/kysely-libsql';
|
|
37
37
|
import { ac, admin, member, owner } from './access-control.js';
|
|
38
|
-
import { provisionPersonalOrgIfNeeded, setDefaultActiveOrganization } from './identity.js';
|
|
38
|
+
import { provisionPersonalOrgIfNeeded, setDefaultActiveOrganization, setDefaultActiveTeam, } from './identity.js';
|
|
39
39
|
/** Absent secret -> undefined, the supported no-auth-mounted state (REQ-5). */
|
|
40
40
|
export function createBetterAuth(deps) {
|
|
41
41
|
if (deps.secret === undefined || deps.secret.length === 0) {
|
|
@@ -77,14 +77,15 @@ export function createBetterAuth(deps) {
|
|
|
77
77
|
if (authInstance === undefined)
|
|
78
78
|
return;
|
|
79
79
|
await provisionPersonalOrgIfNeeded(authInstance, appDb, session.userId);
|
|
80
|
-
await setDefaultActiveOrganization(authInstance, session.userId, session.token);
|
|
80
|
+
const activeOrgId = await setDefaultActiveOrganization(authInstance, session.userId, session.token);
|
|
81
|
+
await setDefaultActiveTeam(authInstance, session.userId, session.token, activeOrgId);
|
|
81
82
|
},
|
|
82
83
|
},
|
|
83
84
|
},
|
|
84
85
|
},
|
|
85
86
|
}),
|
|
86
87
|
plugins: [
|
|
87
|
-
organization({ ac, roles: { owner, admin, member } }),
|
|
88
|
+
organization({ ac, roles: { owner, admin, member }, teams: { enabled: true } }),
|
|
88
89
|
// enableMetadata: projectId + orgId on agent keys (BA5). Rate limit off —
|
|
89
90
|
// agent traffic is bursty; ceilings are permission-based, not request-count.
|
|
90
91
|
apiKey({ enableMetadata: true, rateLimit: { enabled: false } }),
|
package/dist/identity.d.ts
CHANGED
|
@@ -8,6 +8,12 @@ type OrganizationRow = {
|
|
|
8
8
|
slug: string;
|
|
9
9
|
createdAt: Date;
|
|
10
10
|
};
|
|
11
|
+
type TeamRow = {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
organizationId: string;
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
};
|
|
11
17
|
export type IdentityOrganization = Pick<OrganizationRow, 'id' | 'name' | 'slug'> & {
|
|
12
18
|
role: string;
|
|
13
19
|
};
|
|
@@ -18,6 +24,25 @@ export declare function resolveOrganizationsForGithubIdentity(auth: BetterAuthIn
|
|
|
18
24
|
* Ensure the well-known local better-auth organization exists (user-less).
|
|
19
25
|
* Idempotent. Used at serve boot for loopback owner-by-bind.
|
|
20
26
|
*/
|
|
27
|
+
export declare function createTeamForOrg(auth: BetterAuthInstance, organizationId: string, name: string): Promise<TeamRow>;
|
|
28
|
+
/** Read helpers for the dashboard's workspace surfaces (a workspace = a team). */
|
|
29
|
+
export type WorkspaceSummary = {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
export declare function listTeamsForOrg(auth: BetterAuthInstance, organizationId: string): Promise<WorkspaceSummary[]>;
|
|
34
|
+
/** A team by id, only when it belongs to `organizationId` (no cross-org leak). */
|
|
35
|
+
export declare function getTeamInOrg(auth: BetterAuthInstance, teamId: string, organizationId: string): Promise<WorkspaceSummary | undefined>;
|
|
36
|
+
/** Resolve the session's active team for /auth/session, scoped to its org. */
|
|
37
|
+
export declare function getActiveTeamForSession(auth: BetterAuthInstance, sessionToken: string, organizationId: string): Promise<WorkspaceSummary | undefined>;
|
|
38
|
+
export declare function ensureDefaultTeamForOrg(auth: BetterAuthInstance, organizationId: string, _orgName?: string): Promise<string>;
|
|
39
|
+
export declare function backfillDefaultTeams(auth: BetterAuthInstance): Promise<{
|
|
40
|
+
orgsProcessed: number;
|
|
41
|
+
teamsCreated: number;
|
|
42
|
+
}>;
|
|
43
|
+
export declare function backfillProjectWorkspaces(db: Db, auth: BetterAuthInstance): Promise<{
|
|
44
|
+
projectsUpdated: number;
|
|
45
|
+
}>;
|
|
21
46
|
export declare function ensureLocalBetterAuthOrganization(_db: Db, auth: BetterAuthInstance): Promise<OrganizationSummary>;
|
|
22
47
|
export type ProvisionPersonalOrgResult = {
|
|
23
48
|
created: true;
|
|
@@ -34,5 +59,14 @@ export type ProvisionPersonalOrgResult = {
|
|
|
34
59
|
export declare function provisionPersonalOrgIfNeeded(auth: BetterAuthInstance, _db: Db, userId: string): Promise<ProvisionPersonalOrgResult>;
|
|
35
60
|
/** Persist the best active organization for a newly-created session. */
|
|
36
61
|
export declare function setDefaultActiveOrganization(auth: BetterAuthInstance, userId: string, sessionToken: string): Promise<string | undefined>;
|
|
62
|
+
/**
|
|
63
|
+
* The workspace (team) ids in `organizationId` that `userId` is a `teamMember`
|
|
64
|
+
* of, in teamMember-row order. Gates a session member to their own workspaces
|
|
65
|
+
* (owner/admin bypass by role); also backs the active-team self-heal. Empty
|
|
66
|
+
* when the user belongs to no team in the org.
|
|
67
|
+
*/
|
|
68
|
+
export declare function listMemberWorkspaceIds(auth: BetterAuthInstance, userId: string, organizationId: string): Promise<string[]>;
|
|
69
|
+
/** Persist the best active team for a newly-created session. */
|
|
70
|
+
export declare function setDefaultActiveTeam(auth: BetterAuthInstance, userId: string, sessionToken: string, activeOrganizationId: string | undefined): Promise<string | undefined>;
|
|
37
71
|
export {};
|
|
38
72
|
//# sourceMappingURL=identity.d.ts.map
|
package/dist/identity.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_ORG_ID } from '@plandesk/db';
|
|
1
|
+
import { DEFAULT_ORG_ID, DEFAULT_WORKSPACE_ID, updateProject } from '@plandesk/db';
|
|
2
2
|
import { getOrganizationById } from './organizations.js';
|
|
3
3
|
const GITHUB_PROVIDER_ID = 'github';
|
|
4
4
|
const GITHUB_USER_REF_PREFIX = 'github:';
|
|
@@ -56,9 +56,138 @@ export async function resolveOrganizationsForGithubIdentity(auth, identity) {
|
|
|
56
56
|
* Ensure the well-known local better-auth organization exists (user-less).
|
|
57
57
|
* Idempotent. Used at serve boot for loopback owner-by-bind.
|
|
58
58
|
*/
|
|
59
|
+
export async function createTeamForOrg(auth, organizationId, name) {
|
|
60
|
+
const adapter = (await auth.$context).adapter;
|
|
61
|
+
const now = new Date();
|
|
62
|
+
return adapter.create({
|
|
63
|
+
model: 'team',
|
|
64
|
+
data: {
|
|
65
|
+
name,
|
|
66
|
+
organizationId,
|
|
67
|
+
createdAt: now,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function listTeamsForOrg(auth, organizationId) {
|
|
72
|
+
const adapter = (await auth.$context).adapter;
|
|
73
|
+
const teams = await adapter.findMany({
|
|
74
|
+
model: 'team',
|
|
75
|
+
where: [{ field: 'organizationId', value: organizationId }],
|
|
76
|
+
sortBy: { field: 'createdAt', direction: 'asc' },
|
|
77
|
+
});
|
|
78
|
+
return teams.map((team) => ({ id: team.id, name: team.name }));
|
|
79
|
+
}
|
|
80
|
+
/** A team by id, only when it belongs to `organizationId` (no cross-org leak). */
|
|
81
|
+
export async function getTeamInOrg(auth, teamId, organizationId) {
|
|
82
|
+
const adapter = (await auth.$context).adapter;
|
|
83
|
+
const team = await adapter.findOne({
|
|
84
|
+
model: 'team',
|
|
85
|
+
where: [
|
|
86
|
+
{ field: 'id', value: teamId },
|
|
87
|
+
{ field: 'organizationId', value: organizationId },
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
return team === null ? undefined : { id: team.id, name: team.name };
|
|
91
|
+
}
|
|
92
|
+
/** Resolve the session's active team for /auth/session, scoped to its org. */
|
|
93
|
+
export async function getActiveTeamForSession(auth, sessionToken, organizationId) {
|
|
94
|
+
const context = await auth.$context;
|
|
95
|
+
const session = await context.adapter.findOne({
|
|
96
|
+
model: 'session',
|
|
97
|
+
where: [{ field: 'token', value: sessionToken }],
|
|
98
|
+
});
|
|
99
|
+
const activeTeamId = session?.activeTeamId === undefined || session.activeTeamId === null
|
|
100
|
+
? undefined
|
|
101
|
+
: session.activeTeamId;
|
|
102
|
+
if (activeTeamId === undefined) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return getTeamInOrg(auth, activeTeamId, organizationId);
|
|
106
|
+
}
|
|
107
|
+
export async function ensureDefaultTeamForOrg(auth, organizationId, _orgName) {
|
|
108
|
+
const adapter = (await auth.$context).adapter;
|
|
109
|
+
const existingTeams = await adapter.findMany({
|
|
110
|
+
model: 'team',
|
|
111
|
+
where: [{ field: 'organizationId', value: organizationId }],
|
|
112
|
+
});
|
|
113
|
+
let team;
|
|
114
|
+
if (existingTeams.length > 0) {
|
|
115
|
+
team = existingTeams[0];
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
const now = new Date();
|
|
119
|
+
const isLocalOrg = organizationId === DEFAULT_ORG_ID;
|
|
120
|
+
team = await adapter.create({
|
|
121
|
+
model: 'team',
|
|
122
|
+
data: {
|
|
123
|
+
...(isLocalOrg ? { id: DEFAULT_WORKSPACE_ID } : {}),
|
|
124
|
+
name: 'General',
|
|
125
|
+
organizationId,
|
|
126
|
+
createdAt: now,
|
|
127
|
+
},
|
|
128
|
+
...(isLocalOrg ? { forceAllowId: true } : {}),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
const members = await adapter.findMany({
|
|
132
|
+
model: 'member',
|
|
133
|
+
where: [{ field: 'organizationId', value: organizationId }],
|
|
134
|
+
});
|
|
135
|
+
const existingTeamMembers = await adapter.findMany({
|
|
136
|
+
model: 'teamMember',
|
|
137
|
+
where: [{ field: 'teamId', value: team.id }],
|
|
138
|
+
});
|
|
139
|
+
const userIdsInTeam = new Set(existingTeamMembers.map((tm) => tm.userId));
|
|
140
|
+
const now = new Date();
|
|
141
|
+
for (const member of members) {
|
|
142
|
+
if (!userIdsInTeam.has(member.userId)) {
|
|
143
|
+
await adapter.create({
|
|
144
|
+
model: 'teamMember',
|
|
145
|
+
data: {
|
|
146
|
+
teamId: team.id,
|
|
147
|
+
userId: member.userId,
|
|
148
|
+
createdAt: now,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return team.id;
|
|
154
|
+
}
|
|
155
|
+
export async function backfillDefaultTeams(auth) {
|
|
156
|
+
const adapter = (await auth.$context).adapter;
|
|
157
|
+
const orgs = await adapter.findMany({
|
|
158
|
+
model: 'organization',
|
|
159
|
+
});
|
|
160
|
+
let teamsCreated = 0;
|
|
161
|
+
for (const org of orgs) {
|
|
162
|
+
const existingTeams = await adapter.findMany({
|
|
163
|
+
model: 'team',
|
|
164
|
+
where: [{ field: 'organizationId', value: org.id }],
|
|
165
|
+
});
|
|
166
|
+
const hadTeam = existingTeams.length > 0;
|
|
167
|
+
await ensureDefaultTeamForOrg(auth, org.id, org.name);
|
|
168
|
+
if (!hadTeam) {
|
|
169
|
+
teamsCreated++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { orgsProcessed: orgs.length, teamsCreated };
|
|
173
|
+
}
|
|
174
|
+
export async function backfillProjectWorkspaces(db, auth) {
|
|
175
|
+
const result = await db.$client.execute('SELECT id, org_id, workspace_id FROM projects');
|
|
176
|
+
let projectsUpdated = 0;
|
|
177
|
+
for (const row of result.rows) {
|
|
178
|
+
const project = row;
|
|
179
|
+
const teamId = await ensureDefaultTeamForOrg(auth, project.org_id);
|
|
180
|
+
if (project.workspace_id !== teamId) {
|
|
181
|
+
await updateProject(db, project.id, { workspaceId: teamId });
|
|
182
|
+
projectsUpdated++;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { projectsUpdated };
|
|
186
|
+
}
|
|
59
187
|
export async function ensureLocalBetterAuthOrganization(_db, auth) {
|
|
60
188
|
const existing = await getOrganizationById(auth, DEFAULT_ORG_ID);
|
|
61
189
|
if (existing !== undefined) {
|
|
190
|
+
await ensureDefaultTeamForOrg(auth, DEFAULT_ORG_ID, 'Personal');
|
|
62
191
|
return existing;
|
|
63
192
|
}
|
|
64
193
|
const now = new Date();
|
|
@@ -74,6 +203,7 @@ export async function ensureLocalBetterAuthOrganization(_db, auth) {
|
|
|
74
203
|
data,
|
|
75
204
|
forceAllowId: true,
|
|
76
205
|
});
|
|
206
|
+
await ensureDefaultTeamForOrg(auth, DEFAULT_ORG_ID, 'Personal');
|
|
77
207
|
return {
|
|
78
208
|
id: created.id,
|
|
79
209
|
name: created.name,
|
|
@@ -130,6 +260,7 @@ export async function provisionPersonalOrgIfNeeded(auth, _db, userId) {
|
|
|
130
260
|
createdAt: now,
|
|
131
261
|
},
|
|
132
262
|
});
|
|
263
|
+
await ensureDefaultTeamForOrg(auth, organization.id, organization.name);
|
|
133
264
|
return { created: true, orgId: organization.id, role: 'owner' };
|
|
134
265
|
}
|
|
135
266
|
/** Persist the best active organization for a newly-created session. */
|
|
@@ -160,4 +291,54 @@ export async function setDefaultActiveOrganization(auth, userId, sessionToken) {
|
|
|
160
291
|
await context.internalAdapter.updateSession(sessionToken, { activeOrganizationId });
|
|
161
292
|
return activeOrganizationId;
|
|
162
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* The workspace (team) ids in `organizationId` that `userId` is a `teamMember`
|
|
296
|
+
* of, in teamMember-row order. Gates a session member to their own workspaces
|
|
297
|
+
* (owner/admin bypass by role); also backs the active-team self-heal. Empty
|
|
298
|
+
* when the user belongs to no team in the org.
|
|
299
|
+
*/
|
|
300
|
+
export async function listMemberWorkspaceIds(auth, userId, organizationId) {
|
|
301
|
+
const context = await auth.$context;
|
|
302
|
+
const teamMembers = await context.adapter.findMany({
|
|
303
|
+
model: 'teamMember',
|
|
304
|
+
where: [{ field: 'userId', value: userId }],
|
|
305
|
+
});
|
|
306
|
+
if (teamMembers.length === 0) {
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
const candidateTeamIds = teamMembers.map((tm) => tm.teamId);
|
|
310
|
+
const teams = await context.adapter.findMany({
|
|
311
|
+
model: 'team',
|
|
312
|
+
where: [{ field: 'id', value: candidateTeamIds, operator: 'in' }],
|
|
313
|
+
});
|
|
314
|
+
const orgTeamIds = new Set(teams.filter((team) => team.organizationId === organizationId).map((team) => team.id));
|
|
315
|
+
return candidateTeamIds.filter((id) => orgTeamIds.has(id));
|
|
316
|
+
}
|
|
317
|
+
/** Persist the best active team for a newly-created session. */
|
|
318
|
+
export async function setDefaultActiveTeam(auth, userId, sessionToken, activeOrganizationId) {
|
|
319
|
+
if (activeOrganizationId === undefined) {
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const orgTeamIds = await listMemberWorkspaceIds(auth, userId, activeOrganizationId);
|
|
323
|
+
if (orgTeamIds.length === 0) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
const orgTeamIdSet = new Set(orgTeamIds);
|
|
327
|
+
const context = await auth.$context;
|
|
328
|
+
const sessions = await context.adapter.findMany({
|
|
329
|
+
model: 'session',
|
|
330
|
+
where: [{ field: 'userId', value: userId }],
|
|
331
|
+
sortBy: { field: 'updatedAt', direction: 'desc' },
|
|
332
|
+
});
|
|
333
|
+
const priorActiveTeamId = sessions.find((session) => session.token !== sessionToken &&
|
|
334
|
+
session.activeTeamId !== undefined &&
|
|
335
|
+
session.activeTeamId !== null &&
|
|
336
|
+
orgTeamIdSet.has(session.activeTeamId))?.activeTeamId;
|
|
337
|
+
const activeTeamId = priorActiveTeamId ?? orgTeamIds[0];
|
|
338
|
+
if (activeTeamId === undefined) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
await context.internalAdapter.updateSession(sessionToken, { activeTeamId });
|
|
342
|
+
return activeTeamId;
|
|
343
|
+
}
|
|
163
344
|
//# sourceMappingURL=identity.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { createApp, type AppDeps } from './server.js';
|
|
2
2
|
export { createBetterAuth, runBetterAuthMigrations, type BetterAuthDeps, type BetterAuthInstance, } from './better-auth.js';
|
|
3
|
-
export { ensureLocalBetterAuthOrganization } from './identity.js';
|
|
3
|
+
export { backfillDefaultTeams, backfillProjectWorkspaces, createTeamForOrg, ensureDefaultTeamForOrg, ensureLocalBetterAuthOrganization, listTeamsForOrg, } from './identity.js';
|
|
4
4
|
export { createAuthMiddleware, createOrgAuthMiddleware, createWriteGuardMiddleware, isLoopbackBind, isPublicAuthPath, isInvitationAcceptPath, type OrgAuthOptions, } from './auth.js';
|
|
5
5
|
export { acceptOrganizationInvitation, createOrganizationInvitation, ensureShellOwner, invitationClaimUrl, isAuthApiError, isInvitationRole, mintOwnerInvitation, mintSessionCookieHeader, removeOrganizationMember, updateOrganizationMemberRole, INVITATION_ROLES, type InvitationRole, } from './invitations.js';
|
|
6
6
|
export { authorizeUrl, githubConfigFromEnv, resolveGithubIdentity, userRefFromGithubId, GithubOAuthError, type FetchLike, type GithubConfig, type GithubEnv, type GithubIdentity, } from './github.js';
|
|
7
7
|
export { GUEST_SESSION_COOKIE, readGuestSessionCookie } from './session.js';
|
|
8
8
|
export { createAuthRouter, type AuthRouterDeps } from './routes/auth.js';
|
|
9
9
|
export { runWithAuthContext, tryGetAuthContext, getAuthContext, ReadOnlyTokenError, type AuthContext, } from './auth-context.js';
|
|
10
|
-
export { applyAgentKeyPermissionCeiling, createScopedAgentKey, createOrgOwnerKey, verifyBetterAuthApiKey, DEFAULT_AGENT_KEY_PERMISSIONS, DEFAULT_OWNER_KEY_PERMISSIONS, AGENT_FORBIDDEN_RESOURCES, type ApiKeyKind, type CreateScopedAgentKeyInput, type CreatedScopedAgentKey, type CreateOrgOwnerKeyInput, type CreatedOrgOwnerKey, type VerifiedApiKey, } from './agent-keys.js';
|
|
10
|
+
export { applyAgentKeyPermissionCeiling, createScopedAgentKey, createWorkspaceScopedAgentKey, createOrgOwnerKey, verifyBetterAuthApiKey, DEFAULT_AGENT_KEY_PERMISSIONS, DEFAULT_OWNER_KEY_PERMISSIONS, AGENT_FORBIDDEN_RESOURCES, type ApiKeyKind, type CreateScopedAgentKeyInput, type CreatedScopedAgentKey, type CreateWorkspaceScopedAgentKeyInput, type CreatedWorkspaceScopedAgentKey, type CreateOrgOwnerKeyInput, type CreatedOrgOwnerKey, type VerifiedApiKey, } from './agent-keys.js';
|
|
11
11
|
export { requirePermission, hasPermission, hasAnyWritePermission, orgRoleToPermissionSet, PermissionDeniedError, type PermissionSet, } from './permissions.js';
|
|
12
12
|
export { healthRouter } from './routes/health.js';
|
|
13
13
|
export { mountStatic } from './static.js';
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
export { createApp } from './server.js';
|
|
2
2
|
export { createBetterAuth, runBetterAuthMigrations, } from './better-auth.js';
|
|
3
|
-
export { ensureLocalBetterAuthOrganization } from './identity.js';
|
|
3
|
+
export { backfillDefaultTeams, backfillProjectWorkspaces, createTeamForOrg, ensureDefaultTeamForOrg, ensureLocalBetterAuthOrganization, listTeamsForOrg, } from './identity.js';
|
|
4
4
|
export { createAuthMiddleware, createOrgAuthMiddleware, createWriteGuardMiddleware, isLoopbackBind, isPublicAuthPath, isInvitationAcceptPath, } from './auth.js';
|
|
5
5
|
export { acceptOrganizationInvitation, createOrganizationInvitation, ensureShellOwner, invitationClaimUrl, isAuthApiError, isInvitationRole, mintOwnerInvitation, mintSessionCookieHeader, removeOrganizationMember, updateOrganizationMemberRole, INVITATION_ROLES, } from './invitations.js';
|
|
6
6
|
export { authorizeUrl, githubConfigFromEnv, resolveGithubIdentity, userRefFromGithubId, GithubOAuthError, } from './github.js';
|
|
7
7
|
export { GUEST_SESSION_COOKIE, readGuestSessionCookie } from './session.js';
|
|
8
8
|
export { createAuthRouter } from './routes/auth.js';
|
|
9
9
|
export { runWithAuthContext, tryGetAuthContext, getAuthContext, ReadOnlyTokenError, } from './auth-context.js';
|
|
10
|
-
export { applyAgentKeyPermissionCeiling, createScopedAgentKey, createOrgOwnerKey, verifyBetterAuthApiKey, DEFAULT_AGENT_KEY_PERMISSIONS, DEFAULT_OWNER_KEY_PERMISSIONS, AGENT_FORBIDDEN_RESOURCES, } from './agent-keys.js';
|
|
10
|
+
export { applyAgentKeyPermissionCeiling, createScopedAgentKey, createWorkspaceScopedAgentKey, createOrgOwnerKey, verifyBetterAuthApiKey, DEFAULT_AGENT_KEY_PERMISSIONS, DEFAULT_OWNER_KEY_PERMISSIONS, AGENT_FORBIDDEN_RESOURCES, } from './agent-keys.js';
|
|
11
11
|
export { requirePermission, hasPermission, hasAnyWritePermission, orgRoleToPermissionSet, PermissionDeniedError, } from './permissions.js';
|
|
12
12
|
export { healthRouter } from './routes/health.js';
|
|
13
13
|
export { mountStatic } from './static.js';
|
package/dist/invitations.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ type InvitationRow = {
|
|
|
8
8
|
email: string;
|
|
9
9
|
role: string;
|
|
10
10
|
organizationId: string;
|
|
11
|
+
/** better-auth stores team ids comma-joined; null when no team. */
|
|
12
|
+
teamId: string | null;
|
|
11
13
|
inviterId: string;
|
|
12
14
|
status: string;
|
|
13
15
|
expiresAt: Date;
|
|
@@ -30,18 +32,24 @@ export type AuthApiErrorShape = {
|
|
|
30
32
|
};
|
|
31
33
|
export declare function isAuthApiError(err: unknown): err is AuthApiErrorShape;
|
|
32
34
|
/**
|
|
33
|
-
* Create
|
|
34
|
-
*
|
|
35
|
+
* Create a workspace-scoped invitation using the caller's better-auth session
|
|
36
|
+
* (Cookie headers). The invitee joins the org AND the team on accept
|
|
37
|
+
* (better-auth does both when teamId is set). No mailer: caller delivers
|
|
38
|
+
* claimUrl by hand.
|
|
35
39
|
*/
|
|
36
40
|
export declare function createOrganizationInvitation(auth: BetterAuthInstance, opts: {
|
|
37
41
|
email: string;
|
|
38
42
|
role: InvitationRole;
|
|
39
43
|
organizationId: string;
|
|
44
|
+
/** Workspace (better-auth team) the invitee will join. Required. */
|
|
45
|
+
teamId: string;
|
|
40
46
|
headers: Headers;
|
|
41
47
|
baseURL: string;
|
|
42
48
|
}): Promise<{
|
|
43
49
|
invitationId: string;
|
|
44
50
|
claimUrl: string;
|
|
51
|
+
organizationId: string;
|
|
52
|
+
teamId: string;
|
|
45
53
|
invitation: InvitationRow;
|
|
46
54
|
}>;
|
|
47
55
|
/**
|
|
@@ -81,6 +89,9 @@ export declare function ensureShellOwner(auth: BetterAuthInstance, organizationI
|
|
|
81
89
|
}>;
|
|
82
90
|
/**
|
|
83
91
|
* Shell-side owner invitation for `plandesk admin invite-owner` (no GitHub, no mailer).
|
|
92
|
+
* The owner role is org-level; teamId targets the org's default workspace so
|
|
93
|
+
* the bootstrap satisfies the workspace-scoped invite contract (the owner
|
|
94
|
+
* reaches every workspace via the owner role regardless).
|
|
84
95
|
*/
|
|
85
96
|
export declare function mintOwnerInvitation(auth: BetterAuthInstance, opts: {
|
|
86
97
|
email: string;
|