UNPKG

everything-dev

Version:

A consolidated product package for building Module Federation apps with oRPC APIs.

300 lines (298 loc) • 11.5 kB
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); const require_ui_runtime = require('./runtime.cjs'); const require_ui_manifest_index = require('./manifest/index.cjs'); let zod = require("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 = require_ui_runtime.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 = zod.z.string().trim().url(copy.url).or(zod.z.literal("")).transform((value) => value.trim()); const optionalIntegrity = zod.z.string().trim().regex(INTEGRITY_PATTERN, copy.integrity).or(zod.z.literal("")); const optionalManifestName = zod.z.string().trim().regex(/^versions\/[0-9a-f]{16}\.json$/, copy.manifest).or(zod.z.literal("")); return zod.z.object({ title: zod.z.string().trim().min(1, copy.title), description: zod.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$/, "")}/${require_ui_manifest_index.manifest_exports.UI_REMOTE_ENTRY_FILENAME}`; return `${url.replace(/\/$/, "")}/${require_ui_manifest_index.manifest_exports.UI_REMOTE_ENTRY_FILENAME}`; } function resolveServerEntryUrl(url) { return `${url.replace(/\/$/, "")}/${require_ui_manifest_index.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 exports.INTEGRITY_PATTERN = INTEGRITY_PATTERN; exports.buildDraftFromResolvedConfig = buildDraftFromResolvedConfig; exports.buildTenantUrl = buildTenantUrl; exports.computeSsrEntryIntegrity = computeSsrEntryIntegrity; exports.computeSubresourceIntegrity = computeSubresourceIntegrity; exports.computeUiEntryIntegrity = computeUiEntryIntegrity; exports.createTenantConfigDraftSchema = createTenantConfigDraftSchema; exports.defaultTenantConfigDraftMessages = defaultTenantConfigDraftMessages; exports.diffDraft = diffDraft; exports.draftUiOverride = draftUiOverride; exports.emptyTenantConfigDraft = emptyTenantConfigDraft; exports.gatewayForAccount = gatewayForAccount; exports.isLocalHostname = isLocalHostname; exports.normalizeBundleBaseUrl = normalizeBundleBaseUrl; exports.resolveClientEntryUrl = resolveClientEntryUrl; exports.resolveServerEntryUrl = resolveServerEntryUrl; exports.tenantConfigDraftSchema = tenantConfigDraftSchema; exports.tenantLabel = tenantLabel; exports.verifySsrIntegrity = verifySsrIntegrity; exports.verifyUiIntegrity = verifyUiIntegrity; exports.verifyUiPin = verifyUiPin; //# sourceMappingURL=tenant.cjs.map