@geekmidas/cli 0.48.0 → 0.49.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 (49) hide show
  1. package/dist/{dokploy-api-DvzIDxTj.mjs → dokploy-api-94KzmTVf.mjs} +4 -4
  2. package/dist/dokploy-api-94KzmTVf.mjs.map +1 -0
  3. package/dist/dokploy-api-CItuaWTq.mjs +3 -0
  4. package/dist/dokploy-api-DBNE8MDt.cjs +3 -0
  5. package/dist/{dokploy-api-BDLu0qWi.cjs → dokploy-api-YD8WCQfW.cjs} +4 -4
  6. package/dist/dokploy-api-YD8WCQfW.cjs.map +1 -0
  7. package/dist/index.cjs +2392 -1888
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.mjs +2389 -1885
  10. package/dist/index.mjs.map +1 -1
  11. package/package.json +6 -4
  12. package/src/build/__tests__/handler-templates.spec.ts +947 -0
  13. package/src/deploy/__tests__/__fixtures__/entry-apps/async-entry.ts +24 -0
  14. package/src/deploy/__tests__/__fixtures__/entry-apps/nested-config-entry.ts +24 -0
  15. package/src/deploy/__tests__/__fixtures__/entry-apps/no-env-entry.ts +12 -0
  16. package/src/deploy/__tests__/__fixtures__/entry-apps/simple-entry.ts +14 -0
  17. package/src/deploy/__tests__/__fixtures__/entry-apps/throwing-entry.ts +16 -0
  18. package/src/deploy/__tests__/__fixtures__/env-parsers/non-function-export.ts +10 -0
  19. package/src/deploy/__tests__/__fixtures__/env-parsers/parseable-env-parser.ts +18 -0
  20. package/src/deploy/__tests__/__fixtures__/env-parsers/throwing-env-parser.ts +18 -0
  21. package/src/deploy/__tests__/__fixtures__/env-parsers/valid-env-parser.ts +16 -0
  22. package/src/deploy/__tests__/dns-verification.spec.ts +229 -0
  23. package/src/deploy/__tests__/dokploy-api.spec.ts +2 -3
  24. package/src/deploy/__tests__/domain.spec.ts +7 -3
  25. package/src/deploy/__tests__/env-resolver.spec.ts +469 -0
  26. package/src/deploy/__tests__/index.spec.ts +12 -12
  27. package/src/deploy/__tests__/secrets.spec.ts +4 -1
  28. package/src/deploy/__tests__/sniffer.spec.ts +326 -1
  29. package/src/deploy/__tests__/state.spec.ts +844 -0
  30. package/src/deploy/dns/hostinger-api.ts +4 -1
  31. package/src/deploy/dns/index.ts +113 -1
  32. package/src/deploy/docker.ts +1 -2
  33. package/src/deploy/dokploy-api.ts +18 -9
  34. package/src/deploy/domain.ts +5 -4
  35. package/src/deploy/env-resolver.ts +278 -0
  36. package/src/deploy/index.ts +525 -119
  37. package/src/deploy/secrets.ts +7 -2
  38. package/src/deploy/sniffer-envkit-patch.ts +43 -0
  39. package/src/deploy/sniffer-hooks.ts +52 -0
  40. package/src/deploy/sniffer-loader.ts +23 -0
  41. package/src/deploy/sniffer-worker.ts +74 -0
  42. package/src/deploy/sniffer.ts +136 -14
  43. package/src/deploy/state.ts +162 -1
  44. package/src/init/versions.ts +3 -3
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/dist/dokploy-api-BDLu0qWi.cjs.map +0 -1
  47. package/dist/dokploy-api-BN3V57z1.mjs +0 -3
  48. package/dist/dokploy-api-BdCKjFDA.cjs +0 -3
  49. package/dist/dokploy-api-DvzIDxTj.mjs.map +0 -1
@@ -1,6 +1,10 @@
1
1
  import { encryptSecrets } from '../secrets/encryption.js';
2
2
  import { toEmbeddableSecrets } from '../secrets/storage.js';
