@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,30 @@
|
|
|
1
|
+
import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
|
2
|
+
import { closeDb, createDbClient, findMigrationsFolder } from './client';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Run database migrations
|
|
6
|
+
*/
|
|
7
|
+
export async function runMigrations(dbPath?: string) {
|
|
8
|
+
console.log('Running database migrations...');
|
|
9
|
+
|
|
10
|
+
const db = createDbClient(dbPath ? { path: dbPath } : undefined);
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const migrationsFolder = findMigrationsFolder();
|
|
14
|
+
await migrate(db, { migrationsFolder });
|
|
15
|
+
console.log('Migrations completed successfully');
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Migration failed:', error);
|
|
18
|
+
throw error;
|
|
19
|
+
} finally {
|
|
20
|
+
closeDb();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Run migrations if executed directly
|
|
25
|
+
if (import.meta.main) {
|
|
26
|
+
runMigrations().catch((error) => {
|
|
27
|
+
console.error('Failed to run migrations:', error);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { unlink } from 'node:fs/promises';
|
|
4
|
+
import { eq } from 'drizzle-orm';
|
|
5
|
+
import { type DbClient, createDbClient } from './client';
|
|
6
|
+
import { capabilities, moduleConfigs, modules, secrets } from './schema';
|
|
7
|
+
import type { NewCapability, NewModule, NewModuleConfig, NewSecret } from './schema';
|
|
8
|
+
|
|
9
|
+
describe('Database Schema', () => {
|
|
10
|
+
let db: DbClient;
|
|
11
|
+
let testDbPath: string;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
// Create unique database path for each test
|
|
15
|
+
testDbPath = `./test-celilo-${Date.now()}-${Math.random()}.db`;
|
|
16
|
+
|
|
17
|
+
// Create fresh database
|
|
18
|
+
db = createDbClient({ path: testDbPath });
|
|
19
|
+
|
|
20
|
+
// Create tables manually for testing (no migrations yet)
|
|
21
|
+
db.$client.run(`
|
|
22
|
+
CREATE TABLE IF NOT EXISTS modules (
|
|
23
|
+
id TEXT PRIMARY KEY,
|
|
24
|
+
name TEXT NOT NULL,
|
|
25
|
+
version TEXT NOT NULL,
|
|
26
|
+
description TEXT,
|
|
27
|
+
state TEXT NOT NULL DEFAULT 'IMPORTED',
|
|
28
|
+
manifest_data TEXT NOT NULL,
|
|
29
|
+
source_path TEXT NOT NULL,
|
|
30
|
+
imported_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
31
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
32
|
+
error_message TEXT
|
|
33
|
+
)
|
|
34
|
+
`);
|
|
35
|
+
|
|
36
|
+
db.$client.run(`
|
|
37
|
+
CREATE TABLE IF NOT EXISTS module_configs (
|
|
38
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
39
|
+
module_id TEXT NOT NULL,
|
|
40
|
+
key TEXT NOT NULL,
|
|
41
|
+
value TEXT NOT NULL,
|
|
42
|
+
value_json TEXT,
|
|
43
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
44
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
45
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
46
|
+
)
|
|
47
|
+
`);
|
|
48
|
+
|
|
49
|
+
db.$client.run(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS capabilities (
|
|
51
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
52
|
+
module_id TEXT NOT NULL,
|
|
53
|
+
capability_name TEXT NOT NULL,
|
|
54
|
+
version TEXT NOT NULL,
|
|
55
|
+
data TEXT NOT NULL,
|
|
56
|
+
registered_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
57
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
58
|
+
)
|
|
59
|
+
`);
|
|
60
|
+
|
|
61
|
+
db.$client.run(`
|
|
62
|
+
CREATE TABLE IF NOT EXISTS secrets (
|
|
63
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
64
|
+
module_id TEXT NOT NULL,
|
|
65
|
+
name TEXT NOT NULL,
|
|
66
|
+
encrypted_value TEXT NOT NULL,
|
|
67
|
+
iv TEXT NOT NULL,
|
|
68
|
+
auth_tag TEXT NOT NULL,
|
|
69
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
70
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
71
|
+
FOREIGN KEY (module_id) REFERENCES modules(id) ON DELETE CASCADE
|
|
72
|
+
)
|
|
73
|
+
`);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(async () => {
|
|
77
|
+
db.$client.close();
|
|
78
|
+
if (existsSync(testDbPath)) {
|
|
79
|
+
await unlink(testDbPath);
|
|
80
|
+
}
|
|
81
|
+
// Clean up WAL and SHM files
|
|
82
|
+
const walPath = `${testDbPath}-wal`;
|
|
83
|
+
const shmPath = `${testDbPath}-shm`;
|
|
84
|
+
if (existsSync(walPath)) {
|
|
85
|
+
await unlink(walPath);
|
|
86
|
+
}
|
|
87
|
+
if (existsSync(shmPath)) {
|
|
88
|
+
await unlink(shmPath);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('should create a module', () => {
|
|
93
|
+
const newModule: NewModule = {
|
|
94
|
+
id: 'homebridge',
|
|
95
|
+
name: 'Homebridge',
|
|
96
|
+
version: '1.0.0',
|
|
97
|
+
description: 'HomeKit bridge',
|
|
98
|
+
sourcePath: '/data/modules/homebridge',
|
|
99
|
+
manifestData: { id: 'homebridge', requires: {} },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = db.insert(modules).values(newModule).returning().get();
|
|
103
|
+
|
|
104
|
+
expect(result).toBeDefined();
|
|
105
|
+
expect(result.id).toBe('homebridge');
|
|
106
|
+
expect(result.name).toBe('Homebridge');
|
|
107
|
+
expect(result.state).toBe('IMPORTED');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('should create module config', () => {
|
|
111
|
+
// Insert module first
|
|
112
|
+
const newModule: NewModule = {
|
|
113
|
+
id: 'homebridge',
|
|
114
|
+
name: 'Homebridge',
|
|
115
|
+
version: '1.0.0',
|
|
116
|
+
sourcePath: '/data/modules/homebridge',
|
|
117
|
+
manifestData: {},
|
|
118
|
+
};
|
|
119
|
+
db.insert(modules).values(newModule).run();
|
|
120
|
+
|
|
121
|
+
// Insert config
|
|
122
|
+
const newConfig: NewModuleConfig = {
|
|
123
|
+
moduleId: 'homebridge',
|
|
124
|
+
key: 'container_ip',
|
|
125
|
+
value: '192.168.0.50',
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = db.insert(moduleConfigs).values(newConfig).returning().get();
|
|
129
|
+
|
|
130
|
+
expect(result).toBeDefined();
|
|
131
|
+
expect(result.moduleId).toBe('homebridge');
|
|
132
|
+
expect(result.key).toBe('container_ip');
|
|
133
|
+
expect(result.value).toBe('192.168.0.50');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('should create capability', () => {
|
|
137
|
+
// Insert module first
|
|
138
|
+
const newModule: NewModule = {
|
|
139
|
+
id: 'dns-external',
|
|
140
|
+
name: 'DNS External',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
sourcePath: '/data/modules/dns-external',
|
|
143
|
+
manifestData: {},
|
|
144
|
+
};
|
|
145
|
+
db.insert(modules).values(newModule).run();
|
|
146
|
+
|
|
147
|
+
// Insert capability
|
|
148
|
+
const newCapability: NewCapability = {
|
|
149
|
+
moduleId: 'dns-external',
|
|
150
|
+
capabilityName: 'dns_external',
|
|
151
|
+
version: '1.0.0',
|
|
152
|
+
data: {
|
|
153
|
+
nameserver: 'ns1.example.com',
|
|
154
|
+
zone: 'example.com',
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const result = db.insert(capabilities).values(newCapability).returning().get();
|
|
159
|
+
|
|
160
|
+
expect(result).toBeDefined();
|
|
161
|
+
expect(result.capabilityName).toBe('dns_external');
|
|
162
|
+
expect(result.data).toEqual({
|
|
163
|
+
nameserver: 'ns1.example.com',
|
|
164
|
+
zone: 'example.com',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should create secret', () => {
|
|
169
|
+
// Insert module first
|
|
170
|
+
const newModule: NewModule = {
|
|
171
|
+
id: 'dns-external',
|
|
172
|
+
name: 'DNS External',
|
|
173
|
+
version: '1.0.0',
|
|
174
|
+
sourcePath: '/data/modules/dns-external',
|
|
175
|
+
manifestData: {},
|
|
176
|
+
};
|
|
177
|
+
db.insert(modules).values(newModule).run();
|
|
178
|
+
|
|
179
|
+
// Insert secret
|
|
180
|
+
const newSecret: NewSecret = {
|
|
181
|
+
moduleId: 'dns-external',
|
|
182
|
+
name: 'tsig_key',
|
|
183
|
+
encryptedValue: 'encrypted_data_here',
|
|
184
|
+
iv: 'initialization_vector',
|
|
185
|
+
authTag: 'authentication_tag',
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const result = db.insert(secrets).values(newSecret).returning().get();
|
|
189
|
+
|
|
190
|
+
expect(result).toBeDefined();
|
|
191
|
+
expect(result.name).toBe('tsig_key');
|
|
192
|
+
expect(result.encryptedValue).toBe('encrypted_data_here');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('should cascade delete module configs when module is deleted', () => {
|
|
196
|
+
// Insert module
|
|
197
|
+
const newModule: NewModule = {
|
|
198
|
+
id: 'homebridge',
|
|
199
|
+
name: 'Homebridge',
|
|
200
|
+
version: '1.0.0',
|
|
201
|
+
sourcePath: '/data/modules/homebridge',
|
|
202
|
+
manifestData: {},
|
|
203
|
+
};
|
|
204
|
+
db.insert(modules).values(newModule).run();
|
|
205
|
+
|
|
206
|
+
// Insert config
|
|
207
|
+
const newConfig: NewModuleConfig = {
|
|
208
|
+
moduleId: 'homebridge',
|
|
209
|
+
key: 'container_ip',
|
|
210
|
+
value: '192.168.0.50',
|
|
211
|
+
};
|
|
212
|
+
db.insert(moduleConfigs).values(newConfig).run();
|
|
213
|
+
|
|
214
|
+
// Delete module
|
|
215
|
+
db.delete(modules).where(eq(modules.id, 'homebridge')).run();
|
|
216
|
+
|
|
217
|
+
// Config should be deleted
|
|
218
|
+
const configs = db.select().from(moduleConfigs).all();
|
|
219
|
+
expect(configs).toHaveLength(0);
|
|
220
|
+
});
|
|
221
|
+
});
|
package/src/db/schema.ts
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm';
|
|
2
|
+
import { integer, sqliteTable, text, unique } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Module lifecycle states
|
|
6
|
+
* IMPORTED, VALIDATED, CONFIGURED, GENERATING, ERROR, DEPLOYING, INSTALLED, VERIFIED, UNINSTALLING
|
|
7
|
+
*/
|
|
8
|
+
export type ModuleState =
|
|
9
|
+
| 'IMPORTED'
|
|
10
|
+
| 'VALIDATED'
|
|
11
|
+
| 'CONFIGURED'
|
|
12
|
+
| 'GENERATING'
|
|
13
|
+
| 'DEPLOYING'
|
|
14
|
+
| 'INSTALLED'
|
|
15
|
+
| 'VERIFIED'
|
|
16
|
+
| 'ERROR'
|
|
17
|
+
| 'UNINSTALLING';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Modules table - stores module metadata and manifest data
|
|
21
|
+
*/
|
|
22
|
+
export const modules = sqliteTable('modules', {
|
|
23
|
+
id: text('id').primaryKey(),
|
|
24
|
+
name: text('name').notNull(),
|
|
25
|
+
version: text('version').notNull(),
|
|
26
|
+
description: text('description'),
|
|
27
|
+
state: text('state').$type<ModuleState>().notNull().default('IMPORTED'),
|
|
28
|
+
manifestData: text('manifest_data', { mode: 'json' }).$type<Record<string, unknown>>().notNull(),
|
|
29
|
+
sourcePath: text('source_path').notNull(),
|
|
30
|
+
importedAt: integer('imported_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
31
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
32
|
+
errorMessage: text('error_message'),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Module configuration - user-provided key-value pairs
|
|
37
|
+
* Stores configuration like container_ip, zone, etc.
|
|
38
|
+
*
|
|
39
|
+
* For primitive types (string, number, boolean):
|
|
40
|
+
* - value: contains the value (as string)
|
|
41
|
+
* - valueJson: NULL
|
|
42
|
+
*
|
|
43
|
+
* For complex types (arrays, objects):
|
|
44
|
+
* - value: empty string
|
|
45
|
+
* - valueJson: JSON-stringified complex value
|
|
46
|
+
*/
|
|
47
|
+
export const moduleConfigs = sqliteTable(
|
|
48
|
+
'module_configs',
|
|
49
|
+
{
|
|
50
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
51
|
+
moduleId: text('module_id')
|
|
52
|
+
.notNull()
|
|
53
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
54
|
+
key: text('key').notNull(),
|
|
55
|
+
value: text('value').notNull(),
|
|
56
|
+
valueJson: text('value_json'), // JSON for complex types (arrays, objects)
|
|
57
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
58
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
59
|
+
},
|
|
60
|
+
(table) => ({
|
|
61
|
+
uniqueModuleKey: unique().on(table.moduleId, table.key),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Capabilities table - stores registered capabilities provided by modules
|
|
67
|
+
* Example: namecheap module provides dns_registrar capability
|
|
68
|
+
*/
|
|
69
|
+
export const capabilities = sqliteTable('capabilities', {
|
|
70
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
71
|
+
moduleId: text('module_id')
|
|
72
|
+
.notNull()
|
|
73
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
74
|
+
capabilityName: text('capability_name').notNull(),
|
|
75
|
+
version: text('version').notNull(),
|
|
76
|
+
data: text('data', { mode: 'json' }).$type<Record<string, unknown>>().notNull(),
|
|
77
|
+
/** JSON array of zone names this capability applies to, or null for zone-agnostic */
|
|
78
|
+
zones: text('zones', { mode: 'json' }).$type<string[] | null>(),
|
|
79
|
+
registeredAt: integer('registered_at', { mode: 'timestamp' })
|
|
80
|
+
.notNull()
|
|
81
|
+
.default(sql`(unixepoch())`),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Capability secrets table - stores encrypted secrets owned by capabilities
|
|
86
|
+
* Values are encrypted with AES-256-GCM using master key
|
|
87
|
+
* Example: dns-external capability owns TSIG secret
|
|
88
|
+
*/
|
|
89
|
+
export const capabilitySecrets = sqliteTable(
|
|
90
|
+
'capability_secrets',
|
|
91
|
+
{
|
|
92
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
93
|
+
capabilityId: integer('capability_id')
|
|
94
|
+
.notNull()
|
|
95
|
+
.references(() => capabilities.id, { onDelete: 'cascade' }),
|
|
96
|
+
name: text('name').notNull(),
|
|
97
|
+
description: text('description'), // Added to store secret description
|
|
98
|
+
encryptedValue: text('encrypted_value'), // Nullable - allows metadata-only storage during import
|
|
99
|
+
iv: text('iv'), // Nullable - populated when secret value is set
|
|
100
|
+
authTag: text('auth_tag'), // Nullable - populated when secret value is set
|
|
101
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
102
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
103
|
+
},
|
|
104
|
+
(table) => ({
|
|
105
|
+
uniqueCapabilitySecret: unique().on(table.capabilityId, table.name),
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Secrets table - stores encrypted secrets per module
|
|
111
|
+
* Values are encrypted with AES-256-GCM using master key
|
|
112
|
+
*/
|
|
113
|
+
export const secrets = sqliteTable('secrets', {
|
|
114
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
115
|
+
moduleId: text('module_id')
|
|
116
|
+
.notNull()
|
|
117
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
118
|
+
name: text('name').notNull(),
|
|
119
|
+
encryptedValue: text('encrypted_value').notNull(),
|
|
120
|
+
iv: text('iv').notNull(),
|
|
121
|
+
authTag: text('auth_tag').notNull(),
|
|
122
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
123
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* System configuration - system-wide settings
|
|
128
|
+
* Used for $system: variables in templates
|
|
129
|
+
* Examples: DNS servers, network settings, domain names
|
|
130
|
+
*/
|
|
131
|
+
export const systemConfig = sqliteTable('system_config', {
|
|
132
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
133
|
+
key: text('key').notNull().unique(),
|
|
134
|
+
value: text('value').notNull(),
|
|
135
|
+
description: text('description'),
|
|
136
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
137
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* System secrets table - stores encrypted system-level secrets
|
|
142
|
+
* Values are encrypted with AES-256-GCM using master key
|
|
143
|
+
* Examples: Proxmox root password, API tokens, SSH keys
|
|
144
|
+
*/
|
|
145
|
+
export const systemSecrets = sqliteTable('system_secrets', {
|
|
146
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
147
|
+
key: text('key').notNull().unique(),
|
|
148
|
+
encryptedValue: text('encrypted_value').notNull(),
|
|
149
|
+
iv: text('iv').notNull(),
|
|
150
|
+
authTag: text('auth_tag').notNull(),
|
|
151
|
+
description: text('description'),
|
|
152
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
153
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Module integrity table - stores checksums and signature for package verification
|
|
158
|
+
* Used for runtime auditing to detect tampering, missing files, or extra files
|
|
159
|
+
*/
|
|
160
|
+
export const moduleIntegrity = sqliteTable('module_integrity', {
|
|
161
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
162
|
+
moduleId: text('module_id')
|
|
163
|
+
.notNull()
|
|
164
|
+
.unique()
|
|
165
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
166
|
+
checksums: text('checksums', { mode: 'json' }).$type<Record<string, string>>().notNull(), // { "path/to/file": "xxhash", ... }
|
|
167
|
+
signature: text('signature'), // Nullable - null for directory imports, populated for .netapp packages
|
|
168
|
+
importedAt: integer('imported_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
169
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* IPAM (IP Address Management) allocations table
|
|
174
|
+
* Tracks VMID and IP address assignments per module
|
|
175
|
+
* Prevents conflicts and enables automatic allocation from zone subnets
|
|
176
|
+
*/
|
|
177
|
+
export const ipAllocations = sqliteTable('ip_allocations', {
|
|
178
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
179
|
+
moduleId: text('module_id')
|
|
180
|
+
.notNull()
|
|
181
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
182
|
+
vmid: integer('vmid').notNull().unique(),
|
|
183
|
+
containerIp: text('container_ip').notNull().unique(), // CIDR format (e.g., "10.0.10.10/24")
|
|
184
|
+
zone: text('zone').$type<'dmz' | 'app' | 'secure' | 'internal'>().notNull(),
|
|
185
|
+
allocatedAt: integer('allocated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* IP reservations table
|
|
190
|
+
* Allows users to reserve IPs for infrastructure or external services
|
|
191
|
+
* IPAM allocator skips reserved IPs
|
|
192
|
+
*/
|
|
193
|
+
export const ipReservations = sqliteTable('ip_reservations', {
|
|
194
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
195
|
+
ipStart: text('ip_start').notNull(), // Single IP or range start
|
|
196
|
+
ipEnd: text('ip_end'), // NULL for single IP, end IP for range
|
|
197
|
+
zone: text('zone').$type<'dmz' | 'app' | 'secure' | 'internal'>().notNull(),
|
|
198
|
+
reason: text('reason').notNull(),
|
|
199
|
+
reservedAt: integer('reserved_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* VMID reservations table
|
|
204
|
+
* Allows users to reserve VMIDs for existing VMs or external systems
|
|
205
|
+
* IPAM allocator skips reserved VMIDs
|
|
206
|
+
*/
|
|
207
|
+
export const vmidReservations = sqliteTable('vmid_reservations', {
|
|
208
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
209
|
+
vmid: integer('vmid').notNull().unique(),
|
|
210
|
+
reason: text('reason').notNull(),
|
|
211
|
+
reservedAt: integer('reserved_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Module builds table
|
|
216
|
+
* Tracks build metadata for modules with custom compilation requirements
|
|
217
|
+
* Example: Caddy with RFC2136 DNS provider, custom Go binaries
|
|
218
|
+
*/
|
|
219
|
+
export type BuildStatus = 'success' | 'failed' | 'in_progress';
|
|
220
|
+
|
|
221
|
+
export const moduleBuilds = sqliteTable('module_builds', {
|
|
222
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
223
|
+
moduleId: text('module_id')
|
|
224
|
+
.notNull()
|
|
225
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
226
|
+
version: text('version').notNull(),
|
|
227
|
+
builtAt: integer('built_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
228
|
+
artifacts: text('artifacts', { mode: 'json' }).$type<string[]>().notNull(), // Array of artifact paths
|
|
229
|
+
environment: text('environment').$type<'nix' | 'system'>(),
|
|
230
|
+
status: text('status').$type<BuildStatus>().notNull(),
|
|
231
|
+
buildLog: text('build_log'), // Build output for debugging
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Network zones type
|
|
236
|
+
* - internal: Home network (192.168.0.0/24)
|
|
237
|
+
* - dmz: Public-facing services in home lab (10.0.10.0/24)
|
|
238
|
+
* - app: Internal application services (10.0.20.0/24)
|
|
239
|
+
* - secure: Authentication and database services (10.0.30.0/24)
|
|
240
|
+
* - external: Internet-hosted services (outside home network)
|
|
241
|
+
*/
|
|
242
|
+
export type NetworkZone = 'internal' | 'dmz' | 'app' | 'secure' | 'external';
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Container services table
|
|
246
|
+
* Stores container service providers (Proxmox, Digital Ocean, etc.)
|
|
247
|
+
* that can provision new containers/VMs on demand
|
|
248
|
+
*/
|
|
249
|
+
export const containerServices = sqliteTable('container_services', {
|
|
250
|
+
id: text('id').primaryKey(), // UUID
|
|
251
|
+
serviceId: text('service_id').notNull().unique(), // User-facing kebab-case ID
|
|
252
|
+
name: text('name').notNull(),
|
|
253
|
+
providerName: text('provider_name')
|
|
254
|
+
.$type<'proxmox' | 'digitalocean' | 'aws' | 'gcp' | 'azure'>()
|
|
255
|
+
.notNull(),
|
|
256
|
+
zones: text('zones', { mode: 'json' }).$type<NetworkZone[]>().notNull(),
|
|
257
|
+
apiCredentialsEncrypted: text('api_credentials_encrypted').notNull(),
|
|
258
|
+
providerConfig: text('provider_config', { mode: 'json' })
|
|
259
|
+
.$type<Record<string, unknown>>()
|
|
260
|
+
.notNull(),
|
|
261
|
+
verified: integer('verified', { mode: 'boolean' }).notNull().default(false),
|
|
262
|
+
verifiedAt: integer('verified_at', { mode: 'timestamp' }),
|
|
263
|
+
verificationError: text('verification_error'),
|
|
264
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
265
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Machines table
|
|
270
|
+
* Stores pre-existing machines (Raspberry Pi, VPS, bare metal)
|
|
271
|
+
* that users have added to the pool for hosting modules
|
|
272
|
+
*/
|
|
273
|
+
export const machines = sqliteTable('machines', {
|
|
274
|
+
id: text('id').primaryKey(), // UUID
|
|
275
|
+
hostname: text('hostname').notNull(),
|
|
276
|
+
zone: text('zone').$type<NetworkZone>().notNull(),
|
|
277
|
+
ipAddress: text('ip_address').notNull(),
|
|
278
|
+
sshUser: text('ssh_user').notNull(),
|
|
279
|
+
sshKeyEncrypted: text('ssh_key_encrypted').notNull(),
|
|
280
|
+
hardware: text('hardware', { mode: 'json' })
|
|
281
|
+
.$type<{
|
|
282
|
+
cpu_cores: number;
|
|
283
|
+
memory_mb: number;
|
|
284
|
+
disk_gb: number;
|
|
285
|
+
arch?: string;
|
|
286
|
+
}>()
|
|
287
|
+
.notNull(),
|
|
288
|
+
/** Machine classification: 'host' (single interface) or 'router' (multi-interface) */
|
|
289
|
+
role: text('role').$type<'host' | 'router'>().notNull().default('host'),
|
|
290
|
+
/** Detected network interfaces with IPs and zones */
|
|
291
|
+
interfaces: text('interfaces', { mode: 'json' })
|
|
292
|
+
.$type<Array<{ name: string; ipAddress: string; zone: string }>>()
|
|
293
|
+
.notNull()
|
|
294
|
+
.default(sql`'[]'`),
|
|
295
|
+
assignedModuleIds: text('assigned_module_ids', { mode: 'json' })
|
|
296
|
+
.$type<string[]>()
|
|
297
|
+
.notNull()
|
|
298
|
+
.default(sql`'[]'`),
|
|
299
|
+
/** Module ID this machine is earmarked for. If set, only this module can use this machine. */
|
|
300
|
+
earmarkedModule: text('earmarked_module'),
|
|
301
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
302
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Module infrastructure table
|
|
307
|
+
* Tracks which infrastructure (machine or container service) is used for each module
|
|
308
|
+
*/
|
|
309
|
+
export const moduleInfrastructure = sqliteTable('module_infrastructure', {
|
|
310
|
+
id: text('id').primaryKey(), // UUID
|
|
311
|
+
moduleId: text('module_id')
|
|
312
|
+
.notNull()
|
|
313
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
314
|
+
infrastructureType: text('infrastructure_type')
|
|
315
|
+
.$type<'machine' | 'container_service'>()
|
|
316
|
+
.notNull(),
|
|
317
|
+
machineId: text('machine_id').references(() => machines.id),
|
|
318
|
+
serviceId: text('service_id').references(() => containerServices.id),
|
|
319
|
+
containerMetadata: text('container_metadata', { mode: 'json' }).$type<Record<string, unknown>>(), // VMID, droplet ID, etc.
|
|
320
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Web routes table
|
|
325
|
+
* Tracks routes registered by modules via public_web capability functions.
|
|
326
|
+
* Routes are registered during on_install hooks and removed during on_uninstall.
|
|
327
|
+
* The public_web provider (Caddy) uses these to generate its configuration.
|
|
328
|
+
*/
|
|
329
|
+
export const webRoutes = sqliteTable('web_routes', {
|
|
330
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
331
|
+
slug: text('slug').notNull(),
|
|
332
|
+
moduleId: text('module_id')
|
|
333
|
+
.notNull()
|
|
334
|
+
.references(() => modules.id, { onDelete: 'cascade' }),
|
|
335
|
+
type: text('type').$type<'static' | 'reverse_proxy'>().notNull(),
|
|
336
|
+
path: text('path').notNull().unique(),
|
|
337
|
+
targetHost: text('target_host'),
|
|
338
|
+
targetPort: integer('target_port'),
|
|
339
|
+
subdomain: text('subdomain'),
|
|
340
|
+
websocket: integer('websocket', { mode: 'boolean' }).notNull().default(false),
|
|
341
|
+
contentHash: text('content_hash'),
|
|
342
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
343
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Backup storage providers - destinations for backup archives
|
|
348
|
+
* Supports local filesystem and S3-compatible storage (AWS S3, MinIO, Backblaze B2, Wasabi)
|
|
349
|
+
*/
|
|
350
|
+
export type BackupStorageProvider = 'local' | 's3';
|
|
351
|
+
|
|
352
|
+
export const backupStorages = sqliteTable('backup_storages', {
|
|
353
|
+
id: text('id').primaryKey(), // UUID
|
|
354
|
+
storageId: text('storage_id').notNull().unique(), // User-facing kebab-case ID
|
|
355
|
+
name: text('name').notNull(),
|
|
356
|
+
providerName: text('provider_name').$type<BackupStorageProvider>().notNull(),
|
|
357
|
+
credentialsEncrypted: text('credentials_encrypted').notNull(), // Encrypted JSON blob
|
|
358
|
+
providerConfig: text('provider_config', { mode: 'json' })
|
|
359
|
+
.$type<Record<string, unknown>>()
|
|
360
|
+
.notNull(),
|
|
361
|
+
verified: integer('verified', { mode: 'boolean' }).notNull().default(false),
|
|
362
|
+
verifiedAt: integer('verified_at', { mode: 'timestamp' }),
|
|
363
|
+
verificationError: text('verification_error'),
|
|
364
|
+
isDefault: integer('is_default', { mode: 'boolean' }).notNull().default(false),
|
|
365
|
+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
366
|
+
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Backup records - metadata for each backup taken
|
|
371
|
+
* Tracks both system state backups and module data backups
|
|
372
|
+
*/
|
|
373
|
+
export type BackupType = 'module_data' | 'system_state';
|
|
374
|
+
export type BackupStatus = 'in_progress' | 'completed' | 'failed';
|
|
375
|
+
|
|
376
|
+
export const backups = sqliteTable('backups', {
|
|
377
|
+
id: text('id').primaryKey(), // UUID
|
|
378
|
+
moduleId: text('module_id').references(() => modules.id, { onDelete: 'set null' }), // null = system state backup
|
|
379
|
+
storageId: text('storage_id')
|
|
380
|
+
.notNull()
|
|
381
|
+
.references(() => backupStorages.id),
|
|
382
|
+
storagePath: text('storage_path').notNull(), // path/key within storage
|
|
383
|
+
backupType: text('backup_type').$type<BackupType>().notNull(),
|
|
384
|
+
moduleVersion: text('module_version'), // module version at backup time
|
|
385
|
+
schemaVersion: text('schema_version'), // optional data schema version from on_backup hook
|
|
386
|
+
sizeBytes: integer('size_bytes'),
|
|
387
|
+
metadata: text('metadata', { mode: 'json' })
|
|
388
|
+
.$type<Record<string, unknown>>()
|
|
389
|
+
.notNull()
|
|
390
|
+
.default(sql`'{}'`),
|
|
391
|
+
status: text('status').$type<BackupStatus>().notNull().default('in_progress'),
|
|
392
|
+
errorMessage: text('error_message'),
|
|
393
|
+
name: text('name'), // optional human-readable name/annotation
|
|
394
|
+
startedAt: integer('started_at', { mode: 'timestamp' }).notNull().default(sql`(unixepoch())`),
|
|
395
|
+
completedAt: integer('completed_at', { mode: 'timestamp' }),
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Type exports for use in application code
|
|
400
|
+
*/
|
|
401
|
+
export type Module = typeof modules.$inferSelect;
|
|
402
|
+
export type NewModule = typeof modules.$inferInsert;
|
|
403
|
+
export type ModuleConfig = typeof moduleConfigs.$inferSelect;
|
|
404
|
+
export type NewModuleConfig = typeof moduleConfigs.$inferInsert;
|
|
405
|
+
export type Capability = typeof capabilities.$inferSelect;
|
|
406
|
+
export type NewCapability = typeof capabilities.$inferInsert;
|
|
407
|
+
export type Secret = typeof secrets.$inferSelect;
|
|
408
|
+
export type NewSecret = typeof secrets.$inferInsert;
|
|
409
|
+
export type SystemConfig = typeof systemConfig.$inferSelect;
|
|
410
|
+
export type NewSystemConfig = typeof systemConfig.$inferInsert;
|
|
411
|
+
export type SystemSecret = typeof systemSecrets.$inferSelect;
|
|
412
|
+
export type NewSystemSecret = typeof systemSecrets.$inferInsert;
|
|
413
|
+
export type ModuleIntegrity = typeof moduleIntegrity.$inferSelect;
|
|
414
|
+
export type NewModuleIntegrity = typeof moduleIntegrity.$inferInsert;
|
|
415
|
+
export type IpAllocation = typeof ipAllocations.$inferSelect;
|
|
416
|
+
export type NewIpAllocation = typeof ipAllocations.$inferInsert;
|
|
417
|
+
export type IpReservation = typeof ipReservations.$inferSelect;
|
|
418
|
+
export type NewIpReservation = typeof ipReservations.$inferInsert;
|
|
419
|
+
export type VmidReservation = typeof vmidReservations.$inferSelect;
|
|
420
|
+
export type NewVmidReservation = typeof vmidReservations.$inferInsert;
|
|
421
|
+
export type ModuleBuild = typeof moduleBuilds.$inferSelect;
|
|
422
|
+
export type NewModuleBuild = typeof moduleBuilds.$inferInsert;
|
|
423
|
+
export type ContainerService = typeof containerServices.$inferSelect;
|
|
424
|
+
export type NewContainerService = typeof containerServices.$inferInsert;
|
|
425
|
+
export type Machine = typeof machines.$inferSelect;
|
|
426
|
+
export type NewMachine = typeof machines.$inferInsert;
|
|
427
|
+
export type ModuleInfrastructure = typeof moduleInfrastructure.$inferSelect;
|
|
428
|
+
export type NewModuleInfrastructure = typeof moduleInfrastructure.$inferInsert;
|
|
429
|
+
export type WebRoute = typeof webRoutes.$inferSelect;
|
|
430
|
+
export type NewWebRoute = typeof webRoutes.$inferInsert;
|
|
431
|
+
export type BackupStorage = typeof backupStorages.$inferSelect;
|
|
432
|
+
export type NewBackupStorage = typeof backupStorages.$inferInsert;
|
|
433
|
+
export type Backup = typeof backups.$inferSelect;
|
|
434
|
+
export type NewBackup = typeof backups.$inferInsert;
|