The public surface — 17 of the 28 registered routes — reads from a 29KB
TypeScript fixture and never touches the backend. `TECH_LOG_STUDIO_SOURCE`
only ever switched the Studio gateways; `publicContent` was wired to the
static adapter unconditionally, so no configuration could make the public
site show published content. This is the first half of closing that: the
contract and the switch, with the adapter still to come.
The generator now vendors both canonical contracts instead of one. They are
independent — different services on different schedules — so each carries
its own digest and operation list, and updating one leaves the other's drift
gate quiet.
`TECH_LOG_PUBLIC_SOURCE` is deliberately a second flag rather than a rename
of the Studio one. The combination that matters right now is exactly the one
a single flag cannot express: the authoring backend is live while the public
read API does not exist yet. production stays on MOCK for that reason —
pointing it at HTTP today would empty the live site — and moves when the
backend serves /api/v1/public.
Also records the compatibility evidence the registry gate wanted for the
Studio access change in fff5e6f. That gate has been failing since, which is
on me: the change was real and breaking, and it shipped without the note
explaining that route ids and schemas are untouched and only the access
classification moves.
123 lines
4.4 KiB
TypeScript
123 lines
4.4 KiB
TypeScript
/**
|
|
* §6.3. Case-insensitive key fragments that can never appear in a client
|
|
* configuration document.
|
|
*/
|
|
const FORBIDDEN_CONFIG_NAME_FRAGMENTS = Object.freeze([
|
|
"PASSWORD",
|
|
"SECRET",
|
|
"TOKEN",
|
|
"PRIVATE_KEY",
|
|
"CLIENT_SECRET",
|
|
"ACCESS_KEY",
|
|
"REFRESH_TOKEN",
|
|
"COOKIE",
|
|
"AUTHORIZATION",
|
|
]);
|
|
|
|
/**
|
|
* Exact top-level keys whose fragment match is a semantic enum name, not a
|
|
* credential. The allowlist is exact-key only; it is never applied to arbitrary
|
|
* nested keys.
|
|
*/
|
|
const SEMANTIC_KEY_ALLOWLIST = Object.freeze(
|
|
new Set(["AUTH_MODE", "TELEMETRY_ENABLED"]),
|
|
);
|
|
|
|
export type EnvironmentPhase = "build" | "runtime";
|
|
export type EnvironmentDefinition = Readonly<{
|
|
phase: EnvironmentPhase;
|
|
classification: string;
|
|
required: boolean;
|
|
defaultValue: unknown;
|
|
}>;
|
|
|
|
export const ENV_REGISTRY = Object.freeze({
|
|
VITE_BUILD_ID: build("public-metadata", true, null),
|
|
VITE_COMMIT_SHA: build("public-metadata", false, "local"),
|
|
VITE_ROUTER_BASE_PATH: build("compile-time", true, "/"),
|
|
VITE_RUNTIME_CONFIG_URL: build("compile-time", true, "/config.json"),
|
|
APP_ENV: runtime("public", true, null),
|
|
API_BASE_URL: runtime("public-sensitive", true, null),
|
|
REQUEST_TIMEOUT_MS: runtime("public", false, 10_000),
|
|
MAX_RETRY_ATTEMPTS: runtime("public", false, 2),
|
|
TELEMETRY_ENABLED: runtime("public", true, false),
|
|
TELEMETRY_ENDPOINT: runtime("public-sensitive", false, null),
|
|
AUTH_MODE: runtime("public", true, "external"),
|
|
CONFIG_SCHEMA_VERSION: runtime("public", true, null),
|
|
RELEASE_MANIFEST_URL: runtime("public", true, "/release-manifest.json"),
|
|
// §3.5: overrides may only disable an installed capability, never enable one.
|
|
CAPABILITY_OVERRIDES: runtime("public", false, null),
|
|
// §3.5: likewise for features — subtractive, keyed by installed feature id.
|
|
FEATURE_OVERRIDES: runtime("public", false, null),
|
|
// TechLog Studio gateway adapter selection. Defaults to MOCK while the
|
|
// backend does not exist yet.
|
|
TECH_LOG_STUDIO_SOURCE: runtime("public", false, "MOCK"),
|
|
// TechLog public-read adapter selection. Separate from the Studio switch on
|
|
// purpose: the two surfaces are different services on different schedules,
|
|
// and the combination that matters right now — an authoring backend that is
|
|
// live while the public read API is not — is unreachable with one flag.
|
|
TECH_LOG_PUBLIC_SOURCE: runtime("public", false, "MOCK"),
|
|
// §3.5: build-time narrowing of the product manifest. A feature left out
|
|
// here is not imported by any registry and never reaches the bundle.
|
|
VITE_PRODUCT_FEATURES: build("compile-time", false, null),
|
|
});
|
|
|
|
function build(
|
|
classification: string,
|
|
required: boolean,
|
|
defaultValue: unknown,
|
|
): EnvironmentDefinition {
|
|
return Object.freeze({ phase: "build", classification, required, defaultValue });
|
|
}
|
|
|
|
function runtime(
|
|
classification: string,
|
|
required: boolean,
|
|
defaultValue: unknown,
|
|
): EnvironmentDefinition {
|
|
return Object.freeze({ phase: "runtime", classification, required, defaultValue });
|
|
}
|
|
|
|
export function assertSafeConfigNames(
|
|
config: Readonly<Record<string, unknown>>,
|
|
depth = 0,
|
|
): void {
|
|
if (depth > 4) {
|
|
throw new Error("Client configuration nesting exceeds its bound");
|
|
}
|
|
for (const [name, value] of Object.entries(config)) {
|
|
const allowlisted = depth === 0 && SEMANTIC_KEY_ALLOWLIST.has(name);
|
|
if (!allowlisted && isForbiddenConfigName(name)) {
|
|
throw new Error(`Forbidden client configuration key: ${name}`);
|
|
}
|
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
assertSafeConfigNames(value as Record<string, unknown>, depth + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isForbiddenConfigName(name: string): boolean {
|
|
const upper = name.toUpperCase();
|
|
return FORBIDDEN_CONFIG_NAME_FRAGMENTS.some((fragment) =>
|
|
upper.includes(fragment),
|
|
);
|
|
}
|
|
|
|
export type BuildEnvironment = Readonly<{
|
|
VITE_BUILD_ID?: string;
|
|
VITE_COMMIT_SHA?: string;
|
|
VITE_ROUTER_BASE_PATH?: string;
|
|
VITE_RUNTIME_CONFIG_URL?: string;
|
|
}>;
|
|
|
|
export function getBuildConfig(
|
|
environment: BuildEnvironment = import.meta.env as BuildEnvironment,
|
|
) {
|
|
const buildId = environment.VITE_BUILD_ID || "local-build";
|
|
const commitSha = environment.VITE_COMMIT_SHA || "local";
|
|
const routerBasePath = environment.VITE_ROUTER_BASE_PATH || "/";
|
|
const runtimeConfigUrl = environment.VITE_RUNTIME_CONFIG_URL || "/config.json";
|
|
|
|
return Object.freeze({ buildId, commitSha, routerBasePath, runtimeConfigUrl });
|
|
}
|