@geekmidas/cli 0.9.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 (146) 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-CFls09Ey.cjs → config-AmInkU7k.cjs} +10 -8
  7. package/dist/config-AmInkU7k.cjs.map +1 -0
  8. package/dist/{config-Bq72aj8e.mjs → config-DYULeEv8.mjs} +6 -4
  9. package/dist/config-DYULeEv8.mjs.map +1 -0
  10. package/dist/config.cjs +1 -1
  11. package/dist/config.d.cts +2 -1
  12. package/dist/config.d.cts.map +1 -0
  13. package/dist/config.d.mts +2 -1
  14. package/dist/config.d.mts.map +1 -0
  15. package/dist/config.mjs +1 -1
  16. package/dist/encryption-C8H-38Yy.mjs +42 -0
  17. package/dist/encryption-C8H-38Yy.mjs.map +1 -0
  18. package/dist/encryption-Dyf_r1h-.cjs +44 -0
  19. package/dist/encryption-Dyf_r1h-.cjs.map +1 -0
  20. package/dist/index.cjs +2125 -184
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.mjs +2143 -197
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/{openapi--vOy9mo4.mjs → openapi-BfFlOBCG.mjs} +812 -49
  25. package/dist/openapi-BfFlOBCG.mjs.map +1 -0
  26. package/dist/{openapi-CHhTPief.cjs → openapi-Bt_1FDpT.cjs} +805 -42
  27. package/dist/openapi-Bt_1FDpT.cjs.map +1 -0
  28. package/dist/{openapi-react-query-o5iMi8tz.cjs → openapi-react-query-B-sNWHFU.cjs} +5 -5
  29. package/dist/openapi-react-query-B-sNWHFU.cjs.map +1 -0
  30. package/dist/{openapi-react-query-CcciaVu5.mjs → openapi-react-query-B6XTeGqS.mjs} +5 -5
  31. package/dist/openapi-react-query-B6XTeGqS.mjs.map +1 -0
  32. package/dist/openapi-react-query.cjs +1 -1
  33. package/dist/openapi-react-query.d.cts.map +1 -0
  34. package/dist/openapi-react-query.d.mts.map +1 -0
  35. package/dist/openapi-react-query.mjs +1 -1
  36. package/dist/openapi.cjs +2 -2
  37. package/dist/openapi.d.cts +1 -1
  38. package/dist/openapi.d.cts.map +1 -0
  39. package/dist/openapi.d.mts +1 -1
  40. package/dist/openapi.d.mts.map +1 -0
  41. package/dist/openapi.mjs +2 -2
  42. package/dist/storage-BUYQJgz7.cjs +4 -0
  43. package/dist/storage-BXoJvmv2.cjs +149 -0
  44. package/dist/storage-BXoJvmv2.cjs.map +1 -0
  45. package/dist/storage-C9PU_30f.mjs +101 -0
  46. package/dist/storage-C9PU_30f.mjs.map +1 -0
  47. package/dist/storage-DLJAYxzJ.mjs +3 -0
  48. package/dist/{types-b-vwGpqc.d.cts → types-BR0M2v_c.d.mts} +100 -1
  49. package/dist/types-BR0M2v_c.d.mts.map +1 -0
  50. package/dist/{types-DXgiA1sF.d.mts → types-BhkZc-vm.d.cts} +100 -1
  51. package/dist/types-BhkZc-vm.d.cts.map +1 -0
  52. package/examples/cron-example.ts +27 -27
  53. package/examples/env.ts +27 -27
  54. package/examples/function-example.ts +31 -31
  55. package/examples/gkm.config.json +20 -20
  56. package/examples/gkm.config.ts +8 -8
  57. package/examples/gkm.minimal.config.json +5 -5
  58. package/examples/gkm.production.config.json +25 -25
  59. package/examples/logger.ts +2 -2
  60. package/package.json +6 -6
  61. package/src/__tests__/EndpointGenerator.hooks.spec.ts +191 -191
  62. package/src/__tests__/config.spec.ts +55 -55
  63. package/src/__tests__/loadEnvFiles.spec.ts +93 -93
  64. package/src/__tests__/normalizeHooksConfig.spec.ts +58 -58
  65. package/src/__tests__/openapi-react-query.spec.ts +497 -497
  66. package/src/__tests__/openapi.spec.ts +428 -428
  67. package/src/__tests__/test-helpers.ts +77 -76
  68. package/src/auth/__tests__/credentials.spec.ts +204 -0
  69. package/src/auth/__tests__/index.spec.ts +168 -0
  70. package/src/auth/credentials.ts +187 -0
  71. package/src/auth/index.ts +226 -0
  72. package/src/build/__tests__/index-new.spec.ts +474 -474
  73. package/src/build/__tests__/manifests.spec.ts +333 -333
  74. package/src/build/bundler.ts +141 -0
  75. package/src/build/endpoint-analyzer.ts +236 -0
  76. package/src/build/handler-templates.ts +1253 -0
  77. package/src/build/index.ts +250 -179
  78. package/src/build/manifests.ts +52 -52
  79. package/src/build/providerResolver.ts +145 -145
  80. package/src/build/types.ts +64 -43
  81. package/src/config.ts +39 -37
  82. package/src/deploy/__tests__/docker.spec.ts +111 -0
  83. package/src/deploy/__tests__/dokploy.spec.ts +245 -0
  84. package/src/deploy/__tests__/init.spec.ts +662 -0
  85. package/src/deploy/docker.ts +128 -0
  86. package/src/deploy/dokploy.ts +204 -0
  87. package/src/deploy/index.ts +136 -0
  88. package/src/deploy/init.ts +484 -0
  89. package/src/deploy/types.ts +48 -0
  90. package/src/dev/__tests__/index.spec.ts +266 -266
  91. package/src/dev/index.ts +647 -593
  92. package/src/docker/__tests__/compose.spec.ts +531 -0
  93. package/src/docker/__tests__/templates.spec.ts +280 -0
  94. package/src/docker/compose.ts +273 -0
  95. package/src/docker/index.ts +230 -0
  96. package/src/docker/templates.ts +446 -0
  97. package/src/generators/CronGenerator.ts +72 -72
  98. package/src/generators/EndpointGenerator.ts +699 -398
  99. package/src/generators/FunctionGenerator.ts +84 -84
  100. package/src/generators/Generator.ts +72 -72
  101. package/src/generators/OpenApiTsGenerator.ts +589 -589
  102. package/src/generators/SubscriberGenerator.ts +124 -124
  103. package/src/generators/__tests__/CronGenerator.spec.ts +433 -433
  104. package/src/generators/__tests__/EndpointGenerator.spec.ts +532 -382
  105. package/src/generators/__tests__/FunctionGenerator.spec.ts +244 -244
  106. package/src/generators/__tests__/SubscriberGenerator.spec.ts +397 -382
  107. package/src/generators/index.ts +4 -4
  108. package/src/index.ts +628 -206
  109. package/src/init/__tests__/generators.spec.ts +334 -334
  110. package/src/init/__tests__/init.spec.ts +332 -332
  111. package/src/init/__tests__/utils.spec.ts +89 -89
  112. package/src/init/generators/config.ts +175 -175
  113. package/src/init/generators/docker.ts +41 -41
  114. package/src/init/generators/env.ts +72 -72
  115. package/src/init/generators/index.ts +1 -1
  116. package/src/init/generators/models.ts +64 -64
  117. package/src/init/generators/monorepo.ts +161 -161
  118. package/src/init/generators/package.ts +71 -71
  119. package/src/init/generators/source.ts +6 -6
  120. package/src/init/index.ts +203 -208
  121. package/src/init/templates/api.ts +115 -115
  122. package/src/init/templates/index.ts +75 -75
  123. package/src/init/templates/minimal.ts +98 -98
  124. package/src/init/templates/serverless.ts +89 -89
  125. package/src/init/templates/worker.ts +98 -98
  126. package/src/init/utils.ts +54 -56
  127. package/src/openapi-react-query.ts +194 -194
  128. package/src/openapi.ts +63 -63
  129. package/src/secrets/__tests__/encryption.spec.ts +226 -0
  130. package/src/secrets/__tests__/generator.spec.ts +319 -0
  131. package/src/secrets/__tests__/index.spec.ts +91 -0
  132. package/src/secrets/__tests__/storage.spec.ts +403 -0
  133. package/src/secrets/encryption.ts +91 -0
  134. package/src/secrets/generator.ts +164 -0
  135. package/src/secrets/index.ts +383 -0
  136. package/src/secrets/storage.ts +134 -0
  137. package/src/secrets/types.ts +53 -0
  138. package/src/types.ts +295 -176
  139. package/tsconfig.json +9 -0
  140. package/tsdown.config.ts +11 -8
  141. package/dist/config-Bq72aj8e.mjs.map +0 -1
  142. package/dist/config-CFls09Ey.cjs.map +0 -1
  143. package/dist/openapi--vOy9mo4.mjs.map +0 -1
  144. package/dist/openapi-CHhTPief.cjs.map +0 -1
  145. package/dist/openapi-react-query-CcciaVu5.mjs.map +0 -1
  146. package/dist/openapi-react-query-o5iMi8tz.cjs.map +0 -1
