@geekmidas/cli 0.12.0 → 0.13.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.
@@ -0,0 +1,5 @@
1
+ const require_storage = require('./storage-Bj1E26lU.cjs');
2
+
3
+ exports.readStageSecrets = require_storage.readStageSecrets;
4
+ exports.toEmbeddableSecrets = require_storage.toEmbeddableSecrets;
5
+ exports.validateEnvironmentVariables = require_storage.validateEnvironmentVariables;
@@ -96,6 +96,38 @@ function maskPassword(password) {
96
96
  if (password.length <= 8) return "********";
97
97
  return `${password.slice(0, 4)}${"*".repeat(password.length - 6)}${password.slice(-2)}`;
98
98
  }
99
+ /**
100
+ * Validate that all required environment variables are present in secrets.
101
+ *
102
+ * @param requiredVars - Array of environment variable names required by the application
103
+ * @param secrets - Stage secrets to validate against
104
+ * @returns Validation result with missing and provided variables
105
+ *
106
+ * @example
107
+ * ```typescript
108
+ * const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
109
+ * const secrets = await readStageSecrets('production');
110
+ * const result = validateEnvironmentVariables(required, secrets);
111
+ *
112
+ * if (!result.valid) {
113
+ * console.error(`Missing environment variables: ${result.missing.join(', ')}`);
114
+ * }
115
+ * ```
116
+ */
117
+ function validateEnvironmentVariables(requiredVars, secrets) {
118
+ const embeddable = toEmbeddableSecrets(secrets);
119
+ const availableVars = new Set(Object.keys(embeddable));
120
+ const missing = [];
121
+ const provided = [];
122
+ for (const varName of requiredVars) if (availableVars.has(varName)) provided.push(varName);
123
+ else missing.push(varName);
124
+ return {
125
+ valid: missing.length === 0,
126
+ missing: missing.sort(),
127
+ provided: provided.sort(),
128
+ required: [...requiredVars].sort()
129
+ };
130
+ }
99
131
 
100
132
  //#endregion
