78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
import type { DbClient } from "@hivegrid/db/client.server";
|
|
|
|
export type LocalCredentialsEnvironment = {
|
|
ADMIN_APP_HOST?: string;
|
|
CI?: string;
|
|
DATABASE_URL?: string;
|
|
LOCAL_DEV_CREDENTIALS?: string;
|
|
NODE_ENV?: string;
|
|
RLS_RUNTIME_MODE?: string;
|
|
SENTRY_ENVIRONMENT?: string;
|
|
};
|
|
|
|
const LOCAL_DATABASE_HOSTS = new Set(["127.0.0.1", "[::1]", "localhost"]);
|
|
const LOCAL_ADMIN_HOST_PATTERN = /^admin\.localhost(?::\d+)?$/i;
|
|
const LOCAL_DATABASE_MARKER = "app.local_dev_credentials";
|
|
const LOCAL_DATABASE_MARKER_VALUE = "enabled";
|
|
|
|
/**
|
|
* Local credential normalization requires independent process, URL, and
|
|
* database-side proof. A loopback SSH tunnel must never be sufficient.
|
|
*/
|
|
export function assertLocalCredentialsEnvironment(env: LocalCredentialsEnvironment = process.env): URL {
|
|
if (env.LOCAL_DEV_CREDENTIALS !== "1") {
|
|
throw new Error("Local credential normalization requires the canonical local launcher.");
|
|
}
|
|
if (env.NODE_ENV !== "development") {
|
|
throw new Error("Local credential normalization requires NODE_ENV=development.");
|
|
}
|
|
if (env.CI?.trim()) {
|
|
throw new Error("Local credential normalization is disabled in CI.");
|
|
}
|
|
if (env.SENTRY_ENVIRONMENT?.trim()) {
|
|
throw new Error("Local credential normalization is disabled for deployed Sentry environments.");
|
|
}
|
|
if (env.RLS_RUNTIME_MODE?.trim().toLowerCase() === "required") {
|
|
throw new Error("Local credential normalization is disabled when RLS runtime mode is required.");
|
|
}
|
|
if (!LOCAL_ADMIN_HOST_PATTERN.test(env.ADMIN_APP_HOST?.trim() ?? "")) {
|
|
throw new Error("Local credential normalization requires the admin.localhost host.");
|
|
}
|
|
|
|
let databaseUrl: URL;
|
|
try {
|
|
databaseUrl = new URL(env.DATABASE_URL ?? "");
|
|
} catch {
|
|
throw new Error("Local credential normalization requires a valid DATABASE_URL.");
|
|
}
|
|
|
|
// node-postgres accepts query parameters that can override the URI host.
|
|
if (databaseUrl.search !== "") {
|
|
throw new Error("Local credential normalization does not allow PostgreSQL connection query parameters.");
|
|
}
|
|
if (!new Set(["postgres:", "postgresql:"]).has(databaseUrl.protocol) || !LOCAL_DATABASE_HOSTS.has(databaseUrl.hostname)) {
|
|
throw new Error("Local credential normalization requires a loopback PostgreSQL database.");
|
|
}
|
|
|
|
return databaseUrl;
|
|
}
|
|
|
|
export async function assertLocalCredentialsDatabaseMarker(client: Pick<DbClient, "$queryRaw">): Promise<void> {
|
|
const markerRows = await client.$queryRaw<Array<{ marked: boolean }>>`
|
|
SELECT EXISTS (
|
|
SELECT 1
|
|
FROM pg_catalog.pg_db_role_setting AS setting
|
|
JOIN pg_catalog.pg_database AS database
|
|
ON database.oid = setting.setdatabase
|
|
WHERE database.datname = current_database()
|
|
AND setting.setrole = 0
|
|
AND ${`${LOCAL_DATABASE_MARKER}=${LOCAL_DATABASE_MARKER_VALUE}`} = ANY(setting.setconfig)
|
|
) AS marked
|
|
`;
|
|
if (markerRows[0]?.marked !== true) {
|
|
throw new Error(
|
|
"Local credential normalization requires a database marked as disposable local development data. Run pnpm dev:services:mark-local only for the repository's local Postgres container."
|
|
);
|
|
}
|
|
}
|