@celilo/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +1566 -0
  2. package/bin/celilo +16 -0
  3. package/drizzle/0000_complex_puma.sql +179 -0
  4. package/drizzle/0001_dizzy_wolfpack.sql +2 -0
  5. package/drizzle/0002_web_routes.sql +16 -0
  6. package/drizzle/0003_backup_storage.sql +32 -0
  7. package/drizzle/meta/0000_snapshot.json +1151 -0
  8. package/drizzle/meta/0001_snapshot.json +1167 -0
  9. package/drizzle/meta/0002_snapshot.json +1257 -0
  10. package/drizzle/meta/_journal.json +27 -0
  11. package/package.json +64 -0
  12. package/schemas/system_config.json +106 -0
  13. package/src/__integration__/container-services-cli.integration.test.ts +246 -0
  14. package/src/ansible/dependencies.test.ts +309 -0
  15. package/src/ansible/dependencies.ts +896 -0
  16. package/src/ansible/inventory.test.ts +463 -0
  17. package/src/ansible/inventory.ts +445 -0
  18. package/src/ansible/secrets.ts +222 -0
  19. package/src/ansible/validation.test.ts +92 -0
  20. package/src/ansible/validation.ts +272 -0
  21. package/src/api-clients/digitalocean.ts +94 -0
  22. package/src/api-clients/proxmox.ts +655 -0
  23. package/src/capabilities/logging-wrapper.test.ts +217 -0
  24. package/src/capabilities/lookup.test.ts +149 -0
  25. package/src/capabilities/lookup.ts +89 -0
  26. package/src/capabilities/public-web-helpers.test.ts +198 -0
  27. package/src/capabilities/public-web-publish.test.ts +458 -0
  28. package/src/capabilities/registration.test.ts +395 -0
  29. package/src/capabilities/registration.ts +200 -0
  30. package/src/capabilities/route-validation.test.ts +121 -0
  31. package/src/capabilities/route-validation.ts +96 -0
  32. package/src/capabilities/secret-ref.test.ts +313 -0
  33. package/src/capabilities/secret-validation.ts +157 -0
  34. package/src/capabilities/secrets.test.ts +750 -0
  35. package/src/capabilities/secrets.ts +244 -0
  36. package/src/capabilities/validation.test.ts +613 -0
  37. package/src/capabilities/validation.ts +160 -0
  38. package/src/capabilities/well-known.test.ts +238 -0
  39. package/src/capabilities/well-known.ts +222 -0
  40. package/src/cli/cli.test.ts +654 -0
  41. package/src/cli/command-registry.ts +742 -0
  42. package/src/cli/command-tree-parser.test.ts +180 -0
  43. package/src/cli/command-tree-parser.ts +193 -0
  44. package/src/cli/commands/backup-create.ts +137 -0
  45. package/src/cli/commands/backup-delete.ts +74 -0
  46. package/src/cli/commands/backup-import.ts +97 -0
  47. package/src/cli/commands/backup-list.ts +132 -0
  48. package/src/cli/commands/backup-name.ts +73 -0
  49. package/src/cli/commands/backup-prune.ts +98 -0
  50. package/src/cli/commands/backup-restore.ts +122 -0
  51. package/src/cli/commands/capability-info.ts +121 -0
  52. package/src/cli/commands/capability-list.ts +47 -0
  53. package/src/cli/commands/completion.ts +87 -0
  54. package/src/cli/commands/hook-run.ts +176 -0
  55. package/src/cli/commands/ipam.ts +607 -0
  56. package/src/cli/commands/machine-add.ts +235 -0
  57. package/src/cli/commands/machine-earmark.ts +82 -0
  58. package/src/cli/commands/machine-list.ts +77 -0
  59. package/src/cli/commands/machine-remove.ts +90 -0
  60. package/src/cli/commands/machine-status.ts +131 -0
  61. package/src/cli/commands/module-audit.ts +51 -0
  62. package/src/cli/commands/module-build.ts +60 -0
  63. package/src/cli/commands/module-config.ts +170 -0
  64. package/src/cli/commands/module-deploy.ts +71 -0
  65. package/src/cli/commands/module-generate.ts +236 -0
  66. package/src/cli/commands/module-health.ts +108 -0
  67. package/src/cli/commands/module-import.ts +80 -0
  68. package/src/cli/commands/module-list.ts +43 -0
  69. package/src/cli/commands/module-logs.ts +73 -0
  70. package/src/cli/commands/module-remove.ts +162 -0
  71. package/src/cli/commands/module-show.ts +208 -0
  72. package/src/cli/commands/module-status.ts +131 -0
  73. package/src/cli/commands/module-types.ts +189 -0
  74. package/src/cli/commands/module-upgrade.ts +192 -0
  75. package/src/cli/commands/package.ts +68 -0
  76. package/src/cli/commands/secret-list.ts +99 -0
  77. package/src/cli/commands/secret-set.ts +134 -0
  78. package/src/cli/commands/service-add-digitalocean.ts +133 -0
  79. package/src/cli/commands/service-add-proxmox.ts +342 -0
  80. package/src/cli/commands/service-config-get.ts +83 -0
  81. package/src/cli/commands/service-config-set.ts +145 -0
  82. package/src/cli/commands/service-list.ts +74 -0
  83. package/src/cli/commands/service-reconfigure.ts +230 -0
  84. package/src/cli/commands/service-remove.ts +103 -0
  85. package/src/cli/commands/service-verify.ts +240 -0
  86. package/src/cli/commands/status.ts +216 -0
  87. package/src/cli/commands/storage-add-local.ts +106 -0
  88. package/src/cli/commands/storage-add-s3.ts +114 -0
  89. package/src/cli/commands/storage-list.ts +72 -0
  90. package/src/cli/commands/storage-remove.ts +54 -0
  91. package/src/cli/commands/storage-set-default.ts +44 -0
  92. package/src/cli/commands/storage-verify.ts +54 -0
  93. package/src/cli/commands/system-config.ts +168 -0
  94. package/src/cli/commands/system-init.ts +314 -0
  95. package/src/cli/commands/system-secret-get.ts +98 -0
  96. package/src/cli/commands/system-secret-set.ts +76 -0
  97. package/src/cli/commands/system-vault-password.ts +34 -0
  98. package/src/cli/completion.test.ts +37 -0
  99. package/src/cli/completion.ts +482 -0
  100. package/src/cli/fuel-gauge.test.ts +208 -0
  101. package/src/cli/fuel-gauge.ts +405 -0
  102. package/src/cli/generate-zsh-completion.test.ts +95 -0
  103. package/src/cli/generate-zsh-completion.ts +497 -0
  104. package/src/cli/index.ts +1583 -0
  105. package/src/cli/interactive-config.test.ts +201 -0
  106. package/src/cli/interactive-config.ts +62 -0
  107. package/src/cli/parser.test.ts +227 -0
  108. package/src/cli/parser.ts +244 -0
  109. package/src/cli/prompts.test.ts +33 -0
  110. package/src/cli/prompts.ts +121 -0
  111. package/src/cli/types.ts +38 -0
  112. package/src/cli/validators.test.ts +235 -0
  113. package/src/cli/validators.ts +188 -0
  114. package/src/config/env.ts +41 -0
  115. package/src/config/paths.test.ts +172 -0
  116. package/src/config/paths.ts +108 -0
  117. package/src/db/client.ts +190 -0
  118. package/src/db/migrate.ts +30 -0
  119. package/src/db/schema.test.ts +221 -0
  120. package/src/db/schema.ts +434 -0
  121. package/src/hooks/capability-loader-firewall.test.ts +246 -0
  122. package/src/hooks/capability-loader.test.ts +100 -0
  123. package/src/hooks/capability-loader.ts +520 -0
  124. package/src/hooks/define-hook.test.ts +488 -0
  125. package/src/hooks/executor.test.ts +462 -0
  126. package/src/hooks/executor.ts +469 -0
  127. package/src/hooks/logger.test.ts +54 -0
  128. package/src/hooks/logger.ts +95 -0
  129. package/src/hooks/test-fixtures/failing-hook.ts +13 -0
  130. package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
  131. package/src/hooks/test-fixtures/success-hook.ts +20 -0
  132. package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
  133. package/src/hooks/test-fixtures/void-hook.ts +13 -0
  134. package/src/hooks/types.ts +89 -0
  135. package/src/infrastructure/property-extractor.test.ts +194 -0
  136. package/src/infrastructure/property-extractor.ts +151 -0
  137. package/src/ipam/allocator.test.ts +442 -0
  138. package/src/ipam/allocator.ts +369 -0
  139. package/src/ipam/auto-allocator.test.ts +247 -0
  140. package/src/ipam/auto-allocator.ts +270 -0
  141. package/src/ipam/subnet-parser.test.ts +107 -0
  142. package/src/ipam/subnet-parser.ts +136 -0
  143. package/src/manifest/contracts/index.ts +61 -0
  144. package/src/manifest/contracts/v1.ts +118 -0
  145. package/src/manifest/json-schema-roundtrip.test.ts +99 -0
  146. package/src/manifest/schema.ts +367 -0
  147. package/src/manifest/template-validator.test.ts +231 -0
  148. package/src/manifest/template-validator.ts +322 -0
  149. package/src/manifest/validate.test.ts +1180 -0
  150. package/src/manifest/validate.ts +415 -0
  151. package/src/module/import.test.ts +355 -0
  152. package/src/module/import.ts +676 -0
  153. package/src/module/packaging/audit.ts +169 -0
  154. package/src/module/packaging/build.ts +228 -0
  155. package/src/module/packaging/checksum.ts +41 -0
  156. package/src/module/packaging/extract.ts +234 -0
  157. package/src/module/packaging/signature.ts +47 -0
  158. package/src/secrets/encryption.test.ts +284 -0
  159. package/src/secrets/encryption.ts +162 -0
  160. package/src/secrets/generators.test.ts +112 -0
  161. package/src/secrets/generators.ts +127 -0
  162. package/src/secrets/master-key.test.ts +159 -0
  163. package/src/secrets/master-key.ts +114 -0
  164. package/src/secrets/storage.test.ts +115 -0
  165. package/src/secrets/storage.ts +106 -0
  166. package/src/secrets/vault.test.ts +35 -0
  167. package/src/secrets/vault.ts +42 -0
  168. package/src/services/backup-create.ts +532 -0
  169. package/src/services/backup-metadata.ts +198 -0
  170. package/src/services/backup-restore.ts +229 -0
  171. package/src/services/backup-retention.ts +84 -0
  172. package/src/services/backup-storage.ts +281 -0
  173. package/src/services/build-stream.test.ts +122 -0
  174. package/src/services/build-stream.ts +201 -0
  175. package/src/services/config-interview.ts +694 -0
  176. package/src/services/container-service.test.ts +298 -0
  177. package/src/services/container-service.ts +401 -0
  178. package/src/services/cross-module-data-manager.test.ts +405 -0
  179. package/src/services/cross-module-data-manager.ts +412 -0
  180. package/src/services/deploy-ansible.ts +88 -0
  181. package/src/services/deploy-planner.ts +153 -0
  182. package/src/services/deploy-preflight.ts +274 -0
  183. package/src/services/deploy-ssh.ts +131 -0
  184. package/src/services/deploy-terraform.test.ts +55 -0
  185. package/src/services/deploy-terraform.ts +445 -0
  186. package/src/services/deploy-validation.ts +311 -0
  187. package/src/services/dns-auto-register.ts +211 -0
  188. package/src/services/health-runner.ts +184 -0
  189. package/src/services/infrastructure-selector.test.ts +485 -0
  190. package/src/services/infrastructure-selector.ts +245 -0
  191. package/src/services/infrastructure-variable-resolver.test.ts +751 -0
  192. package/src/services/infrastructure-variable-resolver.ts +234 -0
  193. package/src/services/machine-detector.ts +328 -0
  194. package/src/services/machine-pool.test.ts +405 -0
  195. package/src/services/machine-pool.ts +316 -0
  196. package/src/services/manifest-validation.ts +120 -0
  197. package/src/services/module-build.test.ts +290 -0
  198. package/src/services/module-build.ts +431 -0
  199. package/src/services/module-config.test.ts +237 -0
  200. package/src/services/module-config.ts +298 -0
  201. package/src/services/module-deploy.ts +862 -0
  202. package/src/services/module-types-drift.test.ts +73 -0
  203. package/src/services/module-types-generator.test.ts +288 -0
  204. package/src/services/module-types-generator.ts +189 -0
  205. package/src/services/proxmox-state-recovery.ts +140 -0
  206. package/src/services/schema-validation.ts +155 -0
  207. package/src/services/secret-schema-loader.test.ts +311 -0
  208. package/src/services/secret-schema-loader.ts +239 -0
  209. package/src/services/ssh-key-manager.test.ts +283 -0
  210. package/src/services/ssh-key-manager.ts +193 -0
  211. package/src/services/storage-providers/local.ts +105 -0
  212. package/src/services/storage-providers/s3.ts +182 -0
  213. package/src/services/storage-providers/types.ts +24 -0
  214. package/src/services/system-config-schema-types.ts +25 -0
  215. package/src/services/system-config-validator.test.ts +160 -0
  216. package/src/services/system-config-validator.ts +74 -0
  217. package/src/services/system-init.test.ts +153 -0
  218. package/src/services/system-init.ts +253 -0
  219. package/src/services/terraform-safety.ts +174 -0
  220. package/src/services/zone-detector.test.ts +110 -0
  221. package/src/services/zone-detector.ts +102 -0
  222. package/src/services/zone-policy.test.ts +97 -0
  223. package/src/services/zone-policy.ts +126 -0
  224. package/src/templates/generator.test.ts +645 -0
  225. package/src/templates/generator.ts +1119 -0
  226. package/src/templates/types.ts +62 -0
  227. package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
  228. package/src/test-utils/cli-context-interactive.test.ts +152 -0
  229. package/src/test-utils/cli-context-server.test.ts +66 -0
  230. package/src/test-utils/cli-context.test.ts +273 -0
  231. package/src/test-utils/cli-context.ts +677 -0
  232. package/src/test-utils/cli-result.test.ts +282 -0
  233. package/src/test-utils/cli-result.ts +241 -0
  234. package/src/test-utils/cli.ts +55 -0
  235. package/src/test-utils/completion-harness.test.ts +126 -0
  236. package/src/test-utils/completion-harness.ts +82 -0
  237. package/src/test-utils/database.test.ts +182 -0
  238. package/src/test-utils/database.ts +126 -0
  239. package/src/test-utils/filesystem.test.ts +208 -0
  240. package/src/test-utils/filesystem.ts +142 -0
  241. package/src/test-utils/fixtures.test.ts +123 -0
  242. package/src/test-utils/fixtures.ts +160 -0
  243. package/src/test-utils/golden-diff.ts +197 -0
  244. package/src/test-utils/index.ts +77 -0
  245. package/src/test-utils/integration.ts +81 -0
  246. package/src/test-utils/module-fixtures.ts +468 -0
  247. package/src/test-utils/modules.test.ts +144 -0
  248. package/src/test-utils/modules.ts +183 -0
  249. package/src/test-utils/setup-test-db.ts +90 -0
  250. package/src/test-utils/value-extractor.test.ts +231 -0
  251. package/src/test-utils/value-extractor.ts +228 -0
  252. package/src/types/infrastructure.ts +157 -0
  253. package/src/utils/shell.test.ts +365 -0
  254. package/src/utils/shell.ts +159 -0
  255. package/src/validation/schemas.ts +166 -0
  256. package/src/variables/ansible-resolver.test.ts +142 -0
  257. package/src/variables/ansible-resolver.ts +69 -0
  258. package/src/variables/capability-self-ref.test.ts +220 -0
  259. package/src/variables/context.test.ts +1265 -0
  260. package/src/variables/context.ts +624 -0
  261. package/src/variables/declarative-derivation.test.ts +743 -0
  262. package/src/variables/declarative-derivation.ts +200 -0
  263. package/src/variables/parser.test.ts +231 -0
  264. package/src/variables/parser.ts +76 -0
  265. package/src/variables/resolver.test.ts +458 -0
  266. package/src/variables/resolver.ts +282 -0
  267. package/src/variables/types.ts +59 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Capability Access Validation
