fix(proxy): wire games root into asset routes
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Runtime config for the proxy. `GAMES_ROOT` is set at startup (from env or a
|
||||
* default) and read by the asset/trace routes to serve local game assets. It's
|
||||
* a module-level value because the node-server adapter injects its own
|
||||
* `HttpBindings` as the Hono env, not custom bindings.
|
||||
*/
|
||||
export let GAMES_ROOT: string | undefined;
|
||||
|
||||
/** Set the games root at startup. */
|
||||
export function setGamesRoot(root: string | undefined): void {
|
||||
GAMES_ROOT = root;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { Hono } from 'hono';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as path from 'node:path';
|
||||
import { loadEnv, type Bindings } from './env.js';
|
||||
import { setGamesRoot } from './config.js';
|
||||
import asset from './routes/asset.js';
|
||||
import health from './routes/health.js';
|
||||
import items from './routes/items.js';
|
||||
@@ -14,9 +15,10 @@ const env = loadEnv();
|
||||
|
||||
// Default GAMES_ROOT to the repo's `games` folder so local game assets work
|
||||
// without configuration; override via env.
|
||||
const gamesRoot =
|
||||
setGamesRoot(
|
||||
env.GAMES_ROOT ??
|
||||
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'games');
|
||||
path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'games'),
|
||||
);
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import { mkdtempSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import asset from './asset.js';
|
||||
import { setGamesRoot } from '../config.js';
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = mkdtempSync(path.join(tmpdir(), 'games-'));
|
||||
setGamesRoot(dir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
setGamesRoot(undefined);
|
||||
});
|
||||
|
||||
describe('asset route', () => {
|
||||
@@ -45,27 +54,20 @@ describe('asset route', () => {
|
||||
});
|
||||
|
||||
it('serves a local game asset from GAMES_ROOT', async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'games-'));
|
||||
writeFileSync(path.join(dir, 'cards.png'), new Uint8Array([1, 2, 3]));
|
||||
const res = await asset.request('/?url=cards.png', {}, { GAMES_ROOT: dir });
|
||||
const res = await asset.request('/?url=cards.png');
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-type')).toBe('image/png');
|
||||
expect(await res.arrayBuffer()).toEqual(new Uint8Array([1, 2, 3]).buffer);
|
||||
});
|
||||
|
||||
it('returns 404 for a missing local asset', async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'games-'));
|
||||
const res = await asset.request('/?url=nope.png', {}, { GAMES_ROOT: dir });
|
||||
const res = await asset.request('/?url=nope.png');
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it('rejects path traversal outside GAMES_ROOT', async () => {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), 'games-'));
|
||||
const res = await asset.request(
|
||||
'/?url=..%2F..%2Fetc%2Fpasswd',
|
||||
{},
|
||||
{ GAMES_ROOT: dir },
|
||||
);
|
||||
const res = await asset.request('/?url=..%2F..%2Fetc%2Fpasswd');
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { Bindings } from '../env.js';
|
||||
import { GAMES_ROOT } from '../config.js';
|
||||
import { resolveAsset } from './resolveAsset.js';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
const app = new Hono();
|
||||
|
||||
/**
|
||||
* Fetch an external asset (texture, model, etc.) and stream it back to the
|
||||
@@ -20,7 +20,7 @@ app.get('/', async (c) => {
|
||||
return c.json({ error: 'Missing url query param' }, 400);
|
||||
}
|
||||
|
||||
const result = await resolveAsset(raw, c.env?.GAMES_ROOT);
|
||||
const result = await resolveAsset(raw, GAMES_ROOT);
|
||||
if (!result.ok) {
|
||||
// A missing local file vs an invalid reference.
|
||||
return c.json(
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createRequire } from 'module';
|
||||
import { traceRequestSchema, type TraceResult } from '@tts/shared';
|
||||
import { offsetShape, parseSvgShape } from './svgShape.js';
|
||||
import { resolveAsset } from './resolveAsset.js';
|
||||
import type { Bindings } from '../env.js';
|
||||
import { GAMES_ROOT } from '../config.js';
|
||||
|
||||
// vtracer is a CommonJS package; load it via require so the wasm initializes
|
||||
// with the correct `__dirname`.
|
||||
@@ -19,7 +19,7 @@ const vtracer = require('@visioncortex/vtracer') as {
|
||||
): string;
|
||||
};
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
const app = new Hono();
|
||||
|
||||
/**
|
||||
* Trace an image into a vector shape and return it BSON-encoded.
|
||||
@@ -43,7 +43,7 @@ app.get('/', async (c) => {
|
||||
|
||||
// Resolve the image: a relative path is a local game asset under GAMES_ROOT;
|
||||
// otherwise it must be an http(s) URL.
|
||||
const resolved = await resolveAsset(url, c.env?.GAMES_ROOT);
|
||||
const resolved = await resolveAsset(url, GAMES_ROOT);
|
||||
if (!resolved.ok) {
|
||||
return c.json({ error: 'Invalid or missing image url' }, 400);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user