@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/{bundler-kk_XJTRp.mjs → bundler-BvByD6tt.mjs} +2 -2
  3. package/dist/{bundler-kk_XJTRp.mjs.map → bundler-BvByD6tt.mjs.map} +1 -1
  4. package/dist/{bundler-Bm3Az_sv.cjs → bundler-wY-XZDl9.cjs} +2 -2
  5. package/dist/{bundler-Bm3Az_sv.cjs.map → bundler-wY-XZDl9.cjs.map} +1 -1
  6. package/dist/{fullstack-secrets-CtWIYuI0.cjs → fullstack-secrets-AUkUyeW0.cjs} +21 -3
  7. package/dist/fullstack-secrets-AUkUyeW0.cjs.map +1 -0
  8. package/dist/{fullstack-secrets-C2lbdbLZ.mjs → fullstack-secrets-Bnm_QeZr.mjs} +10 -4
  9. package/dist/fullstack-secrets-Bnm_QeZr.mjs.map +1 -0
  10. package/dist/index.cjs +93 -50
  11. package/dist/index.cjs.map +1 -1
  12. package/dist/index.mjs +93 -50
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/{reconcile-BnM6FA6g.mjs → reconcile--yaWO052.mjs} +2 -2
  15. package/dist/{reconcile-BnM6FA6g.mjs.map → reconcile--yaWO052.mjs.map} +1 -1
  16. package/dist/{reconcile-D6u4HSg8.cjs → reconcile-oktp4i_4.cjs} +2 -2
  17. package/dist/{reconcile-D6u4HSg8.cjs.map → reconcile-oktp4i_4.cjs.map} +1 -1
  18. package/dist/{storage-D6BGLgWf.cjs → storage-B3aXQBpI.cjs} +1 -1
  19. package/dist/{storage-Cs13jkJ9.cjs → storage-CEisMvif.cjs} +6 -6
  20. package/dist/{storage-B7H2PPCS.mjs.map → storage-CEisMvif.cjs.map} +1 -1
  21. package/dist/{storage-C1FNm2EP.mjs → storage-D3lh8Xpq.mjs} +1 -1
  22. package/dist/{storage-B7H2PPCS.mjs → storage-Df-NFMY9.mjs} +6 -6
  23. package/dist/{storage-Cs13jkJ9.cjs.map → storage-Df-NFMY9.mjs.map} +1 -1
  24. package/dist/{sync-CyGe5f1I.mjs → sync-CIzj1CQe.mjs} +1 -1
  25. package/dist/{sync-oCqELfeA.cjs → sync-CcDvBUu4.cjs} +2 -2
  26. package/dist/{sync-oCqELfeA.cjs.map → sync-CcDvBUu4.cjs.map} +1 -1
  27. package/dist/{sync-CzXruMzP.mjs → sync-D-7NTKvV.mjs} +2 -2
  28. package/dist/{sync-CzXruMzP.mjs.map → sync-D-7NTKvV.mjs.map} +1 -1
  29. package/dist/sync-QNXT8x13.cjs +4 -0
  30. package/package.json +3 -3
  31. package/src/dev/index.ts +6 -1
  32. package/src/docker/__tests__/compose.spec.ts +24 -11
  33. package/src/docker/compose.ts +25 -21
  34. package/src/secrets/__tests__/generator.spec.ts +5 -5
  35. package/src/secrets/__tests__/storage.spec.ts +7 -7
  36. package/src/secrets/generator.ts +10 -4
  37. package/src/secrets/index.ts +4 -4
  38. package/src/secrets/storage.ts +5 -5
  39. package/src/secrets/types.ts +1 -1
  40. package/src/setup/__tests__/reconcile-secrets.spec.ts +59 -0
  41. package/src/setup/index.ts +56 -16
  42. package/dist/fullstack-secrets-C2lbdbLZ.mjs.map +0 -1
  43. package/dist/fullstack-secrets-CtWIYuI0.cjs.map +0 -1
  44. package/dist/sync-DLlwsrBs.cjs +0 -4
