@geekmidas/cli 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +525 -0
  2. package/dist/bundler-DRXCw_YR.mjs +70 -0
  3. package/dist/bundler-DRXCw_YR.mjs.map +1 -0
  4. package/dist/bundler-WsEvH_b2.cjs +71 -0
  5. package/dist/bundler-WsEvH_b2.cjs.map +1 -0
  6. package/dist/{config-C9aXOHBe.cjs → config-AmInkU7k.cjs} +8 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-BrkUalUh.mjs → config-DYULeEv8.mjs} +3 -3
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +1 -1
  12. package/dist/config.d.mts +1 -1
  13. package/dist/config.mjs +1 -1
  14. package/dist/encryption-C8H-38Yy.mjs +42 -0
  15. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  16. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  17. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  18. package/dist/index.cjs +2116 -179
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.mjs +2134 -192
  21. package/dist/index.mjs.map +1 -1
  22. package/dist/{openapi-CZLI4QTr.mjs → openapi-BfFlOBCG.mjs} +801 -38
  23. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  24. package/dist/{openapi-BeHLKcwP.cjs → openapi-Bt_1FDpT.cjs} +794 -31
  25. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  26. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  27. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  28. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  29. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  30. package/dist/openapi-react-query.cjs +1 -1
  31. package/dist/openapi-react-query.d.cts.map +1 -1
  32. package/dist/openapi-react-query.d.mts.map +1 -1
  33. package/dist/openapi-react-query.mjs +1 -1
  34. package/dist/openapi.cjs +2 -2
  35. package/dist/openapi.d.cts +1 -1
  36. package/dist/openapi.d.cts.map +1 -1
  37. package/dist/openapi.d.mts +1 -1
  38. package/dist/openapi.d.mts.map +1 -1
  39. package/dist/openapi.mjs +2 -2
  40. package/dist/storage-BUYQJgz7.cjs +4 -0
  41. package/dist/storage-BXoJvmv2.cjs +149 -0
  42. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  43. package/dist/storage-C9PU_30f.mjs +101 -0
  44. package/dist/storage-C9PU_30f.mjs.map +1 -0
  45. package/dist/storage-DLJAYxzJ.mjs +3 -0
  46. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  47. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  48. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  49. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  50. package/examples/cron-example.ts +27 -27
  51. package/examples/env.ts +27 -27
  52. package/examples/function-example.ts +31 -31
  53. package/examples/gkm.config.json +20 -20
  54. package/examples/gkm.config.ts +8 -8
  55. package/examples/gkm.minimal.config.json +5 -5
  56. package/examples/gkm.production.config.json +25 -25
  57. package/examples/logger.ts +2 -2
  58. package/package.json +6 -6
  59. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  60. package/src/__tests__/config.spec.ts +55 -55
  61. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  62. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  63. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  64. package/src/__tests__/openapi.spec.ts +428 -428
  65. package/src/__tests__/test-helpers.ts +76 -76
  66. package/src/auth/__tests__/credentials.spec.ts +204 -0
  67. package/src/auth/__tests__/index.spec.ts +168 -0
  68. package/src/auth/credentials.ts +187 -0
  69. package/src/auth/index.ts +226 -0
  70. package/src/build/__tests__/index-new.spec.ts +474 -474
  71. package/src/build/__tests__/manifests.spec.ts +333 -333
  72. package/src/build/bundler.ts +141 -0
  73. package/src/build/endpoint-analyzer.ts +236 -0
  74. package/src/build/handler-templates.ts +1253 -0
  75. package/src/build/index.ts +250 -179
  76. package/src/build/manifests.ts +52 -52
  77. package/src/build/providerResolver.ts +145 -145
  78. package/src/build/types.ts +64 -43
  79. package/src/config.ts +39 -39
  80. package/src/deploy/__tests__/docker.spec.ts +111 -0
  81. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  82. package/src/deploy/__tests__/init.spec.ts +662 -0
  83. package/src/deploy/docker.ts +128 -0
  84. package/src/deploy/dokploy.ts +204 -0
  85. package/src/deploy/index.ts +136 -0
  86. package/src/deploy/init.ts +484 -0
  87. package/src/deploy/types.ts +48 -0
  88. package/src/dev/__tests__/index.spec.ts +266 -266
  89. package/src/dev/index.ts +647 -601
  90. package/src/docker/__tests__/compose.spec.ts +531 -0
  91. package/src/docker/__tests__/templates.spec.ts +280 -0
  92. package/src/docker/compose.ts +273 -0
  93. package/src/docker/index.ts +230 -0
  94. package/src/docker/templates.ts +446 -0
  95. package/src/generators/CronGenerator.ts +72 -72
  96. package/src/generators/EndpointGenerator.ts +699 -398
  97. package/src/generators/FunctionGenerator.ts +84 -84
  98. package/src/generators/Generator.ts +72 -72
  99. package/src/generators/OpenApiTsGenerator.ts +577 -577
  100. package/src/generators/SubscriberGenerator.ts +124 -124
  101. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  102. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  103. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  104. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  105. package/src/generators/index.ts +4 -4
  106. package/src/index.ts +623 -201
  107. package/src/init/__tests__/generators.spec.ts +334 -334
  108. package/src/init/__tests__/init.spec.ts +332 -332
  109. package/src/init/__tests__/utils.spec.ts +89 -89
  110. package/src/init/generators/config.ts +175 -175
  111. package/src/init/generators/docker.ts +41 -41
  112. package/src/init/generators/env.ts +72 -72
  113. package/src/init/generators/index.ts +1 -1
  114. package/src/init/generators/models.ts +64 -64
  115. package/src/init/generators/monorepo.ts +161 -161
  116. package/src/init/generators/package.ts +71 -71
  117. package/src/init/generators/source.ts +6 -6
  118. package/src/init/index.ts +203 -208
  119. package/src/init/templates/api.ts +115 -115
  120. package/src/init/templates/index.ts +75 -75
  121. package/src/init/templates/minimal.ts +98 -98
  122. package/src/init/templates/serverless.ts +89 -89
  123. package/src/init/templates/worker.ts +98 -98
  124. package/src/init/utils.ts +54 -56
  125. package/src/openapi-react-query.ts +194 -194
  126. package/src/openapi.ts +63 -63
  127. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  128. package/src/secrets/__tests__/generator.spec.ts +319 -0
  129. package/src/secrets/__tests__/index.spec.ts +91 -0
  130. package/src/secrets/__tests__/storage.spec.ts +403 -0
  131. package/src/secrets/encryption.ts +91 -0
  132. package/src/secrets/generator.ts +164 -0
  133. package/src/secrets/index.ts +383 -0
  134. package/src/secrets/storage.ts +134 -0
  135. package/src/secrets/types.ts +53 -0
  136. package/src/types.ts +295 -176
  137. package/tsdown.config.ts +11 -8
  138. package/dist/config-BrkUalUh.mjs.map +0 -1
  139. package/dist/config-C9aXOHBe.cjs.map +0 -1
  140. package/dist/openapi-BeHLKcwP.cjs.map +0 -1
  141. package/dist/openapi-CZLI4QTr.mjs.map +0 -1
  142. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  143. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
  144. package/dist/types-DXgiA1sF.d.mts.map +0 -1
  145. package/dist/types-b-vwGpqc.d.cts.map +0 -1