101
133
  Object.defineProperty(exports, 'getSecretsDir', {
@@ -140,10 +172,16 @@ Object.defineProperty(exports, 'toEmbeddableSecrets', {
140
172
  return toEmbeddableSecrets;
141
173
  }
142
174
  });
175
+ Object.defineProperty(exports, 'validateEnvironmentVariables', {
176
+ enumerable: true,
177
+ get: function () {
178
+ return validateEnvironmentVariables;
179
+ }
180
+ });
143
181
  Object.defineProperty(exports, 'writeStageSecrets', {
144
182
  enumerable: true,
145
183
  get: function () {
146
184
  return writeStageSecrets;
147
185
  }
148
186
  });
149
- //# sourceMappingURL=storage-BXoJvmv2.cjs.map
187
+ //# sourceMappingURL=storage-Bj1E26lU.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-Bj1E26lU.cjs","names":["stage: string","secrets: StageSecrets","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/storage.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/**\n * Get the secrets directory path.\n */\nexport function getSecretsDir(cwd = process.cwd()): string {\n\treturn join(cwd, SECRETS_DIR);\n}\n\n/**\n * Get the secrets file path for a stage.\n */\nexport function getSecretsPath(stage: string, cwd = process.cwd()): string {\n\treturn join(getSecretsDir(cwd), `${stage}.json`);\n}\n\n/**\n * Check if secrets exist for a stage.\n */\nexport function secretsExist(stage: string, cwd = process.cwd()): boolean {\n\treturn existsSync(getSecretsPath(stage, cwd));\n}\n\n/**\n * Read secrets for a stage.\n * @returns StageSecrets or null if not found\n */\nexport async function readStageSecrets(\n\tstage: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets | null> {\n\tconst path = getSecretsPath(stage, cwd);\n\n\tif (!existsSync(path)) {\n\t\treturn null;\n\t}\n\n\tconst content = await readFile(path, 'utf-8');\n\treturn JSON.parse(content) as StageSecrets;\n}\n\n/**\n * Write secrets for a stage.\n */\nexport async function writeStageSecrets(\n\tsecrets: StageSecrets,\n\tcwd = process.cwd(),\n): Promise<void> {\n\tconst dir = getSecretsDir(cwd);\n\tconst path = getSecretsPath(secrets.stage, cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Write with pretty formatting\n\tawait writeFile(path, JSON.stringify(secrets, null, 2), 'utf-8');\n}\n\n/**\n * Convert StageSecrets to embeddable format (flat key-value pairs).\n * This is what gets encrypted and embedded in the bundle.\n */\nexport function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {\n\treturn {\n\t\t...secrets.urls,\n\t\t...secrets.custom,\n\t\t// Also include individual service credentials if needed\n\t\t...(secrets.services.postgres && {\n\t\t\tPOSTGRES_USER: secrets.services.postgres.username,\n\t\t\tPOSTGRES_PASSWORD: secrets.services.postgres.password,\n\t\t\tPOSTGRES_DB: secrets.services.postgres.database ?? 'app',\n\t\t\tPOSTGRES_HOST: secrets.services.postgres.host,\n\t\t\tPOSTGRES_PORT: String(secrets.services.postgres.port),\n\t\t}),\n\t\t...(secrets.services.redis && {\n\t\t\tREDIS_PASSWORD: secrets.services.redis.password,\n\t\t\tREDIS_HOST: secrets.services.redis.host,\n\t\t\tREDIS_PORT: String(secrets.services.redis.port),\n\t\t}),\n\t\t...(secrets.services.rabbitmq && {\n\t\t\tRABBITMQ_USER: secrets.services.rabbitmq.username,\n\t\t\tRABBITMQ_PASSWORD: secrets.services.rabbitmq.password,\n\t\t\tRABBITMQ_HOST: secrets.services.rabbitmq.host,\n\t\t\tRABBITMQ_PORT: String(secrets.services.rabbitmq.port),\n\t\t\tRABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',\n\t\t}),\n\t};\n}\n\n/**\n * Update a custom secret in the secrets file.\n */\nexport async function setCustomSecret(\n\tstage: string,\n\tkey: string,\n\tvalue: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets> {\n\tconst secrets = await readStageSecrets(stage, cwd);\n\n\tif (!secrets) {\n\t\tthrow new Error(\n\t\t\t`Secrets not found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t);\n\t}\n\n\tconst updated: StageSecrets = {\n\t\t...secrets,\n\t\tupdatedAt: new Date().toISOString(),\n\t\tcustom: {\n\t\t\t...secrets.custom,\n\t\t\t[key]: value,\n\t\t},\n\t};\n\n\tawait writeStageSecrets(updated, cwd);\n\treturn updated;\n}\n\n/**\n * Mask a password for display (show first 4 and last 2 chars).\n */\nexport function maskPassword(password: string): string {\n\tif (password.length <= 8) {\n\t\treturn '********';\n\t}\n\treturn `${password.slice(0, 4)}${'*'.repeat(password.length - 6)}${password.slice(-2)}`;\n}\n\n/**\n * Result of environment variable validation.\n */\nexport interface EnvValidationResult {\n\t/** Whether all required environment variables are present */\n\tvalid: boolean;\n\t/** List of missing environment variable names */\n\tmissing: string[];\n\t/** List of environment variables that are provided */\n\tprovided: string[];\n\t/** List of environment variables that were required */\n\trequired: string[];\n}\n\n/**\n * Validate that all required environment variables are present in secrets.\n *\n * @param requiredVars - Array of environment variable names required by the application\n * @param secrets - Stage secrets to validate against\n * @returns Validation result with missing and provided variables\n *\n * @example\n * ```typescript\n * const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];\n * const secrets = await readStageSecrets('production');\n * const result = validateEnvironmentVariables(required, secrets);\n *\n * if (!result.valid) {\n * console.error(`Missing environment variables: ${result.missing.join(', ')}`);\n * }\n * ```\n */\nexport function validateEnvironmentVariables(\n\trequiredVars: string[],\n\tsecrets: StageSecrets,\n): EnvValidationResult {\n\tconst embeddable = toEmbeddableSecrets(secrets);\n\tconst availableVars = new Set(Object.keys(embeddable));\n\n\tconst missing: string[] = [];\n\tconst provided: string[] = [];\n\n\tfor (const varName of requiredVars) {\n\t\tif (availableVars.has(varName)) {\n\t\t\tprovided.push(varName);\n\t\t} else {\n\t\t\tmissing.push(varName);\n\t\t}\n\t}\n\n\treturn {\n\t\tvalid: missing.length === 0,\n\t\tmissing: missing.sort(),\n\t\tprovided: provided.sort(),\n\t\trequired: [...requiredVars].sort(),\n\t};\n}\n"],"mappings":"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"}
@@ -95,7 +95,39 @@ function maskPassword(password) {
95
95
  if (password.length <= 8) return "********";
96
96
  return `${password.slice(0, 4)}${"*".repeat(password.length - 6)}${password.slice(-2)}`;
97
97
  }
98
+ /**
99
+ * Validate that all required environment variables are present in secrets.
100
+ *
101
+ * @param requiredVars - Array of environment variable names required by the application
102
+ * @param secrets - Stage secrets to validate against
103
+ * @returns Validation result with missing and provided variables
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];
108
+ * const secrets = await readStageSecrets('production');
109
+ * const result = validateEnvironmentVariables(required, secrets);
110
+ *
111
+ * if (!result.valid) {
112
+ * console.error(`Missing environment variables: ${result.missing.join(', ')}`);
113
+ * }
114
+ * ```
115
+ */
116
+ function validateEnvironmentVariables(requiredVars, secrets) {
117
+ const embeddable = toEmbeddableSecrets(secrets);
118
+ const availableVars = new Set(Object.keys(embeddable));
119
+ const missing = [];
120
+ const provided = [];
121
+ for (const varName of requiredVars) if (availableVars.has(varName)) provided.push(varName);
122
+ else missing.push(varName);
123
+ return {
124
+ valid: missing.length === 0,
125
+ missing: missing.sort(),
126
+ provided: provided.sort(),
127
+ required: [...requiredVars].sort()
128
+ };
129
+ }
98
130
 
99
131
  //#endregion
100
- export { getSecretsDir, getSecretsPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, writeStageSecrets };
101
- //# sourceMappingURL=storage-C9PU_30f.mjs.map
132
+ export { getSecretsDir, getSecretsPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets };
133
+ //# sourceMappingURL=storage-kSxTjkNb.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage-kSxTjkNb.mjs","names":["stage: string","secrets: StageSecrets","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/storage.ts"],"sourcesContent":["import { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/**\n * Get the secrets directory path.\n */\nexport function getSecretsDir(cwd = process.cwd()): string {\n\treturn join(cwd, SECRETS_DIR);\n}\n\n/**\n * Get the secrets file path for a stage.\n */\nexport function getSecretsPath(stage: string, cwd = process.cwd()): string {\n\treturn join(getSecretsDir(cwd), `${stage}.json`);\n}\n\n/**\n * Check if secrets exist for a stage.\n */\nexport function secretsExist(stage: string, cwd = process.cwd()): boolean {\n\treturn existsSync(getSecretsPath(stage, cwd));\n}\n\n/**\n * Read secrets for a stage.\n * @returns StageSecrets or null if not found\n */\nexport async function readStageSecrets(\n\tstage: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets | null> {\n\tconst path = getSecretsPath(stage, cwd);\n\n\tif (!existsSync(path)) {\n\t\treturn null;\n\t}\n\n\tconst content = await readFile(path, 'utf-8');\n\treturn JSON.parse(content) as StageSecrets;\n}\n\n/**\n * Write secrets for a stage.\n */\nexport async function writeStageSecrets(\n\tsecrets: StageSecrets,\n\tcwd = process.cwd(),\n): Promise<void> {\n\tconst dir = getSecretsDir(cwd);\n\tconst path = getSecretsPath(secrets.stage, cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Write with pretty formatting\n\tawait writeFile(path, JSON.stringify(secrets, null, 2), 'utf-8');\n}\n\n/**\n * Convert StageSecrets to embeddable format (flat key-value pairs).\n * This is what gets encrypted and embedded in the bundle.\n */\nexport function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {\n\treturn {\n\t\t...secrets.urls,\n\t\t...secrets.custom,\n\t\t// Also include individual service credentials if needed\n\t\t...(secrets.services.postgres && {\n\t\t\tPOSTGRES_USER: secrets.services.postgres.username,\n\t\t\tPOSTGRES_PASSWORD: secrets.services.postgres.password,\n\t\t\tPOSTGRES_DB: secrets.services.postgres.database ?? 'app',\n\t\t\tPOSTGRES_HOST: secrets.services.postgres.host,\n\t\t\tPOSTGRES_PORT: String(secrets.services.postgres.port),\n\t\t}),\n\t\t...(secrets.services.redis && {\n\t\t\tREDIS_PASSWORD: secrets.services.redis.password,\n\t\t\tREDIS_HOST: secrets.services.redis.host,\n\t\t\tREDIS_PORT: String(secrets.services.redis.port),\n\t\t}),\n\t\t...(secrets.services.rabbitmq && {\n\t\t\tRABBITMQ_USER: secrets.services.rabbitmq.username,\n\t\t\tRABBITMQ_PASSWORD: secrets.services.rabbitmq.password,\n\t\t\tRABBITMQ_HOST: secrets.services.rabbitmq.host,\n\t\t\tRABBITMQ_PORT: String(secrets.services.rabbitmq.port),\n\t\t\tRABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',\n\t\t}),\n\t};\n}\n\n/**\n * Update a custom secret in the secrets file.\n */\nexport async function setCustomSecret(\n\tstage: string,\n\tkey: string,\n\tvalue: string,\n\tcwd = process.cwd(),\n): Promise<StageSecrets> {\n\tconst secrets = await readStageSecrets(stage, cwd);\n\n\tif (!secrets) {\n\t\tthrow new Error(\n\t\t\t`Secrets not found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t);\n\t}\n\n\tconst updated: StageSecrets = {\n\t\t...secrets,\n\t\tupdatedAt: new Date().toISOString(),\n\t\tcustom: {\n\t\t\t...secrets.custom,\n\t\t\t[key]: value,\n\t\t},\n\t};\n\n\tawait writeStageSecrets(updated, cwd);\n\treturn updated;\n}\n\n/**\n * Mask a password for display (show first 4 and last 2 chars).\n */\nexport function maskPassword(password: string): string {\n\tif (password.length <= 8) {\n\t\treturn '********';\n\t}\n\treturn `${password.slice(0, 4)}${'*'.repeat(password.length - 6)}${password.slice(-2)}`;\n}\n\n/**\n * Result of environment variable validation.\n */\nexport interface EnvValidationResult {\n\t/** Whether all required environment variables are present */\n\tvalid: boolean;\n\t/** List of missing environment variable names */\n\tmissing: string[];\n\t/** List of environment variables that are provided */\n\tprovided: string[];\n\t/** List of environment variables that were required */\n\trequired: string[];\n}\n\n/**\n * Validate that all required environment variables are present in secrets.\n *\n * @param requiredVars - Array of environment variable names required by the application\n * @param secrets - Stage secrets to validate against\n * @returns Validation result with missing and provided variables\n *\n * @example\n * ```typescript\n * const required = ['DATABASE_URL', 'API_KEY', 'JWT_SECRET'];\n * const secrets = await readStageSecrets('production');\n * const result = validateEnvironmentVariables(required, secrets);\n *\n * if (!result.valid) {\n * console.error(`Missing environment variables: ${result.missing.join(', ')}`);\n * }\n * ```\n */\nexport function validateEnvironmentVariables(\n\trequiredVars: string[],\n\tsecrets: StageSecrets,\n): EnvValidationResult {\n\tconst embeddable = toEmbeddableSecrets(secrets);\n\tconst availableVars = new Set(Object.keys(embeddable));\n\n\tconst missing: string[] = [];\n\tconst provided: string[] = [];\n\n\tfor (const varName of requiredVars) {\n\t\tif (availableVars.has(varName)) {\n\t\t\tprovided.push(varName);\n\t\t} else {\n\t\t\tmissing.push(varName);\n\t\t}\n\t}\n\n\treturn {\n\t\tvalid: missing.length === 0,\n\t\tmissing: missing.sort(),\n\t\tprovided: provided.sort(),\n\t\trequired: [...requiredVars].sort(),\n\t};\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { getSecretsDir, getSecretsPath, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets } from "./storage-kSxTjkNb.mjs";
2
+
3
+ export { readStageSecrets, toEmbeddableSecrets, validateEnvironmentVariables };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
5
5
  "private": false,
6
6
  "type": "module",
@@ -54,9 +54,9 @@
54
54
  },