3
+ * Validates that consumer modules have permission to access capability secrets
4
+ */
5
+
6
+ import type { Database } from 'bun:sqlite';
7
+ import type { ModuleManifest } from '../manifest/schema';
8
+
9
+ export interface ValidationResult {
10
+ success: boolean;
11
+ error?: string;
12
+ details?: unknown;
13
+ }
14
+
15
+ /**
16
+ * Validate that consumer module can access required capability secrets
17
+ *
18
+ * Execution function (Rule 10.1) - performs database queries
19
+ *
20
+ * @param manifest - Consumer module manifest
21
+ * @param db - Database connection
22
+ * @returns Validation result
23
+ */
24
+ export async function validateCapabilityAccess(
25
+ manifest: ModuleManifest,
26
+ db: Database,
27
+ ): Promise<ValidationResult> {
28
+ // If module doesn't require capabilities, validation passes
29
+ if (!manifest.requires?.capabilities || manifest.requires.capabilities.length === 0) {
30
+ return { success: true };
31
+ }
32
+
33
+ // Get list of capabilities this module provides
34
+ const consumerCapabilities = (manifest.provides?.capabilities || []).map((cap) => cap.name);
35
+
36
+ // Check each required capability
37
+ for (const requiredCapability of manifest.requires.capabilities) {
38
+ // Get the provider module's manifest
39
+ const providerManifest = getProviderManifest(requiredCapability.name, db);
40
+
41
+ if (!providerManifest) {
42
+ return {
43
+ success: false,
44
+ error: `Required capability '${requiredCapability.name}' not found. No module provides this capability.`,
45
+ };
46
+ }
47
+
48
+ // Check if any secrets in the capability require access permissions
49
+ const capabilityDef = providerManifest.provides?.capabilities?.find(
50
+ (cap) => cap.name === requiredCapability.name,
51
+ );
52
+
53
+ if (!capabilityDef?.secrets || capabilityDef.secrets.length === 0) {
54
+ // No secrets to validate
55
+ continue;
56
+ }
57
+
58
+ // Check allowlist for each secret
59
+ for (const secret of capabilityDef.secrets) {
60
+ if (secret.readable_by && secret.readable_by.length > 0) {
61
+ // Check if consumer provides any capability in the allowlist
62
+ const hasAccess = checkAllowlist(consumerCapabilities, secret.readable_by);
63
+
64
+ if (!hasAccess) {
65
+ return {
66
+ success: false,
67
+ error: formatAccessDeniedError(
68
+ manifest.id,
69
+ requiredCapability.name,
70
+ secret.name,
71
+ consumerCapabilities,
72
+ secret.readable_by,
73
+ ),
74
+ };
75
+ }
76
+ }
77
+ // If readable_by is empty or undefined, secret is accessible to all
78
+ }
79
+ }
80
+
81
+ return { success: true };
82
+ }
83
+
84
+ /**
85
+ * Check if consumer capabilities match provider allowlist
86
+ *
87
+ * Policy function (Rule 10.1) - pure logic, no I/O
88
+ *
89
+ * @param consumerCapabilities - Capabilities provided by consumer module
90
+ * @param allowlist - Allowlist from provider's secret definition
91
+ * @returns True if any consumer capability is in allowlist
92
+ */
93
+ export function checkAllowlist(consumerCapabilities: string[], allowlist: string[]): boolean {
94
+ return consumerCapabilities.some((cap) => allowlist.includes(cap));
95
+ }
96
+
97
+ /**
98
+ * Get provider module manifest for a capability
99
+ *
100
+ * Execution function (Rule 10.1) - performs database query
101
+ *
102
+ * @param capabilityName - Name of the capability
103
+ * @param db - Database connection
104
+ * @returns Provider module manifest or null if not found
105
+ */
106
+ export function getProviderManifest(capabilityName: string, db: Database): ModuleManifest | null {
107
+ const result = db
108
+ .prepare(
109
+ `SELECT p.manifest_data
110
+ FROM modules p
111
+ JOIN capabilities c ON p.id = c.module_id
112
+ WHERE c.capability_name = ?
113
+ LIMIT 1`,
114
+ )
115
+ .get(capabilityName) as { manifest_data: string } | undefined;
116
+
117
+ if (!result) {
118
+ return null;
119
+ }
120
+
121
+ try {
122
+ return JSON.parse(result.manifest_data) as ModuleManifest;
123
+ } catch (error) {
124
+ throw new Error(
125
+ `Failed to parse manifest for module providing capability ${capabilityName}: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Format access denied error message
132
+ *
133
+ * Presentation function (Rule 10.1) - formats output for user
134
+ *
135
+ * @param consumerModuleId - Consumer module ID
136
+ * @param capabilityName - Capability name
137
+ * @param secretName - Secret name
138
+ * @param consumerCapabilities - Capabilities provided by consumer
139
+ * @param allowlist - Allowlist from provider
140
+ * @returns Formatted error message
141
+ */
142
+ function formatAccessDeniedError(
143
+ consumerModuleId: string,
144
+ capabilityName: string,
145
+ secretName: string,
146
+ consumerCapabilities: string[],
147
+ allowlist: string[],
148
+ ): string {
149
+ const lines = [
150
+ `Module '${consumerModuleId}' cannot access secret '${secretName}' from capability '${capabilityName}'.`,
151
+ '',
152
+ `The ${capabilityName} capability only allows access to modules that provide: ${allowlist.join(', ')}`,
153
+ '',
154
+ `This module provides: ${consumerCapabilities.length > 0 ? consumerCapabilities.join(', ') : '(none)'}`,
155
+ '',
156
+ `To grant access, update the provider module's manifest to add one of the consumer's capabilities to the readable_by list.`,
157
+ ];
158
+
159
+ return lines.join('\n');
160
+ }
@@ -0,0 +1,238 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import {
3
+ WELL_KNOWN_CAPABILITIES,
4
+ getCapabilityDataSchema,
5
+ getSupportedCapabilities,
6
+ getWellKnownCapability,
7
+ isWellKnown,
8
+ validateZoneRequirement,
9
+ } from './well-known';
10
+
11
+ describe('Well-Known Capabilities Registry', () => {
12
+ describe('WELL_KNOWN_CAPABILITIES', () => {
13
+ test('should have public_web capability', () => {
14
+ expect(WELL_KNOWN_CAPABILITIES.public_web).toBeDefined();
15
+ expect(WELL_KNOWN_CAPABILITIES.public_web.canonical_hostname).toBe('www');
16
+ expect(WELL_KNOWN_CAPABILITIES.public_web.required_zone).toBe('dmz');
17
+ expect(WELL_KNOWN_CAPABILITIES.public_web.zone_enforced).toBe(true);
18
+ });
19
+
20
+ test('should have dns_registrar capability', () => {
21
+ expect(WELL_KNOWN_CAPABILITIES.dns_registrar).toBeDefined();
22
+ expect(WELL_KNOWN_CAPABILITIES.dns_registrar.canonical_hostname).toBe('dns-reg');
23
+ expect(WELL_KNOWN_CAPABILITIES.dns_registrar.required_zone).toBe('dmz');
24
+ expect(WELL_KNOWN_CAPABILITIES.dns_registrar.zone_enforced).toBe(false);
25
+ });
26
+
27
+ test('should have dns_internal capability', () => {
28
+ expect(WELL_KNOWN_CAPABILITIES.dns_internal).toBeDefined();
29
+ expect(WELL_KNOWN_CAPABILITIES.dns_internal.canonical_hostname).toBe('dns-int');
30
+ expect(WELL_KNOWN_CAPABILITIES.dns_internal.required_zone).toBe('internal');
31
+ expect(WELL_KNOWN_CAPABILITIES.dns_internal.zone_enforced).toBe(true);
32
+ });
33
+
34
+ test('should have auth capability', () => {
35
+ expect(WELL_KNOWN_CAPABILITIES.auth).toBeDefined();
36
+ expect(WELL_KNOWN_CAPABILITIES.auth.canonical_hostname).toBe('auth');
37
+ expect(WELL_KNOWN_CAPABILITIES.auth.required_zone).toBe('secure');
38
+ expect(WELL_KNOWN_CAPABILITIES.auth.zone_enforced).toBe(true);
39
+ });
40
+
41
+ test('should have database capability', () => {
42
+ expect(WELL_KNOWN_CAPABILITIES.database).toBeDefined();
43
+ expect(WELL_KNOWN_CAPABILITIES.database.canonical_hostname).toBe('db');
44
+ expect(WELL_KNOWN_CAPABILITIES.database.required_zone).toBe('secure');
45
+ expect(WELL_KNOWN_CAPABILITIES.database.zone_enforced).toBe(true);
46
+ });
47
+
48
+ test('should have dhcp_server capability', () => {
49
+ expect(WELL_KNOWN_CAPABILITIES.dhcp_server).toBeDefined();
50
+ expect(WELL_KNOWN_CAPABILITIES.dhcp_server.canonical_hostname).toBe('dhcp');
51
+ expect(WELL_KNOWN_CAPABILITIES.dhcp_server.required_zone).toBe('internal');
52
+ expect(WELL_KNOWN_CAPABILITIES.dhcp_server.zone_enforced).toBe(false);
53
+ });
54
+
55
+ test('all capabilities should have required fields', () => {
56
+ for (const [name, capability] of Object.entries(WELL_KNOWN_CAPABILITIES)) {
57
+ expect(capability.canonical_hostname, `${name} missing canonical_hostname`).toBeDefined();
58
+ expect(capability.required_zone, `${name} missing required_zone`).toBeDefined();
59
+ expect(capability.zone_enforced, `${name} missing zone_enforced`).toBeDefined();
60
+ expect(capability.data_schema, `${name} missing data_schema`).toBeDefined();
61
+ }
62
+ });
63
+ });
64
+
65
+ describe('isWellKnown', () => {
66
+ test('should return true for well-known capabilities', () => {
67
+ expect(isWellKnown('public_web')).toBe(true);
68
+ expect(isWellKnown('dns_registrar')).toBe(true);
69
+ expect(isWellKnown('dns_internal')).toBe(true);
70
+ expect(isWellKnown('auth')).toBe(true);
71
+ expect(isWellKnown('database')).toBe(true);
72
+ expect(isWellKnown('dhcp_server')).toBe(true);
73
+ });
74
+
75
+ test('should return false for unknown capabilities', () => {
76
+ expect(isWellKnown('unknown_capability')).toBe(false);
77
+ expect(isWellKnown('custom_service')).toBe(false);
78
+ expect(isWellKnown('')).toBe(false);
79
+ });
80
+ });
81
+
82
+ describe('getWellKnownCapability', () => {
83
+ test('should return capability for well-known name', () => {
84
+ const capability = getWellKnownCapability('public_web');
85
+
86
+ expect(capability.canonical_hostname).toBe('www');
87
+ expect(capability.required_zone).toBe('dmz');
88
+ });
89
+
90
+ test('should throw error for unknown capability', () => {
91
+ expect(() => getWellKnownCapability('unknown')).toThrow('Unknown capability: unknown');
92
+ expect(() => getWellKnownCapability('unknown')).toThrow(
93
+ 'Supported capabilities: public_web, dns_registrar, dns_internal, auth, database, dhcp_server',
94
+ );
95
+ });
96
+ });
97
+
98
+ describe('getSupportedCapabilities', () => {
99
+ test('should return all capability names', () => {
100
+ const capabilities = getSupportedCapabilities();
101
+
102
+ expect(capabilities).toContain('public_web');
103
+ expect(capabilities).toContain('dns_registrar');
104
+ expect(capabilities).toContain('dns_internal');
105
+ expect(capabilities).toContain('auth');
106
+ expect(capabilities).toContain('database');
107
+ expect(capabilities).toContain('dhcp_server');
108
+ expect(capabilities.length).toBe(6);
109
+ });
110
+ });
111
+
112
+ describe('validateZoneRequirement', () => {
113
+ test('should validate correct zone for public_web', () => {
114
+ const result = validateZoneRequirement('public_web', 'dmz');
115
+
116
+ expect(result.valid).toBe(true);
117
+ expect(result.error).toBeUndefined();
118
+ });
119
+
120
+ test('should reject wrong zone for public_web', () => {
121
+ const result = validateZoneRequirement('public_web', 'app');
122
+
123
+ expect(result.valid).toBe(false);
124
+ expect(result.required_zone).toBe('dmz');
125
+ expect(result.error).toContain("Capability 'public_web' requires zone='dmz'");
126
+ expect(result.error).toContain("Module specifies zone='app'");
127
+ });
128
+
129
+ test('should validate correct zone for auth', () => {
130
+ const result = validateZoneRequirement('auth', 'secure');
131
+
132
+ expect(result.valid).toBe(true);
133
+ });
134
+
135
+ test('should reject wrong zone for auth', () => {
136
+ const result = validateZoneRequirement('auth', 'dmz');
137
+
138
+ expect(result.valid).toBe(false);
139
+ expect(result.required_zone).toBe('secure');
140
+ expect(result.error).toContain("Capability 'auth' requires zone='secure'");
141
+ });
142
+
143
+ test('should validate correct zone for dns_internal', () => {
144
+ const result = validateZoneRequirement('dns_internal', 'internal');
145
+
146
+ expect(result.valid).toBe(true);
147
+ });
148
+
149
+ test('should allow dns_registrar in any zone (zone_enforced=false)', () => {
150
+ const result = validateZoneRequirement('dns_registrar', 'dmz');
151
+ expect(result.valid).toBe(true);
152
+
153
+ const result2 = validateZoneRequirement('dns_registrar', 'app');
154
+ expect(result2.valid).toBe(true);
155
+ });
156
+
157
+ test('should reject unknown capability', () => {
158
+ const result = validateZoneRequirement('unknown_capability', 'dmz');
159
+
160
+ expect(result.valid).toBe(false);
161
+ expect(result.error).toContain('Unknown capability: unknown_capability');
162
+ expect(result.error).toContain('Supported capabilities');
163
+ });
164
+ });
165
+
166
+ describe('getCapabilityDataSchema', () => {
167
+ test('should return data schema for public_web', () => {
168
+ const schema = getCapabilityDataSchema('public_web');
169
+
170
+ expect(schema).toEqual({
171
+ server: {
172
+ ip: {
173
+ primary: '$self:container_ip',
174
+ },
175
+ port: 443,
176
+ },
177
+ });
178
+ });
179
+
180
+ test('should return data schema for dns_registrar', () => {
181
+ const schema = getCapabilityDataSchema('dns_registrar');
182
+
183
+ expect(schema).toEqual({
184
+ provider: 'namecheap',
185
+ primary_domain: '$self:primary_domain',
186
+ supports: ['dynamic_dns_a_record'],
187
+ });
188
+ });
189
+
190
+ test('should return data schema for auth', () => {
191
+ const schema = getCapabilityDataSchema('auth');
192
+
193
+ // Note: oidc.issuer_url was removed in MANIFEST_V2 D9 — auth providers
194
+ // (e.g. authentik) now derive their issuer URL in their own manifest
195
+ // and expose it through provides.capabilities[].data.
196
+ expect(schema).toEqual({
197
+ server: {
198
+ ip: {
199
+ primary: '$self:container_ip',
200
+ },
201
+ port: 9000,
202
+ },
203
+ });
204
+ });
205
+
206
+ test('should return cloned schema (not reference)', () => {
207
+ const schema1 = getCapabilityDataSchema('public_web');
208
+ const schema2 = getCapabilityDataSchema('public_web');
209
+
210
+ // Modify one schema to test cloning
211
+ // biome-ignore lint/suspicious/noExplicitAny: intentionally mutating to verify independent copies
212
+ (schema1 as any).server.port = 8080;
213
+
214
+ // Other schema should be unchanged
215
+ // biome-ignore lint/suspicious/noExplicitAny: accessing mutated property for comparison
216
+ expect((schema2 as any).server.port).toBe(443);
217
+ });
218
+
219
+ test('should throw error for unknown capability', () => {
220
+ expect(() => getCapabilityDataSchema('unknown')).toThrow('Unknown capability: unknown');
221
+ });
222
+ });
223
+
224
+ describe('Security zone requirements', () => {
225
+ test('home lab public-facing services must be in DMZ', () => {
226
+ expect(WELL_KNOWN_CAPABILITIES.public_web.required_zone).toBe('dmz');
227
+ });
228
+
229
+ test('authentication services must be in secure zone', () => {
230
+ expect(WELL_KNOWN_CAPABILITIES.auth.required_zone).toBe('secure');
231
+ expect(WELL_KNOWN_CAPABILITIES.database.required_zone).toBe('secure');
232
+ });
233
+
234
+ test('internal services must be in internal zone', () => {
235
+ expect(WELL_KNOWN_CAPABILITIES.dns_internal.required_zone).toBe('internal');
236
+ });
237
+ });
238
+ });
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Well-Known Capabilities Registry
3
+ * Defines standardized capabilities with predefined hostnames, zones, and schemas
4
+ * Hardcoded registry (extensibility deferred to future)
5
+ */
6
+
7
+ /**
8
+ * Network zones define security boundaries and deployment locations
9
+ *
10
+ * Home Lab Zones (VLAN-based):
11
+ * - dmz: Public-facing services in home lab (VLAN 10, e.g., 10.0.10.0/24)
12
+ * - app: Internal services in home lab (VLAN 20, e.g., 10.0.20.0/24)
13
+ * - secure: Auth/DB in home lab (VLAN 30, e.g., 10.0.30.0/24)
14
+ *
15
+ * External Zone (Cloud/VPS):
16
+ * - external: Services hosted outside home network (no VLAN, e.g., VPS on internet)
17
+ */
18
+ export type NetworkZone = 'internal' | 'dmz' | 'app' | 'secure' | 'external';
19
+
20
+ export interface WellKnownCapability {
21
+ canonical_hostname: string;
22
+ required_zone: NetworkZone;
23
+ zone_enforced: boolean; // Always true
24
+ data_schema: Record<string, unknown>;
25
+ }
26
+
27
+ /**
28
+ * Registry of well-known capabilities
29
+ * These capabilities have standardized contracts enforced by Celilo
30
+ */
31
+ export const WELL_KNOWN_CAPABILITIES: Record<string, WellKnownCapability> = {
32
+ /**
33
+ * public_web - Public-facing web server
34
+ * Example: Caddy reverse proxy
35
+ * Security: MUST be in DMZ zone (internet-facing)
36
+ */
37
+ public_web: {
38
+ canonical_hostname: 'www',
39
+ required_zone: 'dmz',
40
+ zone_enforced: true,
41
+ data_schema: {
42
+ server: {
43
+ ip: {
44
+ primary: '$self:container_ip',
45
+ },
46
+ port: 443,
47
+ },
48
+ },
49
+ },
50
+
51
+ /**
52
+ * dns_registrar - Domain registrar with Dynamic DNS support
53
+ * Example: Namecheap Dynamic DNS
54
+ * Security: Zone-agnostic (no infrastructure, just API calls)
55
+ */
56
+ dns_registrar: {
57
+ canonical_hostname: 'dns-reg',
58
+ required_zone: 'dmz',
59
+ zone_enforced: false,
60
+ data_schema: {
61
+ provider: 'namecheap',
62
+ primary_domain: '$self:primary_domain',
63
+ supports: ['dynamic_dns_a_record'],
64
+ },
65
+ },
66
+
67
+ /**
68
+ * dns_internal - Internal DNS resolver
69
+ * Example: Technitium DNS server for split-horizon LAN resolution
70
+ * Security: Internal zone (backbone service reachable from all zones)
71
+ */
72
+ dns_internal: {
73
+ canonical_hostname: 'dns-int',
74
+ required_zone: 'internal',
75
+ zone_enforced: true,
76
+ data_schema: {
77
+ server: {
78
+ ip: {
79
+ primary: '$self:container_ip',
80
+ },
81
+ port: 53,
82
+ },
83
+ },
84
+ },
85
+
86
+ /**
87
+ * auth - Authentication/Identity Provider
88
+ * Example: Authentik, Keycloak
89
+ * Security: MUST be in secure zone (handles authentication)
90
+ *
91
+ * Note: `oidc.issuer_url` is no longer derived here. Per the firm rule in
92
+ * design/TECHNICAL_DESIGN_MANIFEST_V2.md D9, well-known capability data
93
+ * templates must not cross-reference other capabilities. The IDP-providing
94
+ * module derives `auth_url` (or whatever it calls the issuer URL) in its
95
+ * own manifest from `$capability:dns_registrar.primary_domain` and exposes
96
+ * it through `provides.capabilities[].data`.
97
+ */
98
+ auth: {
99
+ canonical_hostname: 'auth',
100
+ required_zone: 'secure',
101
+ zone_enforced: true,
102
+ data_schema: {
103
+ server: {
104
+ ip: {
105
+ primary: '$self:container_ip',
106
+ },
107
+ port: 9000,
108
+ },
109
+ },
110
+ },
111
+
112
+ /**
113
+ * database - Database server
114
+ * Example: PostgreSQL, MySQL, MongoDB
115
+ * Security: Recommended secure zone (sensitive data)
116
+ */
117
+ database: {
118
+ canonical_hostname: 'db',
119
+ required_zone: 'secure',
120
+ zone_enforced: true,
121
+ data_schema: {
122
+ server: {
123
+ ip: {
124
+ primary: '$self:container_ip',
125
+ },
126
+ port: '$self:port', // Database-specific port
127
+ },
128
+ connection: {
129
+ host: '$self:container_ip',
130
+ port: '$self:port',
131
+ name: '$self:database_name',
132
+ },
133
+ },
134
+ },
135
+
136
+ /**
137
+ * dhcp_server - DHCP server with configurable DNS
138
+ * Example: ISP router (GreenWave C4000XG) DHCP service
139
+ * Security: Zone-agnostic (external service, no infrastructure)
140
+ */
141
+ dhcp_server: {
142
+ canonical_hostname: 'dhcp',
143
+ required_zone: 'internal',
144
+ zone_enforced: false,
145
+ data_schema: {
146
+ router_ip: '$self:router_ip',
147
+ },
148
+ },
149
+ };
150
+
151
+ /**
152
+ * Check if a capability name is well-known
153
+ */
154
+ export function isWellKnown(capabilityName: string): boolean {
155
+ return capabilityName in WELL_KNOWN_CAPABILITIES;
156
+ }
157
+
158
+ /**
159
+ * Get well-known capability metadata
160
+ * Throws error if capability is not well-known
161
+ */
162
+ export function getWellKnownCapability(capabilityName: string): WellKnownCapability {
163
+ if (!isWellKnown(capabilityName)) {
164
+ throw new Error(
165
+ `Unknown capability: ${capabilityName}. Supported capabilities: ${getSupportedCapabilities().join(', ')}`,
166
+ );
167
+ }
168
+ return WELL_KNOWN_CAPABILITIES[capabilityName];
169
+ }
170
+
171
+ /**
172
+ * Get list of all supported capability names
173
+ */
174
+ export function getSupportedCapabilities(): string[] {
175
+ return Object.keys(WELL_KNOWN_CAPABILITIES);
176
+ }
177
+
178
+ /**
179
+ * Validation result for zone requirements
180
+ */
181
+ export interface ZoneValidationResult {
182
+ valid: boolean;
183
+ error?: string;
184
+ required_zone?: NetworkZone;
185
+ }
186
+
187
+ /**
188
+ * Validate that module zone matches capability requirement
189
+ * Returns validation result with error message if mismatch
190
+ */
191
+ export function validateZoneRequirement(
192
+ capabilityName: string,
193
+ moduleZone: NetworkZone,
194
+ ): ZoneValidationResult {
195
+ if (!isWellKnown(capabilityName)) {
196
+ return {
197
+ valid: false,
198
+ error: `Unknown capability: ${capabilityName}. Supported capabilities: ${getSupportedCapabilities().join(', ')}`,
199
+ };
200
+ }
201
+
202
+ const capability = WELL_KNOWN_CAPABILITIES[capabilityName];
203
+
204
+ if (capability.zone_enforced && moduleZone !== capability.required_zone) {
205
+ return {
206
+ valid: false,
207
+ required_zone: capability.required_zone,
208
+ error: `Capability '${capabilityName}' requires zone='${capability.required_zone}' (security requirement). Module specifies zone='${moduleZone}'.`,
209
+ };
210
+ }
211
+
212
+ return { valid: true };
213
+ }
214
+
215
+ /**
216
+ * Get capability data schema with variables resolved
217
+ * Note: This returns the template schema - actual variable resolution happens during generation
218
+ */
219
+ export function getCapabilityDataSchema(capabilityName: string): Record<string, unknown> {
220
+ const capability = getWellKnownCapability(capabilityName);
221
+ return structuredClone(capability.data_schema);
222
+ }