Add native host management and role-based operator authentication
Introduce an outbound Python host agent, Debian installer, durable remote operations, immutable game-file libraries, host-aware deployments and ports, and occupancy-safe campaigns and draining. Native hosts do not require Komodo. Add administrator, operator and viewer accounts, protected management routes, secure sessions, password recovery, audit history and separate telemetry tokens. Document first-admin setup, reporter migration and live-host acceptance checks. Validation: 1,343 TypeScript tests and 39 agent tests passed, along with type checking, the production build, compiled SSR/API smoke tests and administrator recovery. Real Debian/Docker T5/T6 acceptance remains unrun.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { createReadStream } from 'node:fs';
|
||||
import { stat } from 'node:fs/promises';
|
||||
import { Readable } from 'node:stream';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getLibraryFile } from './index.js';
|
||||
import { assertRegularPath, parseByteRange } from './storage.js';
|
||||
|
||||
export function gameFileError(cause: unknown): Response {
|
||||
const status = typeof cause === 'object' && cause !== null && 'status' in cause && typeof cause.status === 'number' ? cause.status : 400;
|
||||
return json({ error: cause instanceof Error ? cause.message : 'Game file request failed.' }, { status });
|
||||
}
|
||||
export async function readGameJson(request: Request, maxBytes = 64 * 1024): Promise<Record<string, unknown>> {
|
||||
if (!request.body) throw new Error('A JSON request body is required.');
|
||||
const reader = request.body.getReader();
|
||||
const chunks: Uint8Array[] = []; let bytes = 0;
|
||||
try {
|
||||
while (true) {
|
||||
const next = await reader.read(); if (next.done) break;
|
||||
bytes += next.value.byteLength;
|
||||
if (bytes > maxBytes) { await reader.cancel(); throw new Error('Request body exceeds its size limit.'); }
|
||||
chunks.push(next.value);
|
||||
}
|
||||
} finally { reader.releaseLock(); }
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Error('Expected a JSON object.');
|
||||
return body;
|
||||
}
|
||||
|
||||
export async function serveGameFile(request: Request, versionId: string, requestedPath: string): Promise<Response> {
|
||||
const file = getLibraryFile(versionId, requestedPath);
|
||||
if (!file || file.state !== 'ready') return json({ error: 'File is not in a published manifest.' }, { status: 404 });
|
||||
const path = await assertRegularPath(file.storageLocation, file.path);
|
||||
if ((await stat(path)).size !== file.size) return json({ error: 'Stored file failed size verification.' }, { status: 503 });
|
||||
const etag = `"${file.sha256}"`;
|
||||
const headers = new Headers({ 'Content-Type': 'application/octet-stream', 'Accept-Ranges': 'bytes', ETag: etag, 'Cache-Control': 'private, no-cache', 'X-Content-SHA256': file.sha256 });
|
||||
if (request.headers.get('if-none-match') === etag) return new Response(null, { status: 304, headers });
|
||||
let range;
|
||||
try { range = parseByteRange(request.headers.has('if-range') && request.headers.get('if-range') !== etag ? null : request.headers.get('range'), file.size); }
|
||||
catch { headers.set('Content-Range', `bytes */${file.size}`); return new Response(null, { status: 416, headers }); }
|
||||
headers.set('Content-Length', String(range ? range.end - range.start + 1 : file.size));
|
||||
if (range) headers.set('Content-Range', `bytes ${range.start}-${range.end}/${file.size}`);
|
||||
const body = request.method === 'HEAD' ? null : Readable.toWeb(createReadStream(path, range ?? undefined)) as ReadableStream<Uint8Array>;
|
||||
return new Response(body, { status: range ? 206 : 200, headers });
|
||||
}
|
||||
Reference in New Issue
Block a user