@celsoft/platform 0.4.0 → 0.6.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 +114 -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/owner.d.ts +1 -0
- package/dist/adapters/owner.js +38 -0
- package/dist/adapters/owner.js.map +1 -0
- 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 +136 -10
- 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 +11 -0
- package/dist/runtime/guard.js +40 -0
- package/dist/runtime/guard.js.map +1 -0
- package/dist/runtime/index.d.ts +18 -8
- package/dist/runtime/index.js +53 -19
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/screens.d.ts +4 -0
- package/dist/runtime/screens.js +145 -0
- package/dist/runtime/screens.js.map +1 -0
- package/dist/schema.d.ts +2717 -0
- package/dist/schema.js +120 -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,11 @@
|
|
|
1
|
+
export declare const PUBLIC_API = "/api/public/";
|
|
2
|
+
export declare function isCrossSiteForm(request: Request, url: URL): boolean;
|
|
3
|
+
export declare function needsSession(pathname: string): boolean;
|
|
4
|
+
export interface RateLimiter {
|
|
5
|
+
limit(options: {
|
|
6
|
+
key: string;
|
|
7
|
+
}): Promise<{
|
|
8
|
+
success: boolean;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function rateLimited(request: Request, pathname: string, limiter: RateLimiter | undefined): Promise<boolean>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AUTH_PATH } from "./auth.js";
|
|
2
|
+
export const PUBLIC_API = "/api/public/";
|
|
3
|
+
const FORM_TYPES = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"];
|
|
4
|
+
const UNSAFE = ["POST", "PUT", "PATCH", "DELETE"];
|
|
5
|
+
// A browser sends any site's form to any other without asking, cookies included; it always
|
|
6
|
+
// sends an Origin with it, so a missing or foreign one is refused.
|
|
7
|
+
export function isCrossSiteForm(request, url) {
|
|
8
|
+
if (!UNSAFE.includes(request.method))
|
|
9
|
+
return false;
|
|
10
|
+
const type = request.headers.get("content-type")?.split(";", 1)[0].trim().toLowerCase() ?? "";
|
|
11
|
+
if (!FORM_TYPES.includes(type))
|
|
12
|
+
return false;
|
|
13
|
+
return request.headers.get("origin") !== url.origin;
|
|
14
|
+
}
|
|
15
|
+
// Every /api route needs a signed-in user unless its path says otherwise.
|
|
16
|
+
export function needsSession(pathname) {
|
|
17
|
+
if (!pathname.startsWith("/api/"))
|
|
18
|
+
return false;
|
|
19
|
+
return !(pathname.startsWith(`${AUTH_PATH}/`) || pathname.startsWith(PUBLIC_API));
|
|
20
|
+
}
|
|
21
|
+
// Guessing passwords is limited per account, signing up per address.
|
|
22
|
+
const LIMITED = {
|
|
23
|
+
[`${AUTH_PATH}/sign-in/email`]: "email",
|
|
24
|
+
[`${AUTH_PATH}/request-password-reset`]: "email",
|
|
25
|
+
[`${AUTH_PATH}/reset-password`]: "ip",
|
|
26
|
+
[`${AUTH_PATH}/sign-up/email`]: "ip",
|
|
27
|
+
};
|
|
28
|
+
export async function rateLimited(request, pathname, limiter) {
|
|
29
|
+
const by = LIMITED[pathname];
|
|
30
|
+
if (!limiter || !by || request.method !== "POST")
|
|
31
|
+
return false;
|
|
32
|
+
let subject = request.headers.get("cf-connecting-ip") ?? "unknown";
|
|
33
|
+
if (by === "email") {
|
|
34
|
+
const body = (await request.clone().json().catch(() => ({})));
|
|
35
|
+
if (typeof body.email === "string")
|
|
36
|
+
subject = body.email.trim().toLowerCase();
|
|
37
|
+
}
|
|
38
|
+
return !(await limiter.limit({ key: `${pathname}:${subject}` })).success;
|
|
39
|
+
}
|
|
40
|
+
//# 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;AAEzC,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,CAAC,CAAC;AACpF,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;CACrC,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,32 @@
|
|
|
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, rateLimited } from "./guard.js";
|
|
7
|
+
import { isScreen, renderScreen } from "./screens.js";
|
|
8
|
+
const defaultPool = (connectionString) => new Pool({ connectionString, max: 5 });
|
|
9
|
+
const json = (body, status = 200) => Response.json(body, { status });
|
|
6
10
|
// `Secrets` names the app's secrets, such as `defineApp<{ SESSION_SECRET: string }>`; the
|
|
7
|
-
// platform's own bindings are added to it.
|
|
11
|
+
// platform's own bindings are added to it. Pass the app's tables as `schema` for a typed `app.db`.
|
|
12
|
+
// A handler may throw a Response, as `requireUser` does, and it is sent as the answer.
|
|
8
13
|
export function defineApp(handler, options = {}) {
|
|
9
|
-
const
|
|
14
|
+
const openPool = options.pool ?? defaultPool;
|
|
10
15
|
return {
|
|
11
16
|
async fetch(request, env, ctx) {
|
|
12
|
-
|
|
17
|
+
const url = new URL(request.url);
|
|
18
|
+
let pool;
|
|
19
|
+
let db;
|
|
13
20
|
let email;
|
|
14
21
|
let files;
|
|
15
22
|
let auth;
|
|
16
|
-
let
|
|
23
|
+
let session;
|
|
17
24
|
const app = {
|
|
18
25
|
env,
|
|
19
|
-
get
|
|
26
|
+
get db() {
|
|
20
27
|
if (!env.DB)
|
|
21
28
|
throw new Error("this app has no database; add `database` to platform.jsonc");
|
|
22
|
-
return (
|
|
29
|
+
return (db ??= drizzle((pool ??= openPool(env.DB.connectionString)), { schema: options.schema ?? {} }));
|
|
23
30
|
},
|
|
24
31
|
get email() {
|
|
25
32
|
return (email ??= createEmail(env));
|
|
@@ -30,34 +37,61 @@ export function defineApp(handler, options = {}) {
|
|
|
30
37
|
get auth() {
|
|
31
38
|
if (!env.BETTER_AUTH_SECRET)
|
|
32
39
|
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
40
|
return (auth ??= createAuth({
|
|
36
|
-
|
|
41
|
+
db: app.db,
|
|
37
42
|
secret: env.BETTER_AUTH_SECRET,
|
|
38
|
-
baseURL: env.APP_URL ??
|
|
43
|
+
baseURL: env.APP_URL ?? url.origin,
|
|
39
44
|
email: app.email,
|
|
40
45
|
requireVerification: env.EMAIL_MODE === "send",
|
|
41
46
|
}));
|
|
42
47
|
},
|
|
43
|
-
|
|
44
|
-
return env.BETTER_AUTH_SECRET ? app.auth.api.getSession({ headers: request.headers }) : null;
|
|
48
|
+
session() {
|
|
49
|
+
return (session ??= env.BETTER_AUTH_SECRET ? app.auth.api.getSession({ headers: request.headers }) : Promise.resolve(null));
|
|
50
|
+
},
|
|
51
|
+
async requireUser() {
|
|
52
|
+
const current = await app.session();
|
|
53
|
+
if (!current)
|
|
54
|
+
throw json({ error: "sign in first" }, 401);
|
|
55
|
+
return current.user;
|
|
45
56
|
},
|
|
46
57
|
waitUntil: (promise) => ctx.waitUntil(promise),
|
|
47
58
|
};
|
|
48
59
|
try {
|
|
49
|
-
if (
|
|
50
|
-
return
|
|
60
|
+
if (isCrossSiteForm(request, url))
|
|
61
|
+
return json({ error: "cross-site form submissions are refused" }, 403);
|
|
62
|
+
if (env.BETTER_AUTH_SECRET) {
|
|
63
|
+
const platform = await platformRoute(request, url, env, app, options);
|
|
64
|
+
if (platform)
|
|
65
|
+
return platform;
|
|
51
66
|
}
|
|
52
67
|
return await handler(request, app);
|
|
53
68
|
}
|
|
69
|
+
catch (thrown) {
|
|
70
|
+
if (thrown instanceof Response)
|
|
71
|
+
return thrown;
|
|
72
|
+
throw thrown;
|
|
73
|
+
}
|
|
54
74
|
finally {
|
|
55
|
-
if (sql)
|
|
56
|
-
ctx.waitUntil(sql.end({ timeout: 5 }));
|
|
57
75
|
if (pool)
|
|
58
76
|
ctx.waitUntil(pool.end());
|
|
59
77
|
}
|
|
60
78
|
},
|
|
61
79
|
};
|
|
62
80
|
}
|
|
81
|
+
// What the platform answers itself for an app with auth: screens, auth routes, and the sign-in
|
|
82
|
+
// check in front of every private /api route.
|
|
83
|
+
async function platformRoute(request, url, env, app, options) {
|
|
84
|
+
const path = url.pathname;
|
|
85
|
+
if (request.method === "GET" && isScreen(path))
|
|
86
|
+
return renderScreen(path, env.APP_NAME ?? url.hostname, options.screens?.stylesheet);
|
|
87
|
+
if (await rateLimited(request, path, env.AUTH_LIMIT))
|
|
88
|
+
return json({ error: "too many attempts; wait a minute" }, 429);
|
|
89
|
+
if (path.startsWith(`${AUTH_PATH}/organization/`) && env.AUTH_ORGANIZATIONS !== "on")
|
|
90
|
+
return json({ error: "not found" }, 404);
|
|
91
|
+
if (path.startsWith(`${AUTH_PATH}/`))
|
|
92
|
+
return app.auth.handler(request);
|
|
93
|
+
if (needsSession(path))
|
|
94
|
+
await app.requireUser();
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
63
97
|
//# 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":"AAAA,OAAO,EAAE,OAAO,EAAuB,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAC1B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAa,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAkC,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,WAAW,EAAkC,MAAM,YAAY,CAAC;AACzE,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,WAAW,EAAoB,MAAM,YAAY,CAAC;AAC1F,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAyCtD,MAAM,WAAW,GAAG,CAAC,gBAAwB,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,EAAE,gBAAgB,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;AAEzF,MAAM,IAAI,GAAG,CAAC,IAAa,EAAE,MAAM,GAAG,GAAG,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAE9E,0FAA0F;AAC1F,mGAAmG;AACnG,uFAAuF;AACvF,MAAM,UAAU,SAAS,CACvB,OAAoD,EACpD,OAAO,GAAuB,EAAE;IAEhC,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,IAAI,WAAW,CAAC;IAE7C,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,OAAgB,EAAE,GAA+B,EAAE,GAAqB;YAClF,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,IAAsB,CAAC;YAC3B,IAAI,EAAsC,CAAC;YAC3C,IAAI,KAAwB,CAAC;YAC7B,IAAI,KAAwB,CAAC;YAC7B,IAAI,IAAsB,CAAC;YAC3B,IAAI,OAA4C,CAAC;YAEjD,MAAM,GAAG,GAAmD;gBAC1D,GAAG;gBACH,IAAI,EAAE;oBACJ,IAAI,CAAC,GAAG,CAAC,EAAE;wBAAE,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAC;oBAC3F,OAAO,CAAC,EAAE,KAAK,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,IAAK,EAAa,EAAE,CAAC,CAAC,CAAC;gBACtH,CAAC;gBACD,IAAI,KAAK;oBACP,OAAO,CAAC,KAAK,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;gBACtC,CAAC;gBACD,IAAI,KAAK;oBACP,OAAO,CAAC,KAAK,KAAK,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC;gBACtC,CAAC;gBACD,IAAI,IAAI;oBACN,IAAI,CAAC,GAAG,CAAC,kBAAkB;wBAAE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAC;oBACzG,OAAO,CAAC,IAAI,KAAK,UAAU,CAAC;wBAC1B,EAAE,EAAE,GAAG,CAAC,EAA6C;wBACrD,MAAM,EAAE,GAAG,CAAC,kBAAkB;wBAC9B,OAAO,EAAE,GAAG,CAAC,OAAO,IAAI,GAAG,CAAC,MAAM;wBAClC,KAAK,EAAE,GAAG,CAAC,KAAK;wBAChB,mBAAmB,EAAE,GAAG,CAAC,UAAU,KAAK,MAAM;qBAC/C,CAAC,CAAC,CAAC;gBACN,CAAC;gBACD,OAAO;oBACL,OAAO,CAAC,OAAO,KAAK,GAAG,CAAC,kBAAkB,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC9H,CAAC;gBACD,KAAK,CAAC,WAAW;oBACf,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,EAAE,CAAC;oBACpC,IAAI,CAAC,OAAO;wBAAE,MAAM,IAAI,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE,GAAG,CAAC,CAAC;oBAC1D,OAAO,OAAO,CAAC,IAAI,CAAC;gBACtB,CAAC;gBACD,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC;aAC/C,CAAC;YAEF,IAAI,CAAC;gBACH,IAAI,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC;oBAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,yCAAyC,EAAE,EAAE,GAAG,CAAC,CAAC;gBAC1G,IAAI,GAAG,CAAC,kBAAkB,EAAE,CAAC;oBAC3B,MAAM,QAAQ,GAAG,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;oBACtE,IAAI,QAAQ;wBAAE,OAAO,QAAQ,CAAC;gBAChC,CAAC;gBACD,OAAO,MAAM,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YACrC,CAAC;YAAC,OAAO,MAAM,EAAE,CAAC;gBAChB,IAAI,MAAM,YAAY,QAAQ;oBAAE,OAAO,MAAM,CAAC;gBAC9C,MAAM,MAAM,CAAC;YACf,CAAC;oBAAS,CAAC;gBACT,IAAI,IAAI;oBAAE,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC;AAED,+FAA+F;AAC/F,8CAA8C;AAC9C,KAAK,UAAU,aAAa,CAC1B,OAAgB,EAChB,GAAQ,EACR,GAAQ,EACR,GAA4B,EAC5B,OAA2B;IAE3B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC;IAC1B,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,IAAI,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,YAAY,CAAC,IAAI,EAAE,GAAG,CAAC,QAAQ,IAAI,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACrI,IAAI,MAAM,WAAW,CAAC,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,UAAU,CAAC;QAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,kCAAkC,EAAE,EAAE,GAAG,CAAC,CAAC;IACtH,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,SAAS,gBAAgB,CAAC,IAAI,GAAG,CAAC,kBAAkB,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,GAAG,CAAC,CAAC;IAC/H,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,SAAS,GAAG,CAAC;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAEvE,IAAI,YAAY,CAAC,IAAI,CAAC;QAAE,MAAM,GAAG,CAAC,WAAW,EAAE,CAAC;IAChD,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const SCREEN_PATHS: readonly ["/login", "/signup", "/forgot", "/reset"];
|
|
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;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Sign-in, sign-up and password screens every app with auth gets. Class names are part of the
|
|
2
|
+
// public interface: an app restyles them with its own stylesheet, loaded after this one.
|
|
3
|
+
export const SCREEN_PATHS = ["/login", "/signup", "/forgot", "/reset"];
|
|
4
|
+
// Password managers pair a saved password with the field marked "username", even when it holds an email.
|
|
5
|
+
const email = { name: "email", label: "Email", type: "email", autocomplete: "username" };
|
|
6
|
+
const password = (autocomplete, label = "Password") => ({ name: "password", label, type: "password", autocomplete });
|
|
7
|
+
const SCREENS = {
|
|
8
|
+
"/login": {
|
|
9
|
+
title: "Sign in",
|
|
10
|
+
fields: [email, password("current-password")],
|
|
11
|
+
button: "Sign in",
|
|
12
|
+
links: [["/signup", "Create an account"], ["/forgot", "Forgot your password?"]],
|
|
13
|
+
},
|
|
14
|
+
"/signup": {
|
|
15
|
+
title: "Create an account",
|
|
16
|
+
fields: [{ name: "name", label: "Name", type: "text", autocomplete: "name" }, email, password("new-password")],
|
|
17
|
+
button: "Create account",
|
|
18
|
+
links: [["/login", "Already have an account? Sign in"]],
|
|
19
|
+
},
|
|
20
|
+
"/forgot": {
|
|
21
|
+
title: "Reset your password",
|
|
22
|
+
intro: "Enter your email and we'll send you a link to choose a new password.",
|
|
23
|
+
fields: [email],
|
|
24
|
+
button: "Send reset link",
|
|
25
|
+
links: [["/login", "Back to sign in"]],
|
|
26
|
+
},
|
|
27
|
+
"/reset": {
|
|
28
|
+
title: "Choose a new password",
|
|
29
|
+
fields: [password("new-password", "New password")],
|
|
30
|
+
button: "Change password",
|
|
31
|
+
links: [["/login", "Back to sign in"]],
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
export function isScreen(pathname) {
|
|
35
|
+
return SCREEN_PATHS.includes(pathname);
|
|
36
|
+
}
|
|
37
|
+
export function renderScreen(path, appName, stylesheet) {
|
|
38
|
+
const screen = SCREENS[path];
|
|
39
|
+
const fields = screen.fields
|
|
40
|
+
.map((f) => `<label class="platform-field"><span class="platform-label">${f.label}</span>` +
|
|
41
|
+
`<input class="platform-input" id="${f.name}" name="${f.name}" type="${f.type}" autocomplete="${f.autocomplete}" required${f.type === "password" ? ' minlength="8"' : ""}></label>`)
|
|
42
|
+
.join("");
|
|
43
|
+
const links = screen.links.map(([href, text]) => `<a href="${href}" data-carry>${text}</a>`).join("");
|
|
44
|
+
const html = `<!doctype html>
|
|
45
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
46
|
+
<title>${screen.title} · ${escape(appName)}</title>
|
|
47
|
+
<style>${STYLE}</style>${stylesheet ? `<link rel="stylesheet" href="${escape(stylesheet)}">` : ""}
|
|
48
|
+
</head><body class="platform-screen" data-page="${path.slice(1)}">
|
|
49
|
+
<main class="platform-card">
|
|
50
|
+
<p class="platform-app">${escape(appName)}</p>
|
|
51
|
+
<h1 class="platform-title">${screen.title}</h1>
|
|
52
|
+
${screen.intro ? `<p class="platform-intro">${screen.intro}</p>` : ""}
|
|
53
|
+
<form class="platform-form" method="post" action="${path}" novalidate>${path === "/reset" ? '<input id="email" name="email" type="email" autocomplete="username" hidden>' : ""}${fields}
|
|
54
|
+
<p class="platform-error" role="alert" hidden></p>
|
|
55
|
+
<button class="platform-button" type="submit">${screen.button}</button></form>
|
|
56
|
+
<div class="platform-message" role="status" hidden></div>
|
|
57
|
+
<nav class="platform-links">${links}</nav>
|
|
58
|
+
</main>
|
|
59
|
+
<script>${SCRIPT}</script>
|
|
60
|
+
</body></html>`;
|
|
61
|
+
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" } });
|
|
62
|
+
}
|
|
63
|
+
function escape(text) {
|
|
64
|
+
return text.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
65
|
+
}
|
|
66
|
+
const STYLE = `
|
|
67
|
+
:root{--platform-bg:#f4f6f8;--platform-card:#fff;--platform-fg:#1b2430;--platform-muted:#5d6a78;--platform-line:#d5dce3;--platform-accent:#2554c7;--platform-on-accent:#fff;--platform-error:#b42318;color-scheme:light dark}
|
|
68
|
+
@media (prefers-color-scheme:dark){:root{--platform-bg:#11161c;--platform-card:#19212a;--platform-fg:#e6ebf0;--platform-muted:#9aa7b4;--platform-line:#2c3742;--platform-accent:#7aa2ff;--platform-on-accent:#0b1220;--platform-error:#ff8a80}}
|
|
69
|
+
*{box-sizing:border-box}
|
|
70
|
+
[hidden]{display:none!important}
|
|
71
|
+
body.platform-screen{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px 16px;background:var(--platform-bg);color:var(--platform-fg);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
|
|
72
|
+
.platform-card{width:100%;max-width:380px;background:var(--platform-card);border:1px solid var(--platform-line);border-radius:12px;padding:28px 24px;display:flex;flex-direction:column;gap:16px}
|
|
73
|
+
.platform-app{margin:0;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--platform-muted)}
|
|
74
|
+
.platform-title{margin:0;font-size:24px;line-height:1.2}
|
|
75
|
+
.platform-intro{margin:0;color:var(--platform-muted)}
|
|
76
|
+
.platform-form{display:flex;flex-direction:column;gap:14px}
|
|
77
|
+
.platform-field{display:flex;flex-direction:column;gap:6px;font-size:14px;font-weight:600}
|
|
78
|
+
.platform-input{font:inherit;font-weight:400;padding:10px 12px;border:1px solid var(--platform-line);border-radius:8px;background:var(--platform-bg);color:inherit}
|
|
79
|
+
.platform-input:focus-visible,.platform-button:focus-visible,.platform-links a:focus-visible{outline:2px solid var(--platform-accent);outline-offset:2px}
|
|
80
|
+
.platform-button{font:inherit;font-weight:600;padding:11px 14px;border:0;border-radius:8px;background:var(--platform-accent);color:var(--platform-on-accent);cursor:pointer}
|
|
81
|
+
.platform-button[disabled]{opacity:.6;cursor:progress}
|
|
82
|
+
.platform-error{margin:0;color:var(--platform-error);font-size:14px}
|
|
83
|
+
.platform-message{display:flex;flex-direction:column;gap:12px}
|
|
84
|
+
.platform-message p{margin:0}
|
|
85
|
+
.platform-code{font:600 18px/1.4 ui-monospace,Menlo,monospace;letter-spacing:.04em;padding:12px;border:1px dashed var(--platform-line);border-radius:8px;text-align:center;word-break:break-all;user-select:all}
|
|
86
|
+
.platform-links{display:flex;flex-direction:column;gap:6px;font-size:14px}
|
|
87
|
+
.platform-links a{color:var(--platform-accent)}
|
|
88
|
+
`;
|
|
89
|
+
const SCRIPT = `
|
|
90
|
+
const page = document.body.dataset.page;
|
|
91
|
+
const params = new URLSearchParams(location.search);
|
|
92
|
+
const next = (p => p && p.startsWith("/") && !p.startsWith("//") ? p : "/")(params.get("next"));
|
|
93
|
+
const carried = new URLSearchParams([...params].filter(([k]) => k === "next" || k === "invitation")).toString();
|
|
94
|
+
for (const a of document.querySelectorAll("[data-carry]")) if (carried) a.href += "?" + carried;
|
|
95
|
+
const form = document.querySelector(".platform-form");
|
|
96
|
+
const error = document.querySelector(".platform-error");
|
|
97
|
+
const message = document.querySelector(".platform-message");
|
|
98
|
+
|
|
99
|
+
async function post(path, body) {
|
|
100
|
+
const r = await fetch(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), credentials: "same-origin" });
|
|
101
|
+
const data = await r.json().catch(() => ({}));
|
|
102
|
+
if (!r.ok) throw new Error(r.status === 429 ? "Too many attempts. Wait a minute, then try again." : data.message || "That didn't work. Try again.");
|
|
103
|
+
return data || {};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function say(...parts) {
|
|
107
|
+
form.hidden = true;
|
|
108
|
+
message.replaceChildren(...parts.map(p => typeof p === "string" ? Object.assign(document.createElement("p"), { textContent: p }) : p));
|
|
109
|
+
message.hidden = false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function finish() {
|
|
113
|
+
const invitation = params.get("invitation");
|
|
114
|
+
if (invitation) await post("/api/auth/organization/accept-invitation", { invitationId: invitation }).catch(() => {});
|
|
115
|
+
location.assign(next);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const actions = {
|
|
119
|
+
login: async v => { await post("/api/auth/sign-in/email", { email: v.email, password: v.password }); await finish(); },
|
|
120
|
+
signup: async v => {
|
|
121
|
+
const data = await post("/api/auth/sign-up/email", { name: v.name, email: v.email, password: v.password, callbackURL: "/login" + (carried ? "?" + carried : "") });
|
|
122
|
+
if (data.token) return finish();
|
|
123
|
+
say("Check your email for a link to confirm your address. It signs you in.");
|
|
124
|
+
},
|
|
125
|
+
forgot: async v => { await post("/api/auth/request-password-reset", { email: v.email, redirectTo: "/reset?email=" + encodeURIComponent(v.email) }); say("If that address has an account, a reset link is on its way."); },
|
|
126
|
+
reset: async v => { await post("/api/auth/reset-password", { newPassword: v.password, token: params.get("token") }); say("Your password is changed. Sign in with it now."); },
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
form.addEventListener("submit", async e => {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
error.hidden = true;
|
|
132
|
+
if (!form.reportValidity()) return;
|
|
133
|
+
const button = form.querySelector("button");
|
|
134
|
+
button.disabled = true;
|
|
135
|
+
try { await actions[page](Object.fromEntries(new FormData(form))); }
|
|
136
|
+
catch (err) { error.textContent = err.message; error.hidden = false; }
|
|
137
|
+
finally { button.disabled = false; }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// The reset link carries the email, so a password manager knows which saved login to update.
|
|
141
|
+
if (page === "reset") document.getElementById("email").value = params.get("email") || "";
|
|
142
|
+
if (page === "reset" && (params.get("error") || !params.get("token"))) say("That reset link has expired or was already used. Ask for a new one from the sign-in page.");
|
|
143
|
+
if (page === "login") fetch("/api/auth/get-session", { credentials: "same-origin" }).then(r => r.json()).then(s => s && s.session && finish()).catch(() => {});
|
|
144
|
+
`;
|
|
145
|
+
//# sourceMappingURL=screens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screens.js","sourceRoot":"","sources":["../../src/runtime/screens.ts"],"names":[],"mappings":"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"}
|