security update + make easter egg optional.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Browser TypeScript
|
||||
|
||||
Browser source lives here and is bundled into `public/assets/generated/` by
|
||||
`bun run build:web`. Generated assets are ignored by Git and must not be edited
|
||||
directly.
|
||||
|
||||
The development supervisor runs the browser build once and watches this folder.
|
||||
Production and E2E preparation also run the build before serving assets.
|
||||
|
||||
Migrate existing `public/assets/*.js` modules incrementally:
|
||||
|
||||
1. Move one leaf/shared module into this directory and add it as a browser
|
||||
entrypoint in `package.json`.
|
||||
2. Keep its public export names stable and update consumers to import from
|
||||
`/assets/generated/` (or the equivalent relative path).
|
||||
3. Add runtime schemas for data crossing HTTP, WebSocket, storage, or plugin
|
||||
boundaries; TypeScript types alone do not validate external data.
|
||||
4. Run `bun run typecheck`, `bun run build:web`, and the affected browser tests.
|
||||
|
||||
Do not duplicate or replace the generated wire-protocol types here. Wire changes
|
||||
must continue through `protocol/wire-contract.json` and its generator.
|
||||
@@ -0,0 +1,72 @@
|
||||
import { object, record, safeParse, string, unknown } from "valibot";
|
||||
|
||||
export type SharedUiSettings = Record<string, unknown>;
|
||||
|
||||
const sharedUiSettingsResponseSchema = object({
|
||||
settings: record(string(), unknown()),
|
||||
});
|
||||
|
||||
export async function loadSharedUiSettings(scope: string): Promise<SharedUiSettings> {
|
||||
try {
|
||||
const res = await fetch(`/api/ui-settings/${encodeURIComponent(scope)}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
if (!res.ok) return {};
|
||||
|
||||
const input: unknown = await res.json().catch(() => null);
|
||||
const result = safeParse(sharedUiSettingsResponseSchema, input);
|
||||
return result.success ? result.output.settings : {};
|
||||
} catch (err) {
|
||||
console.warn(`ui-settings: failed to load ${scope}`, err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export function createSharedUiSettingsSaver(
|
||||
scope: string,
|
||||
readSettings: () => SharedUiSettings | null | undefined,
|
||||
delayMs = 350,
|
||||
) {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let lastPayload = "";
|
||||
|
||||
async function saveNow(): Promise<void> {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
|
||||
let settings: SharedUiSettings;
|
||||
try {
|
||||
settings = readSettings() ?? {};
|
||||
} catch (err) {
|
||||
console.warn(`ui-settings: failed to read ${scope}`, err);
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = JSON.stringify(settings);
|
||||
if (payload === lastPayload) return;
|
||||
lastPayload = payload;
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/ui-settings/${encodeURIComponent(scope)}`, {
|
||||
method: "PUT",
|
||||
credentials: "include",
|
||||
keepalive: true,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ settings }),
|
||||
});
|
||||
if (!res.ok) lastPayload = "";
|
||||
} catch (err) {
|
||||
lastPayload = "";
|
||||
console.warn(`ui-settings: failed to save ${scope}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSave(): void {
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(saveNow, delayMs);
|
||||
}
|
||||
|
||||
return { scheduleSave, saveNow };
|
||||
}
|
||||
Reference in New Issue
Block a user