@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.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- 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
|
+
}
|