/* RFC-0565: dht.register command handler. Publishes a local registry entry to the DHT. Signs the entry with the operator's Ed25519 keypair. Uses LWW (last- writer-wins) on lastUpdated timestamp for conflict resolution, with owner- signature priority for equal timestamps. Do not implement DHT routing — that lives in node.ts. Do not implement local registry management — the local registry is authoritative. Registry entries are signed before publish — unsigned entries are rejected by consumers. RFC-0565: initial implementation — dht.register with LWW conflict resolution. RFC-1097: step 6 — compass.migrate codemod run Mechanical v1 to v2 header migration across the workspace: 942 files rewritten — CHANGE_SUMMARY windows collapsed into history, forbidden v1 blocks stripped, KEY_DECISIONS seeded from @ai-invariant comments (5 files) or TODO placeholders (103 files), blocks reordered to canonical order. RFC-1097: sweep — werkstatt-engine clean Sweep batch 4: 73 Compass headers on headerless engine files (certification, component-runtime, isolation, evolution, testing), real KEY_DECISIONS on 75 files (kernel, cache, dht, swim, gitmesh, runtime), ~80 purpose expansions (CONTRACT-02/PURPOSE-02), non-goals on 13 CONTRACT-03 files, CS-07 history literal fix repo-wide (253 files). Policy: .template.ts/.template.astro excludedPaths. werkstatt-engine now 0 diagnostics. */ import { join } from "node:path"; import type { KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-shared/kernel"; import type { DHTSiteEntry } from "./types.ts"; import { dhtSiteEntrySchema } from "./types.ts"; import { loadDhtConfig } from "./config.ts"; import { clearCachedEntry } from "./cache.ts"; import { createDhtNode, startDhtNode, stopDhtNode, dhtPut, dhtGet } from "./node.ts"; import { signDhtEntry, verifyDhtEntry } from "@warpgogol/werkstatt-shared/passport/dht-sign"; import { WerkstattIdentityConfigSchema, type WerkstattIdentityConfig, } from "@warpgogol/werkstatt-shared/passport"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const IDENTITY_FILENAME = "werkstatt.identity.json"; const WERKSTATT_OPERATOR_SIGNING_KEY_ENV = "WERKSTATT_OPERATOR_SIGNING_KEY"; interface DhtRegisterResult { registered: boolean; siteId: string; key: string; conflictResolved: boolean; diagnostics?: string[]; } async function loadIdentityConfig( workspaceRoot: string, io: WorkspaceIO = createDefaultIO().io, ): Promise { const identityPath = join(workspaceRoot, IDENTITY_FILENAME); const raw = await io.readFile(identityPath); const parsed = JSON.parse(raw); return WerkstattIdentityConfigSchema.parse(parsed); } function getSigningKey(): string { const key = process.env[WERKSTATT_OPERATOR_SIGNING_KEY_ENV]; if (!key || key.length === 0) { throw new Error(`${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable is not set`); } return key; } /** * Compare two DHT entries by LWW on lastUpdated timestamp. * For equal timestamps, owner-signature priority: the entry with a valid * signature from the declared owner wins. If both have valid signatures, * the existing entry wins (conservative — avoids flapping). */ function resolveConflict( existing: DHTSiteEntry, candidate: DHTSiteEntry, ): { winner: DHTSiteEntry; reason: "newer" | "equal-existing" } { const existingTime = new Date(existing.lastUpdated).getTime(); const candidateTime = new Date(candidate.lastUpdated).getTime(); if (candidateTime > existingTime) { return { winner: candidate, reason: "newer" }; } // Equal or older — keep existing (conservative LWW) return { winner: existing, reason: "equal-existing" }; } export async function runDhtRegister( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot } = context; const siteId = input.flags["site-id"] as string | undefined; const owner = input.flags["owner"] as string | undefined; const workshopEndpoint = input.flags["endpoint"] as string | undefined; const mirrorsFlag = input.flags["mirrors"] as string | string[] | undefined; if (!siteId) { return { data: { registered: false, siteId: "", key: "", conflictResolved: false, diagnostics: ["dht.register: --site-id flag is required"], }, exitCode: 1, summary: "[dht.register] --site-id flag is required", nextSteps: [ { action: "Provide the --site-id flag and re-run dht.register", kind: "required" }, ], }; } if (!owner) { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: ["dht.register: --owner flag is required (did:web identifier)"], }, exitCode: 1, summary: "[dht.register] --owner flag is required", nextSteps: [ { action: "Provide the --owner flag (did:web identifier) and re-run dht.register", kind: "required", }, ], }; } if (!workshopEndpoint) { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: ["dht.register: --endpoint flag is required (host:port)"], }, exitCode: 1, summary: "[dht.register] --endpoint flag is required", nextSteps: [ { action: "Provide the --endpoint flag (host:port) and re-run dht.register", kind: "required", }, ], }; } const mirrors = Array.isArray(mirrorsFlag) ? mirrorsFlag : mirrorsFlag ? [mirrorsFlag] : []; // Load identity config let identity: WerkstattIdentityConfig; try { identity = await loadIdentityConfig(workspaceRoot); } catch { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: [ `dht.register: no ${IDENTITY_FILENAME} found — run identity.bootstrap first (RFC-0558)`, ], }, exitCode: 1, summary: `[dht.register] identity not bootstrapped`, nextSteps: [ { action: "Run identity.bootstrap first (RFC-0558), then re-run dht.register", kind: "required", }, ], }; } // Get signing key let signingKey: string; try { signingKey = getSigningKey(); } catch { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: [ `dht.register: ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable is not set`, ], }, exitCode: 1, summary: `[dht.register] ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} not set`, nextSteps: [ { action: `Set the ${WERKSTATT_OPERATOR_SIGNING_KEY_ENV} environment variable and re-run dht.register`, kind: "required", }, ], }; } // Load DHT config let config; try { config = await loadDhtConfig(workspaceRoot); } catch { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: [`dht.register: no werkstatt.dht.json found — run dht.node.init first`], }, exitCode: 1, summary: `[dht.register] DHT not initialized`, nextSteps: [ { action: "Run dht.node.init first, then re-run dht.register", kind: "required" }, ], }; } // Build the DHT entry const now = new Date().toISOString(); const entryData = { siteId, owner, workshopEndpoint, mirrors, registeredAt: now, lastUpdated: now, }; // Sign the entry let signature: string; try { signature = await signDhtEntry(entryData, signingKey); } catch (err) { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: [ `dht.register: failed to sign entry — ${err instanceof Error ? err.message : String(err)}`, ], }, exitCode: 1, summary: `[dht.register] signing failed`, nextSteps: [ { action: "Check the WERKSTATT_OPERATOR_SIGNING_KEY environment variable and re-run dht.register", kind: "required", }, ], }; } const candidateEntry: DHTSiteEntry = { ...entryData, signature }; // Create ephemeral DHT node let node; try { node = await createDhtNode(config, identity); await startDhtNode(node, config); } catch (err) { return { data: { registered: false, siteId, key: "", conflictResolved: false, diagnostics: [ `dht.register: failed to start DHT node — ${err instanceof Error ? err.message : String(err)}`, ], }, exitCode: 1, summary: `[dht.register] DHT node startup failed`, nextSteps: [ { action: "Check the DHT configuration and network connectivity, then re-run dht.register", kind: "required", }, ], }; } try { const key = `site/${siteId}`; // Check for existing entry (LWW conflict resolution) let conflictResolved = false; const entryToPublish = candidateEntry; const existingBytes = await dhtGet(node, key, 3); if (existingBytes) { try { const existingParsed = JSON.parse(new TextDecoder().decode(existingBytes)); const existingEntry = dhtSiteEntrySchema.parse(existingParsed); // Verify existing entry signature before considering it for conflict resolution const existingSigValid = await verifyDhtEntry( existingEntry, identity.operatorKeyPair.publicKeyMultibase, ); if (existingSigValid) { const { winner, reason } = resolveConflict(existingEntry, candidateEntry); if (winner === candidateEntry) { conflictResolved = true; } else { // Existing entry wins — don't overwrite return { data: { registered: false, siteId, key, conflictResolved: true, diagnostics: [ `dht.register: existing entry is newer or equal (LWW: ${reason}) — not overwriting`, ], }, exitCode: 0, summary: `[dht.register] existing entry wins (LWW ${reason})`, }; } } // If existing entry has invalid signature, overwrite with our valid entry } catch { // Existing entry is malformed — overwrite with our valid entry conflictResolved = true; } } // Publish to DHT const valueBytes = new TextEncoder().encode(JSON.stringify(entryToPublish)); await dhtPut(node, key, valueBytes); // Invalidate cache entry for this site (entry has changed) await clearCachedEntry(workspaceRoot, siteId); return { data: { registered: true, siteId, key, conflictResolved, }, exitCode: 0, summary: `[dht.register] published ${siteId} to DHT key ${key}`, }; } finally { await stopDhtNode(node); } }