@celsoft/platform 0.5.0 → 0.7.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/drizzle.js +11 -8
- package/dist/adapters/drizzle.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 +3 -1
- package/dist/adapters/process.js +13 -1
- package/dist/adapters/process.js.map +1 -1
- package/dist/cli.js +67 -3
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/runtime/guard.d.ts +0 -1
- package/dist/runtime/guard.js +2 -3
- package/dist/runtime/guard.js.map +1 -1
- package/dist/runtime/index.d.ts +16 -2
- package/dist/runtime/index.js +90 -61
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/live-client.d.ts +19 -0
- package/dist/runtime/live-client.js +74 -0
- package/dist/runtime/live-client.js.map +1 -0
- package/dist/runtime/live.d.ts +29 -0
- package/dist/runtime/live.js +158 -0
- package/dist/runtime/live.js.map +1 -0
- package/dist/runtime/screens.d.ts +1 -1
- package/dist/runtime/screens.js +10 -23
- package/dist/runtime/screens.js.map +1 -1
- package/dist/schema.d.ts +0 -58
- package/dist/schema.js +0 -9
- package/dist/schema.js.map +1 -1
- package/dist/wrangler-config.d.ts +2 -0
- package/dist/wrangler-config.js +12 -2
- package/dist/wrangler-config.js.map +1 -1
- package/package.json +3 -2
- package/schema.json +5 -0
- package/dist/runtime/owner.d.ts +0 -12
- package/dist/runtime/owner.js +0 -54
- package/dist/runtime/owner.js.map +0 -1
package/dist/runtime/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { Pool } from "pg";
|
|
|
3
3
|
import { AUTH_PATH, createAuth } from "./auth.js";
|
|
4
4
|
import { createEmail } from "./email.js";
|
|
5
5
|
import { createFiles } from "./files.js";
|
|
6
|
-
import { isCrossSiteForm, needsSession,
|
|
7
|
-
import {
|
|
6
|
+
import { isCrossSiteForm, needsSession, rateLimited } from "./guard.js";
|
|
7
|
+
import { CLIENT_PATH, clientScript, createLive, joinRoom, LIVE_PATH, liveRoom } from "./live.js";
|
|
8
8
|
import { isScreen, renderScreen } from "./screens.js";
|
|
9
9
|
const defaultPool = (connectionString) => new Pool({ connectionString, max: 5 });
|
|
10
10
|
const json = (body, status = 200) => Response.json(body, { status });
|
|
@@ -12,54 +12,17 @@ const json = (body, status = 200) => Response.json(body, { status });
|
|
|
12
12
|
// platform's own bindings are added to it. Pass the app's tables as `schema` for a typed `app.db`.
|
|
13
13
|
// A handler may throw a Response, as `requireUser` does, and it is sent as the answer.
|
|
14
14
|
export function defineApp(handler, options = {}) {
|
|
15
|
-
const
|
|
15
|
+
const onMessage = options.live?.onMessage;
|
|
16
16
|
return {
|
|
17
17
|
async fetch(request, env, ctx) {
|
|
18
18
|
const url = new URL(request.url);
|
|
19
|
-
|
|
20
|
-
let db;
|
|
21
|
-
let email;
|
|
22
|
-
let files;
|
|
23
|
-
let auth;
|
|
24
|
-
let session;
|
|
25
|
-
const app = {
|
|
26
|
-
env,
|
|
27
|
-
get db() {
|
|
28
|
-
if (!env.DB)
|
|
29
|
-
throw new Error("this app has no database; add `database` to platform.jsonc");
|
|
30
|
-
return (db ??= drizzle((pool ??= openPool(env.DB.connectionString)), { schema: options.schema ?? {} }));
|
|
31
|
-
},
|
|
32
|
-
get email() {
|
|
33
|
-
return (email ??= createEmail(env));
|
|
34
|
-
},
|
|
35
|
-
get files() {
|
|
36
|
-
return (files ??= createFiles(env));
|
|
37
|
-
},
|
|
38
|
-
get auth() {
|
|
39
|
-
if (!env.BETTER_AUTH_SECRET)
|
|
40
|
-
throw new Error("this app has no auth; set `auth: true` in platform.jsonc");
|
|
41
|
-
return (auth ??= createAuth({
|
|
42
|
-
db: app.db,
|
|
43
|
-
secret: env.BETTER_AUTH_SECRET,
|
|
44
|
-
baseURL: env.APP_URL ?? url.origin,
|
|
45
|
-
email: app.email,
|
|
46
|
-
requireVerification: env.EMAIL_MODE === "send",
|
|
47
|
-
}));
|
|
48
|
-
},
|
|
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;
|
|
57
|
-
},
|
|
58
|
-
waitUntil: (promise) => ctx.waitUntil(promise),
|
|
59
|
-
};
|
|
19
|
+
const { app, close } = createContext(request, env, ctx, options);
|
|
60
20
|
try {
|
|
61
21
|
if (isCrossSiteForm(request, url))
|
|
62
22
|
return json({ error: "cross-site form submissions are refused" }, 403);
|
|
23
|
+
const live = await liveRoute(request, url, env, app, options);
|
|
24
|
+
if (live)
|
|
25
|
+
return live;
|
|
63
26
|
if (env.BETTER_AUTH_SECRET) {
|
|
64
27
|
const platform = await platformRoute(request, url, env, app, options);
|
|
65
28
|
if (platform)
|
|
@@ -73,14 +36,93 @@ export function defineApp(handler, options = {}) {
|
|
|
73
36
|
throw thrown;
|
|
74
37
|
}
|
|
75
38
|
finally {
|
|
76
|
-
|
|
77
|
-
ctx.waitUntil(pool.end());
|
|
39
|
+
close();
|
|
78
40
|
}
|
|
79
41
|
},
|
|
42
|
+
// The Durable Object class behind live rooms, exported by the deploy's generated entry module.
|
|
43
|
+
// A browser's message reaches the app's onMessage with the same `app` a request gets.
|
|
44
|
+
LiveRoom: liveRoom((rawEnv) => {
|
|
45
|
+
if (!onMessage)
|
|
46
|
+
return undefined;
|
|
47
|
+
const env = rawEnv;
|
|
48
|
+
return async (room, message, from) => {
|
|
49
|
+
const pending = [];
|
|
50
|
+
const { app, close } = createContext(new Request(env.APP_URL ?? "https://app.invalid/"), env, { waitUntil: (p) => void pending.push(p) }, options);
|
|
51
|
+
try {
|
|
52
|
+
await onMessage(room, message, from, app);
|
|
53
|
+
await Promise.allSettled(pending);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
close();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function createContext(request, env, ctx, options) {
|
|
63
|
+
const openPool = options.pool ?? defaultPool;
|
|
64
|
+
let pool;
|
|
65
|
+
let db;
|
|
66
|
+
let email;
|
|
67
|
+
let files;
|
|
68
|
+
let auth;
|
|
69
|
+
let live;
|
|
70
|
+
let session;
|
|
71
|
+
const app = {
|
|
72
|
+
env,
|
|
73
|
+
get db() {
|
|
74
|
+
if (!env.DB)
|
|
75
|
+
throw new Error("this app has no database; add `database` to platform.jsonc");
|
|
76
|
+
return (db ??= drizzle((pool ??= openPool(env.DB.connectionString)), { schema: options.schema ?? {} }));
|
|
77
|
+
},
|
|
78
|
+
get email() {
|
|
79
|
+
return (email ??= createEmail(env));
|
|
80
|
+
},
|
|
81
|
+
get files() {
|
|
82
|
+
return (files ??= createFiles(env));
|
|
83
|
+
},
|
|
84
|
+
get auth() {
|
|
85
|
+
if (!env.BETTER_AUTH_SECRET)
|
|
86
|
+
throw new Error("this app has no auth; set `auth: true` in platform.jsonc");
|
|
87
|
+
return (auth ??= createAuth({
|
|
88
|
+
db: app.db,
|
|
89
|
+
secret: env.BETTER_AUTH_SECRET,
|
|
90
|
+
baseURL: env.APP_URL ?? new URL(request.url).origin,
|
|
91
|
+
email: app.email,
|
|
92
|
+
requireVerification: env.EMAIL_MODE === "send",
|
|
93
|
+
}));
|
|
94
|
+
},
|
|
95
|
+
get live() {
|
|
96
|
+
return (live ??= createLive(env.LIVE, env.LIVE_ROOMS === "on"));
|
|
97
|
+
},
|
|
98
|
+
session() {
|
|
99
|
+
return (session ??= env.BETTER_AUTH_SECRET ? app.auth.api.getSession({ headers: request.headers }) : Promise.resolve(null));
|
|
100
|
+
},
|
|
101
|
+
async requireUser() {
|
|
102
|
+
const current = await app.session();
|
|
103
|
+
if (!current)
|
|
104
|
+
throw json({ error: "sign in first" }, 401);
|
|
105
|
+
return current.user;
|
|
106
|
+
},
|
|
107
|
+
waitUntil: (promise) => ctx.waitUntil(promise),
|
|
80
108
|
};
|
|
109
|
+
return { app, close: () => void (pool && ctx.waitUntil(pool.end())) };
|
|
110
|
+
}
|
|
111
|
+
// Live rooms answer for themselves: the browser client, and joining a room once the app agrees.
|
|
112
|
+
async function liveRoute(request, url, env, app, options) {
|
|
113
|
+
if (!url.pathname.startsWith(LIVE_PATH) || env.LIVE_ROOMS !== "on" || !env.LIVE)
|
|
114
|
+
return undefined;
|
|
115
|
+
if (url.pathname === CLIENT_PATH)
|
|
116
|
+
return clientScript();
|
|
117
|
+
return joinRoom(request, url, env.LIVE, async (room) => {
|
|
118
|
+
const user = (await app.session())?.user ?? null;
|
|
119
|
+
const canJoin = (await options.live?.canJoin?.(room, user, app)) ?? false;
|
|
120
|
+
const canSend = canJoin && ((await options.live?.canSend?.(room, user, app)) ?? false);
|
|
121
|
+
return { user: user && { id: user.id, name: user.name }, canJoin, canSend };
|
|
122
|
+
});
|
|
81
123
|
}
|
|
82
|
-
// What the platform answers itself for an app with auth: screens, auth routes,
|
|
83
|
-
//
|
|
124
|
+
// What the platform answers itself for an app with auth: screens, auth routes, and the sign-in
|
|
125
|
+
// check in front of every private /api route.
|
|
84
126
|
async function platformRoute(request, url, env, app, options) {
|
|
85
127
|
const path = url.pathname;
|
|
86
128
|
if (request.method === "GET" && isScreen(path))
|
|
@@ -91,19 +133,6 @@ async function platformRoute(request, url, env, app, options) {
|
|
|
91
133
|
return json({ error: "not found" }, 404);
|
|
92
134
|
if (path.startsWith(`${AUTH_PATH}/`))
|
|
93
135
|
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
136
|
if (needsSession(path))
|
|
108
137
|
await app.requireUser();
|
|
109
138
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/runtime/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface LivePeer {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface LiveHandlers {
|
|
6
|
+
onMessage?(data: unknown, meta: {
|
|
7
|
+
from: LivePeer | null;
|
|
8
|
+
seq: number;
|
|
9
|
+
}): void;
|
|
10
|
+
onPresence?(people: LivePeer[], anonymous: number): void;
|
|
11
|
+
onStatus?(status: "open" | "closed" | "refused"): void;
|
|
12
|
+
onError?(error: string): void;
|
|
13
|
+
onReload?(): void;
|
|
14
|
+
}
|
|
15
|
+
export interface LiveConnection {
|
|
16
|
+
send(data: unknown): boolean;
|
|
17
|
+
close(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare function live(room: string, handlers?: LiveHandlers): LiveConnection;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The browser side of live rooms. It has no imports and closes over nothing, so the runtime
|
|
2
|
+
// can also serve it as a script with `live.toString()`, for pages that have no bundler.
|
|
3
|
+
export function live(room, handlers = {}) {
|
|
4
|
+
let socket;
|
|
5
|
+
let last;
|
|
6
|
+
let stopped = false;
|
|
7
|
+
let attempts = 0;
|
|
8
|
+
const open = () => {
|
|
9
|
+
const url = new URL(`/api/live/${encodeURIComponent(room)}`, location.href);
|
|
10
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
11
|
+
if (last !== undefined)
|
|
12
|
+
url.searchParams.set("since", String(last));
|
|
13
|
+
const current = new WebSocket(url);
|
|
14
|
+
socket = current;
|
|
15
|
+
current.onopen = () => {
|
|
16
|
+
attempts = 0;
|
|
17
|
+
handlers.onStatus?.("open");
|
|
18
|
+
};
|
|
19
|
+
current.onmessage = (event) => {
|
|
20
|
+
const frame = JSON.parse(String(event.data));
|
|
21
|
+
if (frame.type === "hello") {
|
|
22
|
+
if (last === undefined)
|
|
23
|
+
last = frame.seq;
|
|
24
|
+
}
|
|
25
|
+
else if (frame.type === "message") {
|
|
26
|
+
last = frame.seq;
|
|
27
|
+
handlers.onMessage?.(frame.data, { from: frame.from, seq: frame.seq });
|
|
28
|
+
}
|
|
29
|
+
else if (frame.type === "ack") {
|
|
30
|
+
last = Math.max(last ?? 0, frame.seq);
|
|
31
|
+
}
|
|
32
|
+
else if (frame.type === "presence") {
|
|
33
|
+
handlers.onPresence?.(frame.people, frame.anonymous);
|
|
34
|
+
}
|
|
35
|
+
else if (frame.type === "reload") {
|
|
36
|
+
last = undefined;
|
|
37
|
+
if (handlers.onReload)
|
|
38
|
+
handlers.onReload();
|
|
39
|
+
else
|
|
40
|
+
location.reload();
|
|
41
|
+
}
|
|
42
|
+
else if (frame.type === "error") {
|
|
43
|
+
handlers.onError?.(frame.error);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
current.onclose = (event) => {
|
|
47
|
+
if (socket !== current)
|
|
48
|
+
return;
|
|
49
|
+
// 4403 and 4404 mean the app refused this room; trying again would only be refused again.
|
|
50
|
+
if (event.code === 4403 || event.code === 4404) {
|
|
51
|
+
stopped = true;
|
|
52
|
+
handlers.onStatus?.("refused");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
handlers.onStatus?.("closed");
|
|
56
|
+
if (!stopped)
|
|
57
|
+
setTimeout(open, Math.min(15000, 500 * 2 ** attempts++));
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
open();
|
|
61
|
+
return {
|
|
62
|
+
send(data) {
|
|
63
|
+
if (socket?.readyState !== 1)
|
|
64
|
+
return false;
|
|
65
|
+
socket.send(JSON.stringify({ data }));
|
|
66
|
+
return true;
|
|
67
|
+
},
|
|
68
|
+
close() {
|
|
69
|
+
stopped = true;
|
|
70
|
+
socket?.close();
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=live-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live-client.js","sourceRoot":"","sources":["../../src/runtime/live-client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { LivePeer } from "./live-client.js";
|
|
2
|
+
export declare const LIVE_PATH = "/api/live/";
|
|
3
|
+
export declare const CLIENT_PATH = "/api/live/client.js";
|
|
4
|
+
export declare const MAX_MESSAGE: number;
|
|
5
|
+
export declare const KEPT_MESSAGES = 500;
|
|
6
|
+
export interface LiveNamespace {
|
|
7
|
+
idFromName(name: string): unknown;
|
|
8
|
+
get(id: unknown): {
|
|
9
|
+
fetch(request: Request): Promise<Response>;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface Live {
|
|
13
|
+
publish(room: string, message: unknown): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare const clientScript: () => Response;
|
|
16
|
+
export declare function createLive(namespace: LiveNamespace | undefined, enabled: boolean): Live;
|
|
17
|
+
export interface JoinDecision {
|
|
18
|
+
user: LivePeer | null;
|
|
19
|
+
canJoin: boolean;
|
|
20
|
+
canSend: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function joinRoom(request: Request, url: URL, namespace: LiveNamespace, decide: (room: string) => Promise<JoinDecision>): Promise<Response>;
|
|
23
|
+
export type OnMessage = (room: string, message: unknown, from: LivePeer | null) => Promise<void>;
|
|
24
|
+
export interface LiveRoomClass {
|
|
25
|
+
new (state: unknown, env: unknown): {
|
|
26
|
+
fetch(request: Request): Promise<Response>;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare function liveRoom(onMessage: (env: unknown) => OnMessage | undefined): LiveRoomClass;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { live as client } from "./live-client.js";
|
|
2
|
+
// Live rooms: one Durable Object per room name, holding the open sockets, a numbered window of
|
|
3
|
+
// recent messages for catching up, and who is connected. Rooms carry messages only; what they
|
|
4
|
+
// mean and whether anything is saved is the app's business.
|
|
5
|
+
export const LIVE_PATH = "/api/live/";
|
|
6
|
+
export const CLIENT_PATH = `${LIVE_PATH}client.js`;
|
|
7
|
+
export const MAX_MESSAGE = 64 * 1024;
|
|
8
|
+
export const KEPT_MESSAGES = 500;
|
|
9
|
+
const ROOM_NAME = /^[A-Za-z0-9:_.-]{1,200}$/;
|
|
10
|
+
export const clientScript = () => new Response(`export ${client.toString()}\n`, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } });
|
|
11
|
+
const roomStub = (namespace, room) => namespace.get(namespace.idFromName(room));
|
|
12
|
+
export function createLive(namespace, enabled) {
|
|
13
|
+
return {
|
|
14
|
+
async publish(room, message) {
|
|
15
|
+
if (!enabled || !namespace)
|
|
16
|
+
throw new Error("this app has no live rooms; set `live: true` in platform.jsonc");
|
|
17
|
+
if (!ROOM_NAME.test(room))
|
|
18
|
+
throw new Error(`invalid room name ${room}`);
|
|
19
|
+
const body = JSON.stringify(message);
|
|
20
|
+
if (body.length > MAX_MESSAGE)
|
|
21
|
+
throw new Error(`live messages are limited to ${MAX_MESSAGE} bytes`);
|
|
22
|
+
const response = await roomStub(namespace, room).fetch(new Request("https://room/publish", { method: "POST", headers: { "x-room": room }, body }));
|
|
23
|
+
if (!response.ok)
|
|
24
|
+
throw new Error(`publishing to ${room} failed: ${response.status}`);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// A refusal is sent as a closed socket with a reason, so the browser client knows to stop retrying.
|
|
29
|
+
function refuse(code, reason) {
|
|
30
|
+
const pair = new WebSocketPair();
|
|
31
|
+
const socket = pair[1];
|
|
32
|
+
socket.accept();
|
|
33
|
+
socket.close(code, reason);
|
|
34
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
35
|
+
}
|
|
36
|
+
// The Worker decides who joins and who may send; the room trusts what it is told, since only
|
|
37
|
+
// the Worker can reach it.
|
|
38
|
+
export async function joinRoom(request, url, namespace, decide) {
|
|
39
|
+
if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
|
|
40
|
+
return new Response("expected a WebSocket", { status: 426 });
|
|
41
|
+
const room = decodeURIComponent(url.pathname.slice(LIVE_PATH.length));
|
|
42
|
+
if (!ROOM_NAME.test(room))
|
|
43
|
+
return refuse(4404, "no such room");
|
|
44
|
+
const { user, canJoin, canSend } = await decide(room);
|
|
45
|
+
if (!canJoin)
|
|
46
|
+
return refuse(4403, "not allowed to join");
|
|
47
|
+
const headers = new Headers(request.headers);
|
|
48
|
+
headers.set("x-room", room);
|
|
49
|
+
headers.set("x-attachment", JSON.stringify({ room, user, canSend }));
|
|
50
|
+
const since = url.searchParams.get("since");
|
|
51
|
+
return roomStub(namespace, room).fetch(new Request(`https://room/join${since ? `?since=${encodeURIComponent(since)}` : ""}`, { headers }));
|
|
52
|
+
}
|
|
53
|
+
export function liveRoom(onMessage) {
|
|
54
|
+
return class LiveRoom {
|
|
55
|
+
env;
|
|
56
|
+
state;
|
|
57
|
+
constructor(state, env) {
|
|
58
|
+
this.env = env;
|
|
59
|
+
this.state = state;
|
|
60
|
+
this.state.storage.sql.exec("create table if not exists messages (seq integer primary key autoincrement, body text not null, sender text not null)");
|
|
61
|
+
}
|
|
62
|
+
async fetch(request) {
|
|
63
|
+
const url = new URL(request.url);
|
|
64
|
+
if (url.pathname === "/publish") {
|
|
65
|
+
this.deliver(await request.text(), null);
|
|
66
|
+
return new Response(null, { status: 204 });
|
|
67
|
+
}
|
|
68
|
+
const attachment = JSON.parse(request.headers.get("x-attachment") ?? "null");
|
|
69
|
+
const pair = new WebSocketPair();
|
|
70
|
+
const socket = pair[1];
|
|
71
|
+
this.state.acceptWebSocket(socket);
|
|
72
|
+
socket.serializeAttachment(attachment);
|
|
73
|
+
socket.send(JSON.stringify({ type: "hello", seq: this.latest() }));
|
|
74
|
+
const since = url.searchParams.get("since");
|
|
75
|
+
if (since !== null)
|
|
76
|
+
this.catchUp(socket, Number(since));
|
|
77
|
+
this.presence();
|
|
78
|
+
return new Response(null, { status: 101, webSocket: pair[0] });
|
|
79
|
+
}
|
|
80
|
+
async webSocketMessage(socket, raw) {
|
|
81
|
+
const attachment = socket.deserializeAttachment();
|
|
82
|
+
if (!attachment.canSend)
|
|
83
|
+
return socket.send(JSON.stringify({ type: "error", error: "not allowed to send to this room" }));
|
|
84
|
+
if (typeof raw !== "string" || raw.length > MAX_MESSAGE)
|
|
85
|
+
return socket.send(JSON.stringify({ type: "error", error: `messages are text up to ${MAX_MESSAGE} bytes` }));
|
|
86
|
+
let data;
|
|
87
|
+
try {
|
|
88
|
+
data = JSON.parse(raw).data;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return socket.send(JSON.stringify({ type: "error", error: "messages are JSON" }));
|
|
92
|
+
}
|
|
93
|
+
const seq = this.deliver(JSON.stringify(data ?? null), attachment.user, socket);
|
|
94
|
+
socket.send(JSON.stringify({ type: "ack", seq }));
|
|
95
|
+
const hook = onMessage(this.env);
|
|
96
|
+
if (hook)
|
|
97
|
+
this.state.waitUntil(hook(attachment.room, data, attachment.user));
|
|
98
|
+
}
|
|
99
|
+
webSocketClose(socket, code, reason) {
|
|
100
|
+
try {
|
|
101
|
+
socket.close(code, reason);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// already closed
|
|
105
|
+
}
|
|
106
|
+
this.presence(socket);
|
|
107
|
+
}
|
|
108
|
+
webSocketError(socket) {
|
|
109
|
+
this.presence(socket);
|
|
110
|
+
}
|
|
111
|
+
latest() {
|
|
112
|
+
return Number(this.state.storage.sql.exec("select coalesce(max(seq), 0) as seq from messages").toArray()[0].seq);
|
|
113
|
+
}
|
|
114
|
+
// A browser gone longer than the kept window can only be told to reload.
|
|
115
|
+
catchUp(socket, since) {
|
|
116
|
+
const oldest = Number(this.state.storage.sql.exec("select coalesce(min(seq), 0) as seq from messages").toArray()[0].seq);
|
|
117
|
+
if (since + 1 < oldest)
|
|
118
|
+
return socket.send(JSON.stringify({ type: "reload" }));
|
|
119
|
+
for (const row of this.state.storage.sql.exec("select seq, body, sender from messages where seq > ? order by seq", since).toArray()) {
|
|
120
|
+
socket.send(`{"type":"message","seq":${row.seq},"from":${row.sender},"data":${row.body}}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
deliver(body, from, except) {
|
|
124
|
+
const sender = JSON.stringify(from);
|
|
125
|
+
const seq = Number(this.state.storage.sql.exec("insert into messages (body, sender) values (?, ?) returning seq", body, sender).toArray()[0].seq);
|
|
126
|
+
this.state.storage.sql.exec("delete from messages where seq <= ?", seq - KEPT_MESSAGES);
|
|
127
|
+
const frame = `{"type":"message","seq":${seq},"from":${sender},"data":${body}}`;
|
|
128
|
+
for (const socket of this.state.getWebSockets())
|
|
129
|
+
if (socket !== except)
|
|
130
|
+
trySend(socket, frame);
|
|
131
|
+
return seq;
|
|
132
|
+
}
|
|
133
|
+
presence(leaving) {
|
|
134
|
+
const sockets = this.state.getWebSockets().filter((s) => s !== leaving);
|
|
135
|
+
const people = new Map();
|
|
136
|
+
let anonymous = 0;
|
|
137
|
+
for (const socket of sockets) {
|
|
138
|
+
const { user } = socket.deserializeAttachment();
|
|
139
|
+
if (user)
|
|
140
|
+
people.set(user.id, user);
|
|
141
|
+
else
|
|
142
|
+
anonymous++;
|
|
143
|
+
}
|
|
144
|
+
const frame = JSON.stringify({ type: "presence", people: [...people.values()], anonymous });
|
|
145
|
+
for (const socket of sockets)
|
|
146
|
+
trySend(socket, frame);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function trySend(socket, frame) {
|
|
151
|
+
try {
|
|
152
|
+
socket.send(frame);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// the socket closed between listing and sending; its close handler updates presence
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=live.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"live.js","sourceRoot":"","sources":["../../src/runtime/live.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SCREEN_PATHS: readonly ["/login", "/signup", "/forgot", "/reset"
|
|
1
|
+
export declare const SCREEN_PATHS: readonly ["/login", "/signup", "/forgot", "/reset"];
|
|
2
2
|
export type ScreenPath = (typeof SCREEN_PATHS)[number];
|
|
3
3
|
export declare function isScreen(pathname: string): pathname is ScreenPath;
|
|
4
4
|
export declare function renderScreen(path: ScreenPath, appName: string, stylesheet: string | undefined): Response;
|
package/dist/runtime/screens.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// Sign-in, sign-up and password screens every app with auth gets. Class names are part of the
|
|
2
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
|
-
|
|
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" };
|
|
5
6
|
const password = (autocomplete, label = "Password") => ({ name: "password", label, type: "password", autocomplete });
|
|
6
7
|
const SCREENS = {
|
|
7
8
|
"/login": {
|
|
@@ -29,13 +30,6 @@ const SCREENS = {
|
|
|
29
30
|
button: "Change password",
|
|
30
31
|
links: [["/login", "Back to sign in"]],
|
|
31
32
|
},
|
|
32
|
-
"/recover": {
|
|
33
|
-
title: "Recover the owner account",
|
|
34
|
-
intro: "Use the recovery code you saved when you first signed in. It works once.",
|
|
35
|
-
fields: [email, { name: "code", label: "Recovery code", type: "text", autocomplete: "one-time-code" }, password("new-password", "New password")],
|
|
36
|
-
button: "Reset owner password",
|
|
37
|
-
links: [["/login", "Back to sign in"]],
|
|
38
|
-
},
|
|
39
33
|
};
|
|
40
34
|
export function isScreen(pathname) {
|
|
41
35
|
return SCREEN_PATHS.includes(pathname);
|
|
@@ -44,7 +38,7 @@ export function renderScreen(path, appName, stylesheet) {
|
|
|
44
38
|
const screen = SCREENS[path];
|
|
45
39
|
const fields = screen.fields
|
|
46
40
|
.map((f) => `<label class="platform-field"><span class="platform-label">${f.label}</span>` +
|
|
47
|
-
`<input class="platform-input" name="${f.name}" type="${f.type}" autocomplete="${f.autocomplete}" required${f.type === "password" ? ' minlength="8"' : ""}></label>`)
|
|
41
|
+
`<input class="platform-input" id="${f.name}" name="${f.name}" type="${f.type}" autocomplete="${f.autocomplete}" required${f.type === "password" ? ' minlength="8"' : ""}></label>`)
|
|
48
42
|
.join("");
|
|
49
43
|
const links = screen.links.map(([href, text]) => `<a href="${href}" data-carry>${text}</a>`).join("");
|
|
50
44
|
const html = `<!doctype html>
|
|
@@ -56,7 +50,7 @@ export function renderScreen(path, appName, stylesheet) {
|
|
|
56
50
|
<p class="platform-app">${escape(appName)}</p>
|
|
57
51
|
<h1 class="platform-title">${screen.title}</h1>
|
|
58
52
|
${screen.intro ? `<p class="platform-intro">${screen.intro}</p>` : ""}
|
|
59
|
-
<form class="platform-form" novalidate>${fields}
|
|
53
|
+
<form class="platform-form" method="post" action="${path}" novalidate>${path === "/reset" ? '<input id="email" name="email" type="email" autocomplete="username" hidden>' : ""}${fields}
|
|
60
54
|
<p class="platform-error" role="alert" hidden></p>
|
|
61
55
|
<button class="platform-button" type="submit">${screen.button}</button></form>
|
|
62
56
|
<div class="platform-message" role="status" hidden></div>
|
|
@@ -73,6 +67,7 @@ const STYLE = `
|
|
|
73
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}
|
|
74
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}}
|
|
75
69
|
*{box-sizing:border-box}
|
|
70
|
+
[hidden]{display:none!important}
|
|
76
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}
|
|
77
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}
|
|
78
73
|
.platform-app{margin:0;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--platform-muted)}
|
|
@@ -117,12 +112,7 @@ function say(...parts) {
|
|
|
117
112
|
async function finish() {
|
|
118
113
|
const invitation = params.get("invitation");
|
|
119
114
|
if (invitation) await post("/api/auth/organization/accept-invitation", { invitationId: invitation }).catch(() => {});
|
|
120
|
-
|
|
121
|
-
if (!code) return location.assign(next);
|
|
122
|
-
const box = Object.assign(document.createElement("p"), { className: "platform-code", textContent: code });
|
|
123
|
-
const go = Object.assign(document.createElement("button"), { className: "platform-button", type: "button", textContent: "I saved it, continue" });
|
|
124
|
-
go.onclick = () => location.assign(next);
|
|
125
|
-
say("You're the owner of this app. Save this recovery code somewhere safe: it resets your password if you ever lose access to your email, and it is shown only once.", box, go);
|
|
115
|
+
location.assign(next);
|
|
126
116
|
}
|
|
127
117
|
|
|
128
118
|
const actions = {
|
|
@@ -132,13 +122,8 @@ const actions = {
|
|
|
132
122
|
if (data.token) return finish();
|
|
133
123
|
say("Check your email for a link to confirm your address. It signs you in.");
|
|
134
124
|
},
|
|
135
|
-
forgot: async v => { await post("/api/auth/request-password-reset", { email: v.email, redirectTo: "/reset" }); say("If that address has an account, a reset link is on its way."); },
|
|
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."); },
|
|
136
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."); },
|
|
137
|
-
recover: async v => {
|
|
138
|
-
const { ok } = await post("/api/platform/recover", { email: v.email, code: v.code, password: v.password });
|
|
139
|
-
if (!ok) throw new Error("That email and recovery code don't match.");
|
|
140
|
-
say("The owner password is changed and every session is signed out. Sign in with the new password; you'll get a new recovery code.");
|
|
141
|
-
},
|
|
142
127
|
};
|
|
143
128
|
|
|
144
129
|
form.addEventListener("submit", async e => {
|
|
@@ -152,6 +137,8 @@ form.addEventListener("submit", async e => {
|
|
|
152
137
|
finally { button.disabled = false; }
|
|
153
138
|
});
|
|
154
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") || "";
|
|
155
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.");
|
|
156
143
|
if (page === "login") fetch("/api/auth/get-session", { credentials: "same-origin" }).then(r => r.json()).then(s => s && s.session && finish()).catch(() => {});
|
|
157
144
|
`;
|