/** * Well-Known Capabilities Registry * Defines standardized capabilities with predefined hostnames, zones, and schemas * Hardcoded registry (extensibility deferred to future) */ /** * Network zones define security boundaries and deployment locations * * Home Lab Zones (VLAN-based): * - dmz: Public-facing services in home lab (VLAN 10, e.g., 10.0.10.0/24) * - app: Internal services in home lab (VLAN 20, e.g., 10.0.20.0/24) * - secure: Auth/DB in home lab (VLAN 30, e.g., 10.0.30.0/24) * * Control Plane: * - secure-mgmt: Where celilo's own management server and management-plane modules * run. A placement zone AND the control-plane tier: it sits outside the * dmz->app->secure data-plane chain and reaches every data-plane tier by trust * instead. Modules placed here inherit that reach, so placement is a privilege * decision. celilo-mgmt may equally run in `internal`. * * Semi-trusted LAN: * - internal: Behind the firewall (NAT outbound, port-forward inbound), shielded * from the uncontrolled outside internet. Not firewall-segmented. * * External Zone (Cloud/VPS): * - external: Services hosted outside home network (no VLAN, e.g., VPS on internet) * * Re-exported from db/schema rather than redeclared: this file used to carry its * own hand-written copy of the union, and a second hand-maintained copy is what * let `secure-mgmt` go missing in other places (see NETWORK_ZONES' comment). */ import type { NetworkZone } from '../db/schema'; export type { NetworkZone }; export interface WellKnownCapability { canonical_hostname: string; required_zone: NetworkZone; zone_enforced: boolean; // Always true data_schema: Record; } /** * Registry of well-known capabilities * These capabilities have standardized contracts enforced by Celilo */ export const WELL_KNOWN_CAPABILITIES: Record = { /** * public_web - Public-facing web server * Example: Caddy reverse proxy * Security: MUST be in DMZ zone (internet-facing) */ public_web: { canonical_hostname: 'www', required_zone: 'dmz', zone_enforced: true, data_schema: { server: { ip: { primary: '$self:target_ip', }, port: 443, }, }, }, /** * dns_registrar - Domain registrar with Dynamic DNS support * Example: Namecheap Dynamic DNS * Security: Zone-agnostic (no infrastructure, just API calls) */ dns_registrar: { canonical_hostname: 'dns-reg', required_zone: 'dmz', zone_enforced: false, data_schema: { // The capability contract no longer carries a `primary_domain` // convenience field. Consumers that want a default index into // `domains[]` themselves; consumers that need to commit to a // specific domain (lunacycle, authentik, knot-unbound-internal, // technitium, etc.) declare an explicit user-set `domain` // variable in their own manifest. The implicit primary-domain // hand-off too easily gave a module the household's first // registered domain without anyone deciding to. provider: 'namecheap', domains: '$self:domains', supports: ['dynamic_dns_a_record'], }, }, /** * dns_internal - Internal DNS resolver * Example: Technitium / knot-unbound for split-horizon DNS * Security: a PROTECTED zone (dmz), NOT `internal` (ISS-0156, * openspec/specs/internal-dns-zone-views/spec.md). The resolver must see each querying client's * real source IP to serve source-based split-horizon views; fw-main NATs * protected↔`internal`, so an `internal`-placed resolver sees every protected * query as fw-main's address and can't tell the zones apart. Placed in `dmz` * (a protected zone — protected↔protected is not NAT'd) it sees real * protected-zone sources; `internal` devices reach it via a firewall * DNS-ingress DNAT. The operator accepted the modest posture change (the * `internal` zone is itself un-managed). zone_enforced stays true — just to dmz. */ dns_internal: { canonical_hostname: 'dns-int', required_zone: 'dmz', zone_enforced: true, data_schema: { server: { ip: { primary: '$self:target_ip', }, port: 53, }, }, }, /** * auth - Authentication/Identity Provider * Example: Authentik, Keycloak * Security: MUST be in secure zone (handles authentication) * * Note: `oidc.issuer_url` is no longer derived here. Per the firm rule in * openspec/changes/manifest-v2/proposal.md D9, well-known capability data * templates must not cross-reference other capabilities. The IDP-providing * module declares an explicit user-set `domain` field in its own manifest, * derives `auth_url` from `$self:domain`, and exposes it through * `provides.capabilities[].data`. */ auth: { canonical_hostname: 'auth', required_zone: 'secure', zone_enforced: true, data_schema: { server: { ip: { primary: '$self:target_ip', }, port: 9000, }, }, }, /** * database - Database server * Example: PostgreSQL, MySQL, MongoDB * Security: Recommended secure zone (sensitive data) */ database: { canonical_hostname: 'db', required_zone: 'secure', zone_enforced: true, data_schema: { server: { ip: { primary: '$self:target_ip', }, port: '$self:port', // Database-specific port }, connection: { host: '$self:target_ip', port: '$self:port', name: '$self:database_name', }, }, }, /** * dhcp_server - DHCP server with configurable DNS * Example: ISP router (GreenWave C4000XG) DHCP service * Security: Zone-agnostic (external service, no infrastructure) */ dhcp_server: { canonical_hostname: 'dhcp', required_zone: 'internal', zone_enforced: false, data_schema: { router_ip: '$self:router_ip', }, }, }; /** * Check if a capability name is well-known */ export function isWellKnown(capabilityName: string): boolean { return capabilityName in WELL_KNOWN_CAPABILITIES; } /** * Get well-known capability metadata * Throws error if capability is not well-known */ export function getWellKnownCapability(capabilityName: string): WellKnownCapability { if (!isWellKnown(capabilityName)) { throw new Error( `Unknown capability: ${capabilityName}. Supported capabilities: ${getSupportedCapabilities().join(', ')}`, ); } return WELL_KNOWN_CAPABILITIES[capabilityName]; } /** * Get list of all supported capability names */ export function getSupportedCapabilities(): string[] { return Object.keys(WELL_KNOWN_CAPABILITIES); } /** * Validation result for zone requirements */ export interface ZoneValidationResult { valid: boolean; error?: string; required_zone?: NetworkZone; } /** * Validate that module zone matches capability requirement * Returns validation result with error message if mismatch */ export function validateZoneRequirement( capabilityName: string, moduleZone: NetworkZone, ): ZoneValidationResult { if (!isWellKnown(capabilityName)) { return { valid: false, error: `Unknown capability: ${capabilityName}. Supported capabilities: ${getSupportedCapabilities().join(', ')}`, }; } const capability = WELL_KNOWN_CAPABILITIES[capabilityName]; if (capability.zone_enforced && moduleZone !== capability.required_zone) { return { valid: false, required_zone: capability.required_zone, error: `Capability '${capabilityName}' requires zone='${capability.required_zone}' (security requirement). Module specifies zone='${moduleZone}'.`, }; } return { valid: true }; }