55
55
  "peerDependencies": {
56
56
  "@geekmidas/constructs": "~0.5.0",
57
+ "@geekmidas/envkit": "~0.3.0",
57
58
  "@geekmidas/logger": "~0.4.0",
58
59
  "@geekmidas/schema": "~0.1.0",
59
- "@geekmidas/envkit": "~0.3.0",
60
60
  "@geekmidas/telescope": "~0.4.0"
61
61
  },
62
62
  "peerDependenciesMeta": {
@@ -0,0 +1,444 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import type { Construct } from '@geekmidas/constructs';
4
+ import { itWithDir } from '@geekmidas/testkit/os';
5
+ import { beforeEach, describe, expect, vi } from 'vitest';
6
+ import type { StageSecrets } from '../../secrets/types';
7
+ import { bundleServer } from '../bundler';
8
+
9
+ // Mock child_process to avoid actually running tsdown
10
+ vi.mock('node:child_process', () => ({
11
+ execSync: vi.fn(),
12
+ }));
13
+
14
+ // Mock construct that returns specific environment variables
15
+ function createMockConstruct(envVars: string[]): Construct {
16
+ return {
17
+ getEnvironment: vi.fn().mockResolvedValue(envVars),
18
+ } as unknown as Construct;
19
+ }
20
+
21
+ // Helper to create a minimal secrets file
22
+ async function createSecretsFile(
23
+ dir: string,
24
+ stage: string,
25
+ secrets: Partial<StageSecrets>,
26
+ ): Promise<void> {
27
+ const secretsDir = join(dir, '.gkm', 'secrets');
28
+ await mkdir(secretsDir, { recursive: true });
29
+
30
+ const fullSecrets: StageSecrets = {
31
+ stage,
32
+ createdAt: new Date().toISOString(),
33
+ updatedAt: new Date().toISOString(),
34
+ services: {},
35
+ urls: {},
36
+ custom: {},
37
+ ...secrets,
38
+ };
39
+
40
+ await writeFile(
41
+ join(secretsDir, `${stage}.json`),
42
+ JSON.stringify(fullSecrets, null, 2),
43
+ );
44
+ }
45
+
46
+ // Helper to create a minimal entry point file and mock the bundle output
47
+ async function createEntryPoint(dir: string): Promise<string> {
48
+ const outputDir = join(dir, '.gkm', 'server');
49
+ const distDir = join(outputDir, 'dist');
50
+ await mkdir(outputDir, { recursive: true });
51
+ await mkdir(distDir, { recursive: true });
52
+
53
+ const entryPoint = join(outputDir, 'server.ts');
54
+ await writeFile(entryPoint, 'console.log("hello");');
55
+
56
+ // Create the output file that tsdown would normally create
57
+ // (since we're mocking execSync, the file won't be created automatically)
58
+ await writeFile(join(distDir, 'server.js'), 'console.log("bundled");');
59
+
60
+ return entryPoint;
61
+ }
62
+
63
+ describe('bundleServer environment validation', () => {
64
+ beforeEach(() => {
65
+ vi.clearAllMocks();
66
+ });
67
+
68
+ itWithDir(
69
+ 'should pass validation when all required env vars are present',
70
+ async ({ dir }) => {
71
+ const entryPoint = await createEntryPoint(dir);
72
+ await createSecretsFile(dir, 'production', {
73
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
74
+ custom: { API_KEY: 'sk_test_123' },
75
+ });
76
+
77
+ const constructs = [
78
+ createMockConstruct(['DATABASE_URL']),
79
+ createMockConstruct(['API_KEY']),
80
+ ];
81
+
82
+ const originalCwd = process.cwd();
83
+ process.chdir(dir);
84
+
85
+ try {
86
+ // Should not throw
87
+ const result = await bundleServer({
88
+ entryPoint,
89
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
90
+ minify: false,
91
+ sourcemap: false,
92
+ external: [],
93
+ stage: 'production',
94
+ constructs,
95
+ });
96
+
97
+ expect(result.masterKey).toBeDefined();
98
+ expect(constructs[0].getEnvironment).toHaveBeenCalled();
99
+ expect(constructs[1].getEnvironment).toHaveBeenCalled();
100
+ } finally {
101
+ process.chdir(originalCwd);
102
+ }
103
+ },
104
+ );
105
+
106
+ itWithDir(
107
+ 'should throw error when required env vars are missing',
108
+ async ({ dir }) => {
109
+ const entryPoint = await createEntryPoint(dir);
110
+ await createSecretsFile(dir, 'production', {
111
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
112
+ custom: {},
113
+ });
114
+
115
+ const constructs = [
116
+ createMockConstruct(['DATABASE_URL', 'API_KEY', 'JWT_SECRET']),
117
+ ];
118
+
119
+ const originalCwd = process.cwd();
120
+ process.chdir(dir);
121
+
122
+ try {
123
+ await expect(
124
+ bundleServer({
125
+ entryPoint,
126
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
127
+ minify: false,
128
+ sourcemap: false,
129
+ external: [],
130
+ stage: 'production',
131
+ constructs,
132
+ }),
133
+ ).rejects.toThrow('Missing environment variables');
134
+ } finally {
135
+ process.chdir(originalCwd);
136
+ }
137
+ },
138
+ );
139
+
140
+ itWithDir(
141
+ 'should include missing variables in error message',
142
+ async ({ dir }) => {
143
+ const entryPoint = await createEntryPoint(dir);
144
+ await createSecretsFile(dir, 'staging', {
145
+ custom: { EXISTING_VAR: 'value' },
146
+ });
147
+
148
+ const constructs = [
149
+ createMockConstruct(['EXISTING_VAR', 'MISSING_VAR_1', 'MISSING_VAR_2']),
150
+ ];
151
+
152
+ const originalCwd = process.cwd();
153
+ process.chdir(dir);
154
+
155
+ try {
156
+ await expect(
157
+ bundleServer({
158
+ entryPoint,
159
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
160
+ minify: false,
161
+ sourcemap: false,
162
+ external: [],
163
+ stage: 'staging',
164
+ constructs,
165
+ }),
166
+ ).rejects.toThrow(/MISSING_VAR_1/);
167
+
168
+ await expect(
169
+ bundleServer({
170
+ entryPoint,
171
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
172
+ minify: false,
173
+ sourcemap: false,
174
+ external: [],
175
+ stage: 'staging',
176
+ constructs,
177
+ }),
178
+ ).rejects.toThrow(/MISSING_VAR_2/);
179
+ } finally {
180
+ process.chdir(originalCwd);
181
+ }
182
+ },
183
+ );
184
+
185
+ itWithDir(
186
+ 'should collect env vars from multiple constructs',
187
+ async ({ dir }) => {
188
+ const entryPoint = await createEntryPoint(dir);
189
+ await createSecretsFile(dir, 'production', {
190
+ urls: { DATABASE_URL: 'postgresql://localhost/db' },
191
+ custom: {
192
+ API_KEY: 'key',
193
+ REDIS_URL: 'redis://localhost',
194
+ JWT_SECRET: 'secret',
195
+ },
196
+ });
197
+
198
+ const constructs = [
199
+ createMockConstruct(['DATABASE_URL']),
200
+ createMockConstruct(['API_KEY', 'REDIS_URL']),
201
+ createMockConstruct(['JWT_SECRET']),
202
+ ];
203
+
204
+ const originalCwd = process.cwd();
205
+ process.chdir(dir);
206
+
207
+ try {
208
+ const result = await bundleServer({
209
+ entryPoint,
210
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
211
+ minify: false,
212
+ sourcemap: false,
213
+ external: [],
214
+ stage: 'production',
215
+ constructs,
216
+ });
217
+
218
+ expect(result.masterKey).toBeDefined();
219
+
220
+ // All constructs should have been checked
221
+ for (const construct of constructs) {
222
+ expect(construct.getEnvironment).toHaveBeenCalled();
223
+ }
224
+ } finally {
225
+ process.chdir(originalCwd);
226
+ }
227
+ },
228
+ );
229
+
230
+ itWithDir(
231
+ 'should deduplicate env vars from multiple constructs',
232
+ async ({ dir }) => {
233
+ const entryPoint = await createEntryPoint(dir);
234
+ await createSecretsFile(dir, 'production', {
235
+ custom: { SHARED_VAR: 'value' },
236
+ });
237
+
238
+ // Multiple constructs requiring the same variable
239
+ const constructs = [
240
+ createMockConstruct(['SHARED_VAR']),
241
+ createMockConstruct(['SHARED_VAR']),
242
+ createMockConstruct(['SHARED_VAR']),
243
+ ];
244
+
245
+ const originalCwd = process.cwd();
246
+ process.chdir(dir);
247
+
248
+ try {
249
+ // Should pass since SHARED_VAR is provided once
250
+ const result = await bundleServer({
251
+ entryPoint,
252
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
253
+ minify: false,
254
+ sourcemap: false,
255
+ external: [],
256
+ stage: 'production',
257
+ constructs,
258
+ });
259
+
260
+ expect(result.masterKey).toBeDefined();
261
+ } finally {
262
+ process.chdir(originalCwd);
263
+ }
264
+ },
265
+ );
266
+
267
+ itWithDir(
268
+ 'should skip validation when no constructs provided',
269
+ async ({ dir }) => {
270
+ const entryPoint = await createEntryPoint(dir);
271
+ await createSecretsFile(dir, 'production', {
272
+ custom: {},
273
+ });
274
+
275
+ const originalCwd = process.cwd();
276
+ process.chdir(dir);
277
+
278
+ try {
279
+ // Should not throw even with empty secrets
280
+ const result = await bundleServer({
281
+ entryPoint,
282
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
283
+ minify: false,
284
+ sourcemap: false,
285
+ external: [],
286
+ stage: 'production',
287
+ constructs: [],
288
+ });
289
+
290
+ expect(result.masterKey).toBeDefined();
291
+ } finally {
292
+ process.chdir(originalCwd);
293
+ }
294
+ },
295
+ );
296
+
297
+ itWithDir(
298
+ 'should skip validation when constructs is undefined',
299
+ async ({ dir }) => {
300
+ const entryPoint = await createEntryPoint(dir);
301
+ await createSecretsFile(dir, 'production', {
302
+ custom: {},
303
+ });
304
+
305
+ const originalCwd = process.cwd();
306
+ process.chdir(dir);
307
+
308
+ try {
309
+ const result = await bundleServer({
310
+ entryPoint,
311
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
312
+ minify: false,
313
+ sourcemap: false,
314
+ external: [],
315
+ stage: 'production',
316
+ // No constructs provided
317
+ });
318
+
319
+ expect(result.masterKey).toBeDefined();
320
+ } finally {
321
+ process.chdir(originalCwd);
322
+ }
323
+ },
324
+ );
325
+
326
+ itWithDir(
327
+ 'should recognize service credentials as provided',
328
+ async ({ dir }) => {
329
+ const entryPoint = await createEntryPoint(dir);
330
+ await createSecretsFile(dir, 'production', {
331
+ services: {
332
+ postgres: {
333
+ host: 'localhost',
334
+ port: 5432,
335
+ username: 'app',
336
+ password: 'secret',
337
+ database: 'mydb',
338
+ },
339
+ },
340
+ });
341
+
342
+ const constructs = [
343
+ createMockConstruct([
344
+ 'POSTGRES_HOST',
345
+ 'POSTGRES_PORT',
346
+ 'POSTGRES_USER',
347
+ 'POSTGRES_PASSWORD',
348
+ 'POSTGRES_DB',
349
+ ]),
350
+ ];
351
+
352
+ const originalCwd = process.cwd();
353
+ process.chdir(dir);
354
+
355
+ try {
356
+ const result = await bundleServer({
357
+ entryPoint,
358
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
359
+ minify: false,
360
+ sourcemap: false,
361
+ external: [],
362
+ stage: 'production',
363
+ constructs,
364
+ });
365
+
366
+ expect(result.masterKey).toBeDefined();
367
+ } finally {
368
+ process.chdir(originalCwd);
369
+ }
370
+ },
371
+ );
372
+
373
+ itWithDir(
374
+ 'should throw when secrets file does not exist',
375
+ async ({ dir }) => {
376
+ const entryPoint = await createEntryPoint(dir);
377
+ // Don't create secrets file
378
+
379
+ const constructs = [createMockConstruct(['DATABASE_URL'])];
380
+
381
+ const originalCwd = process.cwd();
382
+ process.chdir(dir);
383
+
384
+ try {
385
+ await expect(
386
+ bundleServer({
387
+ entryPoint,
388
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
389
+ minify: false,
390
+ sourcemap: false,
391
+ external: [],
392
+ stage: 'production',
393
+ constructs,
394
+ }),
395
+ ).rejects.toThrow('No secrets found for stage "production"');
396
+ } finally {
397
+ process.chdir(originalCwd);
398
+ }
399
+ },
400
+ );
401
+
402
+ itWithDir(
403
+ 'should include helpful instructions in error message',
404
+ async ({ dir }) => {
405
+ const entryPoint = await createEntryPoint(dir);
406
+ await createSecretsFile(dir, 'myapp', {
407
+ custom: {},
408
+ });
409
+
410
+ const constructs = [createMockConstruct(['MISSING_VAR'])];
411
+
412
+ const originalCwd = process.cwd();
413
+ process.chdir(dir);
414
+
415
+ try {
416
+ await expect(
417
+ bundleServer({
418
+ entryPoint,
419
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
420
+ minify: false,
421
+ sourcemap: false,
422
+ external: [],
423
+ stage: 'myapp',
424
+ constructs,
425
+ }),
426
+ ).rejects.toThrow(/gkm secrets:set/);
427
+
428
+ await expect(
429
+ bundleServer({
430
+ entryPoint,
431
+ outputDir: join(dir, '.gkm', 'server', 'dist'),
432
+ minify: false,
433
+ sourcemap: false,
434
+ external: [],
435
+ stage: 'myapp',
436
+ constructs,
437
+ }),
438
+ ).rejects.toThrow(/gkm secrets:import/);
439
+ } finally {
440
+ process.chdir(originalCwd);
441
+ }
442
+ },
443
+ );
444
+ });