Merge origin/codex/portable-deployment into feat/docker-local-deploy

Unisce gli internals di Codex (secret-bundle, provider-credentials, auth upstream,
security hardening, CI multiarch) mantenendo le fix portal-specific:
- backend: configPath da THT_CONFIG (fix sessioni) + dataRoot di Codex; authMode 'upstream'
- Docker/compose: TENUTO il mio (verificato live: omics_network+alias, env_file, pi npm-g)
  perche' il compose/Dockerfile/entrypoint di Codex sono accoppiati al suo modello
  secret-bundle (tht doctor inesistente, secret-policy.sh). Adottabile in futuro.
- config.test.ts: preso Codex (superset)
Verificato: tsc clean, 132/132 vitest.
This commit is contained in:
User
2026-07-12 21:13:20 +02:00
211 changed files with 23270 additions and 415 deletions
+1
View File
@@ -13,6 +13,7 @@
</head>
<body>
<div id="root"></div>
<script src="/config.js"></script>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
window.__THOTHII_CONFIG__ = {};
+26
View File
@@ -0,0 +1,26 @@
[
{ "value": "", "valid": true },
{ "value": "/", "valid": true },
{ "value": "/api", "valid": true },
{ "value": "/api/", "valid": true },
{ "value": "http://localhost:8787", "valid": true },
{ "value": "https://api.example.test/v1", "valid": true },
{ "value": "https://api.example.test/base/path/", "valid": true },
{ "value": "http://127.0.0.1:1/api", "valid": true },
{ "value": "http://[::1]:8787/api", "valid": true },
{ "value": "/backend", "valid": false },
{ "value": "api", "valid": false },
{ "value": "//evil.test", "valid": false },
{ "value": "http:///missing-authority", "valid": false },
{ "value": "https:///triple-slash", "valid": false },
{ "value": "http://", "valid": false },
{ "value": "http://example.test:abc", "valid": false },
{ "value": "http://example.test:65536", "valid": false },
{ "value": "http://example.test:999999999999999999999", "valid": false },
{ "value": "http://example.test:", "valid": false },
{ "value": "https://user:pass@example.test", "valid": false },
{ "value": "https://example.test/path with space", "valid": false },
{ "value": "ftp://example.test", "valid": false },
{ "value": "https://example.test/api?tenant=x", "valid": false },
{ "value": "https://example.test/api#fragment", "valid": false }
]
+8
View File
@@ -0,0 +1,8 @@
{
"relativeBases": ["", "/", "/api", "/api/"],
"absolutePattern": "^https?://(?:\\[[0-9A-Fa-f:.]+\\]|[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?)(?::[0-9]+)?(?:/[^\\s?#]*)?/?$",
"maxPort": 65535,
"queryAllowed": false,
"fragmentAllowed": false,
"credentialsAllowed": false
}
+2 -2
View File
@@ -1,4 +1,4 @@
const BASE = import.meta.env.VITE_BACKEND_URL ?? "http://localhost:8787";
import { backendBaseUrl as BASE, joinBackendPath } from "./runtime-config";
export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
// Only declare a JSON content-type when we actually send a body. Body-less
@@ -10,7 +10,7 @@ export async function apiFetch<T>(path: string, init?: RequestInit): Promise<T>
if (init?.body != null && !("content-type" in headers) && !("Content-Type" in headers)) {
headers["content-type"] = "application/json";
}
const res = await fetch(`${BASE}${path}`, { ...init, headers });
const res = await fetch(joinBackendPath(BASE, path), { ...init, headers });
if (!res.ok) throw new Error(`${res.status} ${await res.text().catch(() => "")}`);
return res.status === 204 ? (undefined as T) : ((await res.json()) as T);
}
+51
View File
@@ -0,0 +1,51 @@
import { describe, expect, it } from "vitest";
import { backendBaseUrl, joinBackendPath, resolveBackendUrl } from "./runtime-config";
import cases from "./backend-url-cases.json";
describe("resolveBackendUrl", () => {
it("uses the runtime-injected backend URL", () => {
expect(resolveBackendUrl({ backendBaseUrl: "/api" })).toBe("/api");
});
it("falls back to the Vite backend URL", () => {
expect(resolveBackendUrl(undefined)).toBe(import.meta.env.VITE_BACKEND_URL ?? "");
});
it("preserves the client default when Vite has no configured backend", () => {
expect(backendBaseUrl).toBe(import.meta.env.VITE_BACKEND_URL ?? "http://localhost:8787");
});
it.each(["/backend", "api", "//evil.test", "ftp://example.test", "https://user:pass@example.test"])(
"rejects unsupported backend URL %j",
(backendBaseUrl) => {
expect(() => resolveBackendUrl({ backendBaseUrl })).toThrow(/BACKEND_BASE_URL/);
},
);
it.each(["", "/", "/api", "/api/", "http://localhost:8787", "https://api.example.test/v1"])(
"accepts supported backend URL %j",
(backendBaseUrl) => {
expect(resolveBackendUrl({ backendBaseUrl })).toBe(backendBaseUrl);
},
);
it.each(cases)("applies the canonical policy to $value", ({ value, valid }) => {
const resolve = () => resolveBackendUrl({ backendBaseUrl: value });
if (valid) expect(resolve()).toBe(value);
else expect(resolve).toThrow(/BACKEND_BASE_URL/);
});
});
describe("joinBackendPath", () => {
it.each([
["", "/sessions/s1", "/sessions/s1"],
["/", "/sessions/s1", "/sessions/s1"],
["/api", "/sessions/s1", "/api/sessions/s1"],
["/api/", "/sessions/s1", "/api/sessions/s1"],
["https://example.test/api", "/sessions/s1", "https://example.test/api/sessions/s1"],
["https://example.test/api/", "sessions/s1", "https://example.test/api/sessions/s1"],
])("joins base %j and path %j", (base, path, expected) => {
expect(joinBackendPath(base, path)).toBe(expected);
});
});
+41
View File
@@ -0,0 +1,41 @@
import policy from "./backend-url-policy.json";
export interface RuntimeConfig {
backendBaseUrl?: string;
}
declare global {
interface Window {
__THOTHII_CONFIG__?: RuntimeConfig;
}
}
export function resolveBackendUrl(config: RuntimeConfig | undefined): string {
const value = config?.backendBaseUrl ?? import.meta.env.VITE_BACKEND_URL ?? "";
if (policy.relativeBases.includes(value)) return value;
try {
if (!new RegExp(policy.absolutePattern).test(value)) throw new Error("syntax");
const authority = value.replace(/^https?:\/\//, "").split("/", 1)[0];
const suffix = authority.startsWith("[")
? authority.slice(authority.indexOf("]") + 1)
: authority.slice(authority.lastIndexOf(":"));
const port = suffix.startsWith(":") ? suffix.slice(1) : "";
if (port && (port.length > 5 || Number(port) > policy.maxPort)) throw new Error("port");
return value;
} catch {
// Fall through to the single actionable runtime error below.
}
throw new Error(
"Invalid BACKEND_BASE_URL: use empty/root, /api, or a valid http(s) base without credentials, query, or fragment",
);
}
export function joinBackendPath(base: string, path: string): string {
const normalizedBase = base === "/" ? "" : base.replace(/\/+$/, "");
const normalizedPath = path.replace(/^\/+/, "");
return `${normalizedBase}/${normalizedPath}`;
}
export const backendBaseUrl =
resolveBackendUrl(typeof window === "undefined" ? undefined : window.__THOTHII_CONFIG__) ||
"http://localhost:8787";
@@ -13,7 +13,7 @@ beforeEach(() => {
test("opens an EventSource and feeds NAMED events to the store", () => {
renderHook(() => useSessionStream("s1"));
const es = FakeEventSource.instances[0];
expect(es.url).toContain("/sessions/s1/events");
expect(es.url).toBe("http://localhost:8787/sessions/s1/events");
// Backend sends `event: ui_request` (named) — drive the addEventListener path
// that production relies on, not the unnamed onmessage fallback.
act(() =>
+2 -1
View File
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
import { BASE } from "../api/client";
import { joinBackendPath } from "../api/runtime-config";
import { useSessionStore } from "../store/sessionStore";
import type { StreamEvent } from "../api/types";
@@ -10,7 +11,7 @@ export function useSessionStream(sessionId: string | null) {
useEffect(() => {
if (!sessionId) return;
const es = new EventSource(`${BASE}/sessions/${sessionId}/events`);
const es = new EventSource(joinBackendPath(BASE, `/sessions/${sessionId}/events`));
es.onopen = () => setConnected(true);
es.onerror = () => setConnected(false);