UNPKG

@adonis-agora/agent

Version:

Governed, durable-backed AI agent (chat + tool-calling + governance) for AdonisJS — part of the Agora ecosystem.

111 lines • 5.54 kB
/** * Pure path/asset helpers for serving the `@adonis-agora/agent-dashboard` governance SPA — extracted * so they can be unit-tested without booting an AdonisJS app. Both dashboard providers compose these; * each holds only the router wiring + I/O. Owned here (rather than in `@adonis-agora/agent-dashboard`) * so `packages/adonis`'s OWN embedded dashboard provider (`providers/dashboard_provider.ts`) can reuse * them without depending on the dashboard package at runtime — the dashboard package already depends * on THIS one (for `AgentConfig`/`ActorResolver`/...), and a package.json edge the other way would be * circular. See `providers/dashboard_provider.ts`'s module doc for the full story. */ /** Collapse duplicate slashes and strip leading/trailing ones: `/agent//` → `agent`. */ export function trimSlashes(path) { return path.replace(/\/+/g, '/').replace(/^\/+|\/+$/g, ''); } /** The agent API base as an absolute path with a single leading slash: `agent` → `/agent`. */ export function apiBaseFor(agentPath) { const trimmed = trimSlashes(agentPath); return trimmed === '' ? '/' : `/${trimmed}`; } /** * The canonical dashboard mount path (no trailing slash), default `<agentPath>/dashboard`. A caller * may override with an explicit `dashboardPath`; both are normalized to a single leading slash. */ export function mountPathFor(agentPath, dashboardPath) { if (dashboardPath !== undefined && trimSlashes(dashboardPath) !== '') { return `/${trimSlashes(dashboardPath)}`; } const trimmed = trimSlashes(agentPath); return trimmed === '' ? '/dashboard' : `/${trimmed}/dashboard`; } const CONTENT_TYPES = { html: 'text/html; charset=utf-8', js: 'text/javascript; charset=utf-8', mjs: 'text/javascript; charset=utf-8', css: 'text/css; charset=utf-8', json: 'application/json; charset=utf-8', svg: 'image/svg+xml', png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', ico: 'image/x-icon', woff: 'font/woff', woff2: 'font/woff2', ttf: 'font/ttf', map: 'application/json; charset=utf-8', txt: 'text/plain; charset=utf-8', }; /** MIME type for a filename by extension; `application/octet-stream` for anything unknown. */ export function contentTypeFor(filename) { const dot = filename.lastIndexOf('.'); const ext = dot === -1 ? '' : filename.slice(dot + 1).toLowerCase(); return CONTENT_TYPES[ext] ?? 'application/octet-stream'; } /** * Reject a wildcard asset request that tries to escape the SPA root via `..` or an absolute path. * Returns the safe, normalized relative segments (never starting with `..`), or `null` to deny. */ export function safeAssetSegments(wildcard) { const parts = Array.isArray(wildcard) ? wildcard : (wildcard ?? '').split('/'); const segments = []; for (const raw of parts) { const part = raw.trim(); if (part === '' || part === '.') continue; if (part === '..' || part.includes('\0') || part.includes('\\')) return null; segments.push(part); } return segments; } /** `id` of the JSON data block {@link injectApiBase} emits; the SPA's `resolveApiBase()` reads it. */ export const CONFIG_ELEMENT_ID = 'agent-dashboard-config'; /** * Hand the resolved agent API base to the served `index.html`, so the client calls the exact base * the provider mounted (no build-time coupling). Inserted right before `</head>`. * * It goes in as a JSON DATA BLOCK (`<script type="application/json">{"apiBase":…}</script>`), not * as an inline script assigning `window.__AGENT_DASHBOARD_BASE__`. A data block is never executed, * so no Content-Security-Policy can refuse it; an inline script IS, and a host with * `script-src 'self' 'nonce-…'` (`@adonisjs/shield`'s `@nonce`, the recommended setup) silently * dropped ours — the SPA then derived a base from its own URL, which is right only for the default * mount, and on any other every request from a console that had rendered perfectly answered 404. * The global is still read by the SPA as a fallback, but this is no longer how the provider speaks. */ export function injectApiBase(html, apiBase) { // `<` escaped as `\u003c` inside the JSON: a data block ends at the first `</script`, and a // config value must not be able to close it early. Valid JSON either way. const json = JSON.stringify({ apiBase }).replace(/</g, '\\u003c'); const tag = `<script type="application/json" id="${CONFIG_ELEMENT_ID}">${json}</script>`; if (html.includes('</head>')) return html.replace('</head>', `${tag}</head>`); return `${tag}${html}`; } /** * Inject a `<base href="${mount}/">` as the FIRST thing inside `<head>`, so the SPA's relative * `./assets/*` URLs (Vite `base: './'`) resolve against the mount directory no matter whether the * browser's URL carries a trailing slash. This replaces the old trailing-slash redirect, which the * AdonisJS router (it normalizes trailing slashes) turned into a duplicate-route crash. `mount` is a * leading-slash, no-trailing-slash path (e.g. `/agent/dashboard`). */ export function injectBaseHref(html, mount) { const tag = `<base href="${mount}/">`; const headOpen = html.match(/<head[^>]*>/i); if (headOpen?.index !== undefined) { const at = headOpen.index + headOpen[0].length; return html.slice(0, at) + tag + html.slice(at); } return `${tag}${html}`; } //# sourceMappingURL=paths.js.map