105 lines
2.9 KiB
JavaScript
105 lines
2.9 KiB
JavaScript
import { assertSafeConfigNames, getBuildConfig } from "../contracts/env.js";
|
|
import { validateRuntimeConfig } from "./runtime-config-schema.js";
|
|
|
|
export class BootConfigError extends Error {
|
|
/**
|
|
* @param {string} code
|
|
* @param {{ buildId: string, configSchemaVersion?: string, releaseId?: string }} safe
|
|
*/
|
|
constructor(code, safe) {
|
|
super("Runtime configuration could not be loaded");
|
|
this.name = "BootConfigError";
|
|
this.kind = "BOOT_CONFIG_FAILURE";
|
|
this.code = code;
|
|
this.safe = Object.freeze({
|
|
kind: this.kind,
|
|
code,
|
|
buildId: safe.buildId,
|
|
configSchemaVersion: safe.configSchemaVersion,
|
|
releaseId: safe.releaseId,
|
|
supportReference: `${safe.buildId}:${code}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {{
|
|
* fetcher?: typeof fetch,
|
|
* buildConfig?: ReturnType<typeof getBuildConfig>,
|
|
* now?: () => number
|
|
* }} [options]
|
|
*/
|
|
export async function loadRuntimeConfig(options = {}) {
|
|
const fetcher = options.fetcher ?? fetch;
|
|
const buildConfig = options.buildConfig ?? getBuildConfig();
|
|
const now = options.now ?? performance.now.bind(performance);
|
|
const startedAt = now();
|
|
|
|
let response;
|
|
try {
|
|
response = await fetcher(buildConfig.runtimeConfigUrl, {
|
|
cache: "no-store",
|
|
headers: { Accept: "application/json" },
|
|
});
|
|
} catch {
|
|
throw new BootConfigError("CONFIG_FETCH_FAILED", {
|
|
buildId: buildConfig.buildId,
|
|
});
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new BootConfigError("CONFIG_HTTP_FAILED", {
|
|
buildId: buildConfig.buildId,
|
|
});
|
|
}
|
|
|
|
let rawConfig;
|
|
try {
|
|
rawConfig = await response.json();
|
|
} catch {
|
|
throw new BootConfigError("CONFIG_JSON_INVALID", {
|
|
buildId: buildConfig.buildId,
|
|
});
|
|
}
|
|
|
|
if (!rawConfig || typeof rawConfig !== "object" || Array.isArray(rawConfig)) {
|
|
throw new BootConfigError("CONFIG_SHAPE_INVALID", {
|
|
buildId: buildConfig.buildId,
|
|
});
|
|
}
|
|
|
|
try {
|
|
assertSafeConfigNames(rawConfig);
|
|
} catch {
|
|
throw new BootConfigError("CONFIG_SECRET_NAME_REJECTED", {
|
|
buildId: buildConfig.buildId,
|
|
});
|
|
}
|
|
|
|
const validated = validateRuntimeConfig(rawConfig);
|
|
if (!validated.success) {
|
|
throw new BootConfigError("CONFIG_SCHEMA_INVALID", {
|
|
buildId: buildConfig.buildId,
|
|
configSchemaVersion:
|
|
typeof rawConfig.CONFIG_SCHEMA_VERSION === "string"
|
|
? rawConfig.CONFIG_SCHEMA_VERSION
|
|
: undefined,
|
|
releaseId: typeof rawConfig.RELEASE_ID === "string" ? rawConfig.RELEASE_ID : undefined,
|
|
});
|
|
}
|
|
|
|
if (validated.data.BUILD_ID && validated.data.BUILD_ID !== buildConfig.buildId) {
|
|
throw new BootConfigError("CONFIG_BUILD_MISMATCH", {
|
|
buildId: buildConfig.buildId,
|
|
configSchemaVersion: validated.data.CONFIG_SCHEMA_VERSION,
|
|
releaseId: validated.data.RELEASE_ID,
|
|
});
|
|
}
|
|
|
|
return Object.freeze({
|
|
config: validated.data,
|
|
build: buildConfig,
|
|
validationDurationMs: now() - startedAt,
|
|
});
|
|
}
|