@@ -1,4 +1,4 @@
1
- import { generateFullstackCustomSecrets } from "./fullstack-secrets-C2lbdbLZ.mjs";
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-BnM6FA6g.mjs.map
36
+ //# sourceMappingURL=reconcile--yaWO052.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-BnM6FA6g.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
+ {"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-CtWIYuI0.cjs');
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-D6u4HSg8.cjs.map
36
+ //# sourceMappingURL=reconcile-oktp4i_4.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"reconcile-D6u4HSg8.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"}
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"}
@@ -1,4 +1,4 @@
1
- const require_storage = require('./storage-Cs13jkJ9.cjs');
1
+ const require_storage = require('./storage-CEisMvif.cjs');
2
2
 
3
3
  exports.initStageSecrets = require_storage.initStageSecrets;
4
4
  exports.readStageSecrets = require_storage.readStageSecrets;
@@ -210,11 +210,11 @@ function toEmbeddableSecrets(secrets) {
210
210
  RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? "/"
211
211
  },
212
212
  ...secrets.services.minio && {
213
- S3_ACCESS_KEY_ID: secrets.services.minio.username,
214
- S3_SECRET_ACCESS_KEY: secrets.services.minio.password,
215
- S3_BUCKET: secrets.services.minio.bucket ?? "app",
216
- S3_REGION: "eu-west-1",
217
- S3_FORCE_PATH_STYLE: "true"
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-Cs13jkJ9.cjs.map
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-B7H2PPCS.mjs";
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
- S3_ACCESS_KEY_ID: secrets.services.minio.username,
213
- S3_SECRET_ACCESS_KEY: secrets.services.minio.password,
214
- S3_BUCKET: secrets.services.minio.bucket ?? "app",
215
- S3_REGION: "eu-west-1",
216
- S3_FORCE_PATH_STYLE: "true"
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-B7H2PPCS.mjs.map
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":"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"}
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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"}
@@ -1,3 +1,3 @@
1
- import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-CzXruMzP.mjs";
1
+ import { isSSMConfigured, pullSecrets, pushSecrets } from "./sync-D-7NTKvV.mjs";
2
2
 
3
3
  export { pullSecrets, pushSecrets };
@@ -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-D6BGLgWf.cjs"));
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-oCqELfeA.cjs.map
93
+ //# sourceMappingURL=sync-CcDvBUu4.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-oCqELfeA.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"}
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-C1FNm2EP.mjs");
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-CzXruMzP.mjs.map
76
+ //# sourceMappingURL=sync-D-7NTKvV.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"sync-CzXruMzP.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"}
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"}
@@ -0,0 +1,4 @@
1
+ const require_sync = require('./sync-CcDvBUu4.cjs');
2
+
3
+ exports.pullSecrets = require_sync.pullSecrets;
4
+ exports.pushSecrets = require_sync.pushSecrets;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/cli",
3
- "version": "1.10.9",
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 (!key.endsWith('_URL') && key !== 'DATABASE_URL') continue;
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('- S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000}');
356
- expect(yaml).toContain('- S3_ACCESS_KEY_ID=${MINIO_ACCESS_KEY:-app}');
357
- expect(yaml).toContain('- S3_SECRET_ACCESS_KEY=${MINIO_SECRET_KEY:-app}');
358
- expect(yaml).toContain('- S3_BUCKET=${MINIO_BUCKET:-app}');
359
- expect(yaml).toContain('- S3_REGION=${S3_REGION:-eu-west-1}');
360
- expect(yaml).toContain('- S3_FORCE_PATH_STYLE=true');
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('MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-app}');
390
- expect(yaml).toContain('MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-app}');
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('- "9001:9001"');
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('S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000}');
1048
- expect(yaml).toContain('S3_FORCE_PATH_STYLE=true');
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', () => {
@@ -124,12 +124,12 @@ services:
124
124
  }
125
125
 
126
126
  if (serviceMap.has('minio')) {
127
- yaml += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
128
- - S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
129
- - S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
130
- - S3_BUCKET=\${MINIO_BUCKET:-app}
131
- - S3_REGION=\${S3_REGION:-eu-west-1}
132
- - S3_FORCE_PATH_STYLE=true
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/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /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: \${MINIO_ACCESS_KEY:-app}
236
- MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
235
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${imageName}}
236
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${imageName}}
237
237
  ports:
238
- - "9001:9001" # Console UI
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/\${MINIO_BUCKET:-app} && /usr/bin/docker-entrypoint.sh server --console-address ":9001" /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: \${MINIO_ACCESS_KEY:-app}
440
- MINIO_ROOT_PASSWORD: \${MINIO_SECRET_KEY:-app}
441
+ MINIO_ROOT_USER: \${STORAGE_ACCESS_KEY_ID:-${workspace.name}}
442
+ MINIO_ROOT_PASSWORD: \${STORAGE_SECRET_ACCESS_KEY:-${workspace.name}}
441
443
  ports:
442
- - "9001:9001" # Console UI
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 += ` - S3_ENDPOINT=\${S3_ENDPOINT:-http://minio:9000}
579
- - S3_ACCESS_KEY_ID=\${MINIO_ACCESS_KEY:-app}
580
- - S3_SECRET_ACCESS_KEY=\${MINIO_SECRET_KEY:-app}
581
- - S3_BUCKET=\${MINIO_BUCKET:-app}
582
- - S3_REGION=\${S3_REGION:-eu-west-1}
583
- - S3_FORCE_PATH_STYLE=true
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
  }