@celsoft/platform 0.4.0 → 0.5.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/adapters/cloudflare.d.ts +10 -1
- package/dist/adapters/cloudflare.js +46 -2
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/drizzle.d.ts +15 -0
- package/dist/adapters/drizzle.js +111 -0
- package/dist/adapters/drizzle.js.map +1 -0
- package/dist/adapters/github.d.ts +1 -0
- package/dist/adapters/github.js +4 -0
- package/dist/adapters/github.js.map +1 -1
- package/dist/adapters/neon.d.ts +2 -0
- package/dist/adapters/neon.js +8 -0
- package/dist/adapters/neon.js.map +1 -1
- package/dist/adapters/process.d.ts +1 -7
- package/dist/adapters/process.js +0 -22
- package/dist/adapters/process.js.map +1 -1
- package/dist/adapters/resend.d.ts +1 -0
- package/dist/adapters/resend.js +3 -0
- package/dist/adapters/resend.js.map +1 -1
- package/dist/cli.js +71 -9
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +11 -4
- package/dist/config.js +26 -9
- package/dist/config.js.map +1 -1
- package/dist/fakes.d.ts +24 -4
- package/dist/fakes.js +58 -6
- package/dist/fakes.js.map +1 -1
- package/dist/init.js +5 -5
- package/dist/init.js.map +1 -1
- package/dist/lifecycle.d.ts +7 -0
- package/dist/lifecycle.js +134 -18
- package/dist/lifecycle.js.map +1 -1
- package/dist/ownership.d.ts +16 -0
- package/dist/ownership.js +40 -0
- package/dist/ownership.js.map +1 -0
- package/dist/ports.d.ts +18 -3
- package/dist/runtime/auth.d.ts +1181 -6
- package/dist/runtime/auth.js +44 -14
- package/dist/runtime/auth.js.map +1 -1
- package/dist/runtime/files.js +4 -1
- package/dist/runtime/files.js.map +1 -1
- package/dist/runtime/guard.d.ts +12 -0
- package/dist/runtime/guard.js +42 -0
- package/dist/runtime/guard.js.map +1 -0
- package/dist/runtime/index.d.ts +18 -8
- package/dist/runtime/index.js +67 -19
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/owner.d.ts +12 -0
- package/dist/runtime/owner.js +54 -0
- package/dist/runtime/owner.js.map +1 -0
- package/dist/runtime/screens.d.ts +4 -0
- package/dist/runtime/screens.js +158 -0
- package/dist/runtime/screens.js.map +1 -0
- package/dist/schema.d.ts +2775 -0
- package/dist/schema.js +129 -0
- package/dist/schema.js.map +1 -0
- package/dist/workflow.js +15 -5
- package/dist/workflow.js.map +1 -1
- package/dist/wrangler-config.d.ts +5 -0
- package/dist/wrangler-config.js +11 -2
- package/dist/wrangler-config.js.map +1 -1
- package/package.json +10 -4
- package/schema.json +18 -14
package/dist/runtime/auth.js
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { betterAuth } from "better-auth";
|
|
2
|
-
import {
|
|
2
|
+
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
|
3
|
+
import { admin, organization } from "better-auth/plugins";
|
|
4
|
+
import { adminAc, userAc } from "better-auth/plugins/admin/access";
|
|
5
|
+
import { sql } from "drizzle-orm";
|
|
6
|
+
import { authSchema } from "../schema.js";
|
|
3
7
|
export const AUTH_PATH = "/api/auth";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
return new Pool({ connectionString, max: 1 });
|
|
8
|
-
}
|
|
9
|
-
// Shared by the Worker and by `platform deploy`, which creates the auth tables from it.
|
|
10
|
-
export function authOptions({ pool, secret, baseURL, email, requireVerification }) {
|
|
11
|
-
return {
|
|
8
|
+
export const OWNER_ROLE = "owner";
|
|
9
|
+
export function createAuth({ db, secret, baseURL, email, requireVerification }) {
|
|
10
|
+
return betterAuth({
|
|
12
11
|
secret,
|
|
13
12
|
baseURL,
|
|
14
13
|
basePath: AUTH_PATH,
|
|
15
|
-
database:
|
|
14
|
+
database: drizzleAdapter(db, { provider: "pg", schema: authSchema }),
|
|
15
|
+
// Postgres fills every id with uuidv7(), so Better Auth leaves the column out.
|
|
16
|
+
advanced: { database: { generateId: "uuid" } },
|
|
17
|
+
// The Workers rate limiting binding guards these routes before Better Auth sees them.
|
|
18
|
+
rateLimit: { enabled: false },
|
|
19
|
+
session: {
|
|
20
|
+
// Most requests read the session from a signed cookie instead of the database; a revoked
|
|
21
|
+
// session therefore keeps working until this copy expires.
|
|
22
|
+
cookieCache: { enabled: true, maxAge: 5 * 60 },
|
|
23
|
+
},
|
|
16
24
|
emailAndPassword: {
|
|
17
25
|
enabled: true,
|
|
18
26
|
requireEmailVerification: requireVerification,
|
|
@@ -27,9 +35,31 @@ export function authOptions({ pool, secret, baseURL, email, requireVerification
|
|
|
27
35
|
await email.send({ to: user.email, subject: "Confirm your email", text: `Confirm your email address:\n\n${url}` });
|
|
28
36
|
},
|
|
29
37
|
},
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
databaseHooks: {
|
|
39
|
+
user: {
|
|
40
|
+
create: {
|
|
41
|
+
// The first account becomes the owner; a single statement, so two at once cannot both win.
|
|
42
|
+
after: async (user) => {
|
|
43
|
+
await db.execute(sql `update "user" set role = ${OWNER_ROLE} where id = ${user.id} and not exists (select 1 from "user" where role = ${OWNER_ROLE})`);
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
// Always installed so its tables and types never change; the runtime hides its routes
|
|
49
|
+
// unless platform.jsonc turns organizations on.
|
|
50
|
+
plugins: [
|
|
51
|
+
admin({ defaultRole: "user", adminRoles: [OWNER_ROLE, "admin"], roles: { [OWNER_ROLE]: adminAc, admin: adminAc, user: userAc } }),
|
|
52
|
+
organization({
|
|
53
|
+
teams: { enabled: true },
|
|
54
|
+
sendInvitationEmail: async ({ email: to, organization: org, inviter, id }) => {
|
|
55
|
+
await email.send({
|
|
56
|
+
to,
|
|
57
|
+
subject: `Join ${org.name}`,
|
|
58
|
+
text: `${inviter.user.name} invited you to join ${org.name}:\n\n${baseURL}/login?invitation=${id}`,
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
],
|
|
63
|
+
});
|
|
34
64
|
}
|
|
35
65
|
//# sourceMappingURL=auth.js.map
|
package/dist/runtime/auth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/runtime/auth.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/runtime/auth.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime/files.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BUCKET_MARKER } from "../ownership.js";
|
|
1
2
|
// Paths are "folder/sub/file.json"; "/" makes folders. Previews share one bucket, so each
|
|
2
3
|
// preview's prefix is added here and never seen by app code.
|
|
3
4
|
export function createFiles(env) {
|
|
@@ -25,7 +26,9 @@ export function createFiles(env) {
|
|
|
25
26
|
let cursor;
|
|
26
27
|
do {
|
|
27
28
|
const page = await bucket().list({ prefix: base, delimiter: "/", ...(cursor && { cursor }) });
|
|
28
|
-
|
|
29
|
+
// The platform's ownership marker sits at the bucket root and is never the app's file.
|
|
30
|
+
const objects = page.objects.filter((o) => o.key !== BUCKET_MARKER);
|
|
31
|
+
result.files.push(...objects.map((o) => ({ path: o.key.slice(prefix.length), size: o.size, uploaded: o.uploaded })));
|
|
29
32
|
result.folders.push(...page.delimitedPrefixes.map((p) => p.slice(prefix.length)));
|
|
30
33
|
cursor = page.truncated ? page.cursor : undefined;
|
|
31
34
|
} while (cursor);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/runtime/files.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/runtime/files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare const PUBLIC_API = "/api/public/";
|
|
2
|
+
export declare const PLATFORM_API = "/api/platform/";
|
|
3
|
+
export declare function isCrossSiteForm(request: Request, url: URL): boolean;
|
|
4
|
+
export declare function needsSession(pathname: string): boolean;
|
|
5
|
+
export interface RateLimiter {
|
|
6
|
+
limit(options: {
|
|
7
|
+
key: string;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
success: boolean;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
export declare function rateLimited(request: Request, pathname: string, limiter: RateLimiter | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { AUTH_PATH } from "./auth.js";
|
|
2
|
+
export const PUBLIC_API = "/api/public/";
|
|
3
|
+
export const PLATFORM_API = "/api/platform/";
|
|
4
|
+
const FORM_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
|
|
5
|
+
const UNSAFE = ["POST", "PUT", "PATCH", "DELETE"];
|
|
6
|
+
// A browser sends any site's form to any other without asking, cookies included; it always
|
|
7
|
+
// sends an Origin with it, so a missing or foreign one is refused.
|
|
8
|
+
export function isCrossSiteForm(request, url) {
|
|
9
|
+
if (!UNSAFE.includes(request.method))
|
|
10
|
+
return false;
|
|
11
|
+
const type = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() ?? "";
|
|
12
|
+
if (!FORM_TYPES.includes(type))
|
|
13
|
+
return false;
|
|
14
|
+
return request.headers.get("origin") !== url.origin;
|
|
15
|
+
}
|
|
16
|
+
// Every /api route needs a signed-in user unless its path says otherwise.
|
|
17
|
+
export function needsSession(pathname) {
|
|
18
|
+
if (!pathname.startsWith("/api/"))
|
|
19
|
+
return false;
|
|
20
|
+
return !(pathname.startsWith(`${AUTH_PATH}/`) || pathname.startsWith(PUBLIC_API) || pathname.startsWith(PLATFORM_API));
|
|
21
|
+
}
|
|
22
|
+
// Guessing passwords is limited per account, signing up per address.
|
|
23
|
+
const LIMITED = {
|
|
24
|
+
[`${AUTH_PATH}/sign-in/email`]: "email",
|
|
25
|
+
[`${AUTH_PATH}/request-password-reset`]: "email",
|
|
26
|
+
[`${AUTH_PATH}/reset-password`]: "ip",
|
|
27
|
+
[`${AUTH_PATH}/sign-up/email`]: "ip",
|
|
28
|
+
[`${PLATFORM_API}recover`]: "email",
|
|
29
|
+
};
|
|
30
|
+
export async function rateLimited(request, pathname, limiter) {
|
|
31
|
+
const by = LIMITED[pathname];
|
|
32
|
+
if (!limiter || !by || request.method !== "POST")
|
|
33
|
+
return false;
|
|
34
|
+
let subject = request.headers.get("cf-connecting-ip") ?? "unknown";
|
|
35
|
+
if (by === "email") {
|
|
36
|
+
const body = (await request.clone().json().catch(() => ({})));
|
|
37
|
+
if (typeof body.email === "string")
|
|
38
|
+
subject = body.email.trim().toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
return !(await limiter.limit({ key: `${pathname}:${subject}` })).success;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guard.js","sourceRoot":"","sources":["../../src/runtime/guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,CAAC,MAAM,UAAU,GAAG,cAAc,CAAC;AACzC,MAAM,CAAC,MAAM,YAAY,GAAG,gBAAgB,CAAC;AAE7C,MAAM,UAAU,GAAG,CAAC,mCAAmC,EAAE,qBAAqB,EAAE,YAAY,CAAC,CAAC;AAC9F,MAAM,MAAM,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;AAElD,2FAA2F;AAC3F,mEAAmE;AACnE,MAAM,UAAU,eAAe,CAAC,OAAgB,EAAE,GAAQ;IACxD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACnD,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAE,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,EAAE,CAAC;IAC/F,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAC7C,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC;AACtD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,KAAK,CAAC;IAChD,OAAO,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,UAAU,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC;AACzH,CAAC;AAMD,qEAAqE;AACrE,MAAM,OAAO,GAAmC;IAC9C,CAAC,GAAG,SAAS,gBAAgB,CAAC,EAAE,OAAO;IACvC,CAAC,GAAG,SAAS,yBAAyB,CAAC,EAAE,OAAO;IAChD,CAAC,GAAG,SAAS,iBAAiB,CAAC,EAAE,IAAI;IACrC,CAAC,GAAG,SAAS,gBAAgB,CAAC,EAAE,IAAI;IACpC,CAAC,GAAG,YAAY,SAAS,CAAC,EAAE,OAAO;CACpC,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAgB,EAAE,QAAgB,EAAE,OAAgC;IACpG,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B,IAAI,CAAC,OAAO,IAAI,CAAC,EAAE,IAAI,OAAO,CAAC,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IAC/D,IAAI,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,IAAI,SAAS,CAAC;IACnE,IAAI,EAAE,KAAK,OAAO,EAAE,CAAC;QACnB,MAAM,IAAI,GAAG,CAAC,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAwB,CAAC;QACrF,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ;YAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAChF,CAAC;IACD,OAAO,CAAC,CAAC,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,GAAG,QAAQ,IAAI,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;AAC3E,CAAC"}
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -1,35 +1,45 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool } from "pg";
|
|
2
3
|
import { type Auth } from "./auth.js";
|
|
3
4
|
import { type Email, type EmailBindings } from "./email.js";
|
|
4
5
|
import { type Files, type FilesBindings } from "./files.js";
|
|
6
|
+
import { type RateLimiter } from "./guard.js";
|
|
5
7
|
export type { Auth } from "./auth.js";
|
|
6
8
|
export type { Email, EmailMessage } from "./email.js";
|
|
7
9
|
export type { FileEntry, Files, Folder, StoredObject } from "./files.js";
|
|
8
|
-
export type Sql = postgres.Sql;
|
|
9
10
|
export interface PlatformBindings extends EmailBindings, FilesBindings {
|
|
10
11
|
DB?: {
|
|
11
12
|
connectionString: string;
|
|
12
13
|
};
|
|
13
14
|
APP_URL?: string;
|
|
15
|
+
APP_NAME?: string;
|
|
14
16
|
BETTER_AUTH_SECRET?: string;
|
|
17
|
+
AUTH_ORGANIZATIONS?: string;
|
|
18
|
+
AUTH_LIMIT?: RateLimiter;
|
|
15
19
|
}
|
|
16
20
|
export interface ExecutionContext {
|
|
17
21
|
waitUntil(promise: Promise<unknown>): void;
|
|
18
22
|
}
|
|
19
23
|
export type Session = NonNullable<Awaited<ReturnType<Auth["api"]["getSession"]>>>;
|
|
20
|
-
export
|
|
24
|
+
export type User = Session["user"];
|
|
25
|
+
export interface AppContext<Env, Schema extends Record<string, unknown>> {
|
|
21
26
|
env: Env;
|
|
22
|
-
readonly
|
|
27
|
+
readonly db: NodePgDatabase<Schema>;
|
|
23
28
|
readonly email: Email;
|
|
24
29
|
readonly files: Files;
|
|
25
30
|
readonly auth: Auth;
|
|
26
31
|
session(): Promise<Session | null>;
|
|
32
|
+
requireUser(): Promise<User>;
|
|
27
33
|
waitUntil(promise: Promise<unknown>): void;
|
|
28
34
|
}
|
|
29
|
-
export type Handler<Env
|
|
30
|
-
export interface AppOptions {
|
|
31
|
-
|
|
35
|
+
export type Handler<Env, Schema extends Record<string, unknown>> = (request: Request, app: AppContext<Env, Schema>) => Response | Promise<Response>;
|
|
36
|
+
export interface AppOptions<Schema extends Record<string, unknown>> {
|
|
37
|
+
schema?: Schema;
|
|
38
|
+
screens?: {
|
|
39
|
+
stylesheet?: string;
|
|
40
|
+
};
|
|
41
|
+
pool?: (connectionString: string) => Pool;
|
|
32
42
|
}
|
|
33
|
-
export declare function defineApp<Secrets extends object = object>(handler: Handler<Secrets & PlatformBindings>, options?: AppOptions): {
|
|
43
|
+
export declare function defineApp<Secrets extends object = object, Schema extends Record<string, unknown> = Record<string, never>>(handler: Handler<Secrets & PlatformBindings, Schema>, options?: AppOptions<Schema>): {
|
|
34
44
|
fetch(request: Request, env: Secrets & PlatformBindings, ctx: ExecutionContext): Promise<Response>;
|
|
35
45
|
};
|
package/dist/runtime/index.js
CHANGED
|
@@ -1,25 +1,33 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { Pool } from "pg";
|
|
3
|
+
import { AUTH_PATH, createAuth } from "./auth.js";
|
|
3
4
|
import { createEmail } from "./email.js";
|
|
4
5
|
import { createFiles } from "./files.js";
|
|
5
|
-
|
|
6
|
+
import { isCrossSiteForm, needsSession, PLATFORM_API, rateLimited } from "./guard.js";
|
|
7
|
+
import { issueRecoveryCode, recoverOwner } from "./owner.js";
|
|
8
|
+
import { isScreen, renderScreen } from "./screens.js";
|
|
9
|
+
const defaultPool = (connectionString) => new Pool({ connectionString, max: 5 });
|
|
10
|
+
const json = (body, status = 200) => Response.json(body, { status });
|
|
6
11
|
// `Secrets` names the app's secrets, such as `defineApp<{ SESSION_SECRET: string }>`; the
|
|
7
|
-
// platform's own bindings are added to it.
|
|
12
|
+
// platform's own bindings are added to it. Pass the app's tables as `schema` for a typed `app.db`.
|
|
13
|
+
// A handler may throw a Response, as `requireUser` does, and it is sent as the answer.
|
|
8
14
|
export function defineApp(handler, options = {}) {
|
|
9
|
-
const
|
|
15
|
+
const openPool = options.pool ?? defaultPool;
|
|
10
16
|
return {
|
|
11
17
|
async fetch(request, env, ctx) {
|
|
12
|
-
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
let pool;
|
|
20
|
+
let db;
|
|
13
21
|
let email;
|
|
14
22
|
let files;
|
|
15
23
|
let auth;
|
|
16
|
-
let
|
|
24
|
+
let session;
|
|
17
25
|
const app = {
|
|
18
26
|
env,
|
|
19
|
-
get
|
|
27
|
+
get db() {
|
|
20
28
|
if (!env.DB)
|
|
21
29
|
throw new Error("this app has no database; add `database` to platform.jsonc");
|
|
22
|
-
return (
|
|
30
|
+
return (db ??= drizzle((pool ??= openPool(env.DB.connectionString)), { schema: options.schema ?? {} }));
|
|
23
31
|
},
|
|
24
32
|
get email() {
|
|
25
33
|
return (email ??= createEmail(env));
|
|
@@ -30,34 +38,74 @@ export function defineApp(handler, options = {}) {
|
|
|
30
38
|
get auth() {
|
|
31
39
|
if (!env.BETTER_AUTH_SECRET)
|
|
32
40
|
throw new Error("this app has no auth; set `auth: true` in platform.jsonc");
|
|
33
|
-
if (!env.DB)
|
|
34
|
-
throw new Error("auth needs a database; add `database` to platform.jsonc");
|
|
35
41
|
return (auth ??= createAuth({
|
|
36
|
-
|
|
42
|
+
db: app.db,
|
|
37
43
|
secret: env.BETTER_AUTH_SECRET,
|
|
38
|
-
baseURL: env.APP_URL ??
|
|
44
|
+
baseURL: env.APP_URL ?? url.origin,
|
|
39
45
|
email: app.email,
|
|
40
46
|
requireVerification: env.EMAIL_MODE === "send",
|
|
41
47
|
}));
|
|
42
48
|
},
|
|
43
|
-
|
|
44
|
-
return env.BETTER_AUTH_SECRET ? app.auth.api.getSession({ headers: request.headers }) : null;
|
|
49
|
+
session() {
|
|
50
|
+
return (session ??= env.BETTER_AUTH_SECRET ? app.auth.api.getSession({ headers: request.headers }) : Promise.resolve(null));
|
|
51
|
+
},
|
|
52
|
+
async requireUser() {
|
|
53
|
+
const current = await app.session();
|
|
54
|
+
if (!current)
|
|
55
|
+
throw json({ error: "sign in first" }, 401);
|
|
56
|
+
return current.user;
|
|
45
57
|
},
|
|
46
58
|
waitUntil: (promise) => ctx.waitUntil(promise),
|
|
47
59
|
};
|
|
48
60
|
try {
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
61
|
+
if (isCrossSiteForm(request, url))
|
|
62
|
+
return json({ error: "cross-site form submissions are refused" }, 403);
|
|
63
|
+
if (env.BETTER_AUTH_SECRET) {
|
|
64
|
+
const platform = await platformRoute(request, url, env, app, options);
|
|
65
|
+
if (platform)
|
|
66
|
+
return platform;
|
|
51
67
|
}
|
|
52
68
|
return await handler(request, app);
|
|
53
69
|
}
|
|
70
|
+
catch (thrown) {
|
|
71
|
+
if (thrown instanceof Response)
|
|
72
|
+
return thrown;
|
|
73
|
+
throw thrown;
|
|
74
|
+
}
|
|
54
75
|
finally {
|
|
55
|
-
if (sql)
|
|
56
|
-
ctx.waitUntil(sql.end({ timeout: 5 }));
|
|
57
76
|
if (pool)
|
|
58
77
|
ctx.waitUntil(pool.end());
|
|
59
78
|
}
|
|
60
79
|
},
|
|
61
80
|
};
|
|
62
81
|
}
|
|
82
|
+
// What the platform answers itself for an app with auth: screens, auth routes, owner recovery,
|
|
83
|
+
// and the sign-in check in front of every private /api route.
|
|
84
|
+
async function platformRoute(request, url, env, app, options) {
|
|
85
|
+
const path = url.pathname;
|
|
86
|
+
if (request.method === "GET" && isScreen(path))
|
|
87
|
+
return renderScreen(path, env.APP_NAME ?? url.hostname, options.screens?.stylesheet);
|
|
88
|
+
if (await rateLimited(request, path, env.AUTH_LIMIT))
|
|
89
|
+
return json({ error: "too many attempts; wait a minute" }, 429);
|
|
90
|
+
if (path.startsWith(`${AUTH_PATH}/organization/`) && env.AUTH_ORGANIZATIONS !== "on")
|
|
91
|
+
return json({ error: "not found" }, 404);
|
|
92
|
+
if (path.startsWith(`${AUTH_PATH}/`))
|
|
93
|
+
return app.auth.handler(request);
|
|
94
|
+
const db = app.db;
|
|
95
|
+
if (path === `${PLATFORM_API}recovery-code` && request.method === "POST") {
|
|
96
|
+
const user = await app.requireUser();
|
|
97
|
+
return json({ code: await issueRecoveryCode(db, user.id) });
|
|
98
|
+
}
|
|
99
|
+
if (path === `${PLATFORM_API}recover` && request.method === "POST") {
|
|
100
|
+
const body = (await request.json().catch(() => ({})));
|
|
101
|
+
const { email, code, password } = body;
|
|
102
|
+
if (typeof email !== "string" || typeof code !== "string" || typeof password !== "string" || password.length < 8) {
|
|
103
|
+
return json({ error: "email, code and a password of at least 8 characters are required" }, 400);
|
|
104
|
+
}
|
|
105
|
+
return json({ ok: await recoverOwner(db, app.auth, { email, code, password }) });
|
|
106
|
+
}
|
|
107
|
+
if (needsSession(path))
|
|
108
|
+
await app.requireUser();
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
63
111
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
2
|
+
import { type Auth } from "./auth.js";
|
|
3
|
+
export declare function newRecoveryCode(): string;
|
|
4
|
+
export declare function hashCode(code: string): Promise<string>;
|
|
5
|
+
type Db = NodePgDatabase<Record<string, unknown>>;
|
|
6
|
+
export declare function issueRecoveryCode(db: Db, userId: string): Promise<string | null>;
|
|
7
|
+
export declare function recoverOwner(db: Db, auth: Auth, input: {
|
|
8
|
+
email: string;
|
|
9
|
+
code: string;
|
|
10
|
+
password: string;
|
|
11
|
+
}): Promise<boolean>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { ownerRecovery, user } from "../schema.js";
|
|
3
|
+
import { OWNER_ROLE } from "./auth.js";
|
|
4
|
+
const ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
5
|
+
// Five groups of five from 31 characters: about 124 bits, so a plain hash is enough to store it.
|
|
6
|
+
export function newRecoveryCode() {
|
|
7
|
+
const bytes = crypto.getRandomValues(new Uint8Array(25));
|
|
8
|
+
const chars = Array.from(bytes, (b) => ALPHABET[b % ALPHABET.length]).join("");
|
|
9
|
+
return chars.match(/.{5}/g).join("-");
|
|
10
|
+
}
|
|
11
|
+
export async function hashCode(code) {
|
|
12
|
+
const normalized = code.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
13
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(normalized));
|
|
14
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
15
|
+
}
|
|
16
|
+
// Shown once, right after the owner signs in without one; the screen tells them to save it.
|
|
17
|
+
// The role is read from the database: the session's cached copy predates the owner claim.
|
|
18
|
+
export async function issueRecoveryCode(db, userId) {
|
|
19
|
+
const [row] = await db.select({ role: user.role }).from(user).where(eq(user.id, userId));
|
|
20
|
+
if (row?.role !== OWNER_ROLE)
|
|
21
|
+
return null;
|
|
22
|
+
const code = newRecoveryCode();
|
|
23
|
+
const inserted = await db
|
|
24
|
+
.insert(ownerRecovery)
|
|
25
|
+
.values({ userId, codeHash: await hashCode(code) })
|
|
26
|
+
.onConflictDoNothing()
|
|
27
|
+
.returning({ userId: ownerRecovery.userId });
|
|
28
|
+
return inserted.length > 0 ? code : null;
|
|
29
|
+
}
|
|
30
|
+
// Sets a new password for the owner and signs every session out. The answer never says which
|
|
31
|
+
// part was wrong, so it reveals nothing about who the owner is.
|
|
32
|
+
export async function recoverOwner(db, auth, input) {
|
|
33
|
+
const [owner] = await db
|
|
34
|
+
.select({ id: user.id, codeHash: ownerRecovery.codeHash })
|
|
35
|
+
.from(user)
|
|
36
|
+
.innerJoin(ownerRecovery, eq(ownerRecovery.userId, user.id))
|
|
37
|
+
.where(and(eq(user.email, input.email.trim().toLowerCase()), eq(user.role, OWNER_ROLE)));
|
|
38
|
+
if (!owner || !equal(owner.codeHash, await hashCode(input.code)))
|
|
39
|
+
return false;
|
|
40
|
+
const ctx = await auth.$context;
|
|
41
|
+
await ctx.internalAdapter.updatePassword(owner.id, await ctx.password.hash(input.password));
|
|
42
|
+
await ctx.internalAdapter.deleteUserSessions(owner.id);
|
|
43
|
+
await db.delete(ownerRecovery).where(eq(ownerRecovery.userId, owner.id));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
function equal(a, b) {
|
|
47
|
+
if (a.length !== b.length)
|
|
48
|
+
return false;
|
|
49
|
+
let diff = 0;
|
|
50
|
+
for (let i = 0; i < a.length; i++)
|
|
51
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
52
|
+
return diff === 0;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=owner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"owner.js","sourceRoot":"","sources":["../../src/runtime/owner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const SCREEN_PATHS: readonly ["/login", "/signup", "/forgot", "/reset", "/recover"];
|
|
2
|
+
export type ScreenPath = (typeof SCREEN_PATHS)[number];
|
|
3
|
+
export declare function isScreen(pathname: string): pathname is ScreenPath;
|
|
4
|
+
export declare function renderScreen(path: ScreenPath, appName: string, stylesheet: string | undefined): Response;
|