@geekmidas/cli 1.10.9 → 1.10.11
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/CHANGELOG.md +12 -0
- package/dist/{bundler-kk_XJTRp.mjs → bundler-BvByD6tt.mjs} +2 -2
- package/dist/{bundler-kk_XJTRp.mjs.map → bundler-BvByD6tt.mjs.map} +1 -1
- package/dist/{bundler-Bm3Az_sv.cjs → bundler-wY-XZDl9.cjs} +2 -2
- package/dist/{bundler-Bm3Az_sv.cjs.map → bundler-wY-XZDl9.cjs.map} +1 -1
- package/dist/{fullstack-secrets-CtWIYuI0.cjs → fullstack-secrets-AUkUyeW0.cjs} +21 -3
- package/dist/fullstack-secrets-AUkUyeW0.cjs.map +1 -0
- package/dist/{fullstack-secrets-C2lbdbLZ.mjs → fullstack-secrets-Bnm_QeZr.mjs} +10 -4
- package/dist/fullstack-secrets-Bnm_QeZr.mjs.map +1 -0
- package/dist/index.cjs +93 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +93 -50
- package/dist/index.mjs.map +1 -1
- package/dist/{reconcile-BnM6FA6g.mjs → reconcile--yaWO052.mjs} +2 -2
- package/dist/{reconcile-BnM6FA6g.mjs.map → reconcile--yaWO052.mjs.map} +1 -1
- package/dist/{reconcile-D6u4HSg8.cjs → reconcile-oktp4i_4.cjs} +2 -2
- package/dist/{reconcile-D6u4HSg8.cjs.map → reconcile-oktp4i_4.cjs.map} +1 -1
- package/dist/{storage-D6BGLgWf.cjs → storage-B3aXQBpI.cjs} +1 -1
- package/dist/{storage-Cs13jkJ9.cjs → storage-CEisMvif.cjs} +6 -6
- package/dist/{storage-B7H2PPCS.mjs.map → storage-CEisMvif.cjs.map} +1 -1
- package/dist/{storage-C1FNm2EP.mjs → storage-D3lh8Xpq.mjs} +1 -1
- package/dist/{storage-B7H2PPCS.mjs → storage-Df-NFMY9.mjs} +6 -6
- package/dist/{storage-Cs13jkJ9.cjs.map → storage-Df-NFMY9.mjs.map} +1 -1
- package/dist/{sync-CyGe5f1I.mjs → sync-CIzj1CQe.mjs} +1 -1
- package/dist/{sync-oCqELfeA.cjs → sync-CcDvBUu4.cjs} +2 -2
- package/dist/{sync-oCqELfeA.cjs.map → sync-CcDvBUu4.cjs.map} +1 -1
- package/dist/{sync-CzXruMzP.mjs → sync-D-7NTKvV.mjs} +2 -2
- package/dist/{sync-CzXruMzP.mjs.map → sync-D-7NTKvV.mjs.map} +1 -1
- package/dist/sync-QNXT8x13.cjs +4 -0
- package/package.json +3 -3
- package/src/dev/index.ts +6 -1
- package/src/docker/__tests__/compose.spec.ts +24 -11
- package/src/docker/compose.ts +25 -21
- package/src/secrets/__tests__/generator.spec.ts +5 -5
- package/src/secrets/__tests__/storage.spec.ts +7 -7
- package/src/secrets/generator.ts +10 -4
- package/src/secrets/index.ts +4 -4
- package/src/secrets/storage.ts +5 -5
- package/src/secrets/types.ts +1 -1
- package/src/setup/__tests__/reconcile-secrets.spec.ts +59 -0
- package/src/setup/index.ts +56 -16
- package/dist/fullstack-secrets-C2lbdbLZ.mjs.map +0 -1
- package/dist/fullstack-secrets-CtWIYuI0.cjs.map +0 -1
- package/dist/sync-DLlwsrBs.cjs +0 -4
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { generateFullstackCustomSecrets } from "./fullstack-secrets-
|
|
1
|
+
import { generateFullstackCustomSecrets } from "./fullstack-secrets-Bnm_QeZr.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/secrets/reconcile.ts
|
|
4
4
|
/**
|
|
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
|
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
export { reconcileMissingSecrets };
|
|
36
|
-
//# sourceMappingURL=reconcile
|
|
36
|
+
//# sourceMappingURL=reconcile--yaWO052.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile
|
|
1
|
+
{"version":3,"file":"reconcile--yaWO052.mjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,+BAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const require_fullstack_secrets = require('./fullstack-secrets-
|
|
1
|
+
const require_fullstack_secrets = require('./fullstack-secrets-AUkUyeW0.cjs');
|
|
2
2
|
|
|
3
3
|
//#region src/secrets/reconcile.ts
|
|
4
4
|
/**
|
|
@@ -33,4 +33,4 @@ function reconcileMissingSecrets(secrets, workspace) {
|
|
|
33
33
|
|
|
34
34
|
//#endregion
|
|
35
35
|
exports.reconcileMissingSecrets = reconcileMissingSecrets;
|
|
36
|
-
//# sourceMappingURL=reconcile-
|
|
36
|
+
//# sourceMappingURL=reconcile-oktp4i_4.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reconcile-
|
|
1
|
+
{"version":3,"file":"reconcile-oktp4i_4.cjs","names":["secrets: StageSecrets","workspace: NormalizedWorkspace","addedKeys: string[]"],"sources":["../src/secrets/reconcile.ts"],"sourcesContent":["import { generateFullstackCustomSecrets } from '../setup/fullstack-secrets.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\nexport interface ReconcileResult {\n\t/** The updated secrets with missing keys backfilled */\n\tsecrets: StageSecrets;\n\t/** Keys that were added */\n\taddedKeys: string[];\n}\n\n/**\n * Reconcile missing custom secrets for a workspace.\n *\n * Compares current secrets against what generateFullstackCustomSecrets()\n * would produce and backfills any missing keys without overwriting\n * existing values.\n *\n * @returns ReconcileResult if keys were added, null if secrets are up-to-date\n */\nexport function reconcileMissingSecrets(\n\tsecrets: StageSecrets,\n\tworkspace: NormalizedWorkspace,\n): ReconcileResult | null {\n\tconst isMultiApp = Object.keys(workspace.apps).length > 1;\n\tif (!isMultiApp) {\n\t\treturn null;\n\t}\n\n\tconst expectedCustom = generateFullstackCustomSecrets(workspace);\n\tconst addedKeys: string[] = [];\n\tconst mergedCustom = { ...secrets.custom };\n\n\tfor (const [key, value] of Object.entries(expectedCustom)) {\n\t\tif (!(key in mergedCustom)) {\n\t\t\tmergedCustom[key] = value;\n\t\t\taddedKeys.push(key);\n\t\t}\n\t}\n\n\tif (addedKeys.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn {\n\t\tsecrets: {\n\t\t\t...secrets,\n\t\t\tupdatedAt: new Date().toISOString(),\n\t\t\tcustom: mergedCustom,\n\t\t},\n\t\taddedKeys,\n\t};\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,wBACfA,SACAC,WACyB;CACzB,MAAM,aAAa,OAAO,KAAK,UAAU,KAAK,CAAC,SAAS;AACxD,MAAK,WACJ,QAAO;CAGR,MAAM,iBAAiB,yDAA+B,UAAU;CAChE,MAAMC,YAAsB,CAAE;CAC9B,MAAM,eAAe,EAAE,GAAG,QAAQ,OAAQ;AAE1C,MAAK,MAAM,CAAC,KAAK,MAAM,IAAI,OAAO,QAAQ,eAAe,CACxD,OAAM,OAAO,eAAe;AAC3B,eAAa,OAAO;AACpB,YAAU,KAAK,IAAI;CACnB;AAGF,KAAI,UAAU,WAAW,EACxB,QAAO;AAGR,QAAO;EACN,SAAS;GACR,GAAG;GACH,WAAW,qBAAI,QAAO,aAAa;GACnC,QAAQ;EACR;EACD;CACA;AACD"}
|
|
@@ -210,11 +210,11 @@ function toEmbeddableSecrets(secrets) {
|
|
|
210
210
|
RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? "/"
|
|
211
211
|
},
|
|
212
212
|
...secrets.services.minio && {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
213
|
+
STORAGE_ACCESS_KEY_ID: secrets.services.minio.username,
|
|
214
|
+
STORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,
|
|
215
|
+
STORAGE_BUCKET: secrets.services.minio.bucket ?? "app",
|
|
216
|
+
STORAGE_REGION: "eu-west-1",
|
|
217
|
+
STORAGE_FORCE_PATH_STYLE: "true"
|
|
218
218
|
}
|
|
219
219
|
};
|
|
220
220
|
}
|
|
@@ -342,4 +342,4 @@ Object.defineProperty(exports, 'writeStageSecrets', {
|
|
|
342
342
|
return writeStageSecrets;
|
|
343
343
|
}
|
|
344
344
|
});
|
|
345
|
-
//# sourceMappingURL=storage-
|
|
345
|
+
//# sourceMappingURL=storage-CEisMvif.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-B7H2PPCS.mjs","names":["projectName?: string","stage: string","stage: string","secrets: StageSecrets","keyHex: string","data: EncryptedSecretsFile","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/keystore.ts","../src/secrets/storage.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/** Key length for AES-256 encryption */\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Get the keystore directory for a project.\n * Keys are stored at ~/.gkm/{project-name}/\n *\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the keystore directory\n */\nexport function getKeystoreDir(projectName?: string): string {\n\tconst name = projectName ?? basename(process.cwd());\n\treturn join(homedir(), '.gkm', name);\n}\n\n/**\n * Get the path to a stage's encryption key.\n *\n * @param stage - Stage name (e.g., 'development', 'production')\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the key file\n */\nexport function getKeyPath(stage: string, projectName?: string): string {\n\treturn join(getKeystoreDir(projectName), `${stage}.key`);\n}\n\n/**\n * Check if a key exists for a stage.\n */\nexport function keyExists(stage: string, projectName?: string): boolean {\n\treturn existsSync(getKeyPath(stage, projectName));\n}\n\n/**\n * Generate a new encryption key for a stage.\n * The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The generated key as a hex string\n */\nexport async function generateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst keyDir = getKeystoreDir(projectName);\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\t// Ensure keystore directory exists with restricted permissions\n\tawait mkdir(keyDir, { recursive: true, mode: 0o700 });\n\n\t// Generate random key\n\tconst key = randomBytes(KEY_LENGTH).toString('hex');\n\n\t// Write key with restricted permissions (owner read/write only)\n\tawait writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });\n\n\t// Ensure permissions are set correctly (in case file existed)\n\tawait chmod(keyPath, 0o600);\n\n\treturn key;\n}\n\n/**\n * Read an encryption key for a stage.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string, or null if not found\n */\nexport async function readKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string | null> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (!existsSync(keyPath)) {\n\t\treturn null;\n\t}\n\n\tconst key = await readFile(keyPath, 'utf-8');\n\treturn key.trim();\n}\n\n/**\n * Read an encryption key for a stage, throwing if not found.\n */\nexport async function requireKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst key = await readKey(stage, projectName);\n\n\tif (!key) {\n\t\tconst name = projectName ?? basename(process.cwd());\n\t\tthrow new Error(\n\t\t\t`Encryption key not found for stage \"${stage}\" in project \"${name}\". ` +\n\t\t\t\t`Expected key at: ${getKeyPath(stage, projectName)}`,\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Delete a key for a stage.\n */\nexport async function deleteKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<void> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (existsSync(keyPath)) {\n\t\tawait rm(keyPath);\n\t}\n}\n\n/**\n * Get or create a key for a stage.\n * If the key already exists, it is returned. Otherwise, a new key is generated.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string\n */\nexport async function getOrCreateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst existingKey = await readKey(stage, projectName);\n\n\tif (existingKey) {\n\t\treturn existingKey;\n\t}\n\n\treturn generateKey(stage, projectName);\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport { getOrCreateKey, readKey } from './keystore';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/** AES-256-GCM configuration */\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst AUTH_TAG_LENGTH = 16; // 128 bits\n\n/** Encrypted secrets file structure */\ninterface EncryptedSecretsFile {\n\t/** Version for future format changes */\n\tversion: 1;\n\t/** Base64 encoded encrypted data (ciphertext + auth tag) */\n\tencrypted: string;\n\t/** Hex encoded IV */\n\tiv: string;\n}\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 * Initialize an empty StageSecrets object for a stage.\n */\nexport function initStageSecrets(stage: string): StageSecrets {\n\tconst now = new Date().toISOString();\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: {},\n\t\turls: {},\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Encrypt secrets using a key.\n */\nfunction encryptSecretsData(\n\tsecrets: StageSecrets,\n\tkeyHex: string,\n): EncryptedSecretsFile {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst iv = randomBytes(IV_LENGTH);\n\n\t// Serialize secrets to JSON\n\tconst plaintext = JSON.stringify(secrets);\n\n\t// Encrypt\n\tconst cipher = createCipheriv(ALGORITHM, key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, 'utf-8'),\n\t\tcipher.final(),\n\t]);\n\n\t// Get auth tag\n\tconst authTag = cipher.getAuthTag();\n\n\t// Combine ciphertext + auth tag\n\tconst combined = Buffer.concat([ciphertext, authTag]);\n\n\treturn {\n\t\tversion: 1,\n\t\tencrypted: combined.toString('base64'),\n\t\tiv: iv.toString('hex'),\n\t};\n}\n\n/**\n * Decrypt secrets using a key.\n */\nfunction decryptSecretsData(\n\tdata: EncryptedSecretsFile,\n\tkeyHex: string,\n): StageSecrets {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst ivBuffer = Buffer.from(data.iv, 'hex');\n\tconst combined = Buffer.from(data.encrypted, 'base64');\n\n\t// Split ciphertext and auth tag\n\tconst ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);\n\tconst authTag = combined.subarray(-AUTH_TAG_LENGTH);\n\n\t// Decrypt\n\tconst decipher = createDecipheriv(ALGORITHM, key, ivBuffer);\n\tdecipher.setAuthTag(authTag);\n\n\tconst plaintext = Buffer.concat([\n\t\tdecipher.update(ciphertext),\n\t\tdecipher.final(),\n\t]);\n\n\treturn JSON.parse(plaintext.toString('utf-8')) as StageSecrets;\n}\n\n/**\n * Read secrets for a stage (encrypted).\n * Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key\n *\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\tconst data = JSON.parse(content);\n\n\t// Check if this is an encrypted file (has version field)\n\tif (data.version === 1 && data.encrypted && data.iv) {\n\t\tconst projectName = basename(cwd);\n\t\tconst key = await readKey(stage, projectName);\n\n\t\tif (!key) {\n\t\t\tthrow new Error(\n\t\t\t\t`Decryption key not found for stage \"${stage}\". ` +\n\t\t\t\t\t`Expected key at: ~/.gkm/${projectName}/${stage}.key`,\n\t\t\t);\n\t\t}\n\n\t\treturn decryptSecretsData(data as EncryptedSecretsFile, key);\n\t}\n\n\t// Legacy: unencrypted format (for backwards compatibility)\n\treturn data as StageSecrets;\n}\n\n/**\n * Write secrets for a stage (encrypted).\n * Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key\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\tconst projectName = basename(cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Get or create encryption key\n\tconst key = await getOrCreateKey(secrets.stage, projectName);\n\n\t// Encrypt and write\n\tconst encrypted = encryptSecretsData(secrets, key);\n\tawait writeFile(path, JSON.stringify(encrypted, 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\t...(secrets.services.minio && {\n\t\t\tS3_ACCESS_KEY_ID: secrets.services.minio.username,\n\t\t\tS3_SECRET_ACCESS_KEY: secrets.services.minio.password,\n\t\t\tS3_BUCKET: secrets.services.minio.bucket ?? 'app',\n\t\t\tS3_REGION: 'eu-west-1',\n\t\t\tS3_FORCE_PATH_STYLE: 'true',\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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"}
|
|
1
|
+
{"version":3,"file":"storage-CEisMvif.cjs","names":["projectName?: string","stage: string","stage: string","secrets: StageSecrets","keyHex: string","data: EncryptedSecretsFile","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/keystore.ts","../src/secrets/storage.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/** Key length for AES-256 encryption */\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Get the keystore directory for a project.\n * Keys are stored at ~/.gkm/{project-name}/\n *\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the keystore directory\n */\nexport function getKeystoreDir(projectName?: string): string {\n\tconst name = projectName ?? basename(process.cwd());\n\treturn join(homedir(), '.gkm', name);\n}\n\n/**\n * Get the path to a stage's encryption key.\n *\n * @param stage - Stage name (e.g., 'development', 'production')\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the key file\n */\nexport function getKeyPath(stage: string, projectName?: string): string {\n\treturn join(getKeystoreDir(projectName), `${stage}.key`);\n}\n\n/**\n * Check if a key exists for a stage.\n */\nexport function keyExists(stage: string, projectName?: string): boolean {\n\treturn existsSync(getKeyPath(stage, projectName));\n}\n\n/**\n * Generate a new encryption key for a stage.\n * The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The generated key as a hex string\n */\nexport async function generateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst keyDir = getKeystoreDir(projectName);\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\t// Ensure keystore directory exists with restricted permissions\n\tawait mkdir(keyDir, { recursive: true, mode: 0o700 });\n\n\t// Generate random key\n\tconst key = randomBytes(KEY_LENGTH).toString('hex');\n\n\t// Write key with restricted permissions (owner read/write only)\n\tawait writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });\n\n\t// Ensure permissions are set correctly (in case file existed)\n\tawait chmod(keyPath, 0o600);\n\n\treturn key;\n}\n\n/**\n * Read an encryption key for a stage.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string, or null if not found\n */\nexport async function readKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string | null> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (!existsSync(keyPath)) {\n\t\treturn null;\n\t}\n\n\tconst key = await readFile(keyPath, 'utf-8');\n\treturn key.trim();\n}\n\n/**\n * Read an encryption key for a stage, throwing if not found.\n */\nexport async function requireKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst key = await readKey(stage, projectName);\n\n\tif (!key) {\n\t\tconst name = projectName ?? basename(process.cwd());\n\t\tthrow new Error(\n\t\t\t`Encryption key not found for stage \"${stage}\" in project \"${name}\". ` +\n\t\t\t\t`Expected key at: ${getKeyPath(stage, projectName)}`,\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Delete a key for a stage.\n */\nexport async function deleteKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<void> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (existsSync(keyPath)) {\n\t\tawait rm(keyPath);\n\t}\n}\n\n/**\n * Get or create a key for a stage.\n * If the key already exists, it is returned. Otherwise, a new key is generated.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string\n */\nexport async function getOrCreateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst existingKey = await readKey(stage, projectName);\n\n\tif (existingKey) {\n\t\treturn existingKey;\n\t}\n\n\treturn generateKey(stage, projectName);\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport { getOrCreateKey, readKey } from './keystore';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/** AES-256-GCM configuration */\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst AUTH_TAG_LENGTH = 16; // 128 bits\n\n/** Encrypted secrets file structure */\ninterface EncryptedSecretsFile {\n\t/** Version for future format changes */\n\tversion: 1;\n\t/** Base64 encoded encrypted data (ciphertext + auth tag) */\n\tencrypted: string;\n\t/** Hex encoded IV */\n\tiv: string;\n}\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 * Initialize an empty StageSecrets object for a stage.\n */\nexport function initStageSecrets(stage: string): StageSecrets {\n\tconst now = new Date().toISOString();\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: {},\n\t\turls: {},\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Encrypt secrets using a key.\n */\nfunction encryptSecretsData(\n\tsecrets: StageSecrets,\n\tkeyHex: string,\n): EncryptedSecretsFile {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst iv = randomBytes(IV_LENGTH);\n\n\t// Serialize secrets to JSON\n\tconst plaintext = JSON.stringify(secrets);\n\n\t// Encrypt\n\tconst cipher = createCipheriv(ALGORITHM, key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, 'utf-8'),\n\t\tcipher.final(),\n\t]);\n\n\t// Get auth tag\n\tconst authTag = cipher.getAuthTag();\n\n\t// Combine ciphertext + auth tag\n\tconst combined = Buffer.concat([ciphertext, authTag]);\n\n\treturn {\n\t\tversion: 1,\n\t\tencrypted: combined.toString('base64'),\n\t\tiv: iv.toString('hex'),\n\t};\n}\n\n/**\n * Decrypt secrets using a key.\n */\nfunction decryptSecretsData(\n\tdata: EncryptedSecretsFile,\n\tkeyHex: string,\n): StageSecrets {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst ivBuffer = Buffer.from(data.iv, 'hex');\n\tconst combined = Buffer.from(data.encrypted, 'base64');\n\n\t// Split ciphertext and auth tag\n\tconst ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);\n\tconst authTag = combined.subarray(-AUTH_TAG_LENGTH);\n\n\t// Decrypt\n\tconst decipher = createDecipheriv(ALGORITHM, key, ivBuffer);\n\tdecipher.setAuthTag(authTag);\n\n\tconst plaintext = Buffer.concat([\n\t\tdecipher.update(ciphertext),\n\t\tdecipher.final(),\n\t]);\n\n\treturn JSON.parse(plaintext.toString('utf-8')) as StageSecrets;\n}\n\n/**\n * Read secrets for a stage (encrypted).\n * Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key\n *\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\tconst data = JSON.parse(content);\n\n\t// Check if this is an encrypted file (has version field)\n\tif (data.version === 1 && data.encrypted && data.iv) {\n\t\tconst projectName = basename(cwd);\n\t\tconst key = await readKey(stage, projectName);\n\n\t\tif (!key) {\n\t\t\tthrow new Error(\n\t\t\t\t`Decryption key not found for stage \"${stage}\". ` +\n\t\t\t\t\t`Expected key at: ~/.gkm/${projectName}/${stage}.key`,\n\t\t\t);\n\t\t}\n\n\t\treturn decryptSecretsData(data as EncryptedSecretsFile, key);\n\t}\n\n\t// Legacy: unencrypted format (for backwards compatibility)\n\treturn data as StageSecrets;\n}\n\n/**\n * Write secrets for a stage (encrypted).\n * Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key\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\tconst projectName = basename(cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Get or create encryption key\n\tconst key = await getOrCreateKey(secrets.stage, projectName);\n\n\t// Encrypt and write\n\tconst encrypted = encryptSecretsData(secrets, key);\n\tawait writeFile(path, JSON.stringify(encrypted, 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\t...(secrets.services.minio && {\n\t\t\tSTORAGE_ACCESS_KEY_ID: secrets.services.minio.username,\n\t\t\tSTORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,\n\t\t\tSTORAGE_BUCKET: secrets.services.minio.bucket ?? 'app',\n\t\t\tSTORAGE_REGION: 'eu-west-1',\n\t\t\tSTORAGE_FORCE_PATH_STYLE: 'true',\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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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { getSecretsDir, getSecretsPath, initStageSecrets, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets } from "./storage-
|
|
1
|
+
import { getSecretsDir, getSecretsPath, initStageSecrets, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets } from "./storage-Df-NFMY9.mjs";
|
|
2
2
|
|
|
3
3
|
export { initStageSecrets, readStageSecrets, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets };
|
|
@@ -209,11 +209,11 @@ function toEmbeddableSecrets(secrets) {
|
|
|
209
209
|
RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? "/"
|
|
210
210
|
},
|
|
211
211
|
...secrets.services.minio && {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
STORAGE_ACCESS_KEY_ID: secrets.services.minio.username,
|
|
213
|
+
STORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,
|
|
214
|
+
STORAGE_BUCKET: secrets.services.minio.bucket ?? "app",
|
|
215
|
+
STORAGE_REGION: "eu-west-1",
|
|
216
|
+
STORAGE_FORCE_PATH_STYLE: "true"
|
|
217
217
|
}
|
|
218
218
|
};
|
|
219
219
|
}
|
|
@@ -276,4 +276,4 @@ function validateEnvironmentVariables(requiredVars, secrets) {
|
|
|
276
276
|
|
|
277
277
|
//#endregion
|
|
278
278
|
export { getKeyPath, getSecretsDir, getSecretsPath, initStageSecrets, maskPassword, readStageSecrets, secretsExist, setCustomSecret, toEmbeddableSecrets, validateEnvironmentVariables, writeStageSecrets };
|
|
279
|
-
//# sourceMappingURL=storage-
|
|
279
|
+
//# sourceMappingURL=storage-Df-NFMY9.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-Cs13jkJ9.cjs","names":["projectName?: string","stage: string","stage: string","secrets: StageSecrets","keyHex: string","data: EncryptedSecretsFile","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/keystore.ts","../src/secrets/storage.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/** Key length for AES-256 encryption */\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Get the keystore directory for a project.\n * Keys are stored at ~/.gkm/{project-name}/\n *\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the keystore directory\n */\nexport function getKeystoreDir(projectName?: string): string {\n\tconst name = projectName ?? basename(process.cwd());\n\treturn join(homedir(), '.gkm', name);\n}\n\n/**\n * Get the path to a stage's encryption key.\n *\n * @param stage - Stage name (e.g., 'development', 'production')\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the key file\n */\nexport function getKeyPath(stage: string, projectName?: string): string {\n\treturn join(getKeystoreDir(projectName), `${stage}.key`);\n}\n\n/**\n * Check if a key exists for a stage.\n */\nexport function keyExists(stage: string, projectName?: string): boolean {\n\treturn existsSync(getKeyPath(stage, projectName));\n}\n\n/**\n * Generate a new encryption key for a stage.\n * The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The generated key as a hex string\n */\nexport async function generateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst keyDir = getKeystoreDir(projectName);\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\t// Ensure keystore directory exists with restricted permissions\n\tawait mkdir(keyDir, { recursive: true, mode: 0o700 });\n\n\t// Generate random key\n\tconst key = randomBytes(KEY_LENGTH).toString('hex');\n\n\t// Write key with restricted permissions (owner read/write only)\n\tawait writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });\n\n\t// Ensure permissions are set correctly (in case file existed)\n\tawait chmod(keyPath, 0o600);\n\n\treturn key;\n}\n\n/**\n * Read an encryption key for a stage.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string, or null if not found\n */\nexport async function readKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string | null> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (!existsSync(keyPath)) {\n\t\treturn null;\n\t}\n\n\tconst key = await readFile(keyPath, 'utf-8');\n\treturn key.trim();\n}\n\n/**\n * Read an encryption key for a stage, throwing if not found.\n */\nexport async function requireKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst key = await readKey(stage, projectName);\n\n\tif (!key) {\n\t\tconst name = projectName ?? basename(process.cwd());\n\t\tthrow new Error(\n\t\t\t`Encryption key not found for stage \"${stage}\" in project \"${name}\". ` +\n\t\t\t\t`Expected key at: ${getKeyPath(stage, projectName)}`,\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Delete a key for a stage.\n */\nexport async function deleteKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<void> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (existsSync(keyPath)) {\n\t\tawait rm(keyPath);\n\t}\n}\n\n/**\n * Get or create a key for a stage.\n * If the key already exists, it is returned. Otherwise, a new key is generated.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string\n */\nexport async function getOrCreateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst existingKey = await readKey(stage, projectName);\n\n\tif (existingKey) {\n\t\treturn existingKey;\n\t}\n\n\treturn generateKey(stage, projectName);\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport { getOrCreateKey, readKey } from './keystore';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/** AES-256-GCM configuration */\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst AUTH_TAG_LENGTH = 16; // 128 bits\n\n/** Encrypted secrets file structure */\ninterface EncryptedSecretsFile {\n\t/** Version for future format changes */\n\tversion: 1;\n\t/** Base64 encoded encrypted data (ciphertext + auth tag) */\n\tencrypted: string;\n\t/** Hex encoded IV */\n\tiv: string;\n}\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 * Initialize an empty StageSecrets object for a stage.\n */\nexport function initStageSecrets(stage: string): StageSecrets {\n\tconst now = new Date().toISOString();\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: {},\n\t\turls: {},\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Encrypt secrets using a key.\n */\nfunction encryptSecretsData(\n\tsecrets: StageSecrets,\n\tkeyHex: string,\n): EncryptedSecretsFile {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst iv = randomBytes(IV_LENGTH);\n\n\t// Serialize secrets to JSON\n\tconst plaintext = JSON.stringify(secrets);\n\n\t// Encrypt\n\tconst cipher = createCipheriv(ALGORITHM, key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, 'utf-8'),\n\t\tcipher.final(),\n\t]);\n\n\t// Get auth tag\n\tconst authTag = cipher.getAuthTag();\n\n\t// Combine ciphertext + auth tag\n\tconst combined = Buffer.concat([ciphertext, authTag]);\n\n\treturn {\n\t\tversion: 1,\n\t\tencrypted: combined.toString('base64'),\n\t\tiv: iv.toString('hex'),\n\t};\n}\n\n/**\n * Decrypt secrets using a key.\n */\nfunction decryptSecretsData(\n\tdata: EncryptedSecretsFile,\n\tkeyHex: string,\n): StageSecrets {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst ivBuffer = Buffer.from(data.iv, 'hex');\n\tconst combined = Buffer.from(data.encrypted, 'base64');\n\n\t// Split ciphertext and auth tag\n\tconst ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);\n\tconst authTag = combined.subarray(-AUTH_TAG_LENGTH);\n\n\t// Decrypt\n\tconst decipher = createDecipheriv(ALGORITHM, key, ivBuffer);\n\tdecipher.setAuthTag(authTag);\n\n\tconst plaintext = Buffer.concat([\n\t\tdecipher.update(ciphertext),\n\t\tdecipher.final(),\n\t]);\n\n\treturn JSON.parse(plaintext.toString('utf-8')) as StageSecrets;\n}\n\n/**\n * Read secrets for a stage (encrypted).\n * Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key\n *\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\tconst data = JSON.parse(content);\n\n\t// Check if this is an encrypted file (has version field)\n\tif (data.version === 1 && data.encrypted && data.iv) {\n\t\tconst projectName = basename(cwd);\n\t\tconst key = await readKey(stage, projectName);\n\n\t\tif (!key) {\n\t\t\tthrow new Error(\n\t\t\t\t`Decryption key not found for stage \"${stage}\". ` +\n\t\t\t\t\t`Expected key at: ~/.gkm/${projectName}/${stage}.key`,\n\t\t\t);\n\t\t}\n\n\t\treturn decryptSecretsData(data as EncryptedSecretsFile, key);\n\t}\n\n\t// Legacy: unencrypted format (for backwards compatibility)\n\treturn data as StageSecrets;\n}\n\n/**\n * Write secrets for a stage (encrypted).\n * Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key\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\tconst projectName = basename(cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Get or create encryption key\n\tconst key = await getOrCreateKey(secrets.stage, projectName);\n\n\t// Encrypt and write\n\tconst encrypted = encryptSecretsData(secrets, key);\n\tawait writeFile(path, JSON.stringify(encrypted, 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\t...(secrets.services.minio && {\n\t\t\tS3_ACCESS_KEY_ID: secrets.services.minio.username,\n\t\t\tS3_SECRET_ACCESS_KEY: secrets.services.minio.password,\n\t\t\tS3_BUCKET: secrets.services.minio.bucket ?? 'app',\n\t\t\tS3_REGION: 'eu-west-1',\n\t\t\tS3_FORCE_PATH_STYLE: 'true',\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":";;;;;;;;;AAOA,MAAM,aAAa;;;;;;;;AASnB,SAAgB,eAAeA,aAA8B;CAC5D,MAAM,OAAO,eAAe,wBAAS,QAAQ,KAAK,CAAC;AACnD,QAAO,oBAAK,sBAAS,EAAE,QAAQ,KAAK;AACpC;;;;;;;;AASD,SAAgB,WAAWC,OAAeD,aAA8B;AACvE,QAAO,oBAAK,eAAe,YAAY,GAAG,EAAE,MAAM,MAAM;AACxD;;;;;;;;;AAiBD,eAAsB,YACrBC,OACAD,aACkB;CAClB,MAAM,SAAS,eAAe,YAAY;CAC1C,MAAM,UAAU,WAAW,OAAO,YAAY;AAG9C,OAAM,4BAAM,QAAQ;EAAE,WAAW;EAAM,MAAM;CAAO,EAAC;CAGrD,MAAM,MAAM,6BAAY,WAAW,CAAC,SAAS,MAAM;AAGnD,OAAM,gCAAU,SAAS,KAAK;EAAE,MAAM;EAAO,UAAU;CAAS,EAAC;AAGjE,OAAM,4BAAM,SAAS,IAAM;AAE3B,QAAO;AACP;;;;;;;;AASD,eAAsB,QACrBC,OACAD,aACyB;CACzB,MAAM,UAAU,WAAW,OAAO,YAAY;AAE9C,MAAK,wBAAW,QAAQ,CACvB,QAAO;CAGR,MAAM,MAAM,MAAM,+BAAS,SAAS,QAAQ;AAC5C,QAAO,IAAI,MAAM;AACjB;;;;;;;;;AA4CD,eAAsB,eACrBC,OACAD,aACkB;CAClB,MAAM,cAAc,MAAM,QAAQ,OAAO,YAAY;AAErD,KAAI,YACH,QAAO;AAGR,QAAO,YAAY,OAAO,YAAY;AACtC;;;;;ACvID,MAAM,cAAc;;AAGpB,MAAM,YAAY;AAClB,MAAM,YAAY;AAClB,MAAM,kBAAkB;;;;AAexB,SAAgB,cAAc,MAAM,QAAQ,KAAK,EAAU;AAC1D,QAAO,oBAAK,KAAK,YAAY;AAC7B;;;;AAKD,SAAgB,eAAeE,OAAe,MAAM,QAAQ,KAAK,EAAU;AAC1E,QAAO,oBAAK,cAAc,IAAI,GAAG,EAAE,MAAM,OAAO;AAChD;;;;AAKD,SAAgB,aAAaA,OAAe,MAAM,QAAQ,KAAK,EAAW;AACzE,QAAO,wBAAW,eAAe,OAAO,IAAI,CAAC;AAC7C;;;;AAKD,SAAgB,iBAAiBA,OAA6B;CAC7D,MAAM,MAAM,qBAAI,QAAO,aAAa;AACpC,QAAO;EACN;EACA,WAAW;EACX,WAAW;EACX,UAAU,CAAE;EACZ,MAAM,CAAE;EACR,QAAQ,CAAE;CACV;AACD;;;;AAKD,SAAS,mBACRC,SACAC,QACuB;CACvB,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CACtC,MAAM,KAAK,6BAAY,UAAU;CAGjC,MAAM,YAAY,KAAK,UAAU,QAAQ;CAGzC,MAAM,SAAS,gCAAe,WAAW,KAAK,GAAG;CACjD,MAAM,aAAa,OAAO,OAAO,CAChC,OAAO,OAAO,WAAW,QAAQ,EACjC,OAAO,OAAO,AACd,EAAC;CAGF,MAAM,UAAU,OAAO,YAAY;CAGnC,MAAM,WAAW,OAAO,OAAO,CAAC,YAAY,OAAQ,EAAC;AAErD,QAAO;EACN,SAAS;EACT,WAAW,SAAS,SAAS,SAAS;EACtC,IAAI,GAAG,SAAS,MAAM;CACtB;AACD;;;;AAKD,SAAS,mBACRC,MACAD,QACe;CACf,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CACtC,MAAM,WAAW,OAAO,KAAK,KAAK,IAAI,MAAM;CAC5C,MAAM,WAAW,OAAO,KAAK,KAAK,WAAW,SAAS;CAGtD,MAAM,aAAa,SAAS,SAAS,IAAI,gBAAgB;CACzD,MAAM,UAAU,SAAS,UAAU,gBAAgB;CAGnD,MAAM,WAAW,kCAAiB,WAAW,KAAK,SAAS;AAC3D,UAAS,WAAW,QAAQ;CAE5B,MAAM,YAAY,OAAO,OAAO,CAC/B,SAAS,OAAO,WAAW,EAC3B,SAAS,OAAO,AAChB,EAAC;AAEF,QAAO,KAAK,MAAM,UAAU,SAAS,QAAQ,CAAC;AAC9C;;;;;;;AAQD,eAAsB,iBACrBF,OACA,MAAM,QAAQ,KAAK,EACY;CAC/B,MAAM,OAAO,eAAe,OAAO,IAAI;AAEvC,MAAK,wBAAW,KAAK,CACpB,QAAO;CAGR,MAAM,UAAU,MAAM,+BAAS,MAAM,QAAQ;CAC7C,MAAM,OAAO,KAAK,MAAM,QAAQ;AAGhC,KAAI,KAAK,YAAY,KAAK,KAAK,aAAa,KAAK,IAAI;EACpD,MAAM,cAAc,wBAAS,IAAI;EACjC,MAAM,MAAM,MAAM,QAAQ,OAAO,YAAY;AAE7C,OAAK,IACJ,OAAM,IAAI,OACR,sCAAsC,MAAM,6BACjB,YAAY,GAAG,MAAM;AAInD,SAAO,mBAAmB,MAA8B,IAAI;CAC5D;AAGD,QAAO;AACP;;;;;AAMD,eAAsB,kBACrBC,SACA,MAAM,QAAQ,KAAK,EACH;CAChB,MAAM,MAAM,cAAc,IAAI;CAC9B,MAAM,OAAO,eAAe,QAAQ,OAAO,IAAI;CAC/C,MAAM,cAAc,wBAAS,IAAI;AAGjC,OAAM,4BAAM,KAAK,EAAE,WAAW,KAAM,EAAC;CAGrC,MAAM,MAAM,MAAM,eAAe,QAAQ,OAAO,YAAY;CAG5D,MAAM,YAAY,mBAAmB,SAAS,IAAI;AAClD,OAAM,gCAAU,MAAM,KAAK,UAAU,WAAW,MAAM,EAAE,EAAE,QAAQ;AAClE;;;;;AAMD,SAAgB,oBAAoBA,SAA0C;AAC7E,QAAO;EACN,GAAG,QAAQ;EACX,GAAG,QAAQ;EAEX,GAAI,QAAQ,SAAS,YAAY;GAChC,eAAe,QAAQ,SAAS,SAAS;GACzC,mBAAmB,QAAQ,SAAS,SAAS;GAC7C,aAAa,QAAQ,SAAS,SAAS,YAAY;GACnD,eAAe,QAAQ,SAAS,SAAS;GACzC,eAAe,OAAO,QAAQ,SAAS,SAAS,KAAK;EACrD;EACD,GAAI,QAAQ,SAAS,SAAS;GAC7B,gBAAgB,QAAQ,SAAS,MAAM;GACvC,YAAY,QAAQ,SAAS,MAAM;GACnC,YAAY,OAAO,QAAQ,SAAS,MAAM,KAAK;EAC/C;EACD,GAAI,QAAQ,SAAS,YAAY;GAChC,eAAe,QAAQ,SAAS,SAAS;GACzC,mBAAmB,QAAQ,SAAS,SAAS;GAC7C,eAAe,QAAQ,SAAS,SAAS;GACzC,eAAe,OAAO,QAAQ,SAAS,SAAS,KAAK;GACrD,gBAAgB,QAAQ,SAAS,SAAS,SAAS;EACnD;EACD,GAAI,QAAQ,SAAS,SAAS;GAC7B,kBAAkB,QAAQ,SAAS,MAAM;GACzC,sBAAsB,QAAQ,SAAS,MAAM;GAC7C,WAAW,QAAQ,SAAS,MAAM,UAAU;GAC5C,WAAW;GACX,qBAAqB;EACrB;CACD;AACD;;;;AAKD,eAAsB,gBACrBD,OACAI,KACAC,OACA,MAAM,QAAQ,KAAK,EACK;CACxB,MAAM,UAAU,MAAM,iBAAiB,OAAO,IAAI;AAElD,MAAK,QACJ,OAAM,IAAI,OACR,+BAA+B,MAAM,mCAAmC,MAAM;CAIjF,MAAMC,UAAwB;EAC7B,GAAG;EACH,WAAW,qBAAI,QAAO,aAAa;EACnC,QAAQ;GACP,GAAG,QAAQ;IACV,MAAM;EACP;CACD;AAED,OAAM,kBAAkB,SAAS,IAAI;AACrC,QAAO;AACP;;;;AAKD,SAAgB,aAAaC,UAA0B;AACtD,KAAI,SAAS,UAAU,EACtB,QAAO;AAER,SAAQ,EAAE,SAAS,MAAM,GAAG,EAAE,CAAC,EAAE,IAAI,OAAO,SAAS,SAAS,EAAE,CAAC,EAAE,SAAS,MAAM,GAAG,CAAC;AACtF;;;;;;;;;;;;;;;;;;;AAkCD,SAAgB,6BACfC,cACAP,SACsB;CACtB,MAAM,aAAa,oBAAoB,QAAQ;CAC/C,MAAM,gBAAgB,IAAI,IAAI,OAAO,KAAK,WAAW;CAErD,MAAMQ,UAAoB,CAAE;CAC5B,MAAMC,WAAqB,CAAE;AAE7B,MAAK,MAAM,WAAW,aACrB,KAAI,cAAc,IAAI,QAAQ,CAC7B,UAAS,KAAK,QAAQ;KAEtB,SAAQ,KAAK,QAAQ;AAIvB,QAAO;EACN,OAAO,QAAQ,WAAW;EAC1B,SAAS,QAAQ,MAAM;EACvB,UAAU,SAAS,MAAM;EACzB,UAAU,CAAC,GAAG,YAAa,EAAC,MAAM;CAClC;AACD"}
|
|
1
|
+
{"version":3,"file":"storage-Df-NFMY9.mjs","names":["projectName?: string","stage: string","stage: string","secrets: StageSecrets","keyHex: string","data: EncryptedSecretsFile","key: string","value: string","updated: StageSecrets","password: string","requiredVars: string[]","missing: string[]","provided: string[]"],"sources":["../src/secrets/keystore.ts","../src/secrets/storage.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { chmod, mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { basename, join } from 'node:path';\n\n/** Key length for AES-256 encryption */\nconst KEY_LENGTH = 32; // 256 bits\n\n/**\n * Get the keystore directory for a project.\n * Keys are stored at ~/.gkm/{project-name}/\n *\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the keystore directory\n */\nexport function getKeystoreDir(projectName?: string): string {\n\tconst name = projectName ?? basename(process.cwd());\n\treturn join(homedir(), '.gkm', name);\n}\n\n/**\n * Get the path to a stage's encryption key.\n *\n * @param stage - Stage name (e.g., 'development', 'production')\n * @param projectName - Name of the project (defaults to current directory name)\n * @returns Path to the key file\n */\nexport function getKeyPath(stage: string, projectName?: string): string {\n\treturn join(getKeystoreDir(projectName), `${stage}.key`);\n}\n\n/**\n * Check if a key exists for a stage.\n */\nexport function keyExists(stage: string, projectName?: string): boolean {\n\treturn existsSync(getKeyPath(stage, projectName));\n}\n\n/**\n * Generate a new encryption key for a stage.\n * The key is stored at ~/.gkm/{project-name}/{stage}.key with restricted permissions.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The generated key as a hex string\n */\nexport async function generateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst keyDir = getKeystoreDir(projectName);\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\t// Ensure keystore directory exists with restricted permissions\n\tawait mkdir(keyDir, { recursive: true, mode: 0o700 });\n\n\t// Generate random key\n\tconst key = randomBytes(KEY_LENGTH).toString('hex');\n\n\t// Write key with restricted permissions (owner read/write only)\n\tawait writeFile(keyPath, key, { mode: 0o600, encoding: 'utf-8' });\n\n\t// Ensure permissions are set correctly (in case file existed)\n\tawait chmod(keyPath, 0o600);\n\n\treturn key;\n}\n\n/**\n * Read an encryption key for a stage.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string, or null if not found\n */\nexport async function readKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string | null> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (!existsSync(keyPath)) {\n\t\treturn null;\n\t}\n\n\tconst key = await readFile(keyPath, 'utf-8');\n\treturn key.trim();\n}\n\n/**\n * Read an encryption key for a stage, throwing if not found.\n */\nexport async function requireKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst key = await readKey(stage, projectName);\n\n\tif (!key) {\n\t\tconst name = projectName ?? basename(process.cwd());\n\t\tthrow new Error(\n\t\t\t`Encryption key not found for stage \"${stage}\" in project \"${name}\". ` +\n\t\t\t\t`Expected key at: ${getKeyPath(stage, projectName)}`,\n\t\t);\n\t}\n\n\treturn key;\n}\n\n/**\n * Delete a key for a stage.\n */\nexport async function deleteKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<void> {\n\tconst keyPath = getKeyPath(stage, projectName);\n\n\tif (existsSync(keyPath)) {\n\t\tawait rm(keyPath);\n\t}\n}\n\n/**\n * Get or create a key for a stage.\n * If the key already exists, it is returned. Otherwise, a new key is generated.\n *\n * @param stage - Stage name\n * @param projectName - Project name (defaults to current directory name)\n * @returns The key as a hex string\n */\nexport async function getOrCreateKey(\n\tstage: string,\n\tprojectName?: string,\n): Promise<string> {\n\tconst existingKey = await readKey(stage, projectName);\n\n\tif (existingKey) {\n\t\treturn existingKey;\n\t}\n\n\treturn generateKey(stage, projectName);\n}\n","import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';\nimport { existsSync } from 'node:fs';\nimport { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { basename, join } from 'node:path';\nimport { getOrCreateKey, readKey } from './keystore';\nimport type { EmbeddableSecrets, StageSecrets } from './types';\n\n/** Default secrets directory relative to project root */\nconst SECRETS_DIR = '.gkm/secrets';\n\n/** AES-256-GCM configuration */\nconst ALGORITHM = 'aes-256-gcm';\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst AUTH_TAG_LENGTH = 16; // 128 bits\n\n/** Encrypted secrets file structure */\ninterface EncryptedSecretsFile {\n\t/** Version for future format changes */\n\tversion: 1;\n\t/** Base64 encoded encrypted data (ciphertext + auth tag) */\n\tencrypted: string;\n\t/** Hex encoded IV */\n\tiv: string;\n}\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 * Initialize an empty StageSecrets object for a stage.\n */\nexport function initStageSecrets(stage: string): StageSecrets {\n\tconst now = new Date().toISOString();\n\treturn {\n\t\tstage,\n\t\tcreatedAt: now,\n\t\tupdatedAt: now,\n\t\tservices: {},\n\t\turls: {},\n\t\tcustom: {},\n\t};\n}\n\n/**\n * Encrypt secrets using a key.\n */\nfunction encryptSecretsData(\n\tsecrets: StageSecrets,\n\tkeyHex: string,\n): EncryptedSecretsFile {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst iv = randomBytes(IV_LENGTH);\n\n\t// Serialize secrets to JSON\n\tconst plaintext = JSON.stringify(secrets);\n\n\t// Encrypt\n\tconst cipher = createCipheriv(ALGORITHM, key, iv);\n\tconst ciphertext = Buffer.concat([\n\t\tcipher.update(plaintext, 'utf-8'),\n\t\tcipher.final(),\n\t]);\n\n\t// Get auth tag\n\tconst authTag = cipher.getAuthTag();\n\n\t// Combine ciphertext + auth tag\n\tconst combined = Buffer.concat([ciphertext, authTag]);\n\n\treturn {\n\t\tversion: 1,\n\t\tencrypted: combined.toString('base64'),\n\t\tiv: iv.toString('hex'),\n\t};\n}\n\n/**\n * Decrypt secrets using a key.\n */\nfunction decryptSecretsData(\n\tdata: EncryptedSecretsFile,\n\tkeyHex: string,\n): StageSecrets {\n\tconst key = Buffer.from(keyHex, 'hex');\n\tconst ivBuffer = Buffer.from(data.iv, 'hex');\n\tconst combined = Buffer.from(data.encrypted, 'base64');\n\n\t// Split ciphertext and auth tag\n\tconst ciphertext = combined.subarray(0, -AUTH_TAG_LENGTH);\n\tconst authTag = combined.subarray(-AUTH_TAG_LENGTH);\n\n\t// Decrypt\n\tconst decipher = createDecipheriv(ALGORITHM, key, ivBuffer);\n\tdecipher.setAuthTag(authTag);\n\n\tconst plaintext = Buffer.concat([\n\t\tdecipher.update(ciphertext),\n\t\tdecipher.final(),\n\t]);\n\n\treturn JSON.parse(plaintext.toString('utf-8')) as StageSecrets;\n}\n\n/**\n * Read secrets for a stage (encrypted).\n * Requires the decryption key to be present at ~/.gkm/{project}/{stage}.key\n *\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\tconst data = JSON.parse(content);\n\n\t// Check if this is an encrypted file (has version field)\n\tif (data.version === 1 && data.encrypted && data.iv) {\n\t\tconst projectName = basename(cwd);\n\t\tconst key = await readKey(stage, projectName);\n\n\t\tif (!key) {\n\t\t\tthrow new Error(\n\t\t\t\t`Decryption key not found for stage \"${stage}\". ` +\n\t\t\t\t\t`Expected key at: ~/.gkm/${projectName}/${stage}.key`,\n\t\t\t);\n\t\t}\n\n\t\treturn decryptSecretsData(data as EncryptedSecretsFile, key);\n\t}\n\n\t// Legacy: unencrypted format (for backwards compatibility)\n\treturn data as StageSecrets;\n}\n\n/**\n * Write secrets for a stage (encrypted).\n * Creates or uses existing encryption key at ~/.gkm/{project}/{stage}.key\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\tconst projectName = basename(cwd);\n\n\t// Ensure directory exists\n\tawait mkdir(dir, { recursive: true });\n\n\t// Get or create encryption key\n\tconst key = await getOrCreateKey(secrets.stage, projectName);\n\n\t// Encrypt and write\n\tconst encrypted = encryptSecretsData(secrets, key);\n\tawait writeFile(path, JSON.stringify(encrypted, 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\t...(secrets.services.minio && {\n\t\t\tSTORAGE_ACCESS_KEY_ID: secrets.services.minio.username,\n\t\t\tSTORAGE_SECRET_ACCESS_KEY: secrets.services.minio.password,\n\t\t\tSTORAGE_BUCKET: secrets.services.minio.bucket ?? 'app',\n\t\t\tSTORAGE_REGION: 'eu-west-1',\n\t\t\tSTORAGE_FORCE_PATH_STYLE: 'true',\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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"}
|
|
@@ -30,7 +30,7 @@ async function pushSecrets(stage, workspace) {
|
|
|
30
30
|
if (!workspace.name) throw new Error("Workspace name is required for SSM secrets sync. Set \"name\" in gkm.config.ts.");
|
|
31
31
|
const client = createSSMClient(config);
|
|
32
32
|
const parameterName = getSecretsParameterName(workspace.name, stage);
|
|
33
|
-
const { readStageSecrets } = await Promise.resolve().then(() => require("./storage-
|
|
33
|
+
const { readStageSecrets } = await Promise.resolve().then(() => require("./storage-B3aXQBpI.cjs"));
|
|
34
34
|
const secrets = await readStageSecrets(stage, workspace.root);
|
|
35
35
|
if (!secrets) throw new Error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
36
36
|
await client.send(new __aws_sdk_client_ssm.PutParameterCommand({
|
|
@@ -90,4 +90,4 @@ Object.defineProperty(exports, 'pushSecrets', {
|
|
|
90
90
|
return pushSecrets;
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
|
-
//# sourceMappingURL=sync-
|
|
93
|
+
//# sourceMappingURL=sync-CcDvBUu4.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-
|
|
1
|
+
{"version":3,"file":"sync-CcDvBUu4.cjs","names":["workspaceName: string","stage: string","config: SSMStateConfig","clientConfig: SSMClientConfig","SSMClient","workspace: NormalizedWorkspace","PutParameterCommand","GetParameterCommand","ParameterNotFound"],"sources":["../src/secrets/sync.ts"],"sourcesContent":["/**\n * Secrets Sync via AWS SSM Parameter Store\n *\n * Stores and retrieves encrypted StageSecrets as SecureString parameters.\n * Reuses the SSM infrastructure from the state provider.\n *\n * Parameter naming: /gkm/{workspaceName}/{stage}/secrets\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SSMStateConfig } from '../deploy/StateProvider.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\n/**\n * Get the SSM parameter name for secrets.\n */\nfunction getSecretsParameterName(workspaceName: string, stage: string): string {\n\treturn `/gkm/${workspaceName}/${stage}/secrets`;\n}\n\n/**\n * Create an SSM client from workspace state config.\n */\nfunction createSSMClient(config: SSMStateConfig): SSMClient {\n\tconst clientConfig: SSMClientConfig = {\n\t\tregion: config.region,\n\t};\n\n\tif (config.profile) {\n\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\tclientConfig.credentials = fromIni({ profile: config.profile });\n\t}\n\n\treturn new SSMClient(clientConfig);\n}\n\n/**\n * Push secrets to SSM Parameter Store.\n *\n * Stores the full StageSecrets object as a SecureString parameter.\n */\nexport async function pushSecrets(\n\tstage: string,\n\tworkspace: NormalizedWorkspace,\n): Promise<void> {\n\tconst config = workspace.state;\n\tif (!config || config.provider !== 'ssm') {\n\t\tthrow new Error(\n\t\t\t'SSM state provider not configured. Add state: { provider: \"ssm\", region: \"...\" } to gkm.config.ts.',\n\t\t);\n\t}\n\n\tif (!workspace.name) {\n\t\tthrow new Error(\n\t\t\t'Workspace name is required for SSM secrets sync. Set \"name\" in gkm.config.ts.',\n\t\t);\n\t}\n\n\tconst client = createSSMClient(config as SSMStateConfig);\n\tconst parameterName = getSecretsParameterName(workspace.name, stage);\n\n\tconst { readStageSecrets } = await import('./storage.js');\n\tconst secrets = await readStageSecrets(stage, workspace.root);\n\n\tif (!secrets) {\n\t\tthrow new Error(\n\t\t\t`No secrets found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t);\n\t}\n\n\tawait client.send(\n\t\tnew PutParameterCommand({\n\t\t\tName: parameterName,\n\t\t\tValue: JSON.stringify(secrets),\n\t\t\tType: 'SecureString',\n\t\t\tOverwrite: true,\n\t\t\tDescription: `GKM secrets for ${workspace.name}/${stage}`,\n\t\t}),\n\t);\n}\n\n/**\n * Pull secrets from SSM Parameter Store.\n *\n * @returns StageSecrets or null if no secrets are stored remotely\n */\nexport async function pullSecrets(\n\tstage: string,\n\tworkspace: NormalizedWorkspace,\n): Promise<StageSecrets | null> {\n\tconst config = workspace.state;\n\tif (!config || config.provider !== 'ssm') {\n\t\treturn null;\n\t}\n\n\tif (!workspace.name) {\n\t\treturn null;\n\t}\n\n\tconst client = createSSMClient(config as SSMStateConfig);\n\tconst parameterName = getSecretsParameterName(workspace.name, stage);\n\n\ttry {\n\t\tconst response = await client.send(\n\t\t\tnew GetParameterCommand({\n\t\t\t\tName: parameterName,\n\t\t\t\tWithDecryption: true,\n\t\t\t}),\n\t\t);\n\n\t\tif (!response.Parameter?.Value) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn JSON.parse(response.Parameter.Value) as StageSecrets;\n\t} catch (error) {\n\t\tif (error instanceof ParameterNotFound) {\n\t\t\treturn null;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Check if SSM is configured for the workspace.\n */\nexport function isSSMConfigured(workspace: NormalizedWorkspace): boolean {\n\treturn !!workspace.state && workspace.state.provider === 'ssm';\n}\n"],"mappings":";;;;;;;AAuBA,SAAS,wBAAwBA,eAAuBC,OAAuB;AAC9E,SAAQ,OAAO,cAAc,GAAG,MAAM;AACtC;;;;AAKD,SAAS,gBAAgBC,QAAmC;CAC3D,MAAMC,eAAgC,EACrC,QAAQ,OAAO,OACf;AAED,KAAI,OAAO,SAAS;EACnB,MAAM,EAAE,SAAS,GAAG,QAAQ,gCAAgC;AAC5D,eAAa,cAAc,QAAQ,EAAE,SAAS,OAAO,QAAS,EAAC;CAC/D;AAED,QAAO,IAAIC,+BAAU;AACrB;;;;;;AAOD,eAAsB,YACrBH,OACAI,WACgB;CAChB,MAAM,SAAS,UAAU;AACzB,MAAK,UAAU,OAAO,aAAa,MAClC,OAAM,IAAI,MACT;AAIF,MAAK,UAAU,KACd,OAAM,IAAI,MACT;CAIF,MAAM,SAAS,gBAAgB,OAAyB;CACxD,MAAM,gBAAgB,wBAAwB,UAAU,MAAM,MAAM;CAEpE,MAAM,EAAE,kBAAkB,GAAG,2CAAM;CACnC,MAAM,UAAU,MAAM,iBAAiB,OAAO,UAAU,KAAK;AAE7D,MAAK,QACJ,OAAM,IAAI,OACR,8BAA8B,MAAM,mCAAmC,MAAM;AAIhF,OAAM,OAAO,KACZ,IAAIC,yCAAoB;EACvB,MAAM;EACN,OAAO,KAAK,UAAU,QAAQ;EAC9B,MAAM;EACN,WAAW;EACX,cAAc,kBAAkB,UAAU,KAAK,GAAG,MAAM;CACxD,GACD;AACD;;;;;;AAOD,eAAsB,YACrBL,OACAI,WAC+B;CAC/B,MAAM,SAAS,UAAU;AACzB,MAAK,UAAU,OAAO,aAAa,MAClC,QAAO;AAGR,MAAK,UAAU,KACd,QAAO;CAGR,MAAM,SAAS,gBAAgB,OAAyB;CACxD,MAAM,gBAAgB,wBAAwB,UAAU,MAAM,MAAM;AAEpE,KAAI;EACH,MAAM,WAAW,MAAM,OAAO,KAC7B,IAAIE,yCAAoB;GACvB,MAAM;GACN,gBAAgB;EAChB,GACD;AAED,OAAK,SAAS,WAAW,MACxB,QAAO;AAGR,SAAO,KAAK,MAAM,SAAS,UAAU,MAAM;CAC3C,SAAQ,OAAO;AACf,MAAI,iBAAiBC,uCACpB,QAAO;AAER,QAAM;CACN;AACD;;;;AAKD,SAAgB,gBAAgBH,WAAyC;AACxE,UAAS,UAAU,SAAS,UAAU,MAAM,aAAa;AACzD"}
|
|
@@ -30,7 +30,7 @@ async function pushSecrets(stage, workspace) {
|
|
|
30
30
|
if (!workspace.name) throw new Error("Workspace name is required for SSM secrets sync. Set \"name\" in gkm.config.ts.");
|
|
31
31
|
const client = createSSMClient(config);
|
|
32
32
|
const parameterName = getSecretsParameterName(workspace.name, stage);
|
|
33
|
-
const { readStageSecrets } = await import("./storage-
|
|
33
|
+
const { readStageSecrets } = await import("./storage-D3lh8Xpq.mjs");
|
|
34
34
|
const secrets = await readStageSecrets(stage, workspace.root);
|
|
35
35
|
if (!secrets) throw new Error(`No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`);
|
|
36
36
|
await client.send(new PutParameterCommand({
|
|
@@ -73,4 +73,4 @@ function isSSMConfigured(workspace) {
|
|
|
73
73
|
|
|
74
74
|
//#endregion
|
|
75
75
|
export { isSSMConfigured, pullSecrets, pushSecrets };
|
|
76
|
-
//# sourceMappingURL=sync-
|
|
76
|
+
//# sourceMappingURL=sync-D-7NTKvV.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-
|
|
1
|
+
{"version":3,"file":"sync-D-7NTKvV.mjs","names":["workspaceName: string","stage: string","config: SSMStateConfig","clientConfig: SSMClientConfig","workspace: NormalizedWorkspace"],"sources":["../src/secrets/sync.ts"],"sourcesContent":["/**\n * Secrets Sync via AWS SSM Parameter Store\n *\n * Stores and retrieves encrypted StageSecrets as SecureString parameters.\n * Reuses the SSM infrastructure from the state provider.\n *\n * Parameter naming: /gkm/{workspaceName}/{stage}/secrets\n */\n\nimport {\n\tGetParameterCommand,\n\tParameterNotFound,\n\tPutParameterCommand,\n\tSSMClient,\n\ttype SSMClientConfig,\n} from '@aws-sdk/client-ssm';\nimport type { SSMStateConfig } from '../deploy/StateProvider.js';\nimport type { NormalizedWorkspace } from '../workspace/types.js';\nimport type { StageSecrets } from './types.js';\n\n/**\n * Get the SSM parameter name for secrets.\n */\nfunction getSecretsParameterName(workspaceName: string, stage: string): string {\n\treturn `/gkm/${workspaceName}/${stage}/secrets`;\n}\n\n/**\n * Create an SSM client from workspace state config.\n */\nfunction createSSMClient(config: SSMStateConfig): SSMClient {\n\tconst clientConfig: SSMClientConfig = {\n\t\tregion: config.region,\n\t};\n\n\tif (config.profile) {\n\t\tconst { fromIni } = require('@aws-sdk/credential-providers');\n\t\tclientConfig.credentials = fromIni({ profile: config.profile });\n\t}\n\n\treturn new SSMClient(clientConfig);\n}\n\n/**\n * Push secrets to SSM Parameter Store.\n *\n * Stores the full StageSecrets object as a SecureString parameter.\n */\nexport async function pushSecrets(\n\tstage: string,\n\tworkspace: NormalizedWorkspace,\n): Promise<void> {\n\tconst config = workspace.state;\n\tif (!config || config.provider !== 'ssm') {\n\t\tthrow new Error(\n\t\t\t'SSM state provider not configured. Add state: { provider: \"ssm\", region: \"...\" } to gkm.config.ts.',\n\t\t);\n\t}\n\n\tif (!workspace.name) {\n\t\tthrow new Error(\n\t\t\t'Workspace name is required for SSM secrets sync. Set \"name\" in gkm.config.ts.',\n\t\t);\n\t}\n\n\tconst client = createSSMClient(config as SSMStateConfig);\n\tconst parameterName = getSecretsParameterName(workspace.name, stage);\n\n\tconst { readStageSecrets } = await import('./storage.js');\n\tconst secrets = await readStageSecrets(stage, workspace.root);\n\n\tif (!secrets) {\n\t\tthrow new Error(\n\t\t\t`No secrets found for stage \"${stage}\". Run \"gkm secrets:init --stage ${stage}\" first.`,\n\t\t);\n\t}\n\n\tawait client.send(\n\t\tnew PutParameterCommand({\n\t\t\tName: parameterName,\n\t\t\tValue: JSON.stringify(secrets),\n\t\t\tType: 'SecureString',\n\t\t\tOverwrite: true,\n\t\t\tDescription: `GKM secrets for ${workspace.name}/${stage}`,\n\t\t}),\n\t);\n}\n\n/**\n * Pull secrets from SSM Parameter Store.\n *\n * @returns StageSecrets or null if no secrets are stored remotely\n */\nexport async function pullSecrets(\n\tstage: string,\n\tworkspace: NormalizedWorkspace,\n): Promise<StageSecrets | null> {\n\tconst config = workspace.state;\n\tif (!config || config.provider !== 'ssm') {\n\t\treturn null;\n\t}\n\n\tif (!workspace.name) {\n\t\treturn null;\n\t}\n\n\tconst client = createSSMClient(config as SSMStateConfig);\n\tconst parameterName = getSecretsParameterName(workspace.name, stage);\n\n\ttry {\n\t\tconst response = await client.send(\n\t\t\tnew GetParameterCommand({\n\t\t\t\tName: parameterName,\n\t\t\t\tWithDecryption: true,\n\t\t\t}),\n\t\t);\n\n\t\tif (!response.Parameter?.Value) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn JSON.parse(response.Parameter.Value) as StageSecrets;\n\t} catch (error) {\n\t\tif (error instanceof ParameterNotFound) {\n\t\t\treturn null;\n\t\t}\n\t\tthrow error;\n\t}\n}\n\n/**\n * Check if SSM is configured for the workspace.\n */\nexport function isSSMConfigured(workspace: NormalizedWorkspace): boolean {\n\treturn !!workspace.state && workspace.state.provider === 'ssm';\n}\n"],"mappings":";;;;;;;AAuBA,SAAS,wBAAwBA,eAAuBC,OAAuB;AAC9E,SAAQ,OAAO,cAAc,GAAG,MAAM;AACtC;;;;AAKD,SAAS,gBAAgBC,QAAmC;CAC3D,MAAMC,eAAgC,EACrC,QAAQ,OAAO,OACf;AAED,KAAI,OAAO,SAAS;EACnB,MAAM,EAAE,SAAS,GAAG,UAAQ,gCAAgC;AAC5D,eAAa,cAAc,QAAQ,EAAE,SAAS,OAAO,QAAS,EAAC;CAC/D;AAED,QAAO,IAAI,UAAU;AACrB;;;;;;AAOD,eAAsB,YACrBF,OACAG,WACgB;CAChB,MAAM,SAAS,UAAU;AACzB,MAAK,UAAU,OAAO,aAAa,MAClC,OAAM,IAAI,MACT;AAIF,MAAK,UAAU,KACd,OAAM,IAAI,MACT;CAIF,MAAM,SAAS,gBAAgB,OAAyB;CACxD,MAAM,gBAAgB,wBAAwB,UAAU,MAAM,MAAM;CAEpE,MAAM,EAAE,kBAAkB,GAAG,MAAM,OAAO;CAC1C,MAAM,UAAU,MAAM,iBAAiB,OAAO,UAAU,KAAK;AAE7D,MAAK,QACJ,OAAM,IAAI,OACR,8BAA8B,MAAM,mCAAmC,MAAM;AAIhF,OAAM,OAAO,KACZ,IAAI,oBAAoB;EACvB,MAAM;EACN,OAAO,KAAK,UAAU,QAAQ;EAC9B,MAAM;EACN,WAAW;EACX,cAAc,kBAAkB,UAAU,KAAK,GAAG,MAAM;CACxD,GACD;AACD;;;;;;AAOD,eAAsB,YACrBH,OACAG,WAC+B;CAC/B,MAAM,SAAS,UAAU;AACzB,MAAK,UAAU,OAAO,aAAa,MAClC,QAAO;AAGR,MAAK,UAAU,KACd,QAAO;CAGR,MAAM,SAAS,gBAAgB,OAAyB;CACxD,MAAM,gBAAgB,wBAAwB,UAAU,MAAM,MAAM;AAEpE,KAAI;EACH,MAAM,WAAW,MAAM,OAAO,KAC7B,IAAI,oBAAoB;GACvB,MAAM;GACN,gBAAgB;EAChB,GACD;AAED,OAAK,SAAS,WAAW,MACxB,QAAO;AAGR,SAAO,KAAK,MAAM,SAAS,UAAU,MAAM;CAC3C,SAAQ,OAAO;AACf,MAAI,iBAAiB,kBACpB,QAAO;AAER,QAAM;CACN;AACD;;;;AAKD,SAAgB,gBAAgBA,WAAyC;AACxE,UAAS,UAAU,SAAS,UAAU,MAAM,aAAa;AACzD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.11",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"prompts": "~2.4.2",
|
|
57
57
|
"tsx": "~4.20.3",
|
|
58
58
|
"yaml": "~2.8.2",
|
|
59
|
-
"@geekmidas/logger": "~1.0.0",
|
|
60
|
-
"@geekmidas/envkit": "~1.0.3",
|
|
61
59
|
"@geekmidas/constructs": "~3.0.2",
|
|
60
|
+
"@geekmidas/envkit": "~1.0.3",
|
|
62
61
|
"@geekmidas/schema": "~1.0.0",
|
|
62
|
+
"@geekmidas/logger": "~1.0.0",
|
|
63
63
|
"@geekmidas/errors": "~1.0.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
package/src/dev/index.ts
CHANGED
|
@@ -399,7 +399,12 @@ export function rewriteUrlsWithPorts(
|
|
|
399
399
|
|
|
400
400
|
// Rewrite URLs: replace Docker service hostnames with localhost and fix ports
|
|
401
401
|
for (const [key, value] of Object.entries(result)) {
|
|
402
|
-
if (
|
|
402
|
+
if (
|
|
403
|
+
!key.endsWith('_URL') &&
|
|
404
|
+
!key.endsWith('_ENDPOINT') &&
|
|
405
|
+
key !== 'DATABASE_URL'
|
|
406
|
+
)
|
|
407
|
+
continue;
|
|
403
408
|
|
|
404
409
|
let rewritten = value;
|
|
405
410
|
for (const name of serviceNames) {
|
|
@@ -352,12 +352,18 @@ describe('generateDockerCompose', () => {
|
|
|
352
352
|
services: { minio: true },
|
|
353
353
|
});
|
|
354
354
|
|
|
355
|
-
expect(yaml).toContain(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
expect(yaml).toContain(
|
|
359
|
-
|
|
360
|
-
|
|
355
|
+
expect(yaml).toContain(
|
|
356
|
+
'- STORAGE_ENDPOINT=${STORAGE_ENDPOINT:-http://minio:9000}',
|
|
357
|
+
);
|
|
358
|
+
expect(yaml).toContain(
|
|
359
|
+
'- STORAGE_ACCESS_KEY_ID=${STORAGE_ACCESS_KEY_ID:-my-api}',
|
|
360
|
+
);
|
|
361
|
+
expect(yaml).toContain(
|
|
362
|
+
'- STORAGE_SECRET_ACCESS_KEY=${STORAGE_SECRET_ACCESS_KEY:-my-api}',
|
|
363
|
+
);
|
|
364
|
+
expect(yaml).toContain('- STORAGE_BUCKET=${STORAGE_BUCKET:-my-api}');
|
|
365
|
+
expect(yaml).toContain('- STORAGE_REGION=${STORAGE_REGION:-eu-west-1}');
|
|
366
|
+
expect(yaml).toContain('- STORAGE_FORCE_PATH_STYLE=true');
|
|
361
367
|
});
|
|
362
368
|
|
|
363
369
|
it('should add minio service definition with default image', () => {
|
|
@@ -386,8 +392,12 @@ describe('generateDockerCompose', () => {
|
|
|
386
392
|
services: { minio: true },
|
|
387
393
|
});
|
|
388
394
|
|
|
389
|
-
expect(yaml).toContain(
|
|
390
|
-
|
|
395
|
+
expect(yaml).toContain(
|
|
396
|
+
'MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY_ID:-my-api}',
|
|
397
|
+
);
|
|
398
|
+
expect(yaml).toContain(
|
|
399
|
+
'MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_ACCESS_KEY:-my-api}',
|
|
400
|
+
);
|
|
391
401
|
});
|
|
392
402
|
|
|
393
403
|
it('should expose console UI port', () => {
|
|
@@ -396,7 +406,8 @@ describe('generateDockerCompose', () => {
|
|
|
396
406
|
services: { minio: true },
|
|
397
407
|
});
|
|
398
408
|
|
|
399
|
-
expect(yaml).toContain('- "
|
|
409
|
+
expect(yaml).toContain('- "${MINIO_API_PORT:-9000}:9000"');
|
|
410
|
+
expect(yaml).toContain('- "${MINIO_CONSOLE_PORT:-9001}:9001"');
|
|
400
411
|
});
|
|
401
412
|
|
|
402
413
|
it('should add minio volume', () => {
|
|
@@ -1044,8 +1055,10 @@ describe('generateWorkspaceCompose', () => {
|
|
|
1044
1055
|
});
|
|
1045
1056
|
const yaml = generateWorkspaceCompose(workspace);
|
|
1046
1057
|
|
|
1047
|
-
expect(yaml).toContain(
|
|
1048
|
-
|
|
1058
|
+
expect(yaml).toContain(
|
|
1059
|
+
'STORAGE_ENDPOINT=${STORAGE_ENDPOINT:-http://minio:9000}',
|
|
1060
|
+
);
|
|
1061
|
+
expect(yaml).toContain('STORAGE_FORCE_PATH_STYLE=true');
|
|
1049
1062
|
});
|
|
1050
1063
|
|
|
1051
1064
|
it('should add minio_data volume when minio is enabled', () => {
|
package/src/docker/compose.ts
CHANGED
|
@@ -124,12 +124,12 @@ services:
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
if (serviceMap.has('minio')) {
|
|
127
|
-
yaml += ` -
|
|
128
|
-
-
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
127
|
+
yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
|
|
128
|
+
- STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${imageName}}
|
|
129
|
+
- STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
|
|
130
|
+
- STORAGE_BUCKET=\${STORAGE_BUCKET:-${imageName}}
|
|
131
|
+
- STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
|
|
132
|
+
- STORAGE_FORCE_PATH_STYLE=true
|
|
133
133
|
`;
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -230,12 +230,13 @@ services:
|
|
|
230
230
|
container_name: minio
|
|
231
231
|
restart: unless-stopped
|
|
232
232
|
entrypoint: sh
|
|
233
|
-
command: -c 'mkdir -p /data/\${
|
|
233
|
+
command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${imageName}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
234
234
|
environment:
|
|
235
|
-
MINIO_ROOT_USER: \${
|
|
236
|
-
MINIO_ROOT_PASSWORD: \${
|
|
235
|
+
MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${imageName}}
|
|
236
|
+
MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
|
|
237
237
|
ports:
|
|
238
|
-
- "
|
|
238
|
+
- "\${MINIO_API_PORT:-9000}:9000"
|
|
239
|
+
- "\${MINIO_CONSOLE_PORT:-9001}:9001"
|
|
239
240
|
volumes:
|
|
240
241
|
- minio_data:/data
|
|
241
242
|
healthcheck:
|
|
@@ -366,6 +367,7 @@ services:
|
|
|
366
367
|
for (const [appName, app] of apps) {
|
|
367
368
|
yaml += generateAppService(appName, app, apps, {
|
|
368
369
|
registry,
|
|
370
|
+
projectName: workspace.name,
|
|
369
371
|
hasPostgres,
|
|
370
372
|
hasRedis,
|
|
371
373
|
hasMinio,
|
|
@@ -434,12 +436,13 @@ services:
|
|
|
434
436
|
container_name: ${workspace.name}-minio
|
|
435
437
|
restart: unless-stopped
|
|
436
438
|
entrypoint: sh
|
|
437
|
-
command: -c 'mkdir -p /data/\${
|
|
439
|
+
command: -c 'mkdir -p /data/\${STORAGE_BUCKET:-${workspace.name}} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /data'
|
|
438
440
|
environment:
|
|
439
|
-
MINIO_ROOT_USER: \${
|
|
440
|
-
MINIO_ROOT_PASSWORD: \${
|
|
441
|
+
MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${workspace.name}}
|
|
442
|
+
MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${workspace.name}}
|
|
441
443
|
ports:
|
|
442
|
-
- "
|
|
444
|
+
- "\${MINIO_API_PORT:-9000}:9000"
|
|
445
|
+
- "\${MINIO_CONSOLE_PORT:-9001}:9001"
|
|
443
446
|
volumes:
|
|
444
447
|
- minio_data:/data
|
|
445
448
|
healthcheck:
|
|
@@ -525,12 +528,13 @@ function generateAppService(
|
|
|
525
528
|
allApps: [string, NormalizedAppConfig][],
|
|
526
529
|
options: {
|
|
527
530
|
registry?: string;
|
|
531
|
+
projectName: string;
|
|
528
532
|
hasPostgres: boolean;
|
|
529
533
|
hasRedis: boolean;
|
|
530
534
|
hasMinio: boolean;
|
|
531
535
|
},
|
|
532
536
|
): string {
|
|
533
|
-
const { registry, hasPostgres, hasRedis, hasMinio } = options;
|
|
537
|
+
const { registry, projectName, hasPostgres, hasRedis, hasMinio } = options;
|
|
534
538
|
const imageRef = registry ? `\${REGISTRY:-${registry}}/` : '';
|
|
535
539
|
|
|
536
540
|
// Health check path - frontends use /, backends use /health
|
|
@@ -575,12 +579,12 @@ function generateAppService(
|
|
|
575
579
|
`;
|
|
576
580
|
}
|
|
577
581
|
if (hasMinio) {
|
|
578
|
-
yaml += ` -
|
|
579
|
-
-
|
|
580
|
-
-
|
|
581
|
-
-
|
|
582
|
-
-
|
|
583
|
-
-
|
|
582
|
+
yaml += ` - STORAGE_ENDPOINT=\${STORAGE_ENDPOINT:-http://minio:9000}
|
|
583
|
+
- STORAGE_ACCESS_KEY_ID=\${STORAGE_ACCESS_KEY_ID:-${projectName}}
|
|
584
|
+
- STORAGE_SECRET_ACCESS_KEY=\${STORAGE_SECRET_ACCESS_KEY:-${projectName}}
|
|
585
|
+
- STORAGE_BUCKET=\${STORAGE_BUCKET:-${projectName}}
|
|
586
|
+
- STORAGE_REGION=\${STORAGE_REGION:-eu-west-1}
|
|
587
|
+
- STORAGE_FORCE_PATH_STYLE=true
|
|
584
588
|
`;
|
|
585
589
|
}
|
|
586
590
|
}
|