everything-dev
Version:
A consolidated product package for building Module Federation apps with oRPC APIs.
279 lines (277 loc) • 10.7 kB
JavaScript
import { getRuntimeConfig } from "./runtime.mjs";
import { manifest_exports } from "./manifest/index.mjs";
import { z } from "zod";
//#region src/ui/tenant.ts
/**
* Tenant helpers shared by every ui workspace.
*
* Two halves of the tenant surface live here:
* - origin construction (`buildTenantUrl`, `tenantLabel`, `gatewayForAccount`)
* - the pure helpers for the org node-config editor: a validated draft of the
* fields a tenant may customize in its published bos.config.json, prefilled
* from the currently published config, diffed against it, and an in-browser
* sha384 preflight so a wrong UI bundle cannot brick the tenant site.
*/
const LOCALHOST_SUFFIX = ".localhost";
function isLocalHostname(hostname) {
const normalized = hostname.toLowerCase();
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "[::1]" || normalized.endsWith(LOCALHOST_SUFFIX);
}
/** Strips a `<label>.<gatewayId>` hostname (or a bare label) down to its label. */
function tenantLabel(hostnameOrLabel, gatewayId) {
const normalized = hostnameOrLabel.trim().toLowerCase().replace(/\/+$/, "");
if (!normalized) return "";
const gateway = gatewayId?.trim().toLowerCase();
if (gateway && normalized.endsWith(`.${gateway}`)) return normalized.slice(0, -(gateway.length + 1));
if (normalized.endsWith(LOCALHOST_SUFFIX)) return normalized.slice(0, -10);
return normalized.split(".")[0] ?? "";
}
/**
* Builds the absolute origin for a tenant, dev-aware.
*
* `buildTenantUrl("chicago", "citynode.app")`
* -> `https://chicago.citynode.app` in production
* -> `http://chicago.localhost:3000` when served from localhost
*/
function buildTenantUrl(hostnameOrLabel, gatewayId, options = {}) {
const label = tenantLabel(hostnameOrLabel, gatewayId);
if (!label) return null;
const path = options.path ?? "";
const currentHostname = options.currentHostname ?? (typeof window === "undefined" ? void 0 : window.location.hostname);
if (currentHostname && isLocalHostname(currentHostname)) {
const currentPort = options.currentPort ?? (typeof window === "undefined" ? "" : window.location.port);
const port = currentPort ? `:${currentPort}` : "";
return `http://${label}${LOCALHOST_SUFFIX}${port}${path}`;
}
const gateway = gatewayId.trim().toLowerCase();
if (!gateway) return null;
return `https://${label}.${gateway}${path}`;
}
/**
* The gateway a slug should publish under for the given owner account,
* derived from the runtime config instead of hardcoded per-network domains.
*
* When the account lives on the same network as the runtime, the current
* runtime's gateway id is the answer. When the networks differ, the runtime
* config carries no gateway for that other network, so null is returned and
* the caller surfaces it rather than guessing a domain.
*/
function gatewayForAccount(accountId, config = getRuntimeConfig()) {
const gatewayId = config.runtime?.gatewayId;
if (!gatewayId) return null;
return accountId.trim().toLowerCase().endsWith(".testnet") === (config.networkId === "testnet") ? gatewayId : null;
}
const INTEGRITY_PATTERN = /^sha384-[A-Za-z0-9+/=]+$/;
const defaultTenantConfigDraftMessages = {
url: "must be a valid URL",
integrity: "must look like sha384-… (base64)",
manifest: "must look like versions/<version-id>.json",
title: "title is required",
description: "description is required",
uiPair: "a UI bundle needs both its URL and its integrity (direct hash or manifest pin)",
integrityMode: "choose one integrity mode — a direct entry hash or a version-manifest pin, not both",
pinPair: "a version-manifest pin needs both the manifest filename and its integrity",
ssrPair: "an SSR bundle needs both its URL and its integrity hash"
};
function createTenantConfigDraftSchema(messages = {}) {
const copy = {
...defaultTenantConfigDraftMessages,
...messages
};
const optionalUrl = z.string().trim().url(copy.url).or(z.literal("")).transform((value) => value.trim());
const optionalIntegrity = z.string().trim().regex(INTEGRITY_PATTERN, copy.integrity).or(z.literal(""));
const optionalManifestName = z.string().trim().regex(/^versions\/[0-9a-f]{16}\.json$/, copy.manifest).or(z.literal(""));
return z.object({
title: z.string().trim().min(1, copy.title),
description: z.string().trim().min(1, copy.description),
repository: optionalUrl,
uiProduction: optionalUrl,
uiIntegrity: optionalIntegrity,
uiManifest: optionalManifestName,
uiPinIntegrity: optionalIntegrity,
ssrUrl: optionalUrl,
ssrIntegrity: optionalIntegrity
}).refine((draft) => !!draft.uiProduction === !!(draft.uiIntegrity || draft.uiManifest), {
message: copy.uiPair,
path: ["uiIntegrity"]
}).refine((draft) => !draft.uiIntegrity || !draft.uiManifest, {
message: copy.integrityMode,
path: ["uiManifest"]
}).refine((draft) => !!draft.uiManifest === !!draft.uiPinIntegrity, {
message: copy.pinPair,
path: ["uiPinIntegrity"]
}).refine((draft) => !!draft.ssrUrl === !!draft.ssrIntegrity, {
message: copy.ssrPair,
path: ["ssrIntegrity"]
});
}
const tenantConfigDraftSchema = createTenantConfigDraftSchema();
const emptyTenantConfigDraft = {
title: "",
description: "",
repository: "",
uiProduction: "",
uiIntegrity: "",
uiManifest: "",
uiPinIntegrity: "",
ssrUrl: "",
ssrIntegrity: ""
};
function readUi(resolvedConfig) {
return (resolvedConfig?.app)?.ui ?? {};
}
const asString = (value) => typeof value === "string" ? value : "";
function buildDraftFromResolvedConfig(resolvedConfig, fallback) {
const ui = readUi(resolvedConfig);
const title = asString(resolvedConfig?.title) || fallback.title;
return {
title,
description: asString(resolvedConfig?.description) || title,
repository: asString(resolvedConfig?.repository),
uiProduction: asString(ui.production),
uiIntegrity: asString(ui.integrity),
uiManifest: asString(ui.pin?.manifest),
uiPinIntegrity: asString(ui.pin?.integrity),
ssrUrl: asString(ui.ssr),
ssrIntegrity: asString(ui.ssrIntegrity)
};
}
function diffDraft(draft, resolvedConfig) {
const ui = readUi(resolvedConfig);
return [
[
"title",
asString(resolvedConfig?.title),
draft.title
],
[
"description",
asString(resolvedConfig?.description),
draft.description
],
[
"repository",
asString(resolvedConfig?.repository),
draft.repository
],
[
"ui bundle",
asString(ui.production),
draft.uiProduction
],
[
"ui integrity",
asString(ui.integrity),
draft.uiIntegrity
],
[
"ui manifest pin",
asString(ui.pin?.manifest),
draft.uiManifest
],
[
"ui pin integrity",
asString(ui.pin?.integrity),
draft.uiPinIntegrity
],
[
"ssr bundle",
asString(ui.ssr),
draft.ssrUrl
],
[
"ssr integrity",
asString(ui.ssrIntegrity),
draft.ssrIntegrity
]
].filter(([, from, to]) => from !== to).map(([field, from, to]) => ({
field,
from: from || "—",
to: to || "—"
}));
}
/** The `app` block for the published config, or undefined when no UI override is set. */
function draftUiOverride(draft) {
if (!draft.uiProduction) return void 0;
if (draft.uiManifest && draft.uiPinIntegrity) return { ui: {
production: normalizeBundleBaseUrl(draft.uiProduction),
pin: {
manifest: draft.uiManifest,
integrity: draft.uiPinIntegrity
},
...draft.ssrUrl && draft.ssrIntegrity ? {
ssr: normalizeBundleBaseUrl(draft.ssrUrl),
ssrIntegrity: draft.ssrIntegrity
} : {}
} };
if (!draft.uiIntegrity) return void 0;
return { ui: {
production: normalizeBundleBaseUrl(draft.uiProduction),
integrity: draft.uiIntegrity,
...draft.ssrUrl && draft.ssrIntegrity ? {
ssr: normalizeBundleBaseUrl(draft.ssrUrl),
ssrIntegrity: draft.ssrIntegrity
} : {}
} };
}
function toBase64(bytes) {
let binary = "";
const chunkSize = 32768;
for (let offset = 0; offset < bytes.length; offset += chunkSize) binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
return btoa(binary);
}
async function computeSubresourceIntegrity(url) {
const response = await fetch(url, { cache: "no-store" });
if (!response.ok) throw new Error(`fetching the bundle returned ${response.status}`);
const buffer = await response.arrayBuffer();
const digest = await crypto.subtle.digest("SHA-384", buffer);
return `sha384-${toBase64(new Uint8Array(digest))}`;
}
function resolveClientEntryUrl(url) {
if (/\/remoteEntry(\.[a-f0-9]{8,})?\.js$/.test(url)) return url;
if (url.endsWith("/mf-manifest.json")) return `${url.replace(/\/mf-manifest\.json$/, "")}/${manifest_exports.UI_REMOTE_ENTRY_FILENAME}`;
return `${url.replace(/\/$/, "")}/${manifest_exports.UI_REMOTE_ENTRY_FILENAME}`;
}
function resolveServerEntryUrl(url) {
return `${url.replace(/\/$/, "")}/${manifest_exports.UI_REMOTE_SERVER_ENTRY_FILENAME}`;
}
function normalizeBundleBaseUrl(url) {
return url.trim().replace(/\/remoteEntry(\.server)?(\.[a-f0-9]{8,})?\.js$/, "").replace(/\/mf-manifest\.json$/, "");
}
async function computeUiEntryIntegrity(baseUrl) {
return computeSubresourceIntegrity(resolveClientEntryUrl(baseUrl.trim()));
}
async function computeSsrEntryIntegrity(baseUrl) {
return computeSubresourceIntegrity(resolveServerEntryUrl(baseUrl.trim()));
}
async function verifyEntryIntegrity(entryUrl, expected) {
try {
const computed = await computeSubresourceIntegrity(entryUrl);
return computed === expected ? { status: "match" } : {
status: "mismatch",
computed
};
} catch (error) {
return {
status: "unverified",
reason: error instanceof Error ? error.message : String(error)
};
}
}
async function verifyUiIntegrity(baseUrl, expected) {
return verifyEntryIntegrity(resolveClientEntryUrl(baseUrl.trim()), expected);
}
/**
* Pin preflight: fetch the pinned version-manifest document and SRI-compare it
* against the pin — the manifest's own `entryIntegrity` covers the entry bytes
* at serve time.
*/
async function verifyUiPin(baseUrl, pin) {
return verifyEntryIntegrity(`${baseUrl.trim().replace(/\/$/, "")}/${pin.manifest.replace(/^\//, "")}`, pin.integrity);
}
async function verifySsrIntegrity(baseUrl, expected) {
return verifyEntryIntegrity(resolveServerEntryUrl(baseUrl.trim()), expected);
}
//#endregion
export { INTEGRITY_PATTERN, buildDraftFromResolvedConfig, buildTenantUrl, computeSsrEntryIntegrity, computeSubresourceIntegrity, computeUiEntryIntegrity, createTenantConfigDraftSchema, defaultTenantConfigDraftMessages, diffDraft, draftUiOverride, emptyTenantConfigDraft, gatewayForAccount, isLocalHostname, normalizeBundleBaseUrl, resolveClientEntryUrl, resolveServerEntryUrl, tenantConfigDraftSchema, tenantLabel, verifySsrIntegrity, verifyUiIntegrity, verifyUiPin };
//# sourceMappingURL=tenant.mjs.map