diff --git a/src/handlers/setup.ts b/src/handlers/setup.ts deleted file mode 100644 index b799903..0000000 --- a/src/handlers/setup.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Env } from '../types'; -import { StorageService } from '../services/storage'; -import { jsonResponse } from '../utils/response'; - -// GET /setup/status -export async function handleSetupStatus(request: Request, env: Env): Promise { - void request; - const storage = new StorageService(env.DB); - const registered = (await storage.isRegistered()) || (await storage.getUserCount()) > 0; - return jsonResponse({ registered }); -} diff --git a/src/index.ts b/src/index.ts index 069ba21..73f53f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,11 +4,54 @@ import { handleRequest } from './router'; import { StorageService } from './services/storage'; import { applyCors, jsonResponse } from './utils/response'; import { runScheduledBackupIfDue, seedDefaultBackupSettings } from './handlers/backup'; +import { buildWebBootstrapResponse } from './router-public'; let dbInitialized = false; let dbInitError: string | null = null; let dbInitPromise: Promise | null = null; +function isWorkerHandledPath(path: string): boolean { + return ( + path.startsWith('/api/') || + path.startsWith('/identity/') || + path.startsWith('/icons/') || + path.startsWith('/notifications/') || + path.startsWith('/.well-known/') || + path === '/config' || + path === '/api/config' || + path === '/api/version' + ); +} + +function injectBootstrapIntoHtml(html: string, env: Env): string { + const payload = JSON.stringify(buildWebBootstrapResponse(env)).replace(/window.__NW_BOOT__=${payload};`; + if (html.includes('')) { + return html.replace('', `${script}`); + } + return `${script}${html}`; +} + +async function maybeServeAsset(request: Request, env: Env): Promise { + if (!env.ASSETS) return null; + if (request.method !== 'GET' && request.method !== 'HEAD') return null; + const url = new URL(request.url); + if (isWorkerHandledPath(url.pathname)) return null; + + const assetResponse = await env.ASSETS.fetch(request); + const contentType = String(assetResponse.headers.get('Content-Type') || '').toLowerCase(); + if (request.method === 'GET' && contentType.includes('text/html')) { + 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 { if (dbInitialized) return; @@ -35,6 +78,11 @@ async function ensureDatabaseInitialized(env: Env): Promise { export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { void ctx; + const assetResponse = await maybeServeAsset(request, env); + if (assetResponse) { + return applyCors(request, assetResponse); + } + await ensureDatabaseInitialized(env); if (dbInitError) { // Log full error server-side, return generic message to client. diff --git a/src/router-public.ts b/src/router-public.ts index b602007..561ab5b 100644 --- a/src/router-public.ts +++ b/src/router-public.ts @@ -7,7 +7,6 @@ import { handleAccessSendFileV2, handleDownloadSendFile, } from './handlers/sends'; -import { handleSetupStatus } from './handlers/setup'; import { handleKnownDevice } from './handlers/devices'; import { handleToken, handlePrelogin, handleRevocation } from './handlers/identity'; import { @@ -23,6 +22,13 @@ import { jsonResponse } from './utils/response'; import type { Env } from './types'; type PublicRateLimiter = (category?: string, maxRequests?: number) => Promise; +type JwtUnsafeReason = 'missing' | 'default' | 'too_short' | null; + +export interface WebBootstrapResponse { + defaultKdfIterations: number; + jwtUnsafeReason: JwtUnsafeReason; + jwtSecretMinLength: number; +} function isSameOriginWriteRequest(request: Request): boolean { const targetOrigin = new URL(request.url).origin; @@ -111,7 +117,7 @@ async function handleWebsiteIcon(host: string): Promise { } } -export function buildWebConfigResponse(env: Env, origin: string) { +export function buildWebBootstrapResponse(env: Env): WebBootstrapResponse { const secret = (env.JWT_SECRET || '').trim(); const jwtUnsafeReason = !secret @@ -126,9 +132,6 @@ export function buildWebConfigResponse(env: Env, origin: string) { defaultKdfIterations: LIMITS.auth.defaultKdfIterations, jwtUnsafeReason, jwtSecretMinLength: LIMITS.auth.jwtSecretMinLength, - _icon_service_url: buildIconServiceTemplate(origin), - _icon_service_csp: buildIconServiceCsp(origin), - iconServiceUrl: buildIconServiceTemplate(origin), }; } @@ -139,18 +142,6 @@ export async function handlePublicRoute( method: string, enforcePublicRateLimit: PublicRateLimiter ): Promise { - if (path === '/setup/status' && method === 'GET') { - const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); - if (blocked) return blocked; - return handleSetupStatus(request, env); - } - - if (path === '/api/web/config' && method === 'GET') { - const blocked = await enforcePublicRateLimit('public-read', LIMITS.rateLimit.publicReadRequestsPerMinute); - if (blocked) return blocked; - return jsonResponse(buildWebConfigResponse(env, new URL(request.url).origin)); - } - if (path === '/.well-known/appspecific/com.chrome.devtools.json' && method === 'GET') { return new Response('{}', { status: 200, @@ -161,10 +152,6 @@ export async function handlePublicRoute( }); } - if ((path === '/favicon.ico' || path === '/favicon.svg') && method === 'GET') { - return handleNwFavicon(); - } - const iconMatch = path.match(/^\/icons\/([^/]+)\/icon\.png$/i); if (iconMatch && method === 'GET') { return handleWebsiteIcon(iconMatch[1]); diff --git a/src/types/index.ts b/src/types/index.ts index 5ffdc2d..26b58e8 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,6 +2,9 @@ export interface Env { DB: D1Database; NOTIFICATIONS_HUB: DurableObjectNamespace; + ASSETS?: { + fetch(input: RequestInfo | URL, init?: RequestInit): Promise; + }; // Prefer R2 when available. Optional to support KV-only deployments. ATTACHMENTS?: R2Bucket; // Optional fallback for attachment/send file storage (no credit card required). diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 38880a2..936efca 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -32,6 +32,7 @@ import { } from '@/lib/app-support'; import { bootstrapAppSession, + readInitialAppBootstrapState, performPasswordLogin, performRecoverTwoFactorLogin, performRegistration, @@ -60,14 +61,15 @@ const SIGNALR_UPDATE_TYPE_LOG_OUT = 11; const SIGNALR_UPDATE_TYPE_DEVICE_STATUS = 12; export default function App() { + const initialBootstrap = useMemo(() => readInitialAppBootstrapState(), []); + const initialInviteCode = useMemo(() => readInviteCodeFromUrl(), []); const [pendingAuthAction, setPendingAuthAction] = useState<'login' | 'register' | 'unlock' | null>(null); const [location, navigate] = useLocation(); - const [phase, setPhase] = useState('loading'); - const [session, setSessionState] = useState(null); + const [phase, setPhase] = useState(initialBootstrap.phase); + const [session, setSessionState] = useState(initialBootstrap.session); const [profile, setProfile] = useState(null); - const [defaultKdfIterations, setDefaultKdfIterations] = useState(600000); - const [setupRegistered, setSetupRegistered] = useState(true); - const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(null); + const [defaultKdfIterations, setDefaultKdfIterations] = useState(initialBootstrap.defaultKdfIterations); + const [jwtWarning, setJwtWarning] = useState<{ reason: JwtUnsafeReason; minLength: number } | null>(initialBootstrap.jwtWarning); const [loginValues, setLoginValues] = useState({ email: '', password: '' }); const [registerValues, setRegisterValues] = useState({ @@ -75,9 +77,9 @@ export default function App() { email: '', password: '', password2: '', - inviteCode: '', + inviteCode: initialInviteCode, }); - const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(''); + const [inviteCodeFromUrl, setInviteCodeFromUrl] = useState(initialInviteCode); const [unlockPassword, setUnlockPassword] = useState(''); const [pendingTotp, setPendingTotp] = useState(null); const [totpCode, setTotpCode] = useState(''); @@ -128,7 +130,7 @@ export default function App() { useEffect(() => { if (!inviteCodeFromUrl) return; - if (phase === 'loading' || phase === 'locked' || phase === 'app') return; + if (phase === 'locked' || phase === 'app') return; setPhase('register'); if (location !== '/register') navigate('/register'); if (typeof window !== 'undefined' && typeof window.history?.replaceState === 'function') { @@ -163,11 +165,11 @@ export default function App() { setSession(next); if (!next) { setProfile(null); - setPhase(setupRegistered ? 'login' : 'register'); + setPhase('login'); } } ), - [session, setupRegistered] + [session] ); const importAuthedFetch = useMemo( () => async (input: string, init?: RequestInit) => { @@ -196,7 +198,6 @@ export default function App() { (async () => { const boot = await bootstrapAppSession(); if (!mounted) return; - setSetupRegistered(boot.setupRegistered); setDefaultKdfIterations(boot.defaultKdfIterations); setJwtWarning(boot.jwtWarning); setSession(boot.session); @@ -363,8 +364,8 @@ export default function App() { setSession(null); setProfile(null); setPendingTotp(null); - setPhase(setupRegistered ? 'login' : 'register'); - navigate(setupRegistered ? '/login' : '/register'); + setPhase('login'); + navigate('/login'); } function handleLogout() { @@ -951,21 +952,13 @@ export default function App() { ); } - if (phase === 'loading') { - return ( - <> -
{t('txt_loading_nodewarden')}
- {renderPassiveOverlays()} - - ); - } - if (phase === 'register' || phase === 'login' || phase === 'locked') { return ( <> - diff --git a/webapp/src/components/JwtWarningPage.tsx b/webapp/src/components/JwtWarningPage.tsx index 760c561..bab852b 100644 --- a/webapp/src/components/JwtWarningPage.tsx +++ b/webapp/src/components/JwtWarningPage.tsx @@ -9,6 +9,9 @@ interface JwtWarningPageProps { minLength: number; } +const CLOUDFLARE_SETTINGS_URL = + 'https://dash.cloudflare.com/?to=/:account/workers/services/view/nodewarden/production/settings'; + export default function JwtWarningPage(props: JwtWarningPageProps) { const [seed, setSeed] = useState(0); const [copyHint, setCopyHint] = useState(''); @@ -25,7 +28,8 @@ export default function JwtWarningPage(props: JwtWarningPageProps) { const isMissing = props.reason === 'missing'; const fixTitle = isMissing ? t('txt_jwt_how_to_fix_add') : t('txt_jwt_how_to_fix_replace'); const fixStep1 = isMissing ? t('txt_jwt_add_step_1') : t('txt_jwt_replace_step_1', { min: props.minLength }); - const fixStep2 = isMissing ? t('txt_jwt_add_step_2') : t('txt_jwt_replace_step_2'); + const fixStep2Prefix = isMissing ? t('txt_jwt_add_step_2_prefix') : t('txt_jwt_replace_step_2_prefix'); + const fixStep2Suffix = isMissing ? t('txt_jwt_add_step_2_suffix') : t('txt_jwt_replace_step_2_suffix'); const fixStep3 = isMissing ? t('txt_jwt_add_step_3') : t('txt_jwt_replace_step_3'); return ( @@ -37,10 +41,38 @@ export default function JwtWarningPage(props: JwtWarningPageProps) {
+
{t('txt_jwt_what_is')}
+

{t('txt_jwt_what_is_body')}

+
{fixTitle}
  1. {fixStep1}
  2. -
  3. {fixStep2}
  4. +
  5. + {fixStep2Prefix} + + {t('txt_settings')} + + {fixStep2Suffix} +
    +
    + {t('txt_jwt_secret_type_label')} + {t('txt_jwt_secret_type_value')} +
    +
    + {t('txt_jwt_secret_name_label')} + JWT_SECRET +
    +
    + {t('txt_jwt_secret_value_label')} + {t('txt_jwt_secret_value_requirement', { min: props.minLength })} +
    +
    +
  6. {fixStep3}
diff --git a/webapp/src/lib/api/auth.ts b/webapp/src/lib/api/auth.ts index 94e0353..94895b4 100644 --- a/webapp/src/lib/api/auth.ts +++ b/webapp/src/lib/api/auth.ts @@ -4,10 +4,8 @@ import type { AuthorizedDevice } from '../types'; import type { Profile, SessionState, - SetupStatusResponse, TokenError, TokenSuccess, - WebConfigResponse, } from '../types'; import { parseJson, type AuthedFetch, type SessionSetter } from './shared'; @@ -93,17 +91,6 @@ export function saveSession(session: SessionState | null): void { localStorage.setItem(SESSION_KEY, JSON.stringify(persisted)); } -export async function getSetupStatus(): Promise { - const resp = await fetch('/setup/status'); - const body = await parseJson(resp); - return { registered: !!body?.registered }; -} - -export async function getWebConfig(): Promise { - const resp = await fetch('/api/web/config'); - return (await parseJson(resp)) || {}; -} - export function getCurrentDeviceIdentifier(): string { return (localStorage.getItem(DEVICE_IDENTIFIER_KEY) || '').trim(); } diff --git a/webapp/src/lib/app-auth.ts b/webapp/src/lib/app-auth.ts index 8c1941e..dce2cda 100644 --- a/webapp/src/lib/app-auth.ts +++ b/webapp/src/lib/app-auth.ts @@ -2,8 +2,6 @@ import { createAuthedFetch, deriveLoginHash, getProfile, - getSetupStatus, - getWebConfig, loadSession, loginWithPassword, refreshAccessToken, @@ -11,7 +9,8 @@ import { registerAccount, unlockVaultKey, } from '@/lib/api/auth'; -import type { AppPhase, Profile, SessionState } from '@/lib/types'; +import { readInviteCodeFromUrl } from '@/lib/app-support'; +import type { AppPhase, Profile, SessionState, WebBootstrapResponse } from '@/lib/types'; export interface PendingTotp { email: string; @@ -22,7 +21,6 @@ export interface PendingTotp { export type JwtUnsafeReason = 'missing' | 'default' | 'too_short'; export interface BootstrapAppResult { - setupRegistered: boolean; defaultKdfIterations: number; jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null; session: SessionState | null; @@ -30,6 +28,13 @@ export interface BootstrapAppResult { phase: AppPhase; } +export interface InitialAppBootstrapState { + defaultKdfIterations: number; + jwtWarning: { reason: JwtUnsafeReason; minLength: number } | null; + session: SessionState | null; + phase: AppPhase; +} + export interface CompletedLogin { session: SessionState; profile: Profile; @@ -80,35 +85,56 @@ async function maybeRefreshSession(session: SessionState): Promise { - const [setup, config] = await Promise.all([getSetupStatus(), getWebConfig()]); - const setupRegistered = setup.registered; - const defaultKdfIterations = Number(config.defaultKdfIterations || 600000); - const jwtUnsafeReason = config.jwtUnsafeReason || null; +function readWindowBootstrap(): WebBootstrapResponse { + if (typeof window === 'undefined') return {}; + const raw = (window as Window & { __NW_BOOT__?: WebBootstrapResponse }).__NW_BOOT__; + return raw && typeof raw === 'object' ? raw : {}; +} - if (jwtUnsafeReason) { - return { - setupRegistered, - defaultKdfIterations, - jwtWarning: { +export function readInitialAppBootstrapState(): InitialAppBootstrapState { + const boot = readWindowBootstrap(); + const defaultKdfIterations = Number(boot.defaultKdfIterations || 600000); + const jwtUnsafeReason = boot.jwtUnsafeReason || null; + const jwtWarning = jwtUnsafeReason + ? { reason: jwtUnsafeReason, - minLength: Number(config.jwtSecretMinLength || 32), - }, + minLength: Number(boot.jwtSecretMinLength || 32), + } + : null; + const session = loadSession(); + const hasInviteCode = !!readInviteCodeFromUrl(); + + return { + defaultKdfIterations, + jwtWarning, + session, + phase: jwtWarning ? 'login' : session ? 'locked' : hasInviteCode ? 'register' : 'login', + }; +} + +export async function bootstrapAppSession(): Promise { + const initial = readInitialAppBootstrapState(); + const defaultKdfIterations = initial.defaultKdfIterations; + const jwtWarning = initial.jwtWarning; + + if (jwtWarning) { + return { + defaultKdfIterations, + jwtWarning, session: null, profile: null, phase: 'login', }; } - const loaded = loadSession(); + const loaded = initial.session; if (!loaded) { return { - setupRegistered, defaultKdfIterations, jwtWarning: null, session: null, profile: null, - phase: setupRegistered ? 'login' : 'register', + phase: initial.phase, }; } @@ -124,7 +150,6 @@ export async function bootstrapAppSession(): Promise { ) ); return { - setupRegistered, defaultKdfIterations, jwtWarning: null, session, @@ -133,12 +158,11 @@ export async function bootstrapAppSession(): Promise { }; } catch { return { - setupRegistered, defaultKdfIterations, jwtWarning: null, session: null, profile: null, - phase: setupRegistered ? 'login' : 'register', + phase: initial.phase === 'register' ? 'register' : 'login', }; } } diff --git a/webapp/src/lib/i18n.ts b/webapp/src/lib/i18n.ts index 5f280d2..ff5fa4d 100644 --- a/webapp/src/lib/i18n.ts +++ b/webapp/src/lib/i18n.ts @@ -374,11 +374,20 @@ const messages: Record> = { txt_jwt_how_to_fix_add: "How to add JWT_SECRET", txt_jwt_how_to_fix_replace: "How to replace JWT_SECRET", txt_jwt_add_step_1: "Use the 32-character generator below and copy a new key.", - txt_jwt_add_step_2: "Cloudflare Dashboard -> Workers & Pages -> Your Service -> Settings -> Variables and Secrets, add JWT_SECRET.", + txt_jwt_add_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ", + txt_jwt_add_step_2_suffix: " -> Variables and Secrets -> Add", txt_jwt_add_step_3: "Save and wait for redeploy, then refresh this page.", txt_jwt_replace_step_1: "Use the 32-character generator below and create a stronger key (minimum {min} characters).", - txt_jwt_replace_step_2: "Cloudflare Dashboard -> Workers & Pages -> Your Service -> Settings -> Variables and Secrets, replace JWT_SECRET.", + txt_jwt_replace_step_2_prefix: "Go to Cloudflare Dashboard -> Workers & Pages -> Your Service -> ", + txt_jwt_replace_step_2_suffix: " -> Variables and Secrets -> Update JWT_SECRET", txt_jwt_replace_step_3: "Save and wait for redeploy, then refresh this page.", + txt_jwt_secret_type_label: "Type:", + txt_jwt_secret_type_value: "Secret", + txt_jwt_secret_name_label: "Variable name:", + txt_jwt_secret_value_label: "Value:", + txt_jwt_secret_value_requirement: "Random string with at least {min} characters", + txt_jwt_what_is: "What is JWT?", + txt_jwt_what_is_body: "JWT_SECRET is the server-side signing key used to issue and verify login tokens. If it is missing, too short, or still using the sample value, the instance is not safe to use normally.", txt_how_to_fix: "How to fix", txt_jwt_fix_step_1: "Open your deployment environment variables.", txt_jwt_fix_step_2: "If your current key is not random enough, use the 32-character generator below.", @@ -1147,11 +1156,20 @@ const zhCNOverrides: Record = { txt_jwt_how_to_fix_add: '处理步骤(添加 JWT_SECRET)', txt_jwt_how_to_fix_replace: '处理步骤(更换 JWT_SECRET)', txt_jwt_add_step_1: '使用下方 32 位随机生成器,复制一个新密钥。', - txt_jwt_add_step_2: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> 设置 -> 变量和机密,新增 JWT_SECRET。', + txt_jwt_add_step_2_prefix: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ', + txt_jwt_add_step_2_suffix: ' -> 变量和机密 -> 新增', txt_jwt_add_step_3: '保存并等待重新部署完成,然后刷新本页确认。', txt_jwt_replace_step_1: '使用下方 32 位随机生成器,生成更强的密钥(至少 {min} 位)。', - txt_jwt_replace_step_2: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> 设置 -> 变量和机密,替换 JWT_SECRET。', + txt_jwt_replace_step_2_prefix: '到 Cloudflare 控制台 -> Workers 和 Pages -> 你的服务 -> ', + txt_jwt_replace_step_2_suffix: ' -> 变量和机密 -> 更新 JWT_SECRET', txt_jwt_replace_step_3: '保存并等待重新部署完成,然后刷新本页确认。', + txt_jwt_secret_type_label: '类型:', + txt_jwt_secret_type_value: '密钥', + txt_jwt_secret_name_label: '变量名称:', + txt_jwt_secret_value_label: '值:', + txt_jwt_secret_value_requirement: '最低 {min} 位随机字符', + txt_jwt_what_is: 'JWT 是什么', + txt_jwt_what_is_body: 'JWT_SECRET 是服务端用来签发和校验登录令牌的密钥。如果它缺失、过短,或者仍然使用示例值,实例就不能安全地正常使用。', txt_how_to_fix: '处理步骤(添加 / 更换)', txt_jwt_fix_step_1: '你可以继续下一步,不影响使用。', txt_jwt_fix_step_2: '如果当前密钥不是强随机值,建议使用下方 32 位生成器。', diff --git a/webapp/src/lib/types.ts b/webapp/src/lib/types.ts index 1d38a41..8f58923 100644 --- a/webapp/src/lib/types.ts +++ b/webapp/src/lib/types.ts @@ -1,4 +1,4 @@ -export type AppPhase = 'loading' | 'register' | 'login' | 'locked' | 'app'; +export type AppPhase = 'register' | 'login' | 'locked' | 'app'; export interface SessionState { accessToken: string; @@ -256,17 +256,10 @@ export interface ListResponse { data: T[]; } -export interface SetupStatusResponse { - registered: boolean; -} - -export interface WebConfigResponse { +export interface WebBootstrapResponse { defaultKdfIterations?: number; jwtUnsafeReason?: 'missing' | 'default' | 'too_short' | null; jwtSecretMinLength?: number; - _icon_service_url?: string; - _icon_service_csp?: string; - iconServiceUrl?: string; } export interface TokenSuccess { diff --git a/webapp/src/styles.css b/webapp/src/styles.css index 64579e6..c8947fd 100644 --- a/webapp/src/styles.css +++ b/webapp/src/styles.css @@ -131,6 +131,12 @@ body, margin-bottom: 6px; } +.jwt-warning-copy { + margin: 0 0 14px; + color: #475569; + line-height: 1.6; +} + .jwt-warning-list { margin: 0; padding-left: 18px; @@ -138,6 +144,33 @@ body, line-height: 1.55; } +.jwt-inline-link { + color: #1d4ed8; + font-weight: 700; + text-decoration: none; +} + +.jwt-inline-link:hover { + text-decoration: underline; +} + +.jwt-secret-fields { + margin-top: 8px; + display: grid; + gap: 6px; +} + +.jwt-secret-row { + display: grid; + grid-template-columns: 88px minmax(0, 1fr); + gap: 8px; + align-items: start; +} + +.jwt-secret-row > span { + color: #64748b; +} + .jwt-generator { margin-top: 14px; } diff --git a/wrangler.kv.toml b/wrangler.kv.toml index d1f9688..15bcdfa 100644 --- a/wrangler.kv.toml +++ b/wrangler.kv.toml @@ -3,9 +3,10 @@ main = "src/index.ts" compatibility_date = "2024-01-01" [assets] +binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = [ "/api/*", "/identity/*", "/icons/*", "/setup/*", "/config", "/notifications/*", "/.well-known/*" ] +run_worker_first = true [build] command = "npm run build" diff --git a/wrangler.toml b/wrangler.toml index 3b5d7ed..0cd0f83 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -3,9 +3,10 @@ main = "src/index.ts" compatibility_date = "2024-01-01" [assets] +binding = "ASSETS" directory = "./dist" not_found_handling = "single-page-application" -run_worker_first = [ "/api/*", "/identity/*", "/icons/*", "/setup/*", "/config", "/notifications/*", "/.well-known/*" ] +run_worker_first = true [build] command = "npm run build"