3
- import type { EmbeddableSecrets, EncryptedPayload, StageSecrets } from '../secrets/types.js';
3
+ import type {
4
+ EmbeddableSecrets,
5
+ EncryptedPayload,
6
+ StageSecrets,
7
+ } from '../secrets/types.js';
4
8
  import type { SniffedEnvironment } from './sniffer.js';
5
9
 
6
10
  /**
@@ -152,7 +156,8 @@ export function generateSecretsReport(
152
156
  ): SecretsReport {
153
157
  const appsWithSecrets: string[] = [];
154
158
  const appsWithoutSecrets: string[] = [];
155
- const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> = [];
159
+ const appsWithMissingSecrets: Array<{ appName: string; missing: string[] }> =
160
+ [];
156
161
 
157
162
  for (const [appName, sniffedEnv] of sniffedApps) {
158
163
  if (sniffedEnv.requiredEnvVars.length === 0) {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Patched @geekmidas/envkit module for entry app sniffing.
3
+ *
4
+ * This module re-exports the SnifferEnvironmentParser as EnvironmentParser,
5
+ * allowing entry apps to be imported while capturing their env var usage.
6
+ *
7
+ * The actual sniffer instance is stored in globalThis.__envSniffer
8
+ * so the worker script can retrieve the captured variables.
9
+ */
10
+
11
+ import { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
12
+
13
+ // Extend globalThis type for the sniffer instance
14
+ declare global {
15
+ // eslint-disable-next-line no-var
16
+ var __envSniffer: SnifferEnvironmentParser | undefined;
17
+ }
18
+
19
+ // Create a shared sniffer instance that will be used by all imports
20
+ // This is stored globally so the worker script can access it
21
+ if (!globalThis.__envSniffer) {
22
+ globalThis.__envSniffer = new SnifferEnvironmentParser();
23
+ }
24
+
25
+ /**
26
+ * Patched EnvironmentParser that uses the global sniffer instance.
27
+ *
28
+ * This class wraps the global sniffer to maintain API compatibility
29
+ * with the real EnvironmentParser. The constructor accepts an env
30
+ * parameter for API compatibility but ignores it since we're sniffing.
31
+ */
32
+ class PatchedEnvironmentParser {
33
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
34
+ create(builder: (get: any) => any) {
35
+ return globalThis.__envSniffer!.create(builder);
36
+ }
37
+ }
38
+
39
+ // Export the patched parser as EnvironmentParser
40
+ export { PatchedEnvironmentParser as EnvironmentParser };
41
+
42
+ // Re-export other envkit exports that entry apps might use
43
+ export { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Module loader hooks for entry app sniffing.
3
+ *
4
+ * This module provides the resolve hook that intercepts '@geekmidas/envkit'
5
+ * imports and redirects them to the patched sniffer version.
6
+ *
7
+ * This file is registered via module.register() from sniffer-loader.ts.
8
+ */
9
+
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ // Resolve path to the patched envkit module
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = dirname(__filename);
16
+ const patchedEnvkitPath = join(__dirname, 'sniffer-envkit-patch.ts');
17
+
18
+ type ResolveContext = {
19
+ conditions: string[];
20
+ importAttributes: Record<string, string>;
21
+ parentURL?: string;
22
+ };
23
+
24
+ type ResolveResult = {
25
+ url: string;
26
+ shortCircuit?: boolean;
27
+ format?: string;
28
+ };
29
+
30
+ type NextResolve = (
31
+ specifier: string,
32
+ context: ResolveContext,
33
+ ) => Promise<ResolveResult>;
34
+
35
+ /**
36
+ * Resolve hook - intercepts module resolution for @geekmidas/envkit
37
+ */
38
+ export async function resolve(
39
+ specifier: string,
40
+ context: ResolveContext,
41
+ nextResolve: NextResolve,
42
+ ): Promise<ResolveResult> {
43
+ // Intercept @geekmidas/envkit imports
44
+ if (specifier === '@geekmidas/envkit') {
45
+ return {
46
+ url: `file://${patchedEnvkitPath}`,
47
+ shortCircuit: true,
48
+ };
49
+ }
50
+
51
+ return nextResolve(specifier, context);
52
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Node.js module loader registration for entry app sniffing.
3
+ *
4
+ * This module registers a custom loader hook that intercepts imports of
5
+ * '@geekmidas/envkit' and replaces the EnvironmentParser with
6
+ * SnifferEnvironmentParser, allowing us to capture which environment
7
+ * variables an entry app accesses.
8
+ *
9
+ * Usage:
10
+ * node --import tsx --import ./sniffer-loader.ts ./sniffer-worker.ts /path/to/entry.ts
11
+ */
12
+
13
+ import { register } from 'node:module';
14
+ import { dirname, join } from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+
17
+ // Resolve path to the loader hooks module
18
+ const __filename = fileURLToPath(import.meta.url);
19
+ const __dirname = dirname(__filename);
20
+ const hooksPath = join(__dirname, 'sniffer-hooks.ts');
21
+
22
+ // Register the loader hooks
23
+ register(pathToFileURL(hooksPath).href, import.meta.url);
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Subprocess worker for entry app sniffing.
3
+ *
4
+ * This script is executed in a subprocess with the sniffer-loader.ts
5
+ * registered, which intercepts @geekmidas/envkit imports.
6
+ *
7
+ * Usage:
8
+ * node --import tsx --import ./sniffer-loader.ts ./sniffer-worker.ts /path/to/entry.ts
9
+ *
10
+ * Output (JSON to stdout):
11
+ * { "envVars": ["PORT", "DATABASE_URL", ...], "error": null }
12
+ */
13
+
14
+ import { pathToFileURL } from 'node:url';
15
+ import type { SnifferEnvironmentParser } from '@geekmidas/envkit/sniffer';
16
+
17
+ // Extend globalThis type for the sniffer instance
18
+ declare global {
19
+ // eslint-disable-next-line no-var
20
+ var __envSniffer: SnifferEnvironmentParser | undefined;
21
+ }
22
+
23
+ // Get the entry file path from command line args
24
+ const entryPath = process.argv[2] as string | undefined;
25
+
26
+ if (!entryPath) {
27
+ console.log(
28
+ JSON.stringify({ envVars: [], error: 'No entry file path provided' }),
29
+ );
30
+ process.exit(1);
31
+ }
32
+
33
+ // entryPath is guaranteed to be defined after the check above
34
+ const validEntryPath: string = entryPath;
35
+
36
+ /**
37
+ * Main sniffing function
38
+ */
39
+ async function sniff(): Promise<void> {
40
+ let error: string | null = null;
41
+
42
+ try {
43
+ // Import the entry file - this triggers:
44
+ // 1. Entry imports config module
45
+ // 2. Config module imports @geekmidas/envkit (intercepted by loader)
46
+ // 3. Config creates EnvironmentParser (actually SnifferEnvironmentParser)
47
+ // 4. Config calls .create() and .parse()
48
+ // 5. Sniffer captures all accessed env var names
49
+ const entryUrl = pathToFileURL(validEntryPath).href;
50
+ await import(entryUrl);
51
+ } catch (e) {
52
+ // Entry may fail due to missing env vars or other runtime issues.
53
+ // This is expected - we still capture the env vars that were accessed.
54
+ error = e instanceof Error ? e.message : String(e);
55
+ }
56
+
57
+ // Retrieve captured env vars from the global sniffer
58
+ const sniffer = globalThis.__envSniffer;
59
+ const envVars = sniffer ? sniffer.getEnvironmentVariables() : [];
60
+
61
+ // Output result as JSON
62
+ console.log(JSON.stringify({ envVars, error }));
63
+ }
64
+
65
+ // Handle unhandled rejections (fire-and-forget promises)
66
+ process.on('unhandledRejection', () => {
67
+ // Silently ignore - we only care about env var capture
68
+ });
69
+
70
+ // Run the sniffer
71
+ sniff().catch((e) => {
72
+ console.log(JSON.stringify({ envVars: [], error: e.message || String(e) }));
73
+ process.exit(1);
74
+ });
@@ -1,8 +1,12 @@
1
- import { resolve } from 'node:path';
2
- import { pathToFileURL } from 'node:url';
1
+ import { spawn } from 'node:child_process';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
3
4
  import type { SniffResult } from '@geekmidas/envkit/sniffer';
4
5
  import type { NormalizedAppConfig } from '../workspace/types.js';
5
6
 
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
6
10
  // Re-export SniffResult for consumers
7
11
  export type { SniffResult } from '@geekmidas/envkit/sniffer';
8
12
 
@@ -25,11 +29,12 @@ export interface SniffAppOptions {
25
29
  /**
26
30
  * Get required environment variables for an app.
27
31
  *
28
- * Detection strategy:
29
- * - Frontend apps: Returns empty (no server secrets)
30
- * - Apps with `requiredEnv`: Uses explicit list from config
31
- * - Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
32
- * - Apps with neither: Returns empty
32
+ * Detection strategy (in order):
33
+ * 1. Frontend apps: Returns empty (no server secrets)
34
+ * 2. Apps with `requiredEnv`: Uses explicit list from config
35
+ * 3. Entry apps: Imports entry file in subprocess to capture config.parse() calls
36
+ * 4. Apps with `envParser`: Runs SnifferEnvironmentParser to detect usage
37
+ * 5. Apps with neither: Returns empty
33
38
  *
34
39
  * This function handles "fire and forget" async operations gracefully,
35
40
  * capturing errors and unhandled rejections without failing the build.
@@ -48,17 +53,30 @@ export async function sniffAppEnvironment(
48
53
  ): Promise<SniffedEnvironment> {
49
54
  const { logWarnings = true } = options;
50
55
 
51
- // Frontend apps don't have server-side secrets
56
+ // 1. Frontend apps don't have server-side secrets
52
57
  if (app.type === 'frontend') {
53
58
  return { appName, requiredEnvVars: [] };
54
59
  }
55
60
 
56
- // Entry-based apps with explicit env list
61
+ // 2. Entry-based apps with explicit env list
57
62
  if (app.requiredEnv && app.requiredEnv.length > 0) {
58
63
  return { appName, requiredEnvVars: [...app.requiredEnv] };
59
64
  }
60
65
 
61
- // Apps with envParser - run sniffer to detect env var usage
66
+ // 3. Entry apps - import entry file in subprocess to trigger config.parse()
67
+ if (app.entry) {
68
+ const result = await sniffEntryFile(app.entry, app.path, workspacePath);
69
+
70
+ if (logWarnings && result.error) {
71
+ console.warn(
72
+ `[sniffer] ${appName}: Entry file threw error during sniffing (env vars still captured): ${result.error.message}`,
73
+ );
74
+ }
75
+
76
+ return { appName, requiredEnvVars: result.envVars };
77
+ }
78
+
79
+ // 4. Apps with envParser - run sniffer to detect env var usage
62
80
  if (app.envParser) {
63
81
  const result = await sniffEnvParser(app.envParser, app.path, workspacePath);
64
82
 
@@ -79,10 +97,107 @@ export async function sniffAppEnvironment(
79
97
  return { appName, requiredEnvVars: result.envVars };
80
98
  }
81
99
 
82
- // No env detection method available
100
+ // 5. No env detection method available
83
101
  return { appName, requiredEnvVars: [] };
84
102
  }
85
103
 
104
+ /**
105
+ * Result from sniffing an entry file.
106
+ */
107
+ interface EntrySniffResult {
108
+ envVars: string[];
109
+ error?: Error;
110
+ }
111
+
112
+ /**
113
+ * Sniff an entry file by importing it in a subprocess.
114
+ *
115
+ * Entry apps call `config.parse()` at module load time. To capture which
116
+ * env vars are accessed, we:
117
+ * 1. Spawn a subprocess with a module loader hook
118
+ * 2. The loader intercepts `@geekmidas/envkit` and replaces EnvironmentParser
119
+ * with SnifferEnvironmentParser
120
+ * 3. Import the entry file (triggers config.parse())
121
+ * 4. Capture and return the accessed env var names
122
+ *
123
+ * This approach provides process isolation - each app is sniffed in its own
124
+ * subprocess, preventing module cache pollution.
125
+ *
126
+ * @param entryPath - Relative path to the entry file (e.g., './src/index.ts')
127
+ * @param appPath - The app's path relative to workspace (e.g., 'apps/auth')
128
+ * @param workspacePath - Absolute path to workspace root
129
+ * @returns EntrySniffResult with env vars and optional error
130
+ */
131
+ async function sniffEntryFile(
132
+ entryPath: string,
133
+ appPath: string,
134
+ workspacePath: string,
135
+ ): Promise<EntrySniffResult> {
136
+ const fullEntryPath = resolve(workspacePath, appPath, entryPath);
137
+ const loaderPath = resolve(__dirname, 'sniffer-loader.ts');
138
+ const workerPath = resolve(__dirname, 'sniffer-worker.ts');
139
+
140
+ return new Promise((resolvePromise) => {
141
+ const child = spawn(
142
+ 'node',
143
+ ['--import', loaderPath, workerPath, fullEntryPath],
144
+ {
145
+ cwd: resolve(workspacePath, appPath),
146
+ stdio: ['ignore', 'pipe', 'pipe'],
147
+ env: {
148
+ ...process.env,
149
+ // Ensure tsx is available for TypeScript entry files
150
+ NODE_OPTIONS: '--import tsx',
151
+ },
152
+ },
153
+ );
154
+
155
+ let stdout = '';
156
+ let stderr = '';
157
+
158
+ child.stdout.on('data', (data) => {
159
+ stdout += data.toString();
160
+ });
161
+
162
+ child.stderr.on('data', (data) => {
163
+ stderr += data.toString();
164
+ });
165
+
166
+ child.on('close', (code) => {
167
+ // Try to parse the JSON output from the worker
168
+ try {
169
+ // Find the last JSON object in stdout (worker may emit other output)
170
+ const jsonMatch = stdout.match(/\{[^{}]*"envVars"[^{}]*\}[^{]*$/);
171
+ if (jsonMatch) {
172
+ const result = JSON.parse(jsonMatch[0]);
173
+ resolvePromise({
174
+ envVars: result.envVars || [],
175
+ error: result.error ? new Error(result.error) : undefined,
176
+ });
177
+ return;
178
+ }
179
+ } catch {
180
+ // JSON parse failed
181
+ }
182
+
183
+ // If we couldn't parse the output, return empty with error info
184
+ resolvePromise({
185
+ envVars: [],
186
+ error: new Error(
187
+ `Failed to sniff entry file (exit code ${code}): ${stderr || stdout || 'No output'}`,
188
+ ),
189
+ });
190
+ });
191
+
192
+ child.on('error', (err) => {
193
+ resolvePromise({
194
+ envVars: [],
195
+ error: err,
196
+ });
197
+ });
198
+ });
199
+ }
200
+
86
201
  /**
87
202
  * Run the SnifferEnvironmentParser on an envParser module to detect
88
203
  * which environment variables it accesses.
@@ -118,7 +233,9 @@ async function sniffEnvParser(
118
233
  sniffWithFireAndForget = envkitModule.sniffWithFireAndForget;
119
234
  } catch (error) {
120
235
  const message = error instanceof Error ? error.message : String(error);
121
- console.warn(`[sniffer] Failed to import SnifferEnvironmentParser: ${message}`);
236
+ console.warn(
237
+ `[sniffer] Failed to import SnifferEnvironmentParser: ${message}`,
238
+ );
122
239
  return { envVars: [], unhandledRejections: [] };
123
240
  }
124
241
 
@@ -169,7 +286,12 @@ export async function sniffAllApps(
169
286
  const results = new Map<string, SniffedEnvironment>();
170
287
 
171
288
  for (const [appName, app] of Object.entries(apps)) {
172
- const sniffed = await sniffAppEnvironment(app, appName, workspacePath, options);
289
+ const sniffed = await sniffAppEnvironment(
290
+ app,
291
+ appName,
292
+ workspacePath,
293
+ options,
294
+ );
173
295
  results.set(appName, sniffed);
174
296
  }
175
297
 
@@ -177,4 +299,4 @@ export async function sniffAllApps(
177
299
  }
178
300
 
179
301
  // Export for testing
180
- export { sniffEnvParser as _sniffEnvParser };
302
+ export { sniffEnvParser as _sniffEnvParser, sniffEntryFile as _sniffEntryFile };
@@ -8,6 +8,22 @@
8
8
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
9
9
  import { join } from 'node:path';
10
10
 
11
+ /**
12
+ * Per-app database credentials
13
+ */
14
+ export interface AppDbCredentials {
15
+ dbUser: string;
16
+ dbPassword: string;
17
+ }
18
+
19
+ /**
20
+ * DNS verification record for a hostname
21
+ */
22
+ export interface DnsVerificationRecord {
23
+ serverIp: string;
24
+ verifiedAt: string;
25
+ }
26
+
11
27
  /**
12
28
  * State for a single stage deployment
13
29
  */
@@ -20,6 +36,12 @@ export interface DokployStageState {
20
36
  postgresId?: string;
21
37
  redisId?: string;
22
38
  };
39
+ /** Per-app database credentials for reuse on subsequent deploys */
40
+ appCredentials?: Record<string, AppDbCredentials>;
41
+ /** Auto-generated secrets per app (e.g., BETTER_AUTH_SECRET) */
42
+ generatedSecrets?: Record<string, Record<string, string>>;
43
+ /** DNS verification state per hostname */
44
+ dnsVerified?: Record<string, DnsVerificationRecord>;
23
45
  lastDeployedAt: string;
24
46
  }
25
47
 
@@ -134,7 +156,9 @@ export function setPostgresId(
134
156
  /**
135
157
  * Get redis ID from state
136
158
  */
137
- export function getRedisId(state: DokployStageState | null): string | undefined {
159
+ export function getRedisId(
160
+ state: DokployStageState | null,
161
+ ): string | undefined {
138
162
  return state?.services.redisId;
139
163
  }
140
164
 
@@ -144,3 +168,140 @@ export function getRedisId(state: DokployStageState | null): string | undefined
144
168
  export function setRedisId(state: DokployStageState, redisId: string): void {
145
169
  state.services.redisId = redisId;
146
170
  }
171
+
172
+ /**
173
+ * Get app credentials from state
174
+ */
175
+ export function getAppCredentials(
176
+ state: DokployStageState | null,
177
+ appName: string,
178
+ ): AppDbCredentials | undefined {
179
+ return state?.appCredentials?.[appName];
180
+ }
181
+
182
+ /**
183
+ * Set app credentials in state (mutates state)
184
+ */
185
+ export function setAppCredentials(
186
+ state: DokployStageState,
187
+ appName: string,
188
+ credentials: AppDbCredentials,
189
+ ): void {
190
+ if (!state.appCredentials) {
191
+ state.appCredentials = {};
192
+ }
193
+ state.appCredentials[appName] = credentials;
194
+ }
195
+
196
+ /**
197
+ * Get all app credentials from state
198
+ */
199
+ export function getAllAppCredentials(
200
+ state: DokployStageState | null,
201
+ ): Record<string, AppDbCredentials> {
202
+ return state?.appCredentials ?? {};
203
+ }
204
+
205
+ // ============================================================================
206
+ // Generated Secrets
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Get a generated secret for an app
211
+ */
212
+ export function getGeneratedSecret(
213
+ state: DokployStageState | null,
214
+ appName: string,
215
+ secretName: string,
216
+ ): string | undefined {
217
+ return state?.generatedSecrets?.[appName]?.[secretName];
218
+ }
219
+
220
+ /**
221
+ * Set a generated secret for an app (mutates state)
222
+ */
223
+ export function setGeneratedSecret(
224
+ state: DokployStageState,
225
+ appName: string,
226
+ secretName: string,
227
+ value: string,
228
+ ): void {
229
+ if (!state.generatedSecrets) {
230
+ state.generatedSecrets = {};
231
+ }
232
+ if (!state.generatedSecrets[appName]) {
233
+ state.generatedSecrets[appName] = {};
234
+ }
235
+ state.generatedSecrets[appName][secretName] = value;
236
+ }
237
+
238
+ /**
239
+ * Get all generated secrets for an app
240
+ */
241
+ export function getAppGeneratedSecrets(
242
+ state: DokployStageState | null,
243
+ appName: string,
244
+ ): Record<string, string> {
245
+ return state?.generatedSecrets?.[appName] ?? {};
246
+ }
247
+
248
+ /**
249
+ * Get all generated secrets from state
250
+ */
251
+ export function getAllGeneratedSecrets(
252
+ state: DokployStageState | null,
253
+ ): Record<string, Record<string, string>> {
254
+ return state?.generatedSecrets ?? {};
255
+ }
256
+
257
+ // ============================================================================
258
+ // DNS Verification
259
+ // ============================================================================
260
+
261
+ /**
262
+ * Get DNS verification record for a hostname
263
+ */
264
+ export function getDnsVerification(
265
+ state: DokployStageState | null,
266
+ hostname: string,
267
+ ): DnsVerificationRecord | undefined {
268
+ return state?.dnsVerified?.[hostname];
269
+ }
270
+
271
+ /**
272
+ * Set DNS verification record for a hostname (mutates state)
273
+ */
274
+ export function setDnsVerification(
275
+ state: DokployStageState,
276
+ hostname: string,
277
+ serverIp: string,
278
+ ): void {
279
+ if (!state.dnsVerified) {
280
+ state.dnsVerified = {};
281
+ }
282
+ state.dnsVerified[hostname] = {
283
+ serverIp,
284
+ verifiedAt: new Date().toISOString(),
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Check if a hostname is already verified with the given IP
290
+ */
291
+ export function isDnsVerified(
292
+ state: DokployStageState | null,
293
+ hostname: string,
294
+ serverIp: string,
295
+ ): boolean {
296
+ const record = state?.dnsVerified?.[hostname];
297
+ return record?.serverIp === serverIp;
298
+ }
299
+
300
+ /**
301
+ * Get all DNS verification records from state
302
+ */
303
+ export function getAllDnsVerifications(
304
+ state: DokployStageState | null,
305
+ ): Record<string, DnsVerificationRecord> {
306
+ return state?.dnsVerified ?? {};
307
+ }
@@ -32,10 +32,10 @@ export const GEEKMIDAS_VERSIONS = {
32
32
  '@geekmidas/cli': CLI_VERSION,
33
33
  '@geekmidas/client': '~0.5.0',
34
34
  '@geekmidas/cloud': '~0.2.0',
35
- '@geekmidas/constructs': '~0.7.0',
35
+ '@geekmidas/constructs': '~0.8.0',
36
36
  '@geekmidas/db': '~0.3.0',
37
37
  '@geekmidas/emailkit': '~0.2.0',
38
- '@geekmidas/envkit': '~0.6.0',
38
+ '@geekmidas/envkit': '~0.7.0',
39
39
  '@geekmidas/errors': '~0.1.0',
40
40
  '@geekmidas/events': '~0.2.0',
41
41
  '@geekmidas/logger': '~0.4.0',
@@ -44,7 +44,7 @@ export const GEEKMIDAS_VERSIONS = {
44
44
  '@geekmidas/services': '~0.2.0',
45
45
  '@geekmidas/storage': '~0.1.0',
46
46
  '@geekmidas/studio': '~0.4.0',
47
- '@geekmidas/telescope': '~0.5.0',
47
+ '@geekmidas/telescope': '~0.6.0',
48
48
  '@geekmidas/testkit': '~0.6.0',
49
49
  };
50
50