@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.
- package/dist/{bundler-WsEvH_b2.cjs → bundler-B1qy9b-j.cjs} +44 -3
- package/dist/bundler-B1qy9b-j.cjs.map +1 -0
- package/dist/{bundler-DRXCw_YR.mjs → bundler-DskIqW2t.mjs} +44 -3
- package/dist/bundler-DskIqW2t.mjs.map +1 -0
- package/dist/index.cjs +11 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +11 -4
- package/dist/index.mjs.map +1 -1
- package/dist/storage-BOOpAF8N.cjs +5 -0
- package/dist/{storage-BXoJvmv2.cjs → storage-Bj1E26lU.cjs} +39 -1
- package/dist/storage-Bj1E26lU.cjs.map +1 -0
- package/dist/{storage-C9PU_30f.mjs → storage-kSxTjkNb.mjs} +34 -2
- package/dist/storage-kSxTjkNb.mjs.map +1 -0
- package/dist/storage-tgZSUnKl.mjs +3 -0
- package/package.json +2 -2
- package/src/build/__tests__/bundler.spec.ts +444 -0
- package/src/build/bundler.ts +73 -4
- package/src/build/index.ts +10 -0
- package/src/secrets/__tests__/storage.spec.ts +208 -0
- package/src/secrets/storage.ts +58 -0
- package/dist/bundler-DRXCw_YR.mjs.map +0 -1
- package/dist/bundler-WsEvH_b2.cjs.map +0 -1
- package/dist/storage-BUYQJgz7.cjs +0 -4
- package/dist/storage-BXoJvmv2.cjs.map +0 -1
- package/dist/storage-C9PU_30f.mjs.map +0 -1
- package/dist/storage-DLJAYxzJ.mjs +0 -3
|
@@ -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-
|
|
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-
|
|
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.
|
|
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
|
+
});
|