diff --git a/apps/proxy/src/config.ts b/apps/proxy/src/config.ts new file mode 100644 index 0000000..86bf909 --- /dev/null +++ b/apps/proxy/src/config.ts @@ -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; +} \ No newline at end of file diff --git a/apps/proxy/src/index.ts b/apps/proxy/src/index.ts index 8e13c64..b7fb197 100644 --- a/apps/proxy/src/index.ts +++ b/apps/proxy/src/index.ts @@ -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 }>(); diff --git a/apps/proxy/src/routes/asset.test.ts b/apps/proxy/src/routes/asset.test.ts index be9f76c..cdaf424 100644 --- a/apps/proxy/src/routes/asset.test.ts +++ b/apps/proxy/src/routes/asset.test.ts @@ -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); }); }); \ No newline at end of file diff --git a/apps/proxy/src/routes/asset.ts b/apps/proxy/src/routes/asset.ts index 28ef895..062154e 100644 --- a/apps/proxy/src/routes/asset.ts +++ b/apps/proxy/src/routes/asset.ts @@ -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( diff --git a/apps/proxy/src/routes/trace.ts b/apps/proxy/src/routes/trace.ts index 319f2d0..e5fd5ad 100644 --- a/apps/proxy/src/routes/trace.ts +++ b/apps/proxy/src/routes/trace.ts @@ -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); }