conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

static.js (1878B)


      1 // src/lib/static.js - the interface's static assets
      2 //
      3 // Pages are rendered by the server, so the only static files are the
      4 // stylesheet and the vendored htmx build. An allowlist is used rather than
      5 // a general static file server: nothing can be requested that is not named
      6 // here, which makes path traversal impossible by construction rather than
      7 // by careful escaping.
      8 
      9 import fs from 'node:fs/promises';
     10 import path from 'node:path';
     11 import crypto from 'node:crypto';
     12 import { fileURLToPath } from 'node:url';
     13 
     14 const HERE = path.dirname(fileURLToPath(import.meta.url));
     15 export const ASSET_DIR = path.resolve(HERE, '../../assets');
     16 
     17 const FILES = {
     18   '/style.css': { file: 'style.css', type: 'text/css; charset=utf-8' },
     19   '/vendor/htmx.min.js': { file: 'vendor/htmx.min.js', type: 'text/javascript; charset=utf-8' },
     20   '/vendor/htmx-LICENSE.txt': { file: 'vendor/htmx-LICENSE.txt', type: 'text/plain; charset=utf-8' },
     21 };
     22 
     23 export default async function staticRoutes(fastify, { dir = ASSET_DIR } = {}) {
     24   // Read once at startup. These are small and never change at runtime.
     25   const cache = new Map();
     26 
     27   for (const [route, entry] of Object.entries(FILES)) {
     28     let body;
     29     try {
     30       body = await fs.readFile(path.join(dir, entry.file));
     31     } catch {
     32       // A deployment that does not ship the assets simply has no styling.
     33       continue;
     34     }
     35     const etag = `"${crypto.createHash('sha256').update(body).digest('hex').slice(0, 32)}"`;
     36     cache.set(route, { body, type: entry.type, etag });
     37   }
     38 
     39   for (const [route, entry] of cache) {
     40     fastify.get(route, async (req, reply) => {
     41       if (req.headers['if-none-match'] === entry.etag) return reply.code(304).send();
     42       reply.header('content-type', entry.type);
     43       reply.header('etag', entry.etag);
     44       reply.header('cache-control', 'no-cache');
     45       return reply.send(entry.body);
     46     });
     47   }
     48 }