@plandesk/api 1.0.0-beta.2 → 1.0.0-beta.3
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/auth.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/organizations.d.ts +10 -0
- package/dist/organizations.js +25 -0
- package/dist/routes/orgs.js +15 -1
- package/dist/storage/index.d.ts +4 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/r2.d.ts +40 -0
- package/dist/storage/r2.js +65 -0
- package/dist/worker.d.ts +6 -6
- package/dist/worker.js +33 -10
- package/package.json +2 -2
- package/web/assets/{index-tBJ_K3ur.js → index-B2oJosL6.js} +97 -97
- package/web/index.html +1 -1
package/dist/auth.js
CHANGED
|
@@ -187,6 +187,7 @@ async function resolveBetterAuthApiKeyContext(auth, bearer) {
|
|
|
187
187
|
*/
|
|
188
188
|
const PUBLIC_AUTH_PATHS = new Set([
|
|
189
189
|
'/api/v1/auth/methods',
|
|
190
|
+
'/api/v1/health',
|
|
190
191
|
'/api/auth/*',
|
|
191
192
|
]);
|
|
192
193
|
/** Invitation accept: invitee may have a session but zero org memberships yet. */
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export { requirePermission, hasPermission, hasAnyWritePermission, orgRoleToPermi
|
|
|
12
12
|
export { healthRouter } from './routes/health.js';
|
|
13
13
|
export { mountStatic } from './static.js';
|
|
14
14
|
export { createS3Adapter, type S3AdapterConfig } from './storage/s3.js';
|
|
15
|
+
export { createR2Adapter, type R2BucketLike } from './storage/r2.js';
|
|
15
16
|
export type { StorageAdapter } from './storage/adapter.js';
|
|
16
17
|
export { createServices, type Services, type ServicesDeps } from './services/index.js';
|
|
17
18
|
export { assertProjectInOrg, ProjectNotInOrgError } from './services/scope.js';
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ export { requirePermission, hasPermission, hasAnyWritePermission, orgRoleToPermi
|
|
|
12
12
|
export { healthRouter } from './routes/health.js';
|
|
13
13
|
export { mountStatic } from './static.js';
|
|
14
14
|
export { createS3Adapter } from './storage/s3.js';
|
|
15
|
+
export { createR2Adapter } from './storage/r2.js';
|
|
15
16
|
export { createServices } from './services/index.js';
|
|
16
17
|
export { assertProjectInOrg, ProjectNotInOrgError } from './services/scope.js';
|
|
17
18
|
export { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from './services/goals.js';
|
package/dist/organizations.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ export declare function getOrganizationById(auth: BetterAuthInstance, organizati
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function resolveDefaultOrganization(auth: BetterAuthInstance): Promise<OrganizationSummary | undefined>;
|
|
22
22
|
export declare function listOrganizationsForUser(auth: BetterAuthInstance, userId: string): Promise<UserOrganizationSummary[]>;
|
|
23
|
+
export type OrganizationMemberSummary = {
|
|
24
|
+
id: string;
|
|
25
|
+
userId: string;
|
|
26
|
+
email: string;
|
|
27
|
+
name: string;
|
|
28
|
+
role: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
};
|
|
31
|
+
/** List better-auth members of an organization (email/name joined from user). */
|
|
32
|
+
export declare function listOrganizationMembers(auth: BetterAuthInstance, organizationId: string): Promise<OrganizationMemberSummary[]>;
|
|
23
33
|
/**
|
|
24
34
|
* Display-friendly org for /auth/session and similar.
|
|
25
35
|
* Falls back to DEFAULT_ORG_ID → "Personal" when better-auth is not configured
|
package/dist/organizations.js
CHANGED
|
@@ -43,6 +43,31 @@ export async function listOrganizationsForUser(auth, userId) {
|
|
|
43
43
|
return { id: organization.id, name: organization.name, role: member.role };
|
|
44
44
|
}));
|
|
45
45
|
}
|
|
46
|
+
/** List better-auth members of an organization (email/name joined from user). */
|
|
47
|
+
export async function listOrganizationMembers(auth, organizationId) {
|
|
48
|
+
const adapter = (await auth.$context).adapter;
|
|
49
|
+
const members = await adapter.findMany({
|
|
50
|
+
model: 'member',
|
|
51
|
+
where: [{ field: 'organizationId', value: organizationId }],
|
|
52
|
+
sortBy: { field: 'createdAt', direction: 'asc' },
|
|
53
|
+
});
|
|
54
|
+
return Promise.all(members.map(async (member) => {
|
|
55
|
+
const user = await adapter.findOne({
|
|
56
|
+
model: 'user',
|
|
57
|
+
where: [{ field: 'id', value: member.userId }],
|
|
58
|
+
});
|
|
59
|
+
return {
|
|
60
|
+
id: member.id,
|
|
61
|
+
userId: member.userId,
|
|
62
|
+
email: user?.email ?? '',
|
|
63
|
+
name: user?.name ?? '',
|
|
64
|
+
role: member.role,
|
|
65
|
+
createdAt: member.createdAt instanceof Date
|
|
66
|
+
? member.createdAt.toISOString()
|
|
67
|
+
: String(member.createdAt),
|
|
68
|
+
};
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
46
71
|
/**
|
|
47
72
|
* Display-friendly org for /auth/session and similar.
|
|
48
73
|
* 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 } from '../organizations.js';
|
|
6
|
+
import { 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)) {
|
|
@@ -107,6 +107,20 @@ export function createOrgsRouter(db, options = {}) {
|
|
|
107
107
|
const minted = await createScopedAgentKey(mintInput);
|
|
108
108
|
return c.json({ token: minted.key, project_id: projectId }, 200);
|
|
109
109
|
});
|
|
110
|
+
/**
|
|
111
|
+
* List better-auth org members (role + email). Any org member may read.
|
|
112
|
+
*/
|
|
113
|
+
router.get('/orgs/:id/members', async (c) => {
|
|
114
|
+
const orgId = c.req.param('id');
|
|
115
|
+
if (!(await requireKnownOrg(orgId))) {
|
|
116
|
+
return c.json({ error: 'not_found' }, 404);
|
|
117
|
+
}
|
|
118
|
+
if (betterAuth === undefined) {
|
|
119
|
+
return c.json({ error: 'unavailable' }, 503);
|
|
120
|
+
}
|
|
121
|
+
const members = await listOrganizationMembers(betterAuth, orgId);
|
|
122
|
+
return c.json({ members }, 200);
|
|
123
|
+
});
|
|
110
124
|
/**
|
|
111
125
|
* BA3c: invite by email (link-only, no mailer). Session owner only
|
|
112
126
|
* (member:create). Returns claimUrl for the inviter to deliver by hand.
|
package/dist/storage/index.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Db } from '@plandesk/db';
|
|
2
2
|
import type { StorageAdapter } from './adapter.js';
|
|
3
|
+
import { type R2BucketLike } from './r2.js';
|
|
3
4
|
export * from './adapter.js';
|
|
4
5
|
export { createLocalBlobAdapter } from './local.js';
|
|
6
|
+
export { createR2Adapter, type R2BucketLike } from './r2.js';
|
|
5
7
|
export { createS3Adapter, readS3ConfigFromEnv, type S3AdapterConfig } from './s3.js';
|
|
6
8
|
export type CreateStorageAdapterDeps = {
|
|
7
9
|
db: Db;
|
|
8
10
|
env?: NodeJS.ProcessEnv;
|
|
11
|
+
/** When set (Workers), prefer the native R2 binding over S3/local. */
|
|
12
|
+
r2?: R2BucketLike;
|
|
9
13
|
};
|
|
10
14
|
export declare function createStorageAdapter(deps: CreateStorageAdapterDeps): StorageAdapter;
|
|
11
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/storage/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { createLocalBlobAdapter } from './local.js';
|
|
2
|
+
import { createR2Adapter } from './r2.js';
|
|
2
3
|
import { createS3Adapter, readS3ConfigFromEnv } from './s3.js';
|
|
3
4
|
export * from './adapter.js';
|
|
4
5
|
export { createLocalBlobAdapter } from './local.js';
|
|
6
|
+
export { createR2Adapter } from './r2.js';
|
|
5
7
|
export { createS3Adapter, readS3ConfigFromEnv } from './s3.js';
|
|
6
8
|
export function createStorageAdapter(deps) {
|
|
9
|
+
if (deps.r2 !== undefined) {
|
|
10
|
+
return createR2Adapter({ db: deps.db, bucket: deps.r2 });
|
|
11
|
+
}
|
|
7
12
|
const env = deps.env ?? process.env;
|
|
8
13
|
const kind = env.PLANDESK_STORAGE ?? 'local';
|
|
9
14
|
if (kind === 's3') {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type Db } from '@plandesk/db';
|
|
2
|
+
import { type StorageAdapter } from './adapter.js';
|
|
3
|
+
/**
|
|
4
|
+
* Minimal R2Bucket surface used by the adapter (matches Workers binding API).
|
|
5
|
+
* Tests supply an in-memory fake; production passes env.FILES.
|
|
6
|
+
*/
|
|
7
|
+
export type R2BucketLike = {
|
|
8
|
+
put(key: string, value: ArrayBuffer | ArrayBufferView | string | null | Blob, options?: {
|
|
9
|
+
httpMetadata?: {
|
|
10
|
+
contentType?: string;
|
|
11
|
+
};
|
|
12
|
+
customMetadata?: Record<string, string>;
|
|
13
|
+
}): Promise<unknown>;
|
|
14
|
+
get(key: string): Promise<{
|
|
15
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
16
|
+
httpMetadata?: {
|
|
17
|
+
contentType?: string;
|
|
18
|
+
};
|
|
19
|
+
customMetadata?: Record<string, string>;
|
|
20
|
+
} | null>;
|
|
21
|
+
head(key: string): Promise<{
|
|
22
|
+
httpMetadata?: {
|
|
23
|
+
contentType?: string;
|
|
24
|
+
};
|
|
25
|
+
customMetadata?: Record<string, string>;
|
|
26
|
+
} | null>;
|
|
27
|
+
};
|
|
28
|
+
export type R2AdapterDeps = {
|
|
29
|
+
db: Db;
|
|
30
|
+
bucket: R2BucketLike;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Content-addressed R2 storage via the native Workers binding (not S3 creds).
|
|
34
|
+
* Bytes live in the bucket under `{projectId}/{sha256}`; the `files` row records
|
|
35
|
+
* metadata + org linkage. `resolve` is org-scoped through `getFileInOrg` — the
|
|
36
|
+
* bare content id is never a lookup key, so one tenant cannot read another's
|
|
37
|
+
* bytes by knowing the hash (mirrors the local/s3 adapters).
|
|
38
|
+
*/
|
|
39
|
+
export declare function createR2Adapter(deps: R2AdapterDeps): StorageAdapter;
|
|
40
|
+
//# sourceMappingURL=r2.d.ts.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createFile, getFileInOrg } from '@plandesk/db';
|
|
3
|
+
import { tryGetAuthContext } from '../auth-context.js';
|
|
4
|
+
import { fileUrl } from './adapter.js';
|
|
5
|
+
// Per-project object key, mirroring the (project_id, id) primary key of the
|
|
6
|
+
// `files` table: identical bytes in different projects never collide.
|
|
7
|
+
function objectKey(projectId, id) {
|
|
8
|
+
return `${projectId}/${id}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Content-addressed R2 storage via the native Workers binding (not S3 creds).
|
|
12
|
+
* Bytes live in the bucket under `{projectId}/{sha256}`; the `files` row records
|
|
13
|
+
* metadata + org linkage. `resolve` is org-scoped through `getFileInOrg` — the
|
|
14
|
+
* bare content id is never a lookup key, so one tenant cannot read another's
|
|
15
|
+
* bytes by knowing the hash (mirrors the local/s3 adapters).
|
|
16
|
+
*/
|
|
17
|
+
export function createR2Adapter(deps) {
|
|
18
|
+
const { db, bucket } = deps;
|
|
19
|
+
return {
|
|
20
|
+
async put(input) {
|
|
21
|
+
const id = createHash('sha256').update(input.bytes).digest('hex');
|
|
22
|
+
const body = new Uint8Array(input.bytes);
|
|
23
|
+
await bucket.put(objectKey(input.projectId, id), body, {
|
|
24
|
+
httpMetadata: { contentType: input.mime },
|
|
25
|
+
customMetadata: {
|
|
26
|
+
filename: input.filename,
|
|
27
|
+
mime: input.mime,
|
|
28
|
+
projectId: input.projectId,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
// Persist the metadata row so FileService.create can read it back and so
|
|
32
|
+
// resolve() can org-scope. Bytes stay in R2 (bytes: null in the DB).
|
|
33
|
+
await createFile(db, {
|
|
34
|
+
id,
|
|
35
|
+
projectId: input.projectId,
|
|
36
|
+
filename: input.filename,
|
|
37
|
+
mime: input.mime,
|
|
38
|
+
size: input.bytes.length,
|
|
39
|
+
bytes: null,
|
|
40
|
+
externalUrl: null,
|
|
41
|
+
});
|
|
42
|
+
return { id, url: fileUrl(id) };
|
|
43
|
+
},
|
|
44
|
+
async resolve(id) {
|
|
45
|
+
const auth = tryGetAuthContext();
|
|
46
|
+
if (auth === undefined || auth.kind === 'guest') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const file = await getFileInOrg(db, id, auth.orgId);
|
|
50
|
+
if (!file) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (file.externalUrl) {
|
|
54
|
+
return { redirectUrl: file.externalUrl };
|
|
55
|
+
}
|
|
56
|
+
const obj = await bucket.get(objectKey(file.projectId, id));
|
|
57
|
+
if (obj === null) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const bytes = Buffer.from(await obj.arrayBuffer());
|
|
61
|
+
return { bytes, mime: file.mime, filename: file.filename };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=r2.js.map
|
package/dist/worker.d.ts
CHANGED
|
@@ -3,10 +3,10 @@ export interface Env {
|
|
|
3
3
|
PLANDESK_DB_URL: string;
|
|
4
4
|
/** Turso/libSQL auth token — set via `wrangler secret put PLANDESK_DB_TOKEN` */
|
|
5
5
|
PLANDESK_DB_TOKEN: string;
|
|
6
|
-
PLANDESK_S3_BUCKET
|
|
7
|
-
PLANDESK_S3_REGION
|
|
8
|
-
PLANDESK_S3_ACCESS_KEY_ID
|
|
9
|
-
PLANDESK_S3_SECRET_ACCESS_KEY
|
|
6
|
+
PLANDESK_S3_BUCKET?: string;
|
|
7
|
+
PLANDESK_S3_REGION?: string;
|
|
8
|
+
PLANDESK_S3_ACCESS_KEY_ID?: string;
|
|
9
|
+
PLANDESK_S3_SECRET_ACCESS_KEY?: string;
|
|
10
10
|
PLANDESK_S3_ENDPOINT?: string;
|
|
11
11
|
PLANDESK_AUTH_PASSWORD?: string;
|
|
12
12
|
/**
|
|
@@ -32,8 +32,8 @@ export interface Env {
|
|
|
32
32
|
*/
|
|
33
33
|
PLANDESK_GITHUB_CALLBACK_URL?: string;
|
|
34
34
|
PLANDESK_DASHBOARD_URL?: string;
|
|
35
|
-
/** R2 bucket for file blobs (
|
|
36
|
-
FILES
|
|
35
|
+
/** R2 bucket for file blobs (native Workers binding — preferred over S3 creds). */
|
|
36
|
+
FILES?: R2Bucket;
|
|
37
37
|
/** Built web SPA (wrangler [assets]). */
|
|
38
38
|
ASSETS?: Fetcher;
|
|
39
39
|
}
|
package/dist/worker.js
CHANGED
|
@@ -9,19 +9,45 @@ import { createApp } from './server.js';
|
|
|
9
9
|
import { createBetterAuth } from './better-auth.js';
|
|
10
10
|
import { createServices } from './services/index.js';
|
|
11
11
|
import { githubConfigFromEnv } from './github.js';
|
|
12
|
+
import { createR2Adapter } from './storage/r2.js';
|
|
12
13
|
import { createS3Adapter } from './storage/s3.js';
|
|
13
14
|
import { hostedMisconfigResponse, resolveHostedBetterAuth } from './hosted-auth.js';
|
|
14
15
|
let cache;
|
|
15
16
|
let authCache;
|
|
16
17
|
function s3ConfigFromEnv(env) {
|
|
18
|
+
const bucket = env.PLANDESK_S3_BUCKET;
|
|
19
|
+
const region = env.PLANDESK_S3_REGION;
|
|
20
|
+
const accessKeyId = env.PLANDESK_S3_ACCESS_KEY_ID;
|
|
21
|
+
const secretAccessKey = env.PLANDESK_S3_SECRET_ACCESS_KEY;
|
|
22
|
+
if (bucket === undefined ||
|
|
23
|
+
bucket === '' ||
|
|
24
|
+
region === undefined ||
|
|
25
|
+
region === '' ||
|
|
26
|
+
accessKeyId === undefined ||
|
|
27
|
+
accessKeyId === '' ||
|
|
28
|
+
secretAccessKey === undefined ||
|
|
29
|
+
secretAccessKey === '') {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
17
32
|
return {
|
|
18
|
-
bucket
|
|
19
|
-
region
|
|
20
|
-
accessKeyId
|
|
21
|
-
secretAccessKey
|
|
33
|
+
bucket,
|
|
34
|
+
region,
|
|
35
|
+
accessKeyId,
|
|
36
|
+
secretAccessKey,
|
|
22
37
|
endpoint: env.PLANDESK_S3_ENDPOINT,
|
|
23
38
|
};
|
|
24
39
|
}
|
|
40
|
+
function resolveWorkerStorage(env, db) {
|
|
41
|
+
// Prefer the native R2 binding over S3 credentials when present.
|
|
42
|
+
if (env.FILES !== undefined) {
|
|
43
|
+
return createR2Adapter({ db, bucket: env.FILES });
|
|
44
|
+
}
|
|
45
|
+
const s3 = s3ConfigFromEnv(env);
|
|
46
|
+
if (s3 !== undefined) {
|
|
47
|
+
return createS3Adapter({ db, config: s3 });
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
25
51
|
async function getDb(env) {
|
|
26
52
|
const key = `${env.PLANDESK_DB_URL}\0${env.PLANDESK_DB_TOKEN}`;
|
|
27
53
|
if (cache !== undefined && cache.key === key) {
|
|
@@ -83,12 +109,9 @@ export default {
|
|
|
83
109
|
}
|
|
84
110
|
const db = await getDb(env);
|
|
85
111
|
const authInstance = await getBetterAuth(env, db, betterAuth);
|
|
86
|
-
// Storage is optional:
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
const storage = env.PLANDESK_S3_BUCKET !== undefined && env.PLANDESK_S3_BUCKET !== ''
|
|
90
|
-
? createS3Adapter({ db, config: s3ConfigFromEnv(env) })
|
|
91
|
-
: undefined;
|
|
112
|
+
// Storage is optional: prefer R2 binding, then S3 creds, else unavailable
|
|
113
|
+
// (file uploads/artifacts off — no crash). Mirrors `plandesk serve`.
|
|
114
|
+
const storage = resolveWorkerStorage(env, db);
|
|
92
115
|
const services = createServices({ db, ...(storage !== undefined ? { storage } : {}) });
|
|
93
116
|
const app = createApp({
|
|
94
117
|
db,
|
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.3",
|
|
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.3"
|
|
30
30
|
},
|
|
31
31
|
"description": "Plan Desk REST + SSE API and service layer (local-first planning workspace).",
|
|
32
32
|
"license": "MIT",
|