@@ -0,0 +1,383 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { loadConfig } from '../config';
4
+ import type { ComposeServiceName, ComposeServicesConfig } from '../types';
5
+ import { createStageSecrets, rotateServicePassword } from './generator';
6
+ import {
7
+ maskPassword,
8
+ readStageSecrets,
9
+ secretsExist,
10
+ setCustomSecret,
11
+ writeStageSecrets,
12
+ } from './storage';
13
+
14
+ const logger = console;
15
+
16
+ export interface SecretsInitOptions {
17
+ stage: string;
18
+ force?: boolean;
19
+ }
20
+
21
+ export interface SecretsSetOptions {
22
+ stage: string;
23
+ }
24
+
25
+ export interface SecretsShowOptions {
26
+ stage: string;
27
+ reveal?: boolean;
28
+ }
29
+
30
+ export interface SecretsRotateOptions {
31
+ stage: string;
32
+ service?: ComposeServiceName;
33
+ }
34
+
35
+ export interface SecretsImportOptions {
36
+ stage: string;
37
+ /** Merge with existing secrets (default: true) */
38
+ merge?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Extract service names from compose config.
43
+ */
44
+ export function getServicesFromConfig(
45
+ services: ComposeServicesConfig | ComposeServiceName[] | undefined,
46
+ ): ComposeServiceName[] {
47
+ if (!services) {
48
+ return [];
49
+ }
50
+
51
+ if (Array.isArray(services)) {
52
+ return services;
53
+ }
54
+
55
+ // Object format - get keys where value is truthy
56
+ return (Object.entries(services) as [ComposeServiceName, unknown][])
57
+ .filter(([, config]) => config)
58
+ .map(([name]) => name);
59
+ }
60
+
61
+ /**
62
+ * Initialize secrets for a stage.
63
+ * Generates secure random passwords for configured services.
64
+ */
65
+ export async function secretsInitCommand(
66
+ options: SecretsInitOptions,
67
+ ): Promise<void> {
68
+ const { stage, force } = options;
69
+
70
+ // Check if secrets already exist
71
+ if (!force && secretsExist(stage)) {
72
+ logger.error(
73
+ `Secrets already exist for stage "${stage}". Use --force to overwrite.`,
74
+ );
75
+ process.exit(1);
76
+ }
77
+
78
+ // Load config to get services
79
+ const config = await loadConfig();
80
+ const services = getServicesFromConfig(config.docker?.compose?.services);
81
+
82
+ if (services.length === 0) {
83
+ logger.warn(
84
+ 'No services configured in docker.compose.services. Creating secrets with empty services.',
85
+ );
86
+ }
87
+
88
+ // Generate secrets
89
+ const secrets = createStageSecrets(stage, services);
90
+
91
+ // Write to file
92
+ await writeStageSecrets(secrets);
93
+
94
+ logger.log(`\n✓ Secrets initialized for stage "${stage}"`);
95
+ logger.log(` Location: .gkm/secrets/${stage}.json`);
96
+ logger.log('\n Generated credentials for:');
97
+
98
+ for (const service of services) {
99
+ logger.log(` - ${service}`);
100
+ }
101
+
102
+ if (secrets.urls.DATABASE_URL) {
103
+ logger.log(`\n DATABASE_URL: ${maskUrl(secrets.urls.DATABASE_URL)}`);
104
+ }
105
+ if (secrets.urls.REDIS_URL) {
106
+ logger.log(` REDIS_URL: ${maskUrl(secrets.urls.REDIS_URL)}`);
107
+ }
108
+ if (secrets.urls.RABBITMQ_URL) {
109
+ logger.log(` RABBITMQ_URL: ${maskUrl(secrets.urls.RABBITMQ_URL)}`);
110
+ }
111
+
112
+ logger.log(`\n Use "gkm secrets:show --stage ${stage}" to view secrets`);
113
+ logger.log(
114
+ ' Use "gkm secrets:set <KEY> <VALUE> --stage ' +
115
+ stage +
116
+ '" to add custom secrets',
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Read all data from stdin.
122
+ */
123
+ async function readStdin(): Promise<string> {
124
+ const chunks: Buffer[] = [];
125
+
126
+ for await (const chunk of process.stdin) {
127
+ chunks.push(chunk);
128
+ }
129
+
130
+ return Buffer.concat(chunks).toString('utf-8').trim();
131
+ }
132
+
133
+ /**
134
+ * Set a custom secret.
135
+ * If value is not provided, reads from stdin.
136
+ */
137
+ export async function secretsSetCommand(
138
+ key: string,
139
+ value: string | undefined,
140
+ options: SecretsSetOptions,
141
+ ): Promise<void> {
142
+ const { stage } = options;
143
+
144
+ // Read from stdin if value not provided
145
+ let secretValue = value;
146
+ if (!secretValue) {
147
+ if (process.stdin.isTTY) {
148
+ logger.error(
149
+ 'No value provided. Use: gkm secrets:set KEY VALUE --stage <stage>',
150
+ );
151
+ logger.error(
152
+ 'Or pipe from stdin: echo "value" | gkm secrets:set KEY --stage <stage>',
153
+ );
154
+ process.exit(1);
155
+ }
156
+ secretValue = await readStdin();
157
+ if (!secretValue) {
158
+ logger.error('No value received from stdin');
159
+ process.exit(1);
160
+ }
161
+ }
162
+
163
+ try {
164
+ await setCustomSecret(stage, key, secretValue);
165
+ logger.log(`\n✓ Secret "${key}" set for stage "${stage}"`);
166
+ } catch (error) {
167
+ logger.error(
168
+ error instanceof Error ? error.message : 'Failed to set secret',
169
+ );
170
+ process.exit(1);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Show secrets for a stage.
176
+ */
177
+ export async function secretsShowCommand(
178
+ options: SecretsShowOptions,
179
+ ): Promise<void> {
180
+ const { stage, reveal } = options;
181
+
182
+ const secrets = await readStageSecrets(stage);
183
+
184
+ if (!secrets) {
185
+ logger.error(
186
+ `No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
187
+ );
188
+ process.exit(1);
189
+ }
190
+
191
+ logger.log(`\nSecrets for stage "${stage}":`);
192
+ logger.log(` Created: ${secrets.createdAt}`);
193
+ logger.log(` Updated: ${secrets.updatedAt}`);
194
+
195
+ // Show service credentials
196
+ logger.log('\nService Credentials:');
197
+ for (const [service, creds] of Object.entries(secrets.services)) {
198
+ if (creds) {
199
+ logger.log(`\n ${service}:`);
200
+ logger.log(` host: ${creds.host}`);
201
+ logger.log(` port: ${creds.port}`);
202
+ logger.log(` username: ${creds.username}`);
203
+ logger.log(
204
+ ` password: ${reveal ? creds.password : maskPassword(creds.password)}`,
205
+ );
206
+ if (creds.database) {
207
+ logger.log(` database: ${creds.database}`);
208
+ }
209
+ if (creds.vhost) {
210
+ logger.log(` vhost: ${creds.vhost}`);
211
+ }
212
+ }
213
+ }
214
+
215
+ // Show URLs
216
+ logger.log('\nConnection URLs:');
217
+ if (secrets.urls.DATABASE_URL) {
218
+ logger.log(
219
+ ` DATABASE_URL: ${reveal ? secrets.urls.DATABASE_URL : maskUrl(secrets.urls.DATABASE_URL)}`,
220
+ );
221
+ }
222
+ if (secrets.urls.REDIS_URL) {
223
+ logger.log(
224
+ ` REDIS_URL: ${reveal ? secrets.urls.REDIS_URL : maskUrl(secrets.urls.REDIS_URL)}`,
225
+ );
226
+ }
227
+ if (secrets.urls.RABBITMQ_URL) {
228
+ logger.log(
229
+ ` RABBITMQ_URL: ${reveal ? secrets.urls.RABBITMQ_URL : maskUrl(secrets.urls.RABBITMQ_URL)}`,
230
+ );
231
+ }
232
+
233
+ // Show custom secrets
234
+ const customKeys = Object.keys(secrets.custom);
235
+ if (customKeys.length > 0) {
236
+ logger.log('\nCustom Secrets:');
237
+ for (const [key, value] of Object.entries(secrets.custom)) {
238
+ logger.log(` ${key}: ${reveal ? value : maskPassword(value)}`);
239
+ }
240
+ }
241
+
242
+ if (!reveal) {
243
+ logger.log('\nUse --reveal to show actual values');
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Rotate passwords for services.
249
+ */
250
+ export async function secretsRotateCommand(
251
+ options: SecretsRotateOptions,
252
+ ): Promise<void> {
253
+ const { stage, service } = options;
254
+
255
+ const secrets = await readStageSecrets(stage);
256
+
257
+ if (!secrets) {
258
+ logger.error(
259
+ `No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
260
+ );
261
+ process.exit(1);
262
+ }
263
+
264
+ if (service) {
265
+ // Rotate specific service
266
+ if (!secrets.services[service]) {
267
+ logger.error(`Service "${service}" not configured in stage "${stage}"`);
268
+ process.exit(1);
269
+ }
270
+
271
+ const updated = rotateServicePassword(secrets, service);
272
+ await writeStageSecrets(updated);
273
+ logger.log(`\n✓ Password rotated for ${service} in stage "${stage}"`);
274
+ } else {
275
+ // Rotate all services
276
+ let updated = secrets;
277
+ const services = Object.keys(secrets.services) as ComposeServiceName[];
278
+
279
+ for (const svc of services) {
280
+ updated = rotateServicePassword(updated, svc);
281
+ }
282
+
283
+ await writeStageSecrets(updated);
284
+ logger.log(
285
+ `\n✓ Passwords rotated for all services in stage "${stage}": ${services.join(', ')}`,
286
+ );
287
+ }
288
+
289
+ logger.log(`\nUse "gkm secrets:show --stage ${stage}" to view new values`);
290
+ }
291
+
292
+ /**
293
+ * Import secrets from a JSON file.
294
+ */
295
+ export async function secretsImportCommand(
296
+ file: string,
297
+ options: SecretsImportOptions,
298
+ ): Promise<void> {
299
+ const { stage, merge = true } = options;
300
+
301
+ // Check if file exists
302
+ if (!existsSync(file)) {
303
+ logger.error(`File not found: ${file}`);
304
+ process.exit(1);
305
+ }
306
+
307
+ // Read and parse JSON file
308
+ let importedSecrets: Record<string, string>;
309
+ try {
310
+ const content = await readFile(file, 'utf-8');
311
+ importedSecrets = JSON.parse(content);
312
+
313
+ // Validate it's a flat object with string values
314
+ if (typeof importedSecrets !== 'object' || importedSecrets === null) {
315
+ throw new Error('JSON must be an object');
316
+ }
317
+
318
+ for (const [key, value] of Object.entries(importedSecrets)) {
319
+ if (typeof value !== 'string') {
320
+ throw new Error(
321
+ `Value for "${key}" must be a string, got ${typeof value}`,
322
+ );
323
+ }
324
+ }
325
+ } catch (error) {
326
+ logger.error(
327
+ `Failed to parse JSON file: ${error instanceof Error ? error.message : 'Invalid JSON'}`,
328
+ );
329
+ process.exit(1);
330
+ }
331
+
332
+ // Check if secrets exist for stage
333
+ const secrets = await readStageSecrets(stage);
334
+
335
+ if (!secrets) {
336
+ logger.error(
337
+ `No secrets found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
338
+ );
339
+ process.exit(1);
340
+ }
341
+
342
+ // Merge or replace custom secrets
343
+ const updatedCustom = merge
344
+ ? { ...secrets.custom, ...importedSecrets }
345
+ : importedSecrets;
346
+
347
+ const updated = {
348
+ ...secrets,
349
+ updatedAt: new Date().toISOString(),
350
+ custom: updatedCustom,
351
+ };
352
+
353
+ await writeStageSecrets(updated);
354
+
355
+ const importedCount = Object.keys(importedSecrets).length;
356
+ const totalCount = Object.keys(updatedCustom).length;
357
+
358
+ logger.log(`\n✓ Imported ${importedCount} secrets for stage "${stage}"`);
359
+
360
+ if (merge && totalCount > importedCount) {
361
+ logger.log(` Total custom secrets: ${totalCount}`);
362
+ }
363
+
364
+ logger.log('\n Imported keys:');
365
+ for (const key of Object.keys(importedSecrets)) {
366
+ logger.log(` - ${key}`);
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Mask password in a URL for display.
372
+ */
373
+ export function maskUrl(url: string): string {
374
+ try {
375
+ const parsed = new URL(url);
376
+ if (parsed.password) {
377
+ parsed.password = maskPassword(parsed.password);
378
+ }
379
+ return parsed.toString();
380
+ } catch {
381
+ return url;
382
+ }
383
+ }
@@ -0,0 +1,134 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import type { EmbeddableSecrets, StageSecrets } from './types';
5
+
6
+ /** Default secrets directory relative to project root */
7
+ const SECRETS_DIR = '.gkm/secrets';
8
+
9
+ /**
10
+ * Get the secrets directory path.
11
+ */
12
+ export function getSecretsDir(cwd = process.cwd()): string {
13
+ return join(cwd, SECRETS_DIR);
14
+ }
15
+
16
+ /**
17
+ * Get the secrets file path for a stage.
18
+ */
19
+ export function getSecretsPath(stage: string, cwd = process.cwd()): string {
20
+ return join(getSecretsDir(cwd), `${stage}.json`);
21
+ }
22
+
23
+ /**
24
+ * Check if secrets exist for a stage.
25
+ */
26
+ export function secretsExist(stage: string, cwd = process.cwd()): boolean {
27
+ return existsSync(getSecretsPath(stage, cwd));
28
+ }
29
+
30
+ /**
31
+ * Read secrets for a stage.
32
+ * @returns StageSecrets or null if not found
33
+ */
34
+ export async function readStageSecrets(
35
+ stage: string,
36
+ cwd = process.cwd(),
37
+ ): Promise<StageSecrets | null> {
38
+ const path = getSecretsPath(stage, cwd);
39
+
40
+ if (!existsSync(path)) {
41
+ return null;
42
+ }
43
+
44
+ const content = await readFile(path, 'utf-8');
45
+ return JSON.parse(content) as StageSecrets;
46
+ }
47
+
48
+ /**
49
+ * Write secrets for a stage.
50
+ */
51
+ export async function writeStageSecrets(
52
+ secrets: StageSecrets,
53
+ cwd = process.cwd(),
54
+ ): Promise<void> {
55
+ const dir = getSecretsDir(cwd);
56
+ const path = getSecretsPath(secrets.stage, cwd);
57
+
58
+ // Ensure directory exists
59
+ await mkdir(dir, { recursive: true });
60
+
61
+ // Write with pretty formatting
62
+ await writeFile(path, JSON.stringify(secrets, null, 2), 'utf-8');
63
+ }
64
+
65
+ /**
66
+ * Convert StageSecrets to embeddable format (flat key-value pairs).
67
+ * This is what gets encrypted and embedded in the bundle.
68
+ */
69
+ export function toEmbeddableSecrets(secrets: StageSecrets): EmbeddableSecrets {
70
+ return {
71
+ ...secrets.urls,
72
+ ...secrets.custom,
73
+ // Also include individual service credentials if needed
74
+ ...(secrets.services.postgres && {
75
+ POSTGRES_USER: secrets.services.postgres.username,
76
+ POSTGRES_PASSWORD: secrets.services.postgres.password,
77
+ POSTGRES_DB: secrets.services.postgres.database ?? 'app',
78
+ POSTGRES_HOST: secrets.services.postgres.host,
79
+ POSTGRES_PORT: String(secrets.services.postgres.port),
80
+ }),
81
+ ...(secrets.services.redis && {
82
+ REDIS_PASSWORD: secrets.services.redis.password,
83
+ REDIS_HOST: secrets.services.redis.host,
84
+ REDIS_PORT: String(secrets.services.redis.port),
85
+ }),
86
+ ...(secrets.services.rabbitmq && {
87
+ RABBITMQ_USER: secrets.services.rabbitmq.username,
88
+ RABBITMQ_PASSWORD: secrets.services.rabbitmq.password,
89
+ RABBITMQ_HOST: secrets.services.rabbitmq.host,
90
+ RABBITMQ_PORT: String(secrets.services.rabbitmq.port),
91
+ RABBITMQ_VHOST: secrets.services.rabbitmq.vhost ?? '/',
92
+ }),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Update a custom secret in the secrets file.
98
+ */
99
+ export async function setCustomSecret(
100
+ stage: string,
101
+ key: string,
102
+ value: string,
103
+ cwd = process.cwd(),
104
+ ): Promise<StageSecrets> {
105
+ const secrets = await readStageSecrets(stage, cwd);
106
+
107
+ if (!secrets) {
108
+ throw new Error(
109
+ `Secrets not found for stage "${stage}". Run "gkm secrets:init --stage ${stage}" first.`,
110
+ );
111
+ }
112
+
113
+ const updated: StageSecrets = {
114
+ ...secrets,
115
+ updatedAt: new Date().toISOString(),
116
+ custom: {
117
+ ...secrets.custom,
118
+ [key]: value,
119
+ },
120
+ };
121
+
122
+ await writeStageSecrets(updated, cwd);
123
+ return updated;
124
+ }
125
+
126
+ /**
127
+ * Mask a password for display (show first 4 and last 2 chars).
128
+ */
129
+ export function maskPassword(password: string): string {
130
+ if (password.length <= 8) {
131
+ return '********';
132
+ }
133
+ return `${password.slice(0, 4)}${'*'.repeat(password.length - 6)}${password.slice(-2)}`;
134
+ }
@@ -0,0 +1,53 @@
1
+ import type { ComposeServiceName } from '../types';
2
+
3
+ /** Credentials for a specific service */
4
+ export interface ServiceCredentials {
5
+ host: string;
6
+ port: number;
7
+ username: string;
8
+ password: string;
9
+ /** Database name (for postgres) */
10
+ database?: string;
11
+ /** Virtual host (for rabbitmq) */
12
+ vhost?: string;
13
+ }
14
+
15
+ /** Stage secrets configuration */
16
+ export interface StageSecrets {
17
+ /** Stage name (e.g., 'production', 'staging') */
18
+ stage: string;
19
+ /** ISO timestamp when secrets were created */
20
+ createdAt: string;
21
+ /** ISO timestamp when secrets were last updated */
22
+ updatedAt: string;
23
+ /** Service-specific credentials */
24
+ services: {
25
+ postgres?: ServiceCredentials;
26
+ redis?: ServiceCredentials;
27
+ rabbitmq?: ServiceCredentials;
28
+ };
29
+ /** Generated connection URLs */
30
+ urls: {
31
+ DATABASE_URL?: string;
32
+ REDIS_URL?: string;
33
+ RABBITMQ_URL?: string;
34
+ };
35
+ /** Custom user-defined secrets */
36
+ custom: Record<string, string>;
37
+ }
38
+
39
+ /** Encrypted payload for build-time injection */
40
+ export interface EncryptedPayload {
41
+ /** Base64 encoded encrypted data (ciphertext + auth tag) */
42
+ encrypted: string;
43
+ /** Hex encoded IV */
44
+ iv: string;
45
+ /** Hex encoded ephemeral master key (for deployment) */
46
+ masterKey: string;
47
+ }
48
+
49
+ /** Secrets that get encrypted and embedded in the bundle */
50
+ export type EmbeddableSecrets = Record<string, string>;
51
+
52
+ /** Services that support automatic credential generation */
53
+ export type SecretServiceName = ComposeServiceName;