@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,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
+ });