@plandesk/api 1.0.0-beta.3 → 1.0.0-beta.5
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 +1 -1
- package/dist/access-control.js +4 -0
- package/dist/auth.js +4 -1
- package/dist/organizations.d.ts +15 -0
- package/dist/organizations.js +30 -0
- package/dist/routes/orgs.js +25 -5
- package/package.json +2 -2
- package/web/assets/{index-B2oJosL6.js → index-DOg4prbp.js} +88 -88
- package/web/assets/index-vdnxEKRS.css +2 -0
- package/web/index.html +2 -2
- package/web/_redirects +0 -1
- package/web/assets/index-D29SyYFS.css +0 -2
package/dist/access-control.d.ts
CHANGED
|
@@ -90,6 +90,7 @@ export declare const member: import("better-auth/plugins/access").Role<import("b
|
|
|
90
90
|
}>;
|
|
91
91
|
export declare const admin: import("better-auth/plugins/access").Role<import("better-auth/plugins/access").ExactRoleStatements<{
|
|
92
92
|
readonly project: ["create", "delete"];
|
|
93
|
+
readonly invitation: ["create", "cancel"];
|
|
93
94
|
readonly task: readonly ["read", "create", "update", "delete"];
|
|
94
95
|
readonly document: readonly ["read", "create", "update", "delete"];
|
|
95
96
|
readonly edge: readonly ["read", "create", "update", "delete"];
|
|
@@ -98,7 +99,6 @@ export declare const admin: import("better-auth/plugins/access").Role<import("be
|
|
|
98
99
|
readonly agent_run: readonly ["read", "create", "update", "delete"];
|
|
99
100
|
readonly organization: readonly [];
|
|
100
101
|
readonly member: readonly [];
|
|
101
|
-
readonly invitation: readonly [];
|
|
102
102
|
readonly team: readonly [];
|
|
103
103
|
readonly ac: readonly [];
|
|
104
104
|
readonly apiKey: readonly [];
|
package/dist/access-control.js
CHANGED
|
@@ -35,6 +35,10 @@ export const member = ac.newRole(memberPermissions);
|
|
|
35
35
|
export const admin = ac.newRole({
|
|
36
36
|
...memberPermissions,
|
|
37
37
|
project: ['create', 'delete'],
|
|
38
|
+
// Admins can invite teammates (as member or admin) and cancel invites, but
|
|
39
|
+
// cannot manage members directly (member:update/delete) or mint owners —
|
|
40
|
+
// better-auth blocks a non-owner inviting the creatorRole. Owner-only stays owner-only.
|
|
41
|
+
invitation: ['create', 'cancel'],
|
|
38
42
|
});
|
|
39
43
|
export const owner = ac.newRole({
|
|
40
44
|
...memberPermissions,
|
package/dist/auth.js
CHANGED
|
@@ -192,6 +192,8 @@ const PUBLIC_AUTH_PATHS = new Set([
|
|
|
192
192
|
]);
|
|
193
193
|
/** Invitation accept: invitee may have a session but zero org memberships yet. */
|
|
194
194
|
const INVITATION_ACCEPT_PATH = /^\/api\/v1\/invitations\/[^/]+\/accept$/;
|
|
195
|
+
/** Invitation preview (GET only): the claim page renders "invited to X" pre-auth. */
|
|
196
|
+
const INVITATION_PREVIEW_PATH = /^\/api\/v1\/invitations\/[^/]+$/;
|
|
195
197
|
export function isInvitationAcceptPath(path) {
|
|
196
198
|
return INVITATION_ACCEPT_PATH.test(path);
|
|
197
199
|
}
|
|
@@ -204,7 +206,8 @@ export function isPublicAuthPath(path) {
|
|
|
204
206
|
return true;
|
|
205
207
|
}
|
|
206
208
|
// BA3c: accept is session-checked in-handler, not org-gated.
|
|
207
|
-
|
|
209
|
+
// Preview (GET /invitations/:id) is capability-gated by the unguessable id.
|
|
210
|
+
return isInvitationAcceptPath(path) || INVITATION_PREVIEW_PATH.test(path);
|
|
208
211
|
}
|
|
209
212
|
/**
|
|
210
213
|
* Pre-join share surfaces: meta (render "X invited you"), join (claim a guest
|
package/dist/organizations.d.ts
CHANGED
|
@@ -30,6 +30,21 @@ export type OrganizationMemberSummary = {
|
|
|
30
30
|
};
|
|
31
31
|
/** List better-auth members of an organization (email/name joined from user). */
|
|
32
32
|
export declare function listOrganizationMembers(auth: BetterAuthInstance, organizationId: string): Promise<OrganizationMemberSummary[]>;
|
|
33
|
+
export type InvitationPreview = {
|
|
34
|
+
organizationId: string;
|
|
35
|
+
organizationName: string;
|
|
36
|
+
role: string;
|
|
37
|
+
email: string;
|
|
38
|
+
status: string;
|
|
39
|
+
expiresAt: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Preview an invitation by id (capability: holding the link is authorization).
|
|
43
|
+
* Returns the org name + role + invited email so the claim page can orient the
|
|
44
|
+
* invitee before they sign in. No session required — mirrors the link-only,
|
|
45
|
+
* deliver-by-hand model; ids are unguessable so enumeration is infeasible.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getInvitationPreview(auth: BetterAuthInstance, invitationId: string): Promise<InvitationPreview | undefined>;
|
|
33
48
|
/**
|
|
34
49
|
* Display-friendly org for /auth/session and similar.
|
|
35
50
|
* Falls back to DEFAULT_ORG_ID → "Personal" when better-auth is not configured
|
package/dist/organizations.js
CHANGED
|
@@ -68,6 +68,36 @@ export async function listOrganizationMembers(auth, organizationId) {
|
|
|
68
68
|
};
|
|
69
69
|
}));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Preview an invitation by id (capability: holding the link is authorization).
|
|
73
|
+
* Returns the org name + role + invited email so the claim page can orient the
|
|
74
|
+
* invitee before they sign in. No session required — mirrors the link-only,
|
|
75
|
+
* deliver-by-hand model; ids are unguessable so enumeration is infeasible.
|
|
76
|
+
*/
|
|
77
|
+
export async function getInvitationPreview(auth, invitationId) {
|
|
78
|
+
const adapter = (await auth.$context).adapter;
|
|
79
|
+
const invitation = await adapter.findOne({
|
|
80
|
+
model: 'invitation',
|
|
81
|
+
where: [{ field: 'id', value: invitationId }],
|
|
82
|
+
});
|
|
83
|
+
if (invitation === null) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const org = await adapter.findOne({
|
|
87
|
+
model: 'organization',
|
|
88
|
+
where: [{ field: 'id', value: invitation.organizationId }],
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
organizationId: invitation.organizationId,
|
|
92
|
+
organizationName: org?.name ?? '',
|
|
93
|
+
role: invitation.role,
|
|
94
|
+
email: invitation.email,
|
|
95
|
+
status: invitation.status,
|
|
96
|
+
expiresAt: invitation.expiresAt instanceof Date
|
|
97
|
+
? invitation.expiresAt.toISOString()
|
|
98
|
+
: String(invitation.expiresAt),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
71
101
|
/**
|
|
72
102
|
* Display-friendly org for /auth/session and similar.
|
|
73
103
|
* Falls back to DEFAULT_ORG_ID → "Personal" when better-auth is not configured
|
package/dist/routes/orgs.js
CHANGED
|
@@ -3,7 +3,7 @@ import { getProjectInOrg, importProject, InvalidExportVersionError, PLANDESK_EXP
|
|
|
3
3
|
import { createScopedAgentKey } from '../agent-keys.js';
|
|
4
4
|
import { getAuthContext, getOrgAuthContext } from '../auth-context.js';
|
|
5
5
|
import { acceptOrganizationInvitation, createOrganizationInvitation, isAuthApiError, isInvitationRole, } from '../invitations.js';
|
|
6
|
-
import { getOrganizationById, listOrganizationMembers } from '../organizations.js';
|
|
6
|
+
import { getInvitationPreview, getOrganizationById, listOrganizationMembers, } from '../organizations.js';
|
|
7
7
|
import { requirePermission } from '../permissions.js';
|
|
8
8
|
function isPermissionSet(value) {
|
|
9
9
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -122,8 +122,9 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
122
122
|
return c.json({ members }, 200);
|
|
123
123
|
});
|
|
124
124
|
/**
|
|
125
|
-
* BA3c: invite by email (link-only, no mailer).
|
|
126
|
-
* (
|
|
125
|
+
* BA3c: invite by email (link-only, no mailer). Owners and admins may invite
|
|
126
|
+
* (invitation:create); better-auth still blocks a non-owner inviting an owner.
|
|
127
|
+
* Returns claimUrl for the inviter to deliver by hand.
|
|
127
128
|
*/
|
|
128
129
|
router.post('/orgs/:id/invitations', async (c) => {
|
|
129
130
|
const orgId = c.req.param('id');
|
|
@@ -131,11 +132,11 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
131
132
|
return c.json({ error: 'not_found' }, 404);
|
|
132
133
|
}
|
|
133
134
|
const authCtx = getOrgAuthContext();
|
|
134
|
-
// Session
|
|
135
|
+
// Session only — token/loopback cannot drive better-auth createInvitation.
|
|
135
136
|
if (authCtx.kind !== 'session') {
|
|
136
137
|
return c.json({ error: 'forbidden' }, 403);
|
|
137
138
|
}
|
|
138
|
-
requirePermission(authCtx, '
|
|
139
|
+
requirePermission(authCtx, 'invitation', 'create');
|
|
139
140
|
if (betterAuth === undefined) {
|
|
140
141
|
return c.json({ error: 'unavailable' }, 503);
|
|
141
142
|
}
|
|
@@ -169,6 +170,25 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
169
170
|
throw err;
|
|
170
171
|
}
|
|
171
172
|
});
|
|
173
|
+
/**
|
|
174
|
+
* Preview an invitation (capability: the unguessable id is the authorization).
|
|
175
|
+
* Public so the claim page can render "you've been invited to X as Y" before
|
|
176
|
+
* the invitee signs in. Returns org name + role + invited email + status.
|
|
177
|
+
*/
|
|
178
|
+
router.get('/invitations/:invitationId', async (c) => {
|
|
179
|
+
if (betterAuth === undefined) {
|
|
180
|
+
return c.json({ error: 'unavailable' }, 503);
|
|
181
|
+
}
|
|
182
|
+
const invitationId = c.req.param('invitationId');
|
|
183
|
+
if (invitationId.trim() === '') {
|
|
184
|
+
return c.json({ error: 'invalid_argument' }, 400);
|
|
185
|
+
}
|
|
186
|
+
const preview = await getInvitationPreview(betterAuth, invitationId);
|
|
187
|
+
if (preview === undefined) {
|
|
188
|
+
return c.json({ error: 'not_found' }, 404);
|
|
189
|
+
}
|
|
190
|
+
return c.json(preview, 200);
|
|
191
|
+
});
|
|
172
192
|
/**
|
|
173
193
|
* BA3c: accept invitation. Session-gated at the handler (public path so
|
|
174
194
|
* org-less invitees can reach it); not org-gated. Single-use → 410 on retry.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plandesk/api",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@libsql/kysely-libsql": "^0.4.1",
|
|
27
27
|
"better-auth": "1.6.23",
|
|
28
28
|
"hono": "^4.12.23",
|
|
29
|
-
"@plandesk/db": "1.0.0-beta.
|
|
29
|
+
"@plandesk/db": "1.0.0-beta.5"
|
|
30
30
|
},
|
|
31
31
|
"description": "Plan Desk REST + SSE API and service layer (local-first planning workspace).",
|
|
32
32
|
"license": "MIT",
|