/* RFC-0565: dht.lookup command handler. Resolves a site id to its DHT entry by querying the DHT (or local cache if fresh). Validates entry signatures against the owner's public key. Routes around dead workshops detected by SWIM. Do not implement DHT routing — that lives in node.ts. Do not implement cache invalidation push — TTL-only per RFC-0565 design. Lookup queries the DHT first and falls back to the local cache on failure. RFC-0565: initial implementation — dht.lookup with cache and signature validation. 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 { DHTLookupResult, DHTSiteEntry } from "./types.ts"; import { dhtSiteEntrySchema } from "./types.ts"; import { loadDhtConfig } from "./config.ts"; import { getCachedEntry, setCachedEntry, loadCache } from "./cache.ts"; import { createDhtNode, startDhtNode, stopDhtNode, dhtGet } from "./node.ts"; import { 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"; 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); } /** * Check if a workshop endpoint is in the SWIM dead members list. * Returns true if the workshop is alive (or SWIM is not configured). */ async function isWorkshopAlive(_workspaceRoot: string, _endpoint: string): Promise { // In the pilot, SWIM dead-workshop detection is handled by the DHT routing // layer itself (it skips unreachable peers). This function is a seam for // future integration with SWIM membership view. return true; } export async function runDhtLookup( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const { workspaceRoot } = context; const siteId = input.flags["site-id"] as string | undefined; const forceRefresh = input.flags["force"] === true; if (!siteId) { return { data: { found: false, entry: null, hops: 0, latencyMs: 0, cached: false, signatureValid: false, diagnostics: ["dht.lookup: --site-id flag is required"], }, exitCode: 1, summary: "[dht.lookup] --site-id flag is required", nextSteps: [{ action: "Provide the --site-id flag and re-run dht.lookup", kind: "required" }], }; } const startTime = Date.now(); // 1. Check local cache first (unless --force) if (!forceRefresh) { const cache = await loadCache(workspaceRoot); const cached = getCachedEntry(cache, siteId); if (cached) { const alive = await isWorkshopAlive(workspaceRoot, cached.workshopEndpoint); if (alive) { return { data: { found: true, entry: cached, hops: 0, latencyMs: Date.now() - startTime, cached: true, signatureValid: true, }, exitCode: 0, summary: `[dht.lookup] cache hit for ${siteId}`, }; } } } // 2. Load DHT config let config; try { config = await loadDhtConfig(workspaceRoot); } catch { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, diagnostics: [`dht.lookup: no werkstatt.dht.json found — run dht.node.init first`], }, exitCode: 1, summary: `[dht.lookup] DHT not initialized — run dht.node.init first`, nextSteps: [{ action: "Run dht.node.init first, then re-run dht.lookup", kind: "required" }], }; } // 3. Load identity config let identity: WerkstattIdentityConfig; try { identity = await loadIdentityConfig(workspaceRoot); } catch { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, diagnostics: [ `dht.lookup: no ${IDENTITY_FILENAME} found — run identity.bootstrap first (RFC-0558)`, ], }, exitCode: 1, summary: `[dht.lookup] identity not bootstrapped`, nextSteps: [ { action: "Run identity.bootstrap first (RFC-0558), then re-run dht.lookup", kind: "required", }, ], }; } // 4. Create ephemeral DHT node and query let node; try { node = await createDhtNode(config, identity); await startDhtNode(node, config); } catch (err) { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, diagnostics: [ `dht.lookup: failed to start DHT node — ${err instanceof Error ? err.message : String(err)}`, ], }, exitCode: 1, summary: `[dht.lookup] DHT node startup failed`, nextSteps: [ { action: "Check the DHT configuration and network connectivity, then re-run dht.lookup", kind: "required", }, ], }; } try { const key = `site/${siteId}`; const valueBytes = await dhtGet(node, key, 3); if (!valueBytes) { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, }, exitCode: 0, summary: `[dht.lookup] site ${siteId} not found in DHT`, }; } // 5. Parse and validate the DHT entry let entry: DHTSiteEntry; try { const parsed = JSON.parse(new TextDecoder().decode(valueBytes)); entry = dhtSiteEntrySchema.parse(parsed); } catch { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, diagnostics: [`dht.lookup: DHT entry for ${siteId} is malformed`], }, exitCode: 1, summary: `[dht.lookup] malformed DHT entry for ${siteId}`, nextSteps: [ { action: `The DHT entry for ${siteId} is malformed — re-register the site with dht.register`, kind: "required", }, ], }; } // 6. Verify entry signature let signatureValid = false; try { signatureValid = await verifyDhtEntry(entry, identity.operatorKeyPair.publicKeyMultibase); } catch { signatureValid = false; } if (!signatureValid) { return { data: { found: true, entry, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: false, diagnostics: [`dht.lookup: DHT entry for ${siteId} has invalid signature — rejected`], }, exitCode: 1, summary: `[dht.lookup] invalid signature for ${siteId}`, nextSteps: [ { action: `The DHT entry for ${siteId} has an invalid signature — re-register the site with dht.register`, kind: "required", }, ], }; } // 7. Check if workshop is alive (SWIM integration) const alive = await isWorkshopAlive(workspaceRoot, entry.workshopEndpoint); if (!alive) { return { data: { found: false, entry: null, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: true, diagnostics: [ `dht.lookup: workshop ${entry.workshopEndpoint} is dead (SWIM) — routing around`, ], }, exitCode: 0, summary: `[dht.lookup] workshop dead, routing around ${siteId}`, }; } // 8. Cache the result await setCachedEntry(workspaceRoot, siteId, entry, config.cacheTtlMs); return { data: { found: true, entry, hops: 0, latencyMs: Date.now() - startTime, cached: false, signatureValid: true, }, exitCode: 0, summary: `[dht.lookup] found ${siteId} at ${entry.workshopEndpoint}`, }; } finally { await stopDhtNode(node); } }