@@ -0,0 +1,226 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ decryptSecrets,
4
+ encryptSecrets,
5
+ generateDefineOptions,
6
+ } from '../encryption';
7
+ import type { EmbeddableSecrets, EncryptedPayload } from '../types';
8
+
9
+ describe('encryptSecrets', () => {
10
+ it('should return encrypted payload with all required fields', () => {
11
+ const secrets: EmbeddableSecrets = {
12
+ DATABASE_URL: 'postgresql://test:pass@localhost/db',
13
+ };
14
+
15
+ const payload = encryptSecrets(secrets);
16
+
17
+ expect(payload.encrypted).toBeDefined();
18
+ expect(payload.iv).toBeDefined();
19
+ expect(payload.masterKey).toBeDefined();
20
+ });
21
+
22
+ it('should generate different ciphertext for same input', () => {
23
+ const secrets: EmbeddableSecrets = {
24
+ DATABASE_URL: 'postgresql://test:pass@localhost/db',
25
+ };
26
+
27
+ const payload1 = encryptSecrets(secrets);
28
+ const payload2 = encryptSecrets(secrets);
29
+
30
+ expect(payload1.encrypted).not.toBe(payload2.encrypted);
31
+ expect(payload1.iv).not.toBe(payload2.iv);
32
+ expect(payload1.masterKey).not.toBe(payload2.masterKey);
33
+ });
34
+
35
+ it('should generate hex-encoded IV of correct length', () => {
36
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
37
+ const payload = encryptSecrets(secrets);
38
+
39
+ // 12 bytes = 24 hex characters
40
+ expect(payload.iv).toHaveLength(24);
41
+ expect(payload.iv).toMatch(/^[0-9a-f]+$/);
42
+ });
43
+
44
+ it('should generate hex-encoded master key of correct length', () => {
45
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
46
+ const payload = encryptSecrets(secrets);
47
+
48
+ // 32 bytes = 64 hex characters
49
+ expect(payload.masterKey).toHaveLength(64);
50
+ expect(payload.masterKey).toMatch(/^[0-9a-f]+$/);
51
+ });
52
+
53
+ it('should produce base64-encoded ciphertext', () => {
54
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
55
+ const payload = encryptSecrets(secrets);
56
+
57
+ // Should be valid base64
58
+ expect(() => Buffer.from(payload.encrypted, 'base64')).not.toThrow();
59
+ });
60
+ });
61
+
62
+ describe('decryptSecrets', () => {
63
+ it('should decrypt back to original secrets', () => {
64
+ const secrets: EmbeddableSecrets = {
65
+ DATABASE_URL: 'postgresql://test:pass@localhost/db',
66
+ REDIS_URL: 'redis://localhost:6379',
67
+ CUSTOM_KEY: 'custom-value',
68
+ };
69
+
70
+ const payload = encryptSecrets(secrets);
71
+ const decrypted = decryptSecrets(
72
+ payload.encrypted,
73
+ payload.iv,
74
+ payload.masterKey,
75
+ );
76
+
77
+ expect(decrypted).toEqual(secrets);
78
+ });
79
+
80
+ it('should handle empty secrets', () => {
81
+ const secrets: EmbeddableSecrets = {};
82
+
83
+ const payload = encryptSecrets(secrets);
84
+ const decrypted = decryptSecrets(
85
+ payload.encrypted,
86
+ payload.iv,
87
+ payload.masterKey,
88
+ );
89
+
90
+ expect(decrypted).toEqual({});
91
+ });
92
+
93
+ it('should handle secrets with special characters', () => {
94
+ const secrets: EmbeddableSecrets = {
95
+ PASSWORD: 'p@ss/word!#$%^&*(){}[]|\\:";\'<>,.?/',
96
+ URL: 'https://user:pass@host.com/path?query=value&foo=bar',
97
+ };
98
+
99
+ const payload = encryptSecrets(secrets);
100
+ const decrypted = decryptSecrets(
101
+ payload.encrypted,
102
+ payload.iv,
103
+ payload.masterKey,
104
+ );
105
+
106
+ expect(decrypted).toEqual(secrets);
107
+ });
108
+
109
+ it('should handle secrets with unicode characters', () => {
110
+ const secrets: EmbeddableSecrets = {
111
+ MESSAGE: '你好世界 🔐 مرحبا',
112
+ };
113
+
114
+ const payload = encryptSecrets(secrets);
115
+ const decrypted = decryptSecrets(
116
+ payload.encrypted,
117
+ payload.iv,
118
+ payload.masterKey,
119
+ );
120
+
121
+ expect(decrypted).toEqual(secrets);
122
+ });
123
+
124
+ it('should throw with wrong master key', () => {
125
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
126
+ const payload = encryptSecrets(secrets);
127
+
128
+ // Generate a different key
129
+ const wrongKey = '0'.repeat(64);
130
+
131
+ expect(() =>
132
+ decryptSecrets(payload.encrypted, payload.iv, wrongKey),
133
+ ).toThrow();
134
+ });
135
+
136
+ it('should throw with wrong IV', () => {
137
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
138
+ const payload = encryptSecrets(secrets);
139
+
140
+ // Use wrong IV
141
+ const wrongIv = '0'.repeat(24);
142
+
143
+ expect(() =>
144
+ decryptSecrets(payload.encrypted, wrongIv, payload.masterKey),
145
+ ).toThrow();
146
+ });
147
+
148
+ it('should throw with tampered ciphertext', () => {
149
+ const secrets: EmbeddableSecrets = { KEY: 'value' };
150
+ const payload = encryptSecrets(secrets);
151
+
152
+ // Tamper with ciphertext
153
+ const tamperedBuffer = Buffer.from(payload.encrypted, 'base64');
154
+ tamperedBuffer[0] = tamperedBuffer[0] ^ 0xff;
155
+ const tampered = tamperedBuffer.toString('base64');
156
+
157
+ expect(() =>
158
+ decryptSecrets(tampered, payload.iv, payload.masterKey),
159
+ ).toThrow();
160
+ });
161
+ });
162
+
163
+ describe('generateDefineOptions', () => {
164
+ it('should return define options for bundler', () => {
165
+ const payload: EncryptedPayload = {
166
+ encrypted: 'base64-ciphertext',
167
+ iv: 'hex-iv-value',
168
+ masterKey: 'hex-master-key',
169
+ };
170
+
171
+ const options = generateDefineOptions(payload);
172
+
173
+ expect(options.__GKM_ENCRYPTED_CREDENTIALS__).toBe(
174
+ JSON.stringify('base64-ciphertext'),
175
+ );
176
+ expect(options.__GKM_CREDENTIALS_IV__).toBe(JSON.stringify('hex-iv-value'));
177
+ });
178
+
179
+ it('should not include master key in define options', () => {
180
+ const payload: EncryptedPayload = {
181
+ encrypted: 'encrypted',
182
+ iv: 'iv',
183
+ masterKey: 'secret-key',
184
+ };
185
+
186
+ const options = generateDefineOptions(payload);
187
+
188
+ expect(options).not.toHaveProperty('__GKM_MASTER_KEY__');
189
+ expect(JSON.stringify(options)).not.toContain('secret-key');
190
+ });
191
+ });
192
+
193
+ describe('encryption roundtrip', () => {
194
+ it('should handle large secrets', () => {
195
+ const secrets: EmbeddableSecrets = {};
196
+ for (let i = 0; i < 100; i++) {
197
+ secrets[`KEY_${i}`] = `value-${i}-${'x'.repeat(100)}`;
198
+ }
199
+
200
+ const payload = encryptSecrets(secrets);
201
+ const decrypted = decryptSecrets(
202
+ payload.encrypted,
203
+ payload.iv,
204
+ payload.masterKey,
205
+ );
206
+
207
+ expect(decrypted).toEqual(secrets);
208
+ });
209
+
210
+ it('should handle secrets with newlines and whitespace', () => {
211
+ const secrets: EmbeddableSecrets = {
212
+ MULTILINE: 'line1\nline2\nline3',
213
+ TABS: 'col1\tcol2\tcol3',
214
+ SPACES: ' leading and trailing ',
215
+ };
216
+
217
+ const payload = encryptSecrets(secrets);
218
+ const decrypted = decryptSecrets(
219
+ payload.encrypted,
220
+ payload.iv,
221
+ payload.masterKey,
222
+ );
223
+
224
+ expect(decrypted).toEqual(secrets);
225
+ });
226
+ });
@@ -0,0 +1,319 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createStageSecrets,
4
+ generateConnectionUrls,
5
+ generatePostgresUrl,
6
+ generateRabbitmqUrl,
7
+ generateRedisUrl,
8
+ generateSecurePassword,
9
+ generateServiceCredentials,
10
+ generateServicesCredentials,
11
+ rotateServicePassword,
12
+ } from '../generator';
13
+ import type { ServiceCredentials, StageSecrets } from '../types';
14
+
15
+ describe('generateSecurePassword', () => {
16
+ it('should generate password of default length (32)', () => {
17
+ const password = generateSecurePassword();
18
+ expect(password).toHaveLength(32);
19
+ });
20
+
21
+ it('should generate password of custom length', () => {
22
+ const password = generateSecurePassword(16);
23
+ expect(password).toHaveLength(16);
24
+ });
25
+
26
+ it('should generate different passwords each call', () => {
27
+ const password1 = generateSecurePassword();
28
+ const password2 = generateSecurePassword();
29
+ expect(password1).not.toBe(password2);
30
+ });
31
+
32
+ it('should only contain URL-safe base64 characters', () => {
33
+ const password = generateSecurePassword(64);
34
+ // base64url uses A-Z, a-z, 0-9, -, _
35
+ expect(password).toMatch(/^[A-Za-z0-9_-]+$/);
36
+ });
37
+ });
38
+
39
+ describe('generateServiceCredentials', () => {
40
+ it('should generate postgres credentials with defaults', () => {
41
+ const creds = generateServiceCredentials('postgres');
42
+
43
+ expect(creds.host).toBe('postgres');
44
+ expect(creds.port).toBe(5432);
45
+ expect(creds.username).toBe('app');
46
+ expect(creds.database).toBe('app');
47
+ expect(creds.password).toHaveLength(32);
48
+ });
49
+
50
+ it('should generate redis credentials with defaults', () => {
51
+ const creds = generateServiceCredentials('redis');
52
+
53
+ expect(creds.host).toBe('redis');
54
+ expect(creds.port).toBe(6379);
55
+ expect(creds.username).toBe('default');
56
+ expect(creds.password).toHaveLength(32);
57
+ });
58
+
59
+ it('should generate rabbitmq credentials with defaults', () => {
60
+ const creds = generateServiceCredentials('rabbitmq');
61
+
62
+ expect(creds.host).toBe('rabbitmq');
63
+ expect(creds.port).toBe(5672);
64
+ expect(creds.username).toBe('app');
65
+ expect(creds.vhost).toBe('/');
66
+ expect(creds.password).toHaveLength(32);
67
+ });
68
+ });
69
+
70
+ describe('generateServicesCredentials', () => {
71
+ it('should generate credentials for multiple services', () => {
72
+ const creds = generateServicesCredentials(['postgres', 'redis']);
73
+
74
+ expect(creds.postgres).toBeDefined();
75
+ expect(creds.redis).toBeDefined();
76
+ expect(creds.rabbitmq).toBeUndefined();
77
+ });
78
+
79
+ it('should generate unique passwords for each service', () => {
80
+ const creds = generateServicesCredentials([
81
+ 'postgres',
82
+ 'redis',
83
+ 'rabbitmq',
84
+ ]);
85
+
86
+ expect(creds.postgres!.password).not.toBe(creds.redis!.password);
87
+ expect(creds.redis!.password).not.toBe(creds.rabbitmq!.password);
88
+ });
89
+ });
90
+
91
+ describe('generatePostgresUrl', () => {
92
+ it('should generate valid postgres URL', () => {
93
+ const creds: ServiceCredentials = {
94
+ host: 'postgres',
95
+ port: 5432,
96
+ username: 'app',
97
+ password: 'secret123',
98
+ database: 'mydb',
99
+ };
100
+
101
+ const url = generatePostgresUrl(creds);
102
+ expect(url).toBe('postgresql://app:secret123@postgres:5432/mydb');
103
+ });
104
+
105
+ it('should encode special characters in password', () => {
106
+ const creds: ServiceCredentials = {
107
+ host: 'postgres',
108
+ port: 5432,
109
+ username: 'app',
110
+ password: 'pass@word/test',
111
+ database: 'mydb',
112
+ };
113
+
114
+ const url = generatePostgresUrl(creds);
115
+ expect(url).toBe('postgresql://app:pass%40word%2Ftest@postgres:5432/mydb');
116
+ });
117
+ });
118
+
119
+ describe('generateRedisUrl', () => {
120
+ it('should generate valid redis URL', () => {
121
+ const creds: ServiceCredentials = {
122
+ host: 'redis',
123
+ port: 6379,
124
+ username: 'default',
125
+ password: 'secret123',
126
+ };
127
+
128
+ const url = generateRedisUrl(creds);
129
+ expect(url).toBe('redis://:secret123@redis:6379');
130
+ });
131
+
132
+ it('should encode special characters in password', () => {
133
+ const creds: ServiceCredentials = {
134
+ host: 'redis',
135
+ port: 6379,
136
+ username: 'default',
137
+ password: 'pass@word/test',
138
+ };
139
+
140
+ const url = generateRedisUrl(creds);
141
+ expect(url).toBe('redis://:pass%40word%2Ftest@redis:6379');
142
+ });
143
+ });
144
+
145
+ describe('generateRabbitmqUrl', () => {
146
+ it('should generate valid rabbitmq URL with default vhost', () => {
147
+ const creds: ServiceCredentials = {
148
+ host: 'rabbitmq',
149
+ port: 5672,
150
+ username: 'app',
151
+ password: 'secret123',
152
+ vhost: '/',
153
+ };
154
+
155
+ const url = generateRabbitmqUrl(creds);
156
+ expect(url).toBe('amqp://app:secret123@rabbitmq:5672/%2F');
157
+ });
158
+
159
+ it('should handle custom vhost', () => {
160
+ const creds: ServiceCredentials = {
161
+ host: 'rabbitmq',
162
+ port: 5672,
163
+ username: 'app',
164
+ password: 'secret123',
165
+ vhost: '/myapp',
166
+ };
167
+
168
+ const url = generateRabbitmqUrl(creds);
169
+ expect(url).toBe('amqp://app:secret123@rabbitmq:5672/%2Fmyapp');
170
+ });
171
+ });
172
+
173
+ describe('generateConnectionUrls', () => {
174
+ it('should generate DATABASE_URL for postgres', () => {
175
+ const urls = generateConnectionUrls({
176
+ postgres: {
177
+ host: 'postgres',
178
+ port: 5432,
179
+ username: 'app',
180
+ password: 'secret',
181
+ database: 'app',
182
+ },
183
+ });
184
+
185
+ expect(urls.DATABASE_URL).toBe('postgresql://app:secret@postgres:5432/app');
186
+ expect(urls.REDIS_URL).toBeUndefined();
187
+ expect(urls.RABBITMQ_URL).toBeUndefined();
188
+ });
189
+
190
+ it('should generate all URLs when all services present', () => {
191
+ const urls = generateConnectionUrls({
192
+ postgres: {
193
+ host: 'postgres',
194
+ port: 5432,
195
+ username: 'app',
196
+ password: 'pg-pass',
197
+ database: 'app',
198
+ },
199
+ redis: {
200
+ host: 'redis',
201
+ port: 6379,
202
+ username: 'default',
203
+ password: 'redis-pass',
204
+ },
205
+ rabbitmq: {
206
+ host: 'rabbitmq',
207
+ port: 5672,
208
+ username: 'app',
209
+ password: 'rmq-pass',
210
+ vhost: '/',
211
+ },
212
+ });
213
+
214
+ expect(urls.DATABASE_URL).toBeDefined();
215
+ expect(urls.REDIS_URL).toBeDefined();
216
+ expect(urls.RABBITMQ_URL).toBeDefined();
217
+ });
218
+ });
219
+
220
+ describe('createStageSecrets', () => {
221
+ it('should create stage secrets with stage name', () => {
222
+ const secrets = createStageSecrets('production', ['postgres']);
223
+
224
+ expect(secrets.stage).toBe('production');
225
+ expect(secrets.createdAt).toBeDefined();
226
+ expect(secrets.updatedAt).toBeDefined();
227
+ expect(secrets.custom).toEqual({});
228
+ });
229
+
230
+ it('should include service credentials', () => {
231
+ const secrets = createStageSecrets('staging', ['postgres', 'redis']);
232
+
233
+ expect(secrets.services.postgres).toBeDefined();
234
+ expect(secrets.services.redis).toBeDefined();
235
+ expect(secrets.services.rabbitmq).toBeUndefined();
236
+ });
237
+
238
+ it('should generate connection URLs', () => {
239
+ const secrets = createStageSecrets('production', [
240
+ 'postgres',
241
+ 'redis',
242
+ 'rabbitmq',
243
+ ]);
244
+
245
+ expect(secrets.urls.DATABASE_URL).toBeDefined();
246
+ expect(secrets.urls.REDIS_URL).toBeDefined();
247
+ expect(secrets.urls.RABBITMQ_URL).toBeDefined();
248
+ });
249
+ });
250
+
251
+ describe('rotateServicePassword', () => {
252
+ it('should rotate password for specified service', () => {
253
+ const original = createStageSecrets('production', ['postgres', 'redis']);
254
+ const originalPassword = original.services.postgres!.password;
255
+
256
+ const rotated = rotateServicePassword(original, 'postgres');
257
+
258
+ expect(rotated.services.postgres!.password).not.toBe(originalPassword);
259
+ expect(rotated.services.redis!.password).toBe(
260
+ original.services.redis!.password,
261
+ );
262
+ });
263
+
264
+ it('should update updatedAt timestamp', () => {
265
+ // Create with a fixed past timestamp
266
+ const original: StageSecrets = {
267
+ stage: 'production',
268
+ createdAt: '2024-01-01T00:00:00.000Z',
269
+ updatedAt: '2024-01-01T00:00:00.000Z',
270
+ services: {
271
+ postgres: {
272
+ host: 'postgres',
273
+ port: 5432,
274
+ username: 'app',
275
+ password: 'original-pass',
276
+ database: 'app',
277
+ },
278
+ },
279
+ urls: {
280
+ DATABASE_URL: 'postgresql://app:original-pass@postgres:5432/app',
281
+ },
282
+ custom: {},
283
+ };
284
+
285
+ const rotated = rotateServicePassword(original, 'postgres');
286
+
287
+ expect(rotated.updatedAt).not.toBe(original.updatedAt);
288
+ });
289
+
290
+ it('should regenerate connection URL', () => {
291
+ const original = createStageSecrets('production', ['postgres']);
292
+ const originalUrl = original.urls.DATABASE_URL;
293
+
294
+ const rotated = rotateServicePassword(original, 'postgres');
295
+
296
+ expect(rotated.urls.DATABASE_URL).not.toBe(originalUrl);
297
+ });
298
+
299
+ it('should throw for unconfigured service', () => {
300
+ const secrets = createStageSecrets('production', ['postgres']);
301
+
302
+ expect(() => rotateServicePassword(secrets, 'redis')).toThrow(
303
+ 'Service "redis" not configured in secrets',
304
+ );
305
+ });
306
+
307
+ it('should preserve other service credentials', () => {
308
+ const original = createStageSecrets('production', [
309
+ 'postgres',
310
+ 'redis',
311
+ 'rabbitmq',
312
+ ]);
313
+
314
+ const rotated = rotateServicePassword(original, 'postgres');
315
+
316
+ expect(rotated.services.redis).toEqual(original.services.redis);
317
+ expect(rotated.services.rabbitmq).toEqual(original.services.rabbitmq);
318
+ });
319
+ });
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getServicesFromConfig, maskUrl } from '../index';
3
+
4
+ describe('getServicesFromConfig', () => {
5
+ it('should return empty array when services is undefined', () => {
6
+ const result = getServicesFromConfig(undefined);
7
+ expect(result).toEqual([]);
8
+ });
9
+
10
+ it('should return array as-is when services is an array', () => {
11
+ const services = ['postgres', 'redis'] as const;
12
+ const result = getServicesFromConfig([...services]);
13
+ expect(result).toEqual(['postgres', 'redis']);
14
+ });
15
+
16
+ it('should extract service names from object config', () => {
17
+ const services = {
18
+ postgres: true,
19
+ redis: { port: 6379 },
20
+ rabbitmq: false,
21
+ };
22
+ const result = getServicesFromConfig(services);
23
+ expect(result).toContain('postgres');
24
+ expect(result).toContain('redis');
25
+ expect(result).not.toContain('rabbitmq');
26
+ });
27
+
28
+ it('should handle empty object', () => {
29
+ const result = getServicesFromConfig({});
30
+ expect(result).toEqual([]);
31
+ });
32
+
33
+ it('should handle all falsy values in object', () => {
34
+ const services = {
35
+ postgres: false,
36
+ redis: null,
37
+ rabbitmq: undefined,
38
+ };
39
+ const result = getServicesFromConfig(services as Record<string, unknown>);
40
+ expect(result).toEqual([]);
41
+ });
42
+ });
43
+
44
+ describe('maskUrl', () => {
45
+ it('should mask password in URL', () => {
46
+ const url = 'postgres://user:secretpassword@localhost:5432/db';
47
+ const masked = maskUrl(url);
48
+ expect(masked).not.toContain('secretpassword');
49
+ expect(masked).toContain('user');
50
+ expect(masked).toContain('localhost');
51
+ expect(masked).toContain('5432');
52
+ expect(masked).toContain('db');
53
+ });
54
+
55
+ it('should handle URL without password', () => {
56
+ const url = 'redis://localhost:6379';
57
+ const masked = maskUrl(url);
58
+ // URL object normalizes the format
59
+ expect(masked).toContain('redis://');
60
+ expect(masked).toContain('localhost:6379');
61
+ });
62
+
63
+ it('should handle URL with username only', () => {
64
+ const url = 'postgres://user@localhost:5432/db';
65
+ const masked = maskUrl(url);
66
+ expect(masked).toContain('user');
67
+ expect(masked).toContain('localhost');
68
+ });
69
+
70
+ it('should return original string for invalid URL', () => {
71
+ const invalid = 'not-a-url';
72
+ const result = maskUrl(invalid);
73
+ expect(result).toBe('not-a-url');
74
+ });
75
+
76
+ it('should handle amqp URLs', () => {
77
+ const url = 'amqp://guest:guestpass@localhost:5672/vhost';
78
+ const masked = maskUrl(url);
79
+ expect(masked).not.toContain('guestpass');
80
+ expect(masked).toContain('guest');
81
+ expect(masked).toContain('vhost');
82
+ });
83
+
84
+ it('should handle https URLs with credentials', () => {
85
+ const url = 'https://user:apikey123@api.example.com/v1';
86
+ const masked = maskUrl(url);
87
+ expect(masked).not.toContain('apikey123');
88
+ expect(masked).toContain('user');
89
+ expect(masked).toContain('api.example.com');
90
+ });
91
+ });