Files
2026-07-21 04:24:39 +02:00

229 lines
15 KiB
TypeScript

/*
* SPEC: PJ2026-010102 HWPOD 工具;PJ2026-010103 HWPOD 服务;
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
* 责任: 为 L0-L3 提供唯一 host PostgreSQL runtime spec registry。
*/
import { createHash } from "node:crypto";
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import pg from "pg";
import { parse as parseYaml } from "yaml";
import { buildPostgresPoolConfig, postgresSslModeFromConnectionString } from "../db/runtime-store-core.ts";
import { normalizeHwpodSpec } from "./spec-document.ts";
const { Pool } = pg;
const CONTRACT_VERSION = "hwpod-spec-registry-v3";
type BuiltInInput = { configRef: string; sha256?: string; document: unknown };
export type HwpodRuntimeSpecRecord = { authority: "runtime"; hwpodId: string; createdAt: string; updatedAt: string; document: unknown };
export interface HwpodRuntimeSpecStore {
list(): Promise<HwpodRuntimeSpecRecord[]>;
get(hwpodId: string): Promise<HwpodRuntimeSpecRecord | null>;
create(record: HwpodRuntimeSpecRecord): Promise<boolean>;
update(hwpodId: string, document: unknown, updatedAt: string): Promise<HwpodRuntimeSpecRecord | null>;
delete(hwpodId: string): Promise<HwpodRuntimeSpecRecord | null>;
close?(): Promise<void>;
}
export function hwpodSpecRegistryFromEnv(env: Record<string, string | undefined> = process.env) {
const databaseUrl = requiredEnv(env, "HWPOD_SPEC_DATABASE_URL");
const schema = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_SCHEMA"), "HWPOD_SPEC_DATABASE_SCHEMA");
const table = postgresIdentifier(requiredEnv(env, "HWPOD_SPEC_DATABASE_TABLE"), "HWPOD_SPEC_DATABASE_TABLE");
const connectionTimeoutMs = positiveIntegerEnv(env, "HWPOD_SPEC_DATABASE_CONNECTION_TIMEOUT_MS");
const builtIns = parseBuiltIns(env.HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON, env.HWPOD_BUILTIN_SPEC_CONFIG_DIRECTORY);
return createHwpodSpecRegistry({ store: createPostgresHwpodRuntimeSpecStore({ databaseUrl, schema, table, connectionTimeoutMs }), builtIns });
}
export function createHwpodSpecRegistry(options: { store: HwpodRuntimeSpecStore; builtIns?: BuiltInInput[]; now?: () => string }) {
const store = options.store;
const now = options.now ?? (() => new Date().toISOString());
const builtIns = normalizeBuiltIns(options.builtIns ?? []);
async function list() {
const runtime = new Map((await store.list()).map((record) => [record.hwpodId, record]));
assertNoAuthorityConflicts(builtIns, runtime);
const specs = [...builtIns.values(), ...runtime.values()].map(summary).sort((left, right) => left.hwpodId.localeCompare(right.hwpodId));
return { ok: true, status: specs.length === 0 ? "empty" : "completed", contractVersion: CONTRACT_VERSION, count: specs.length, specs, valuesPrinted: false };
}
async function get(hwpodId: string) {
const id = validId(hwpodId);
const builtIn = builtIns.get(id);
if (builtIn) return entity(builtIn);
const runtime = await store.get(id);
if (!runtime) throw codedError("hwpod_spec_not_found", `HWPOD spec was not found: ${id}`, { hwpodId: id });
return entity(runtime);
}
async function create(documentInput: unknown) {
const normalized = normalizedDocument(documentInput, "runtime:create");
const id = specId(normalized);
assertNotFrozen(id, builtIns);
const observedAt = now();
const record: HwpodRuntimeSpecRecord = { authority: "runtime", hwpodId: id, createdAt: observedAt, updatedAt: observedAt, document: normalized };
if (!await store.create(record)) throw codedError("hwpod_spec_exists", `HWPOD runtime spec already exists: ${id}`, { hwpodId: id });
return mutation("created", record);
}
async function update(hwpodId: string, documentInput: unknown) {
const id = validId(hwpodId);
assertNotFrozen(id, builtIns);
const normalized = normalizedDocument(documentInput, `runtime:${id}`);
const documentId = specId(normalized);
if (documentId !== id) throw codedError("hwpod_spec_id_mismatch", `HWPOD spec document id ${documentId} does not match route id ${id}`, { hwpodId: id, documentHwpodId: documentId });
const record = await store.update(id, normalized, now());
if (!record) throw codedError("hwpod_spec_not_found", `HWPOD runtime spec was not found: ${id}`, { hwpodId: id });
return mutation("updated", record);
}
async function remove(hwpodId: string) {
const id = validId(hwpodId);
assertNotFrozen(id, builtIns);
const existing = await store.delete(id);
if (!existing) throw codedError("hwpod_spec_not_found", `HWPOD runtime spec was not found: ${id}`, { hwpodId: id });
return { ok: true, status: "deleted", contractVersion: CONTRACT_VERSION, hwpodId: id, authority: "runtime", mutable: true, frozen: false, mutation: true, previousFingerprint: documentFingerprint(existing.document), valuesPrinted: false };
}
return { list, get, create, update, delete: remove, close: async () => store.close?.() };
}
export function createPostgresHwpodRuntimeSpecStore(options: { databaseUrl: string; schema: string; table: string; connectionTimeoutMs: number }): HwpodRuntimeSpecStore {
const schema = postgresIdentifier(options.schema, "schema");
const table = postgresIdentifier(options.table, "table");
const qualifiedTable = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
const pool = new Pool(buildPostgresPoolConfig({
dbUrl: options.databaseUrl,
sslMode: postgresSslModeFromConnectionString(options.databaseUrl) ?? "disable",
timeoutMs: options.connectionTimeoutMs,
}));
let ready: Promise<void> | undefined;
function ensureSchema() {
ready ??= migrate();
return ready;
}
async function migrate() {
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(schema)}`);
await client.query(`CREATE TABLE IF NOT EXISTS ${qualifiedTable} (
hwpod_id TEXT PRIMARY KEY,
authority TEXT NOT NULL DEFAULT 'runtime' CHECK (authority = 'runtime'),
document JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
)`);
await client.query(`CREATE INDEX IF NOT EXISTS ${quoteIdentifier(`${table}_updated_at_idx`)} ON ${qualifiedTable} (updated_at DESC, hwpod_id)`);
await client.query("COMMIT");
} catch (error) {
await client.query("ROLLBACK").catch(() => {});
ready = undefined;
throw error;
} finally {
client.release();
}
}
return {
async list() {
await ensureSchema();
const result = await pool.query(`SELECT hwpod_id, authority, document, created_at, updated_at FROM ${qualifiedTable} ORDER BY hwpod_id`);
return result.rows.map(runtimeRow);
},
async get(hwpodId) {
await ensureSchema();
const result = await pool.query(`SELECT hwpod_id, authority, document, created_at, updated_at FROM ${qualifiedTable} WHERE hwpod_id=$1`, [hwpodId]);
return result.rows[0] ? runtimeRow(result.rows[0]) : null;
},
async create(record) {
await ensureSchema();
const result = await pool.query(`INSERT INTO ${qualifiedTable} (hwpod_id, authority, document, created_at, updated_at) VALUES ($1,'runtime',$2::jsonb,$3,$4) ON CONFLICT (hwpod_id) DO NOTHING RETURNING hwpod_id`, [record.hwpodId, JSON.stringify(record.document), record.createdAt, record.updatedAt]);
return result.rowCount === 1;
},
async update(hwpodId, document, updatedAt) {
await ensureSchema();
const result = await pool.query(`UPDATE ${qualifiedTable} SET document=$2::jsonb, updated_at=$3 WHERE hwpod_id=$1 AND authority='runtime' RETURNING hwpod_id, authority, document, created_at, updated_at`, [hwpodId, JSON.stringify(document), updatedAt]);
return result.rows[0] ? runtimeRow(result.rows[0]) : null;
},
async delete(hwpodId) {
await ensureSchema();
const result = await pool.query(`DELETE FROM ${qualifiedTable} WHERE hwpod_id=$1 AND authority='runtime' RETURNING hwpod_id, authority, document, created_at, updated_at`, [hwpodId]);
return result.rows[0] ? runtimeRow(result.rows[0]) : null;
},
close: async () => pool.end(),
};
}
function runtimeRow(row: any): HwpodRuntimeSpecRecord {
if (row?.authority !== "runtime") throw codedError("invalid_hwpod_runtime_spec", `runtime HWPOD spec authority is invalid: ${String(row?.hwpod_id ?? "<unknown>")}`);
const document = normalizedDocument(row.document, `postgres:${String(row.hwpod_id ?? "")}`);
const hwpodId = specId(document);
if (hwpodId !== row.hwpod_id) throw codedError("invalid_hwpod_runtime_spec", `runtime HWPOD spec identity is invalid: ${String(row.hwpod_id ?? "<unknown>")}`);
return { authority: "runtime", hwpodId, createdAt: isoTimestamp(row.created_at), updatedAt: isoTimestamp(row.updated_at), document };
}
function parseBuiltIns(raw: string | undefined, configDirectory: string | undefined): BuiltInInput[] {
if (String(raw ?? "").trim() && String(configDirectory ?? "").trim()) throw codedError("invalid_hwpod_builtin_specs", "HWPOD built-in specs must use either JSON or a config directory, not both");
if (String(configDirectory ?? "").trim()) return parseBuiltInConfigDirectory(String(configDirectory));
if (!String(raw ?? "").trim()) return [];
let value: unknown;
try { value = JSON.parse(String(raw)); }
catch (error: any) { throw codedError("invalid_hwpod_builtin_specs", `HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON is invalid JSON: ${error?.message ?? String(error)}`); }
if (!Array.isArray(value)) throw codedError("invalid_hwpod_builtin_specs", "HWPOD_BUILTIN_SPEC_DOCUMENTS_JSON must be an array");
return value.map((item: any, index) => {
if (!item || typeof item !== "object" || Array.isArray(item)) throw codedError("invalid_hwpod_builtin_specs", `built-in spec item ${index} must be an object`);
const configRef = String(item.configRef ?? "").trim();
if (!configRef) throw codedError("invalid_hwpod_builtin_specs", `built-in spec item ${index} is missing configRef`);
return { configRef, sha256: String(item.sha256 ?? "").trim() || undefined, document: item.document };
});
}
function parseBuiltInConfigDirectory(directoryInput: string): BuiltInInput[] {
const directory = path.resolve(directoryInput.trim());
return readdirSync(directory, { withFileTypes: true })
.filter((entry) => entry.isFile() && /\.ya?ml$/iu.test(entry.name))
.map((entry) => entry.name)
.sort()
.map((fileName) => {
const source = readFileSync(path.join(directory, fileName), "utf8");
const document = parseYaml(source);
return {
configRef: `configMap:hwlab-v03-hwpod-preinstalled-specs/${fileName}`,
sha256: createHash("sha256").update(stableJson(document)).digest("hex"),
document,
};
});
}
function normalizeBuiltIns(inputs: BuiltInInput[]) {
const records = new Map<string, any>();
for (const input of inputs) {
const document = normalizedDocument(input.document, input.configRef);
const hwpodId = specId(document);
if (records.has(hwpodId)) throw codedError("hwpod_spec_authority_conflict", `duplicate YAML-first built-in HWPOD spec: ${hwpodId}`, { hwpodId });
records.set(hwpodId, { authority: "yaml-first-builtin", hwpodId, configRef: input.configRef, sourceFingerprint: input.sha256 ?? null, document });
}
return records;
}
function requiredEnv(env: Record<string, string | undefined>, key: string) { const value = String(env[key] ?? "").trim(); if (!value) throw codedError("hwpod_spec_database_config_required", `${key} is required`); return value; }
function positiveIntegerEnv(env: Record<string, string | undefined>, key: string) { const value = Number(requiredEnv(env, key)); if (!Number.isSafeInteger(value) || value <= 0) throw codedError("invalid_hwpod_spec_database_config", `${key} must be a positive integer`); return value; }
function postgresIdentifier(value: string, name: string) { if (!/^[a-z_][a-z0-9_]*$/u.test(value)) throw codedError("invalid_hwpod_spec_database_identifier", `${name} must be a lowercase PostgreSQL identifier`); return value; }
function quoteIdentifier(value: string) { return `"${postgresIdentifier(value, "identifier")}"`; }
function isoTimestamp(value: unknown) { const date = value instanceof Date ? value : new Date(String(value ?? "")); if (!Number.isFinite(date.getTime())) throw codedError("invalid_hwpod_runtime_spec", "runtime HWPOD spec timestamp is invalid"); return date.toISOString(); }
function normalizedDocument(value: unknown, source: string) { return normalizeHwpodSpec(value, source); }
function specId(document: any) { return validId(String(document?.metadata?.name ?? document?.metadata?.uid ?? "")); }
function validId(value: string) { const id = String(value ?? "").trim(); if (!/^[A-Za-z0-9_.:-]{1,120}$/u.test(id)) throw codedError("invalid_hwpod_spec_id", `invalid HWPOD spec id: ${id || "<empty>"}`); return id; }
function assertNotFrozen(id: string, builtIns: Map<string, any>) { const item = builtIns.get(id); if (item) throw codedError("hwpod_spec_frozen", `YAML-first built-in HWPOD spec is frozen: ${id}`, { hwpodId: id, authority: item.authority, configRef: item.configRef, mutable: false, frozen: true, documentFingerprint: documentFingerprint(item.document), mutation: false }); }
function assertNoAuthorityConflicts(builtIns: Map<string, any>, runtime: Map<string, HwpodRuntimeSpecRecord>) { for (const id of runtime.keys()) if (builtIns.has(id)) throw codedError("hwpod_spec_authority_conflict", `runtime HWPOD spec conflicts with YAML-first built-in spec: ${id}`, { hwpodId: id }); }
function summary(record: any) { const document = record.document; return { hwpodId: record.hwpodId, authority: record.authority, mutable: record.authority === "runtime", frozen: record.authority !== "runtime", documentFingerprint: documentFingerprint(document), ...(record.configRef ? { configRef: record.configRef, sourceFingerprint: record.sourceFingerprint } : { createdAt: record.createdAt, updatedAt: record.updatedAt }), nodeId: document.spec.nodeBinding.nodeId, workspacePath: document.spec.workspace.path }; }
function entity(record: any) { return { ok: true, status: "completed", contractVersion: CONTRACT_VERSION, ...summary(record), document: record.document, valuesPrinted: false }; }
function mutation(status: "created" | "updated", record: HwpodRuntimeSpecRecord) { return { ...entity(record), status, mutation: true }; }
function documentFingerprint(document: unknown) { return `sha256:${createHash("sha256").update(stableJson(document)).digest("hex")}`; }
function stableJson(value: unknown): string { if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; if (value && typeof value === "object") return `{${Object.keys(value as any).sort().map((key) => `${JSON.stringify(key)}:${stableJson((value as any)[key])}`).join(",")}}`; return JSON.stringify(value) ?? "null"; }
function codedError(code: string, message: string, details?: unknown) { return Object.assign(new Error(message), { code, details }); }