@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,395 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { ModuleManifest } from '../manifest/schema';
3
+ import { buildCapabilityData, promptForSecretValue } from './registration';
4
+
5
+ describe('Capability Registration', () => {
6
+ describe('buildCapabilityData', () => {
7
+ test('should return capability data unchanged', () => {
8
+ const capability = {
9
+ name: 'test_capability',
10
+ version: '1.0.0',
11
+ data: {
12
+ server: {
13
+ port: 443,
14
+ protocol: 'https',
15
+ },
16
+ },
17
+ };
18
+
19
+ const manifest: ModuleManifest = {
20
+ celilo_contract: '1.0',
21
+ id: 'test-module',
22
+ name: 'Test Module',
23
+ version: '1.0.0',
24
+ description: 'Test',
25
+ requires: { capabilities: [] },
26
+ provides: { capabilities: [] },
27
+ variables: { owns: [], imports: [] },
28
+ };
29
+
30
+ const result = buildCapabilityData(capability, manifest);
31
+
32
+ expect(result).toEqual({
33
+ server: {
34
+ port: 443,
35
+ protocol: 'https',
36
+ },
37
+ });
38
+ });
39
+
40
+ test('should preserve $self: variables (not resolved)', () => {
41
+ const capability = {
42
+ name: 'test_capability',
43
+ version: '1.0.0',
44
+ data: {
45
+ server: {
46
+ ip: '$self:container_ip',
47
+ port: 443,
48
+ },
49
+ },
50
+ };
51
+
52
+ const manifest: ModuleManifest = {
53
+ celilo_contract: '1.0',
54
+ id: 'test-module',
55
+ name: 'Test Module',
56
+ version: '1.0.0',
57
+ description: 'Test',
58
+ requires: { capabilities: [] },
59
+ provides: { capabilities: [] },
60
+ variables: { owns: [], imports: [] },
61
+ };
62
+
63
+ const result = buildCapabilityData(capability, manifest);
64
+
65
+ // Variables are preserved, not resolved
66
+ expect(result).toEqual({
67
+ server: {
68
+ ip: '$self:container_ip',
69
+ port: 443,
70
+ },
71
+ });
72
+ });
73
+
74
+ test('should preserve nested $self: variables', () => {
75
+ const capability = {
76
+ name: 'dns_external',
77
+ version: '1.0.0',
78
+ data: {
79
+ server: {
80
+ ip: {
81
+ primary: '$self:vps_ip',
82
+ },
83
+ port: 53,
84
+ },
85
+ zone: {
86
+ primary_domain: '$self:primary_domain',
87
+ },
88
+ },
89
+ };
90
+
91
+ const manifest: ModuleManifest = {
92
+ celilo_contract: '1.0',
93
+ id: 'dns-external',
94
+ name: 'DNS External',
95
+ version: '1.0.0',
96
+ description: 'Test',
97
+ requires: { capabilities: [] },
98
+ provides: { capabilities: [] },
99
+ variables: { owns: [], imports: [] },
100
+ };
101
+
102
+ const result = buildCapabilityData(capability, manifest);
103
+
104
+ expect(result).toEqual({
105
+ server: {
106
+ ip: {
107
+ primary: '$self:vps_ip',
108
+ },
109
+ port: 53,
110
+ },
111
+ zone: {
112
+ primary_domain: '$self:primary_domain',
113
+ },
114
+ });
115
+ });
116
+
117
+ test('should preserve multiple variables in same object', () => {
118
+ const capability = {
119
+ name: 'test_capability',
120
+ version: '1.0.0',
121
+ data: {
122
+ url: '$self:base_url',
123
+ api_version: '$self:api_version',
124
+ timeout: 30,
125
+ },
126
+ };
127
+
128
+ const manifest: ModuleManifest = {
129
+ celilo_contract: '1.0',
130
+ id: 'test-module',
131
+ name: 'Test Module',
132
+ version: '1.0.0',
133
+ description: 'Test',
134
+ requires: { capabilities: [] },
135
+ provides: { capabilities: [] },
136
+ variables: { owns: [], imports: [] },
137
+ };
138
+
139
+ const result = buildCapabilityData(capability, manifest);
140
+
141
+ expect(result).toEqual({
142
+ url: '$self:base_url',
143
+ api_version: '$self:api_version',
144
+ timeout: 30,
145
+ });
146
+ });
147
+
148
+ test('should preserve variables in arrays', () => {
149
+ const capability = {
150
+ name: 'test_capability',
151
+ version: '1.0.0',
152
+ data: {
153
+ servers: ['$self:primary_server', '$self:backup_server'],
154
+ },
155
+ };
156
+
157
+ const manifest: ModuleManifest = {
158
+ celilo_contract: '1.0',
159
+ id: 'test-module',
160
+ name: 'Test Module',
161
+ version: '1.0.0',
162
+ description: 'Test',
163
+ requires: { capabilities: [] },
164
+ provides: { capabilities: [] },
165
+ variables: { owns: [], imports: [] },
166
+ };
167
+
168
+ const result = buildCapabilityData(capability, manifest);
169
+
170
+ expect(result).toEqual({
171
+ servers: ['$self:primary_server', '$self:backup_server'],
172
+ });
173
+ });
174
+
175
+ test('should preserve all string types including variables', () => {
176
+ const capability = {
177
+ name: 'test_capability',
178
+ version: '1.0.0',
179
+ data: {
180
+ variable: '$self:container_ip',
181
+ literal: 'not-a-variable',
182
+ with_dollar: '$100',
183
+ empty: '',
184
+ },
185
+ };
186
+
187
+ const manifest: ModuleManifest = {
188
+ celilo_contract: '1.0',
189
+ id: 'test-module',
190
+ name: 'Test Module',
191
+ version: '1.0.0',
192
+ description: 'Test',
193
+ requires: { capabilities: [] },
194
+ provides: { capabilities: [] },
195
+ variables: { owns: [], imports: [] },
196
+ };
197
+
198
+ const result = buildCapabilityData(capability, manifest);
199
+
200
+ expect(result).toEqual({
201
+ variable: '$self:container_ip',
202
+ literal: 'not-a-variable',
203
+ with_dollar: '$100',
204
+ empty: '',
205
+ });
206
+ });
207
+
208
+ test('should handle empty data object', () => {
209
+ const capability = {
210
+ name: 'test_capability',
211
+ version: '1.0.0',
212
+ data: {},
213
+ };
214
+
215
+ const manifest: ModuleManifest = {
216
+ celilo_contract: '1.0',
217
+ id: 'test-module',
218
+ name: 'Test Module',
219
+ version: '1.0.0',
220
+ description: 'Test',
221
+ requires: { capabilities: [] },
222
+ provides: { capabilities: [] },
223
+ variables: { owns: [], imports: [] },
224
+ };
225
+
226
+ const result = buildCapabilityData(capability, manifest);
227
+
228
+ expect(result).toEqual({});
229
+ });
230
+
231
+ test('should preserve deeply nested structures', () => {
232
+ const capability = {
233
+ name: 'test_capability',
234
+ version: '1.0.0',
235
+ data: {
236
+ level1: {
237
+ level2: {
238
+ level3: {
239
+ value: '$self:deep_value',
240
+ },
241
+ },
242
+ },
243
+ },
244
+ };
245
+
246
+ const manifest: ModuleManifest = {
247
+ celilo_contract: '1.0',
248
+ id: 'test-module',
249
+ name: 'Test Module',
250
+ version: '1.0.0',
251
+ description: 'Test',
252
+ requires: { capabilities: [] },
253
+ provides: { capabilities: [] },
254
+ variables: { owns: [], imports: [] },
255
+ };
256
+
257
+ const result = buildCapabilityData(capability, manifest);
258
+
259
+ expect(result).toEqual({
260
+ level1: {
261
+ level2: {
262
+ level3: {
263
+ value: '$self:deep_value',
264
+ },
265
+ },
266
+ },
267
+ });
268
+ });
269
+
270
+ test('should preserve number and boolean types', () => {
271
+ const capability = {
272
+ name: 'test_capability',
273
+ version: '1.0.0',
274
+ data: {
275
+ port: 443,
276
+ enabled: true,
277
+ timeout: 0,
278
+ disabled: false,
279
+ },
280
+ };
281
+
282
+ const manifest: ModuleManifest = {
283
+ celilo_contract: '1.0',
284
+ id: 'test-module',
285
+ name: 'Test Module',
286
+ version: '1.0.0',
287
+ description: 'Test',
288
+ requires: { capabilities: [] },
289
+ provides: { capabilities: [] },
290
+ variables: { owns: [], imports: [] },
291
+ };
292
+
293
+ const result = buildCapabilityData(capability, manifest);
294
+
295
+ expect(result).toEqual({
296
+ port: 443,
297
+ enabled: true,
298
+ timeout: 0,
299
+ disabled: false,
300
+ });
301
+ });
302
+
303
+ test('should handle null and undefined values', () => {
304
+ const capability = {
305
+ name: 'test_capability',
306
+ version: '1.0.0',
307
+ data: {
308
+ nullable: null,
309
+ optional: undefined,
310
+ present: 'value',
311
+ },
312
+ };
313
+
314
+ const manifest: ModuleManifest = {
315
+ celilo_contract: '1.0',
316
+ id: 'test-module',
317
+ name: 'Test Module',
318
+ version: '1.0.0',
319
+ description: 'Test',
320
+ requires: { capabilities: [] },
321
+ provides: { capabilities: [] },
322
+ variables: { owns: [], imports: [] },
323
+ };
324
+
325
+ const result = buildCapabilityData(capability, manifest);
326
+
327
+ expect(result).toEqual({
328
+ nullable: null,
329
+ optional: undefined,
330
+ present: 'value',
331
+ });
332
+ });
333
+
334
+ test('should return cloned data (not mutate original)', () => {
335
+ const capability = {
336
+ name: 'test_capability',
337
+ version: '1.0.0',
338
+ data: {
339
+ server: {
340
+ ip: '$self:container_ip',
341
+ },
342
+ },
343
+ };
344
+
345
+ const manifest: ModuleManifest = {
346
+ celilo_contract: '1.0',
347
+ id: 'test-module',
348
+ name: 'Test Module',
349
+ version: '1.0.0',
350
+ description: 'Test',
351
+ requires: { capabilities: [] },
352
+ provides: { capabilities: [] },
353
+ variables: { owns: [], imports: [] },
354
+ };
355
+
356
+ const result = buildCapabilityData(capability, manifest);
357
+
358
+ // Modify result to test deep copy
359
+ // biome-ignore lint/suspicious/noExplicitAny: intentionally mutating result to verify deep copy behavior
360
+ (result as any).server.ip = 'modified';
361
+
362
+ // Original should be unchanged
363
+ expect(capability.data.server.ip).toBe('$self:container_ip');
364
+ });
365
+ });
366
+
367
+ describe('promptForSecretValue', () => {
368
+ test('should generate base64-encoded secret when auto-generate is true', async () => {
369
+ const result = await promptForSecretValue('tsig', 'TSIG secret for DNS', true);
370
+
371
+ // Should be base64-encoded 32-byte value
372
+ expect(result).toMatch(/^[A-Za-z0-9+/]+=*$/);
373
+ expect(result.length).toBeGreaterThan(40); // base64 of 32 bytes is ~44 chars
374
+ });
375
+
376
+ test('should generate different secrets on each call', async () => {
377
+ const secret1 = await promptForSecretValue('secret1', 'Test secret', true);
378
+ const secret2 = await promptForSecretValue('secret2', 'Test secret', true);
379
+
380
+ expect(secret1).not.toBe(secret2);
381
+ });
382
+
383
+ test('should generate secrets of consistent length', async () => {
384
+ const results = await Promise.all([
385
+ promptForSecretValue('s1', 'Test', true),
386
+ promptForSecretValue('s2', 'Test', true),
387
+ promptForSecretValue('s3', 'Test', true),
388
+ ]);
389
+
390
+ const lengths = results.map((r) => r.length);
391
+ expect(lengths[0]).toBe(lengths[1]);
392
+ expect(lengths[1]).toBe(lengths[2]);
393
+ });
394
+ });
395
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Capability Registration
3
+ * Handles registration of capabilities during module import
4
+ */
5
+
6
+ import type { Database } from 'bun:sqlite';
7
+ import { randomBytes } from 'node:crypto';
8
+ import type { ModuleManifest } from '../manifest/schema';
9
+ import { encryptSecret } from '../secrets/encryption';
10
+ import type { EncryptedSecret } from '../secrets/encryption';
11
+ import { getOrCreateMasterKey } from '../secrets/master-key';
12
+
13
+ export interface RegistrationResult {
14
+ success: boolean;
15
+ error?: string;
16
+ details?: unknown;
17
+ }
18
+
19
+ export interface CapabilitySecretDefinition {
20
+ name: string;
21
+ type: 'string' | 'number' | 'boolean';
22
+ description?: string;
23
+ readable_by?: string[];
24
+ }
25
+
26
+ /**
27
+ * Register all capabilities provided by a module
28
+ *
29
+ * Execution function (Rule 10.1) - performs database operations
30
+ *
31
+ * @param moduleId - Module identifier
32
+ * @param manifest - Module manifest
33
+ * @param db - Database connection
34
+ * @param flags - CLI flags (e.g., auto-generate-secrets)
35
+ * @returns Registration result
36
+ */
37
+ export async function registerModuleCapabilities(
38
+ moduleId: string,
39
+ manifest: ModuleManifest,
40
+ db: Database,
41
+ _flags: Record<string, unknown> = {},
42
+ ): Promise<RegistrationResult> {
43
+ if (!manifest.provides?.capabilities || manifest.provides.capabilities.length === 0) {
44
+ return { success: true }; // No capabilities to register
45
+ }
46
+
47
+ try {
48
+ const _masterKey = await getOrCreateMasterKey();
49
+
50
+ for (const capability of manifest.provides.capabilities) {
51
+ // Build capability data by resolving $self: variables
52
+ const capabilityData = buildCapabilityData(capability, manifest);
53
+
54
+ // Insert capability into database (with optional zones)
55
+ const zones = capability.zones ? JSON.stringify(capability.zones) : null;
56
+ const capabilityRecord = db
57
+ .prepare(
58
+ `INSERT INTO capabilities (module_id, capability_name, version, data, zones, registered_at)
59
+ VALUES (?, ?, ?, ?, ?, unixepoch())
60
+ RETURNING id`,
61
+ )
62
+ .get(
63
+ moduleId,
64
+ capability.name,
65
+ capability.version,
66
+ JSON.stringify(capabilityData),
67
+ zones,
68
+ ) as {
69
+ id: number;
70
+ };
71
+
72
+ // Register capability secret metadata (values will be prompted during generation)
73
+ if (capability.secrets && capability.secrets.length > 0) {
74
+ for (const secret of capability.secrets) {
75
+ // Store secret metadata only (no value yet)
76
+ db.prepare(
77
+ `INSERT INTO capability_secrets (capability_id, name, description, created_at, updated_at)
78
+ VALUES (?, ?, ?, unixepoch(), unixepoch())`,
79
+ ).run(capabilityRecord.id, secret.name, secret.description || null);
80
+ }
81
+ }
82
+ }
83
+
84
+ return { success: true };
85
+ } catch (error) {
86
+ return {
87
+ success: false,
88
+ error: 'Failed to register capabilities',
89
+ details: error,
90
+ };
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Build capability data by resolving $self: variables
96
+ *
97
+ * Policy function (Rule 10.1) - pure data transformation
98
+ *
99
+ * Note: Only resolves $self: variables. Other variable types ($system:, $capability:)
100
+ * are resolved during generation, not registration.
101
+ *
102
+ * @param capability - Capability definition from manifest
103
+ * @param manifest - Module manifest (for $self: variable resolution)
104
+ * @returns Capability data with resolved variables
105
+ */
106
+ export function buildCapabilityData(
107
+ capability: { data?: Record<string, unknown> },
108
+ _manifest: ModuleManifest,
109
+ ): Record<string, unknown> {
110
+ if (!capability.data) {
111
+ return {};
112
+ }
113
+
114
+ // Capability data is static (no $self: variables in well-known capabilities)
115
+ // This function is here for future extensibility
116
+ return structuredClone(capability.data);
117
+ }
118
+
119
+ /**
120
+ * Prompt user for secret value or auto-generate
121
+ *
122
+ * Execution function (Rule 10.1) - performs I/O (console input)
123
+ *
124
+ * @param secretName - Name of the secret
125
+ * @param description - Description of the secret
126
+ * @param autoGenerate - Whether to auto-generate without prompting
127
+ * @returns Secret value
128
+ */
129
+ export async function promptForSecretValue(
130
+ secretName: string,
131
+ description: string,
132
+ autoGenerate: boolean,
133
+ ): Promise<string> {
134
+ if (autoGenerate) {
135
+ // Auto-generate secret (TSIG keys are base64-encoded 32-byte values)
136
+ const randomValue = randomBytes(32);
137
+ return randomValue.toString('base64');
138
+ }
139
+
140
+ // Check if stdin is available (TTY check)
141
+ if (!process.stdin.isTTY) {
142
+ throw new Error(
143
+ `Cannot prompt for secret '${secretName}': stdin is not available.\nUse --auto-generate-secrets flag to auto-generate capability secrets.`,
144
+ );
145
+ }
146
+
147
+ // Prompt user for secret value
148
+ console.log('\nCapability secret required:');
149
+ console.log(` • ${secretName}: ${description}`);
150
+ console.log(`\nEnter value for ${secretName} (or press Enter to auto-generate):`);
151
+
152
+ // Read from stdin
153
+ const readline = await import('node:readline');
154
+ const rl = readline.createInterface({
155
+ input: process.stdin,
156
+ output: process.stdout,
157
+ });
158
+
159
+ return new Promise((resolve) => {
160
+ rl.question('> ', (answer) => {
161
+ rl.close();
162
+
163
+ if (answer.trim() === '') {
164
+ // User pressed Enter - auto-generate
165
+ const randomValue = randomBytes(32);
166
+ const generated = randomValue.toString('base64');
167
+ console.log(' ✓ Auto-generated secret (hmac-sha256)');
168
+ resolve(generated);
169
+ } else {
170
+ resolve(answer.trim());
171
+ }
172
+ });
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Encrypt and store capability secret
178
+ *
179
+ * Execution function (Rule 10.1) - performs database operations
180
+ *
181
+ * @param capabilityId - Capability ID from database
182
+ * @param name - Secret name
183
+ * @param value - Plaintext secret value
184
+ * @param masterKey - Master encryption key
185
+ * @param db - Database connection
186
+ */
187
+ export async function storeCapabilitySecret(
188
+ capabilityId: number,
189
+ name: string,
190
+ value: string,
191
+ masterKey: Buffer,
192
+ db: Database,
193
+ ): Promise<void> {
194
+ const encrypted: EncryptedSecret = encryptSecret(value, masterKey);
195
+
196
+ db.prepare(
197
+ `INSERT INTO capability_secrets (capability_id, name, encrypted_value, iv, auth_tag, created_at, updated_at)
198
+ VALUES (?, ?, ?, ?, ?, unixepoch(), unixepoch())`,
199
+ ).run(capabilityId, name, encrypted.encryptedValue, encrypted.iv, encrypted.authTag);
200
+ }
@@ -0,0 +1,121 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { validatePath, validateRouteRequest, validateSlug } from './route-validation';
3
+
4
+ describe('validateSlug', () => {
5
+ test('accepts valid kebab-case slugs', () => {
6
+ expect(validateSlug('lunacycle').valid).toBe(true);
7
+ expect(validateSlug('my-app').valid).toBe(true);
8
+ expect(validateSlug('app1').valid).toBe(true);
9
+ expect(validateSlug('dns-external').valid).toBe(true);
10
+ expect(validateSlug('a').valid).toBe(true);
11
+ });
12
+
13
+ test('rejects invalid slugs', () => {
14
+ expect(validateSlug('MyApp').valid).toBe(false);
15
+ expect(validateSlug('my_app').valid).toBe(false);
16
+ expect(validateSlug('my--app').valid).toBe(false);
17
+ expect(validateSlug('-app').valid).toBe(false);
18
+ expect(validateSlug('app-').valid).toBe(false);
19
+ expect(validateSlug('').valid).toBe(false);
20
+ expect(validateSlug('App').valid).toBe(false);
21
+ });
22
+ });
23
+
24
+ describe('validatePath', () => {
25
+ test('accepts valid paths', () => {
26
+ expect(validatePath('/lunacycle').valid).toBe(true);
27
+ expect(validatePath('/lunacycle/api').valid).toBe(true);
28
+ expect(validatePath('/a').valid).toBe(true);
29
+ expect(validatePath('/foo/bar/baz').valid).toBe(true);
30
+ });
31
+
32
+ test('rejects paths without leading slash', () => {
33
+ const result = validatePath('lunacycle');
34
+ expect(result.valid).toBe(false);
35
+ expect(result.errors[0]).toContain('start with /');
36
+ });
37
+
38
+ test('rejects paths with trailing slash', () => {
39
+ const result = validatePath('/lunacycle/');
40
+ expect(result.valid).toBe(false);
41
+ expect(result.errors[0]).toContain('trailing slash');
42
+ });
43
+
44
+ test('rejects paths with double slashes', () => {
45
+ const result = validatePath('/luna//cycle');
46
+ expect(result.valid).toBe(false);
47
+ expect(result.errors[0]).toContain('double slashes');
48
+ });
49
+
50
+ test('rejects empty path', () => {
51
+ expect(validatePath('').valid).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('validateRouteRequest', () => {
56
+ test('accepts valid static route', () => {
57
+ const result = validateRouteRequest({
58
+ slug: 'lunacycle',
59
+ type: 'static',
60
+ path: '/lunacycle',
61
+ });
62
+ expect(result.valid).toBe(true);
63
+ });
64
+
65
+ test('accepts valid reverse_proxy route', () => {
66
+ const result = validateRouteRequest({
67
+ slug: 'lunacycle',
68
+ type: 'reverse_proxy',
69
+ path: '/lunacycle/api',
70
+ targetHost: '10.0.20.5',
71
+ targetPort: 3000,
72
+ });
73
+ expect(result.valid).toBe(true);
74
+ });
75
+
76
+ test('rejects reverse_proxy without targetHost', () => {
77
+ const result = validateRouteRequest({
78
+ slug: 'lunacycle',
79
+ type: 'reverse_proxy',
80
+ path: '/lunacycle/api',
81
+ targetPort: 3000,
82
+ });
83
+ expect(result.valid).toBe(false);
84
+ expect(result.errors).toContain('reverse_proxy route requires targetHost');
85
+ });
86
+
87
+ test('rejects reverse_proxy without targetPort', () => {
88
+ const result = validateRouteRequest({
89
+ slug: 'lunacycle',
90
+ type: 'reverse_proxy',
91
+ path: '/lunacycle/api',
92
+ targetHost: '10.0.20.5',
93
+ });
94
+ expect(result.valid).toBe(false);
95
+ expect(result.errors).toContain('reverse_proxy route requires targetPort');
96
+ });
97
+
98
+ test('rejects invalid port numbers', () => {
99
+ const result = validateRouteRequest({
100
+ slug: 'lunacycle',
101
+ type: 'reverse_proxy',
102
+ path: '/lunacycle/api',
103
+ targetHost: '10.0.20.5',
104
+ targetPort: 70000,
105
+ });
106
+ expect(result.valid).toBe(false);
107
+ expect(result.errors[0]).toContain('between 1 and 65535');
108
+ });
109
+
110
+ test('collects multiple errors', () => {
111
+ const result = validateRouteRequest({
112
+ slug: 'BAD SLUG',
113
+ type: 'reverse_proxy',
114
+ path: 'no-slash',
115
+ targetHost: undefined,
116
+ targetPort: undefined,
117
+ });
118
+ expect(result.valid).toBe(false);
119
+ expect(result.errors.length).toBeGreaterThan(2);
120
+ });
121
+ });