@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,246 @@
1
+ /**
2
+ * Tests for firewall chain building in capability loader
3
+ *
4
+ * Tests the buildFirewallChain logic that wires iptables → greenwave.
5
+ */
6
+
7
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
8
+ import { mkdirSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import type { HookLogger } from '@celilo/capabilities';
12
+ import type { DbClient } from '../db/client';
13
+ import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
14
+ import { loadCapabilityFunctions } from './capability-loader';
15
+
16
+ const noopLogger: HookLogger = {
17
+ info() {},
18
+ warn() {},
19
+ error() {},
20
+ success() {},
21
+ };
22
+
23
+ // Mock greenwave firewall factory using defineCapabilityFunction (the
24
+ // post-Phase-8 pattern). The compiled factory takes a single
25
+ // `{ config, secrets, logger }` context and the framework wraps the
26
+ // returned method table with auto-logging.
27
+ //
28
+ // We use an absolute import path to the workspace package because the
29
+ // temp dir where the mock is dropped has no node_modules link to
30
+ // resolve the bare `@celilo/capabilities` specifier.
31
+ const CAPABILITIES_PKG_PATH = join(__dirname, '../../../..', 'packages/capabilities/src/index.ts');
32
+ const MOCK_GREENWAVE_MODULE = `
33
+ import { defineCapabilityFunction } from '${CAPABILITIES_PKG_PATH}';
34
+
35
+ export default defineCapabilityFunction({
36
+ capability: 'firewall',
37
+ handler: ({ config, secrets }) => ({
38
+ exposeService: async (opts) => ({
39
+ externalIp: '71.36.99.96',
40
+ natIp: opts.internalIp,
41
+ }),
42
+ unexposeService: async () => {},
43
+ listExposedServices: async () => [],
44
+ }),
45
+ });
46
+ `;
47
+
48
+ // Mock iptables firewall factory — kept on the legacy
49
+ // `createFirewall(config, upstreamFirewall)` shape because its
50
+ // upstream-injection signature doesn't fit the
51
+ // defineCapabilityFunction `{ config, secrets, logger }` context.
52
+ // buildFirewallChain detects this and wires the upstream manually.
53
+ const MOCK_IPTABLES_MODULE = `
54
+ export function createFirewall(config, upstreamFirewall) {
55
+ return {
56
+ exposeService: async (opts) => {
57
+ if (!upstreamFirewall) {
58
+ throw new Error('No upstream firewall');
59
+ }
60
+ // Delegate upstream first (with NAT IP)
61
+ const upstream = await upstreamFirewall.exposeService({
62
+ ...opts,
63
+ internalIp: config.natIp,
64
+ });
65
+ // Then "create" local rules (just tracking for test)
66
+ return {
67
+ externalIp: upstream.externalIp,
68
+ natIp: config.natIp,
69
+ localRulesCreated: true,
70
+ originalIp: opts.internalIp,
71
+ };
72
+ },
73
+ unexposeService: async () => {},
74
+ listExposedServices: async () => [],
75
+ };
76
+ }
77
+ `;
78
+
79
+ describe('Firewall Chain Building', () => {
80
+ let db: DbClient;
81
+ let tempDir: string;
82
+
83
+ beforeEach(async () => {
84
+ db = await setupTestDatabase();
85
+ tempDir = join(tmpdir(), `celilo-fw-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
86
+ mkdirSync(tempDir, { recursive: true });
87
+ });
88
+
89
+ afterEach(async () => {
90
+ await cleanupTestDatabase(db);
91
+ });
92
+
93
+ test('loads single firewall provider (greenwave only)', async () => {
94
+ const gwPath = join(tempDir, 'greenwave');
95
+ const gwScripts = join(gwPath, 'scripts');
96
+ mkdirSync(gwScripts, { recursive: true });
97
+ writeFileSync(join(gwScripts, 'firewall-functions.ts'), MOCK_GREENWAVE_MODULE);
98
+
99
+ db.$client.run(
100
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('greenwave', 'GreenWave', '1.0.0', '${gwPath}', '{}')`,
101
+ );
102
+ db.$client.run(
103
+ `INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{"has_external":true}', '["internal"]')`,
104
+ );
105
+ db.$client.run(
106
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('greenwave', 'router_ip', '192.168.0.1')`,
107
+ );
108
+
109
+ // Set up encrypted secrets
110
+ const { encryptSecret } = await import('../secrets/encryption');
111
+ const { getOrCreateMasterKey } = await import('../secrets/master-key');
112
+ const masterKey = await getOrCreateMasterKey();
113
+ for (const [name, value] of [
114
+ ['router_username', 'admin'],
115
+ ['router_password', 'test'],
116
+ ]) {
117
+ const enc = encryptSecret(value, masterKey);
118
+ db.$client.run(
119
+ `INSERT INTO secrets (module_id, name, encrypted_value, iv, auth_tag) VALUES ('greenwave', '${name}', '${enc.encryptedValue}', '${enc.iv}', '${enc.authTag}')`,
120
+ );
121
+ }
122
+
123
+ const result = await loadCapabilityFunctions('test-consumer', db, noopLogger);
124
+ expect(result.firewall).toBeTruthy();
125
+
126
+ // Test the interface
127
+ const fw = result.firewall as {
128
+ exposeService: (opts: {
129
+ internalIp: string;
130
+ ports: number[];
131
+ description: string;
132
+ }) => Promise<{ externalIp: string }>;
133
+ };
134
+ const exposed = await fw.exposeService({
135
+ internalIp: '10.0.10.10',
136
+ ports: [80],
137
+ description: 'test',
138
+ });
139
+ expect(exposed.externalIp).toBe('71.36.99.96');
140
+ });
141
+
142
+ test('builds chain with two providers (iptables → greenwave)', async () => {
143
+ // Set up greenwave
144
+ const gwPath = join(tempDir, 'greenwave');
145
+ const gwScripts = join(gwPath, 'scripts');
146
+ mkdirSync(gwScripts, { recursive: true });
147
+ writeFileSync(join(gwScripts, 'firewall-functions.ts'), MOCK_GREENWAVE_MODULE);
148
+
149
+ db.$client.run(
150
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('greenwave', 'GreenWave', '1.0.0', '${gwPath}', '{}')`,
151
+ );
152
+ db.$client.run(
153
+ `INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{"has_external":true}', '["internal"]')`,
154
+ );
155
+ db.$client.run(
156
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('greenwave', 'router_ip', '192.168.0.1')`,
157
+ );
158
+
159
+ const { encryptSecret } = await import('../secrets/encryption');
160
+ const { getOrCreateMasterKey } = await import('../secrets/master-key');
161
+ const masterKey = await getOrCreateMasterKey();
162
+ for (const [name, value] of [
163
+ ['router_username', 'admin'],
164
+ ['router_password', 'test'],
165
+ ]) {
166
+ const enc = encryptSecret(value, masterKey);
167
+ db.$client.run(
168
+ `INSERT INTO secrets (module_id, name, encrypted_value, iv, auth_tag) VALUES ('greenwave', '${name}', '${enc.encryptedValue}', '${enc.iv}', '${enc.authTag}')`,
169
+ );
170
+ }
171
+
172
+ // Set up iptables
173
+ const iptPath = join(tempDir, 'iptables');
174
+ const iptScripts = join(iptPath, 'scripts');
175
+ mkdirSync(iptScripts, { recursive: true });
176
+ writeFileSync(join(iptScripts, 'firewall-functions.ts'), MOCK_IPTABLES_MODULE);
177
+
178
+ db.$client.run(
179
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('iptables', 'iptables', '1.0.0', '${iptPath}', '{}')`,
180
+ );
181
+ db.$client.run(
182
+ `INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz","app","secure"]')`,
183
+ );
184
+ db.$client.run(
185
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('iptables', 'firewall_ip', '192.168.0.254')`,
186
+ );
187
+ db.$client.run(
188
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('iptables', 'nat_ip', '192.168.0.253')`,
189
+ );
190
+ db.$client.run(
191
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('iptables', 'upstream_zone', 'internal')`,
192
+ );
193
+
194
+ const result = await loadCapabilityFunctions('test-consumer', db, noopLogger);
195
+ expect(result.firewall).toBeTruthy();
196
+
197
+ // Test the chain: iptables delegates to greenwave
198
+ const fw = result.firewall as {
199
+ exposeService: (opts: {
200
+ internalIp: string;
201
+ ports: number[];
202
+ description: string;
203
+ }) => Promise<{
204
+ externalIp: string;
205
+ natIp: string;
206
+ localRulesCreated: boolean;
207
+ originalIp: string;
208
+ }>;
209
+ };
210
+
211
+ const exposed = await fw.exposeService({
212
+ internalIp: '10.0.10.10',
213
+ ports: [80],
214
+ description: 'Caddy',
215
+ });
216
+
217
+ // External IP came from greenwave (leaf)
218
+ expect(exposed.externalIp).toBe('71.36.99.96');
219
+ // NAT IP is iptables' NAT address
220
+ expect(exposed.natIp).toBe('192.168.0.253');
221
+ // iptables created local rules
222
+ expect(exposed.localRulesCreated).toBe(true);
223
+ // iptables tracked the original internal IP
224
+ expect(exposed.originalIp).toBe('10.0.10.10');
225
+ });
226
+
227
+ test('returns null when no firewall has external interface', async () => {
228
+ const iptPath = join(tempDir, 'iptables');
229
+ const iptScripts = join(iptPath, 'scripts');
230
+ mkdirSync(iptScripts, { recursive: true });
231
+ writeFileSync(join(iptScripts, 'firewall-functions.ts'), MOCK_IPTABLES_MODULE);
232
+
233
+ db.$client.run(
234
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('iptables', 'iptables', '1.0.0', '${iptPath}', '{}')`,
235
+ );
236
+ // Note: has_external is NOT set
237
+ db.$client.run(
238
+ `INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz"]')`,
239
+ );
240
+
241
+ const _result = await loadCapabilityFunctions('test-consumer', db, noopLogger);
242
+ // Single provider without has_external — still loads (just can't delegate)
243
+ // But buildFirewallChain is only called with >1 providers
244
+ // Single provider loads normally via the standard path
245
+ });
246
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Tests for capability function loader
3
+ */
4
+
5
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
6
+ import { mkdirSync, writeFileSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import type { HookLogger } from '@celilo/capabilities';
10
+ import type { DbClient } from '../db/client';
11
+ import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
12
+ import { loadCapabilityFunctions } from './capability-loader';
13
+
14
+ const noopLogger: HookLogger = {
15
+ info() {},
16
+ warn() {},
17
+ error() {},
18
+ success() {},
19
+ };
20
+
21
+ // Minimal test module that exports a default factory
22
+ const TEST_REGISTER_HOST_MODULE = `
23
+ export default function registerHost(context) {
24
+ return {
25
+ host: context.config.primary_domain,
26
+ hasSecrets: !!context.secrets.ddns_password,
27
+ };
28
+ }
29
+ `;
30
+
31
+ describe('Capability Loader', () => {
32
+ let db: DbClient;
33
+ let tempDir: string;
34
+
35
+ beforeEach(async () => {
36
+ db = await setupTestDatabase();
37
+ tempDir = join(
38
+ tmpdir(),
39
+ `celilo-cap-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
40
+ );
41
+ mkdirSync(tempDir, { recursive: true });
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await cleanupTestDatabase(db);
46
+ });
47
+
48
+ test('returns empty object when no capabilities are registered', async () => {
49
+ const result = await loadCapabilityFunctions('test-module', db, noopLogger);
50
+ expect(result).toEqual({});
51
+ });
52
+
53
+ test('returns empty object when capability is registered but module does not exist', async () => {
54
+ const modulePath = join(tempDir, 'namecheap');
55
+ mkdirSync(modulePath, { recursive: true });
56
+
57
+ db.$client.run(
58
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('namecheap', 'Namecheap', '2.0.0', '${modulePath}', '{}')`,
59
+ );
60
+ db.$client.run(
61
+ `INSERT INTO capabilities (module_id, capability_name, version, data, registered_at) VALUES ('namecheap', 'dns_registrar', '2.0.0', '{}', unixepoch())`,
62
+ );
63
+
64
+ const result = await loadCapabilityFunctions('caddy', db, noopLogger);
65
+ expect(result).toEqual({});
66
+ });
67
+
68
+ test('loads dns_registrar capability when module, config, and secrets exist', async () => {
69
+ // Create module directory with register-host module
70
+ const modulePath = join(tempDir, 'namecheap');
71
+ const scriptsDir = join(modulePath, 'scripts');
72
+ mkdirSync(scriptsDir, { recursive: true });
73
+ writeFileSync(join(scriptsDir, 'register-host.ts'), TEST_REGISTER_HOST_MODULE);
74
+
75
+ db.$client.run(
76
+ `INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('namecheap', 'Namecheap', '2.0.0', '${modulePath}', '{}')`,
77
+ );
78
+ db.$client.run(
79
+ `INSERT INTO capabilities (module_id, capability_name, version, data, registered_at) VALUES ('namecheap', 'dns_registrar', '2.0.0', '{}', unixepoch())`,
80
+ );
81
+ db.$client.run(
82
+ `INSERT INTO module_configs (module_id, key, value) VALUES ('namecheap', 'primary_domain', 'example.com')`,
83
+ );
84
+
85
+ // Set encrypted secrets
86
+ const { encryptSecret } = await import('../secrets/encryption');
87
+ const { getOrCreateMasterKey } = await import('../secrets/master-key');
88
+ const masterKey = await getOrCreateMasterKey();
89
+
90
+ const ddnsPasswordEnc = encryptSecret('test-ddns-password', masterKey);
91
+ db.$client.run(
92
+ `INSERT INTO secrets (module_id, name, encrypted_value, iv, auth_tag) VALUES ('namecheap', 'ddns_password', '${ddnsPasswordEnc.encryptedValue}', '${ddnsPasswordEnc.iv}', '${ddnsPasswordEnc.authTag}')`,
93
+ );
94
+
95
+ const result = await loadCapabilityFunctions('caddy', db, noopLogger);
96
+
97
+ expect(result).toHaveProperty('dns_registrar');
98
+ expect(result.dns_registrar).toBeTruthy();
99
+ });
100
+ });