fix: preserve route limits and harden restore marker
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
export const RESTORE_MARKER_CONFIRMATION = 'DISPOSABLE_LOOPBACK_ONLY';
|
||||
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']);
|
||||
|
||||
export type RestoreMarkerPhase = 'verify' | 'write' | 'expect-invalidated';
|
||||
|
||||
export function parseRestoreMarkerPhase(raw: string | undefined): RestoreMarkerPhase {
|
||||
if (raw === undefined || raw === '') return 'verify';
|
||||
if (raw === 'write' || raw === 'verify' || raw === 'expect-invalidated') return raw;
|
||||
throw new Error('RESTORE_MARKER_PHASE must be verify, write, or expect-invalidated');
|
||||
}
|
||||
|
||||
export function validateRestoreMarkerTarget(raw: string | undefined): URL {
|
||||
if (!raw) throw new Error('CONVEX_SELF_HOSTED_URL is required');
|
||||
let target: URL;
|
||||
try {
|
||||
target = new URL(raw);
|
||||
} catch {
|
||||
throw new Error('CONVEX_SELF_HOSTED_URL must be a valid disposable loopback URL');
|
||||
}
|
||||
if (target.protocol !== 'http:' || !LOOPBACK_HOSTS.has(target.hostname)) {
|
||||
throw new Error('CONVEX_SELF_HOSTED_URL must use an http loopback target (127.0.0.1, localhost, or ::1)');
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function validateRestoreMarkerWrite(input: {
|
||||
phase: RestoreMarkerPhase;
|
||||
allowWrite: string | undefined;
|
||||
confirmation: string | undefined;
|
||||
}): void {
|
||||
if (input.phase !== 'write') return;
|
||||
if (input.allowWrite !== '1' || input.confirmation !== RESTORE_MARKER_CONFIRMATION) {
|
||||
throw new Error(`Writing requires SMOKE_ALLOW_WRITE=1 and RESTORE_MARKER_CONFIRMATION=${RESTORE_MARKER_CONFIRMATION}`);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,22 @@
|
||||
import type { LayoutServerLoad } from './$types';
|
||||
import { configuration } from '$lib/server/config';
|
||||
import { session } from '$lib/server/application';
|
||||
import { capabilityEnabled } from '$lib/server/release-gate';
|
||||
import { capabilityEnabled, type ReleaseCapability } from '$lib/server/release-gate';
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
const config = configuration();
|
||||
const path = event.url.pathname;
|
||||
const identityPage = path === '/characters' || path.startsWith('/characters/');
|
||||
const assetPage = path === '/assets' || path.startsWith('/assets/');
|
||||
const sessionNeeded = !identityPage && !assetPage
|
||||
|| identityPage && capabilityEnabled('identity', config.capabilityAllowlist)
|
||||
|| assetPage && (capabilityEnabled('asset_search', config.capabilityAllowlist) || capabilityEnabled('asset_valuation', config.capabilityAllowlist));
|
||||
const sessionNeeded = identityPage
|
||||
? capabilityEnabled('identity', config.capabilityAllowlist)
|
||||
: assetPage
|
||||
? capabilityEnabled('asset_search', config.capabilityAllowlist) || capabilityEnabled('asset_valuation', config.capabilityAllowlist)
|
||||
: (['assistant_dispatch', 'asset_search', 'asset_valuation', 'identity', 'session_token', 'subscriptions'] as readonly ReleaseCapability[])
|
||||
.some((capability) => capabilityEnabled(capability, config.capabilityAllowlist));
|
||||
// The root shell owns the assistant/nav session indicator. Skip its backend
|
||||
// lookup only when every capability represented by that shell is disabled;
|
||||
// separately enabled capabilities retain the existing authenticated state.
|
||||
const current = sessionNeeded ? await session(event) : null;
|
||||
return { mode: config.mode, userId: current?.userId ?? null, convexUrl: config.publicConvexUrl, jevEnabled: !!config.jevKey };
|
||||
};
|
||||
|
||||
@@ -10,13 +10,13 @@ import { capabilityDisabledReason, capabilityEnabled } from '$lib/server/release
|
||||
const schema = z.object({ message: z.string().trim().min(1).max(2000), requestId: z.uuid() });
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const config = configuration();
|
||||
// Keep disabled routes from touching Convex/session/provider state. CSRF and
|
||||
// rate protection still apply before the fail-closed response.
|
||||
protectMutation(event, `chat:${event.getClientAddress()}`);
|
||||
// Keep disabled routes from touching Convex/session/provider state. The
|
||||
// normal user-keyed limiter is restored after session resolution below.
|
||||
if (!capabilityEnabled('assistant_dispatch', config.capabilityAllowlist)) {
|
||||
return json({ error: capabilityDisabledReason('assistant_dispatch') }, { status: 503 });
|
||||
}
|
||||
const current = await session(event);
|
||||
protectMutation(event, `chat:${current?.userId ?? event.getClientAddress()}`);
|
||||
const parsed = schema.safeParse(await boundedJson(event));
|
||||
if (!parsed.success) return json({ error: 'Provide a message (1–2000 characters) and a UUID requestId.' }, { status: 400 });
|
||||
const { message, requestId } = parsed.data;
|
||||
|
||||
@@ -6,11 +6,11 @@ import { capabilityDisabledReason, capabilityEnabled } from '$lib/server/release
|
||||
import { configuration } from '$lib/server/config';
|
||||
export const POST: RequestHandler = async (event) => {
|
||||
const config = configuration();
|
||||
protectMutation(event, `sync:${event.getClientAddress()}`, 6);
|
||||
if (!capabilityEnabled('asset_sync', config.capabilityAllowlist)) {
|
||||
return json({ error: capabilityDisabledReason('asset_sync') }, { status: 503 });
|
||||
}
|
||||
const current = await session(event);
|
||||
protectMutation(event, `sync:${current?.userId ?? event.getClientAddress()}`, 6);
|
||||
if (!current) return json({ error: 'Connect an EVE character first.' }, { status: 401 });
|
||||
try {
|
||||
const result = await synchronize(current.userId);
|
||||
|
||||
Reference in New Issue
Block a user