feat: simplify asset serving and enhance bootstrap response handling

This commit is contained in:
shuaiplus
2026-03-19 00:52:58 +08:00
parent facd0ea5f7
commit 5ff322d809
5 changed files with 43 additions and 43 deletions
+1 -30
View File
@@ -4,7 +4,6 @@ import { handleRequest } from './router';
import { StorageService } from './services/storage'; import { StorageService } from './services/storage';
import { applyCors, jsonResponse } from './utils/response'; import { applyCors, jsonResponse } from './utils/response';
import { runScheduledBackupIfDue } from './handlers/backup'; import { runScheduledBackupIfDue } from './handlers/backup';
import { buildWebBootstrapResponse } from './router-public';
let dbInitialized = false; let dbInitialized = false;
let dbInitError: string | null = null; let dbInitError: string | null = null;
@@ -23,41 +22,13 @@ function isWorkerHandledPath(path: string): boolean {
); );
} }
function injectBootstrapIntoHtml(html: string, env: Env): string {
const payload = JSON.stringify(buildWebBootstrapResponse(env)).replace(/</g, '\\u003c');
const script = `<script>window.__NW_BOOT__=${payload};</script>`;
if (html.includes('</head>')) {
return html.replace('</head>', `${script}</head>`);
}
return `${script}${html}`;
}
function responseStatusCannotHaveBody(status: number): boolean {
return status === 101 || status === 204 || status === 205 || status === 304;
}
async function maybeServeAsset(request: Request, env: Env): Promise<Response | null> { async function maybeServeAsset(request: Request, env: Env): Promise<Response | null> {
if (!env.ASSETS) return null; if (!env.ASSETS) return null;
if (request.method !== 'GET' && request.method !== 'HEAD') return null; if (request.method !== 'GET' && request.method !== 'HEAD') return null;
const url = new URL(request.url); const url = new URL(request.url);
if (isWorkerHandledPath(url.pathname)) return null; if (isWorkerHandledPath(url.pathname)) return null;
const assetResponse = await env.ASSETS.fetch(request); return env.ASSETS.fetch(request);
const contentType = String(assetResponse.headers.get('Content-Type') || '').toLowerCase();
if (
request.method === 'GET' &&
contentType.includes('text/html') &&
!responseStatusCannotHaveBody(assetResponse.status)
) {
const html = await assetResponse.text();
const injected = injectBootstrapIntoHtml(html, env);
return new Response(injected, {
status: assetResponse.status,
statusText: assetResponse.statusText,
headers: assetResponse.headers,
});
}
return assetResponse;
} }
async function ensureDatabaseInitialized(env: Env): Promise<void> { async function ensureDatabaseInitialized(env: Env): Promise<void> {
+6
View File
@@ -175,6 +175,12 @@ export async function handlePublicRoute(
}); });
} }
if ((path === '/api/web-bootstrap' || path === '/web-bootstrap') && method === 'GET') {
const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute);
if (blocked) return blocked;
return jsonResponse(buildWebBootstrapResponse(env));
}
const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i); const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i);
if (iconMatch && method === 'GET') { if (iconMatch && method === 'GET') {
return handleWebsiteIcon(iconMatch[1]); return handleWebsiteIcon(iconMatch[1]);
+34 -11
View File
@@ -92,6 +92,35 @@ function readWindowBootstrap(): WebBootstrapResponse {
return raw && typeof raw === 'object' ? raw : {}; return raw && typeof raw === 'object' ? raw : {};
} }
function normalizeBootstrapResponse(boot: WebBootstrapResponse): Pick<InitialAppBootstrapState, 'defaultKdfIterations' | 'jwtWarning'> {
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
const jwtUnsafeReason = boot.jwtUnsafeReason || null;
const jwtWarning = jwtUnsafeReason
? {
reason: jwtUnsafeReason,
minLength: Number(boot.jwtSecretMinLength || 32),
}
: null;
return {
defaultKdfIterations,
jwtWarning,
};
}
async function fetchBootstrapConfig(): Promise<WebBootstrapResponse> {
try {
const resp = await fetch('/api/web-bootstrap', {
method: 'GET',
headers: { Accept: 'application/json' },
});
if (!resp.ok) return {};
return ((await resp.json()) as WebBootstrapResponse) || {};
} catch {
return {};
}
}
interface AccessTokenClaims { interface AccessTokenClaims {
sub?: string; sub?: string;
email?: string; email?: string;
@@ -129,15 +158,7 @@ function buildTransientProfile(token: TokenSuccess, email: string): Profile {
} }
export function readInitialAppBootstrapState(): InitialAppBootstrapState { export function readInitialAppBootstrapState(): InitialAppBootstrapState {
const boot = readWindowBootstrap(); const { defaultKdfIterations, jwtWarning } = normalizeBootstrapResponse(readWindowBootstrap());
const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000);
const jwtUnsafeReason = boot.jwtUnsafeReason || null;
const jwtWarning = jwtUnsafeReason
? {
reason: jwtUnsafeReason,
minLength: Number(boot.jwtSecretMinLength || 32),
}
: null;
const session = loadSession(); const session = loadSession();
const hasInviteCode = !!readInviteCodeFromUrl(); const hasInviteCode = !!readInviteCodeFromUrl();
@@ -150,8 +171,10 @@ export function readInitialAppBootstrapState(): InitialAppBootstrapState {
} }
export async function bootstrapAppSession(initial: InitialAppBootstrapState = readInitialAppBootstrapState()): Promise<BootstrapAppResult> { export async function bootstrapAppSession(initial: InitialAppBootstrapState = readInitialAppBootstrapState()): Promise<BootstrapAppResult> {
const defaultKdfIterations = initial.defaultKdfIterations; const remoteBoot = await fetchBootstrapConfig();
const jwtWarning = initial.jwtWarning; const normalizedBoot = normalizeBootstrapResponse(remoteBoot);
const defaultKdfIterations = normalizedBoot.defaultKdfIterations || initial.defaultKdfIterations;
const jwtWarning = normalizedBoot.jwtWarning ?? initial.jwtWarning;
if (jwtWarning) { if (jwtWarning) {
return { return {
+1 -1
View File
@@ -18,7 +18,7 @@ enabled = false
binding = "ASSETS" binding = "ASSETS"
directory = "./dist" directory = "./dist"
not_found_handling = "single-page-application" not_found_handling = "single-page-application"
run_worker_first = true run_worker_first = false
[build] [build]
command = "npm run build" command = "npm run build"
+1 -1
View File
@@ -18,7 +18,7 @@ enabled = false
binding = "ASSETS" binding = "ASSETS"
directory = "./dist" directory = "./dist"
not_found_handling = "single-page-application" not_found_handling = "single-page-application"
run_worker_first = true run_worker_first = false
[build] [build]
command = "npm run build" command = "npm run build"