@geekmidas/cli 0.38.0 → 0.40.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.
- package/dist/{bundler-DQIuE3Kn.mjs → bundler-Db83tLti.mjs} +2 -2
- package/dist/{bundler-DQIuE3Kn.mjs.map → bundler-Db83tLti.mjs.map} +1 -1
- package/dist/{bundler-CyHg1v_T.cjs → bundler-DsXfFSCU.cjs} +2 -2
- package/dist/{bundler-CyHg1v_T.cjs.map → bundler-DsXfFSCU.cjs.map} +1 -1
- package/dist/{config-BC5n1a2D.mjs → config-C0b0jdmU.mjs} +2 -2
- package/dist/{config-BC5n1a2D.mjs.map → config-C0b0jdmU.mjs.map} +1 -1
- package/dist/{config-BAE9LFC1.cjs → config-xVZsRjN7.cjs} +2 -2
- package/dist/{config-BAE9LFC1.cjs.map → config-xVZsRjN7.cjs.map} +1 -1
- package/dist/config.cjs +2 -2
- package/dist/config.d.cts +1 -1
- package/dist/config.d.mts +2 -2
- package/dist/config.mjs +2 -2
- package/dist/dokploy-api-Bdmk5ImW.cjs +3 -0
- package/dist/{dokploy-api-C5czOZoc.cjs → dokploy-api-BdxOMH_V.cjs} +43 -1
- package/dist/{dokploy-api-C5czOZoc.cjs.map → dokploy-api-BdxOMH_V.cjs.map} +1 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs → dokploy-api-DWsqNjwP.mjs} +43 -1
- package/dist/{dokploy-api-B9qR2Yn1.mjs.map → dokploy-api-DWsqNjwP.mjs.map} +1 -1
- package/dist/dokploy-api-tZSZaHd9.mjs +3 -0
- package/dist/{encryption-JtMsiGNp.mjs → encryption-BC4MAODn.mjs} +1 -1
- package/dist/{encryption-JtMsiGNp.mjs.map → encryption-BC4MAODn.mjs.map} +1 -1
- package/dist/encryption-Biq0EZ4m.cjs +4 -0
- package/dist/encryption-CQXBZGkt.mjs +3 -0
- package/dist/{encryption-BAz0xQ1Q.cjs → encryption-DaCB_NmS.cjs} +13 -3
- package/dist/{encryption-BAz0xQ1Q.cjs.map → encryption-DaCB_NmS.cjs.map} +1 -1
- package/dist/{index-C7TkoYmt.d.mts → index-CXa3odEw.d.mts} +68 -7
- package/dist/index-CXa3odEw.d.mts.map +1 -0
- package/dist/{index-CpchsC9w.d.cts → index-E8Nu2Rxl.d.cts} +67 -6
- package/dist/index-E8Nu2Rxl.d.cts.map +1 -0
- package/dist/index.cjs +787 -145
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +767 -125
- package/dist/index.mjs.map +1 -1
- package/dist/{openapi-CjYeF-Tg.mjs → openapi-D3pA6FfZ.mjs} +2 -2
- package/dist/{openapi-CjYeF-Tg.mjs.map → openapi-D3pA6FfZ.mjs.map} +1 -1
- package/dist/{openapi-a-e3Y8WA.cjs → openapi-DhcCtKzM.cjs} +2 -2
- package/dist/{openapi-a-e3Y8WA.cjs.map → openapi-DhcCtKzM.cjs.map} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs → openapi-react-query-C_MxpBgF.cjs} +1 -1
- package/dist/{openapi-react-query-DvNpdDpM.cjs.map → openapi-react-query-C_MxpBgF.cjs.map} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs → openapi-react-query-ZoP9DPbY.mjs} +1 -1
- package/dist/{openapi-react-query-5rSortLH.mjs.map → openapi-react-query-ZoP9DPbY.mjs.map} +1 -1
- package/dist/openapi-react-query.cjs +1 -1
- package/dist/openapi-react-query.mjs +1 -1
- package/dist/openapi.cjs +3 -3
- package/dist/openapi.d.mts +1 -1
- package/dist/openapi.mjs +3 -3
- package/dist/{types-K2uQJ-FO.d.mts → types-BtGL-8QS.d.mts} +1 -1
- package/dist/{types-K2uQJ-FO.d.mts.map → types-BtGL-8QS.d.mts.map} +1 -1
- package/dist/workspace/index.cjs +1 -1
- package/dist/workspace/index.d.cts +2 -2
- package/dist/workspace/index.d.mts +3 -3
- package/dist/workspace/index.mjs +1 -1
- package/dist/{workspace-My0A4IRO.cjs → workspace-BDAhr6Kb.cjs} +33 -4
- package/dist/{workspace-My0A4IRO.cjs.map → workspace-BDAhr6Kb.cjs.map} +1 -1
- package/dist/{workspace-DFJ3sWfY.mjs → workspace-D_6ZCaR_.mjs} +33 -4
- package/dist/{workspace-DFJ3sWfY.mjs.map → workspace-D_6ZCaR_.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/build/index.ts +23 -6
- package/src/deploy/__tests__/domain.spec.ts +231 -0
- package/src/deploy/__tests__/secrets.spec.ts +300 -0
- package/src/deploy/__tests__/sniffer.spec.ts +221 -0
- package/src/deploy/docker.ts +58 -29
- package/src/deploy/dokploy-api.ts +99 -0
- package/src/deploy/domain.ts +125 -0
- package/src/deploy/index.ts +364 -145
- package/src/deploy/secrets.ts +182 -0
- package/src/deploy/sniffer.ts +180 -0
- package/src/dev/index.ts +155 -9
- package/src/docker/index.ts +17 -2
- package/src/docker/templates.ts +171 -1
- package/src/index.ts +18 -1
- package/src/init/generators/auth.ts +2 -0
- package/src/init/versions.ts +2 -2
- package/src/workspace/index.ts +2 -0
- package/src/workspace/schema.ts +32 -6
- package/src/workspace/types.ts +64 -2
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/dokploy-api-B0w17y4_.mjs +0 -3
- package/dist/dokploy-api-BnGeUqN4.cjs +0 -3
- package/dist/index-C7TkoYmt.d.mts.map +0 -1
- package/dist/index-CpchsC9w.d.cts.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geekmidas/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "CLI tools for building Lambda handlers, server applications, and generating OpenAPI specs",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"lodash.kebabcase": "^4.1.1",
|
|
49
49
|
"openapi-typescript": "^7.4.2",
|
|
50
50
|
"prompts": "~2.4.2",
|
|
51
|
-
"@geekmidas/schema": "~0.1.0",
|
|
52
|
-
"@geekmidas/constructs": "~0.6.0",
|
|
53
|
-
"@geekmidas/envkit": "~0.5.0",
|
|
54
51
|
"@geekmidas/errors": "~0.1.0",
|
|
55
|
-
"@geekmidas/logger": "~0.4.0"
|
|
52
|
+
"@geekmidas/logger": "~0.4.0",
|
|
53
|
+
"@geekmidas/envkit": "~0.6.0",
|
|
54
|
+
"@geekmidas/constructs": "~0.7.0",
|
|
55
|
+
"@geekmidas/schema": "~0.1.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/lodash.kebabcase": "^4.1.9",
|
package/src/build/index.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { mkdir } from 'node:fs/promises';
|
|
4
|
-
import { join, relative } from 'node:path';
|
|
4
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
5
|
import type { Cron } from '@geekmidas/constructs/crons';
|
|
6
6
|
import type { Endpoint } from '@geekmidas/constructs/endpoints';
|
|
7
7
|
import type { Function } from '@geekmidas/constructs/functions';
|
|
8
8
|
import type { Subscriber } from '@geekmidas/constructs/subscribers';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
loadAppConfig,
|
|
11
|
+
loadConfig,
|
|
12
|
+
loadWorkspaceConfig,
|
|
13
|
+
parseModuleConfig,
|
|
14
|
+
} from '../config';
|
|
10
15
|
import {
|
|
11
16
|
getProductionConfigFromGkm,
|
|
12
17
|
normalizeHooksConfig,
|
|
@@ -49,13 +54,25 @@ export async function buildCommand(
|
|
|
49
54
|
const loadedConfig = await loadWorkspaceConfig();
|
|
50
55
|
|
|
51
56
|
// Route to workspace build mode for multi-app workspaces
|
|
57
|
+
// BUT only if we're at the workspace root (prevents recursive builds when
|
|
58
|
+
// Turbo runs gkm build in each app subdirectory)
|
|
52
59
|
if (loadedConfig.type === 'workspace') {
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
const cwd = resolve(process.cwd());
|
|
61
|
+
const workspaceRoot = resolve(loadedConfig.workspace.root);
|
|
62
|
+
const isAtWorkspaceRoot = cwd === workspaceRoot;
|
|
63
|
+
|
|
64
|
+
if (isAtWorkspaceRoot) {
|
|
65
|
+
logger.log('📦 Detected workspace configuration');
|
|
66
|
+
return workspaceBuildCommand(loadedConfig.workspace, options);
|
|
67
|
+
}
|
|
68
|
+
// When running from inside an app directory, use app-specific config
|
|
55
69
|
}
|
|
56
70
|
|
|
57
|
-
// Single-app build - use
|
|
58
|
-
const config =
|
|
71
|
+
// Single-app build - use app config if in workspace, otherwise legacy config
|
|
72
|
+
const config =
|
|
73
|
+
loadedConfig.type === 'workspace'
|
|
74
|
+
? (await loadAppConfig()).gkmConfig
|
|
75
|
+
: await loadConfig();
|
|
59
76
|
|
|
60
77
|
// Resolve providers from new config format
|
|
61
78
|
const resolved = resolveProviders(config, options);
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { NormalizedAppConfig } from '../../workspace/types';
|
|
3
|
+
import {
|
|
4
|
+
generatePublicUrlBuildArgs,
|
|
5
|
+
getPublicUrlArgNames,
|
|
6
|
+
isMainFrontendApp,
|
|
7
|
+
resolveHost,
|
|
8
|
+
} from '../domain';
|
|
9
|
+
|
|
10
|
+
describe('resolveHost', () => {
|
|
11
|
+
const dokployConfig = {
|
|
12
|
+
endpoint: 'https://dokploy.example.com',
|
|
13
|
+
projectId: 'test-project',
|
|
14
|
+
domains: {
|
|
15
|
+
development: 'dev.myapp.com',
|
|
16
|
+
staging: 'staging.myapp.com',
|
|
17
|
+
production: 'myapp.com',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const createApp = (
|
|
22
|
+
overrides: Partial<NormalizedAppConfig> = {},
|
|
23
|
+
): NormalizedAppConfig => ({
|
|
24
|
+
type: 'backend',
|
|
25
|
+
path: 'apps/api',
|
|
26
|
+
port: 3000,
|
|
27
|
+
dependencies: [],
|
|
28
|
+
resolvedDeployTarget: 'dokploy',
|
|
29
|
+
...overrides,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should return explicit app domain override (string)', () => {
|
|
33
|
+
const app = createApp({ domain: 'api.custom.com' });
|
|
34
|
+
const host = resolveHost('api', app, 'production', dokployConfig, false);
|
|
35
|
+
expect(host).toBe('api.custom.com');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should return stage-specific domain override', () => {
|
|
39
|
+
const app = createApp({
|
|
40
|
+
domain: {
|
|
41
|
+
production: 'login.myapp.com',
|
|
42
|
+
staging: 'login.staging.myapp.com',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
const host = resolveHost('auth', app, 'production', dokployConfig, false);
|
|
46
|
+
expect(host).toBe('login.myapp.com');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should fallback to base domain pattern when no stage match in override', () => {
|
|
50
|
+
const app = createApp({
|
|
51
|
+
domain: { production: 'custom.myapp.com' },
|
|
52
|
+
});
|
|
53
|
+
const host = resolveHost('api', app, 'development', dokployConfig, false);
|
|
54
|
+
expect(host).toBe('api.dev.myapp.com');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return base domain for main frontend app', () => {
|
|
58
|
+
const app = createApp({ type: 'frontend' });
|
|
59
|
+
const host = resolveHost('web', app, 'production', dokployConfig, true);
|
|
60
|
+
expect(host).toBe('myapp.com');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should return prefixed domain for non-main apps', () => {
|
|
64
|
+
const app = createApp();
|
|
65
|
+
const host = resolveHost('api', app, 'production', dokployConfig, false);
|
|
66
|
+
expect(host).toBe('api.myapp.com');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should use correct base domain for each stage', () => {
|
|
70
|
+
const app = createApp();
|
|
71
|
+
|
|
72
|
+
expect(resolveHost('api', app, 'development', dokployConfig, false)).toBe(
|
|
73
|
+
'api.dev.myapp.com',
|
|
74
|
+
);
|
|
75
|
+
expect(resolveHost('api', app, 'staging', dokployConfig, false)).toBe(
|
|
76
|
+
'api.staging.myapp.com',
|
|
77
|
+
);
|
|
78
|
+
expect(resolveHost('api', app, 'production', dokployConfig, false)).toBe(
|
|
79
|
+
'api.myapp.com',
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error when no domain configured for stage', () => {
|
|
84
|
+
const app = createApp();
|
|
85
|
+
expect(() =>
|
|
86
|
+
resolveHost('api', app, 'unknown-stage', dokployConfig, false),
|
|
87
|
+
).toThrow('No domain configured for stage "unknown-stage"');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should throw error when dokployConfig has no domains', () => {
|
|
91
|
+
const app = createApp();
|
|
92
|
+
const configWithoutDomains = {
|
|
93
|
+
endpoint: 'https://dokploy.example.com',
|
|
94
|
+
projectId: 'test-project',
|
|
95
|
+
};
|
|
96
|
+
expect(() =>
|
|
97
|
+
resolveHost('api', app, 'production', configWithoutDomains, false),
|
|
98
|
+
).toThrow('No domain configured for stage "production"');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('isMainFrontendApp', () => {
|
|
103
|
+
const createApp = (
|
|
104
|
+
type: 'backend' | 'frontend',
|
|
105
|
+
): NormalizedAppConfig => ({
|
|
106
|
+
type,
|
|
107
|
+
path: 'apps/test',
|
|
108
|
+
port: 3000,
|
|
109
|
+
dependencies: [],
|
|
110
|
+
resolvedDeployTarget: 'dokploy',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should return false for backend apps', () => {
|
|
114
|
+
const apps = {
|
|
115
|
+
api: createApp('backend'),
|
|
116
|
+
web: createApp('frontend'),
|
|
117
|
+
};
|
|
118
|
+
expect(isMainFrontendApp('api', apps.api, apps)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should return true for app named "web" if it is frontend', () => {
|
|
122
|
+
const apps = {
|
|
123
|
+
api: createApp('backend'),
|
|
124
|
+
web: createApp('frontend'),
|
|
125
|
+
admin: createApp('frontend'),
|
|
126
|
+
};
|
|
127
|
+
expect(isMainFrontendApp('web', apps.web, apps)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return true for first frontend app when no "web" app', () => {
|
|
131
|
+
const apps = {
|
|
132
|
+
api: createApp('backend'),
|
|
133
|
+
dashboard: createApp('frontend'),
|
|
134
|
+
admin: createApp('frontend'),
|
|
135
|
+
};
|
|
136
|
+
expect(isMainFrontendApp('dashboard', apps.dashboard, apps)).toBe(true);
|
|
137
|
+
expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should return false for non-first frontend when no "web" app', () => {
|
|
141
|
+
const apps = {
|
|
142
|
+
api: createApp('backend'),
|
|
143
|
+
dashboard: createApp('frontend'),
|
|
144
|
+
admin: createApp('frontend'),
|
|
145
|
+
};
|
|
146
|
+
expect(isMainFrontendApp('admin', apps.admin, apps)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('generatePublicUrlBuildArgs', () => {
|
|
151
|
+
const createApp = (dependencies: string[]): NormalizedAppConfig => ({
|
|
152
|
+
type: 'frontend',
|
|
153
|
+
path: 'apps/web',
|
|
154
|
+
port: 3001,
|
|
155
|
+
dependencies,
|
|
156
|
+
resolvedDeployTarget: 'dokploy',
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should generate build args for dependencies', () => {
|
|
160
|
+
const app = createApp(['api', 'auth']);
|
|
161
|
+
const deployedUrls = {
|
|
162
|
+
api: 'https://api.myapp.com',
|
|
163
|
+
auth: 'https://auth.myapp.com',
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
|
|
167
|
+
|
|
168
|
+
expect(buildArgs).toEqual([
|
|
169
|
+
'NEXT_PUBLIC_API_URL=https://api.myapp.com',
|
|
170
|
+
'NEXT_PUBLIC_AUTH_URL=https://auth.myapp.com',
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should skip missing dependencies', () => {
|
|
175
|
+
const app = createApp(['api', 'auth', 'missing']);
|
|
176
|
+
const deployedUrls = {
|
|
177
|
+
api: 'https://api.myapp.com',
|
|
178
|
+
// auth and missing are not deployed yet
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
|
|
182
|
+
|
|
183
|
+
expect(buildArgs).toEqual(['NEXT_PUBLIC_API_URL=https://api.myapp.com']);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should return empty array when no dependencies', () => {
|
|
187
|
+
const app = createApp([]);
|
|
188
|
+
const deployedUrls = { api: 'https://api.myapp.com' };
|
|
189
|
+
|
|
190
|
+
const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
|
|
191
|
+
|
|
192
|
+
expect(buildArgs).toEqual([]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle uppercase conversion correctly', () => {
|
|
196
|
+
const app = createApp(['my-api', 'auth-service']);
|
|
197
|
+
const deployedUrls = {
|
|
198
|
+
'my-api': 'https://my-api.myapp.com',
|
|
199
|
+
'auth-service': 'https://auth-service.myapp.com',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const buildArgs = generatePublicUrlBuildArgs(app, deployedUrls);
|
|
203
|
+
|
|
204
|
+
expect(buildArgs).toEqual([
|
|
205
|
+
'NEXT_PUBLIC_MY-API_URL=https://my-api.myapp.com',
|
|
206
|
+
'NEXT_PUBLIC_AUTH-SERVICE_URL=https://auth-service.myapp.com',
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('getPublicUrlArgNames', () => {
|
|
212
|
+
const createApp = (dependencies: string[]): NormalizedAppConfig => ({
|
|
213
|
+
type: 'frontend',
|
|
214
|
+
path: 'apps/web',
|
|
215
|
+
port: 3001,
|
|
216
|
+
dependencies,
|
|
217
|
+
resolvedDeployTarget: 'dokploy',
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should return arg names for dependencies', () => {
|
|
221
|
+
const app = createApp(['api', 'auth']);
|
|
222
|
+
const argNames = getPublicUrlArgNames(app);
|
|
223
|
+
expect(argNames).toEqual(['NEXT_PUBLIC_API_URL', 'NEXT_PUBLIC_AUTH_URL']);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('should return empty array when no dependencies', () => {
|
|
227
|
+
const app = createApp([]);
|
|
228
|
+
const argNames = getPublicUrlArgNames(app);
|
|
229
|
+
expect(argNames).toEqual([]);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { StageSecrets } from '../../secrets/types';
|
|
3
|
+
import {
|
|
4
|
+
encryptSecretsForApp,
|
|
5
|
+
filterSecretsForApp,
|
|
6
|
+
generateSecretsReport,
|
|
7
|
+
prepareSecretsForAllApps,
|
|
8
|
+
prepareSecretsForApp,
|
|
9
|
+
} from '../secrets';
|
|
10
|
+
import type { SniffedEnvironment } from '../sniffer';
|
|
11
|
+
|
|
12
|
+
describe('filterSecretsForApp', () => {
|
|
13
|
+
const createStageSecrets = (
|
|
14
|
+
custom: Record<string, string> = {},
|
|
15
|
+
): StageSecrets => ({
|
|
16
|
+
stage: 'production',
|
|
17
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
18
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
19
|
+
services: {},
|
|
20
|
+
urls: {
|
|
21
|
+
DATABASE_URL: 'postgresql://localhost:5432/db',
|
|
22
|
+
REDIS_URL: 'redis://localhost:6379',
|
|
23
|
+
},
|
|
24
|
+
custom,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should filter secrets to only required env vars', () => {
|
|
28
|
+
const secrets = createStageSecrets({ API_KEY: 'secret123' });
|
|
29
|
+
const sniffed: SniffedEnvironment = {
|
|
30
|
+
appName: 'api',
|
|
31
|
+
requiredEnvVars: ['DATABASE_URL', 'API_KEY'],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const result = filterSecretsForApp(secrets, sniffed);
|
|
35
|
+
|
|
36
|
+
expect(result.appName).toBe('api');
|
|
37
|
+
expect(result.secrets).toEqual({
|
|
38
|
+
DATABASE_URL: 'postgresql://localhost:5432/db',
|
|
39
|
+
API_KEY: 'secret123',
|
|
40
|
+
});
|
|
41
|
+
expect(result.found).toEqual(['API_KEY', 'DATABASE_URL']);
|
|
42
|
+
expect(result.missing).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should track missing secrets', () => {
|
|
46
|
+
const secrets = createStageSecrets();
|
|
47
|
+
const sniffed: SniffedEnvironment = {
|
|
48
|
+
appName: 'api',
|
|
49
|
+
requiredEnvVars: ['DATABASE_URL', 'STRIPE_KEY', 'JWT_SECRET'],
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = filterSecretsForApp(secrets, sniffed);
|
|
53
|
+
|
|
54
|
+
expect(result.found).toEqual(['DATABASE_URL']);
|
|
55
|
+
expect(result.missing).toEqual(['JWT_SECRET', 'STRIPE_KEY']);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should return empty secrets when no env vars required', () => {
|
|
59
|
+
const secrets = createStageSecrets({ API_KEY: 'secret' });
|
|
60
|
+
const sniffed: SniffedEnvironment = {
|
|
61
|
+
appName: 'web',
|
|
62
|
+
requiredEnvVars: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const result = filterSecretsForApp(secrets, sniffed);
|
|
66
|
+
|
|
67
|
+
expect(result.secrets).toEqual({});
|
|
68
|
+
expect(result.found).toEqual([]);
|
|
69
|
+
expect(result.missing).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should include service credentials when referenced', () => {
|
|
73
|
+
const secrets: StageSecrets = {
|
|
74
|
+
stage: 'production',
|
|
75
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
76
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
77
|
+
services: {
|
|
78
|
+
postgres: {
|
|
79
|
+
host: 'localhost',
|
|
80
|
+
port: 5432,
|
|
81
|
+
username: 'user',
|
|
82
|
+
password: 'pass',
|
|
83
|
+
database: 'mydb',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
urls: { DATABASE_URL: 'postgresql://user:pass@localhost:5432/mydb' },
|
|
87
|
+
custom: {},
|
|
88
|
+
};
|
|
89
|
+
const sniffed: SniffedEnvironment = {
|
|
90
|
+
appName: 'api',
|
|
91
|
+
requiredEnvVars: ['DATABASE_URL', 'POSTGRES_PASSWORD'],
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const result = filterSecretsForApp(secrets, sniffed);
|
|
95
|
+
|
|
96
|
+
expect(result.secrets.DATABASE_URL).toBe(
|
|
97
|
+
'postgresql://user:pass@localhost:5432/mydb',
|
|
98
|
+
);
|
|
99
|
+
expect(result.secrets.POSTGRES_PASSWORD).toBe('pass');
|
|
100
|
+
expect(result.found).toContain('DATABASE_URL');
|
|
101
|
+
expect(result.found).toContain('POSTGRES_PASSWORD');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('encryptSecretsForApp', () => {
|
|
106
|
+
it('should encrypt filtered secrets and return master key', () => {
|
|
107
|
+
const filtered = {
|
|
108
|
+
appName: 'api',
|
|
109
|
+
secrets: { DATABASE_URL: 'postgresql://localhost:5432/db' },
|
|
110
|
+
found: ['DATABASE_URL'],
|
|
111
|
+
missing: [],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = encryptSecretsForApp(filtered);
|
|
115
|
+
|
|
116
|
+
expect(result.appName).toBe('api');
|
|
117
|
+
expect(result.masterKey).toHaveLength(64); // 32 bytes hex
|
|
118
|
+
expect(result.payload.encrypted).toBeTruthy();
|
|
119
|
+
expect(result.payload.iv).toBeTruthy();
|
|
120
|
+
expect(result.secretCount).toBe(1);
|
|
121
|
+
expect(result.missingSecrets).toEqual([]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should track missing secrets in result', () => {
|
|
125
|
+
const filtered = {
|
|
126
|
+
appName: 'api',
|
|
127
|
+
secrets: { DATABASE_URL: 'postgresql://localhost:5432/db' },
|
|
128
|
+
found: ['DATABASE_URL'],
|
|
129
|
+
missing: ['STRIPE_KEY', 'JWT_SECRET'],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const result = encryptSecretsForApp(filtered);
|
|
133
|
+
|
|
134
|
+
expect(result.missingSecrets).toEqual(['STRIPE_KEY', 'JWT_SECRET']);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should handle empty secrets', () => {
|
|
138
|
+
const filtered = {
|
|
139
|
+
appName: 'web',
|
|
140
|
+
secrets: {},
|
|
141
|
+
found: [],
|
|
142
|
+
missing: [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const result = encryptSecretsForApp(filtered);
|
|
146
|
+
|
|
147
|
+
expect(result.secretCount).toBe(0);
|
|
148
|
+
expect(result.masterKey).toHaveLength(64);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('prepareSecretsForApp', () => {
|
|
153
|
+
it('should filter and encrypt in one step', () => {
|
|
154
|
+
const secrets: StageSecrets = {
|
|
155
|
+
stage: 'production',
|
|
156
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
157
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
158
|
+
services: {},
|
|
159
|
+
urls: { DATABASE_URL: 'postgresql://localhost:5432/db' },
|
|
160
|
+
custom: { API_KEY: 'key123' },
|
|
161
|
+
};
|
|
162
|
+
const sniffed: SniffedEnvironment = {
|
|
163
|
+
appName: 'api',
|
|
164
|
+
requiredEnvVars: ['DATABASE_URL', 'API_KEY'],
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const result = prepareSecretsForApp(secrets, sniffed);
|
|
168
|
+
|
|
169
|
+
expect(result.appName).toBe('api');
|
|
170
|
+
expect(result.secretCount).toBe(2);
|
|
171
|
+
expect(result.masterKey).toHaveLength(64);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('prepareSecretsForAllApps', () => {
|
|
176
|
+
const secrets: StageSecrets = {
|
|
177
|
+
stage: 'production',
|
|
178
|
+
createdAt: '2024-01-01T00:00:00Z',
|
|
179
|
+
updatedAt: '2024-01-01T00:00:00Z',
|
|
180
|
+
services: {},
|
|
181
|
+
urls: {
|
|
182
|
+
DATABASE_URL: 'postgresql://localhost:5432/db',
|
|
183
|
+
REDIS_URL: 'redis://localhost:6379',
|
|
184
|
+
},
|
|
185
|
+
custom: { BETTER_AUTH_SECRET: 'auth-secret' },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
it('should prepare secrets for multiple apps', () => {
|
|
189
|
+
const sniffedApps = new Map<string, SniffedEnvironment>([
|
|
190
|
+
['api', { appName: 'api', requiredEnvVars: ['DATABASE_URL', 'REDIS_URL'] }],
|
|
191
|
+
[
|
|
192
|
+
'auth',
|
|
193
|
+
{
|
|
194
|
+
appName: 'auth',
|
|
195
|
+
requiredEnvVars: ['DATABASE_URL', 'BETTER_AUTH_SECRET'],
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
const results = prepareSecretsForAllApps(secrets, sniffedApps);
|
|
201
|
+
|
|
202
|
+
expect(results.size).toBe(2);
|
|
203
|
+
expect(results.get('api')?.secretCount).toBe(2);
|
|
204
|
+
expect(results.get('auth')?.secretCount).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should skip apps with no required env vars', () => {
|
|
208
|
+
const sniffedApps = new Map<string, SniffedEnvironment>([
|
|
209
|
+
['api', { appName: 'api', requiredEnvVars: ['DATABASE_URL'] }],
|
|
210
|
+
['web', { appName: 'web', requiredEnvVars: [] }], // Frontend - no secrets
|
|
211
|
+
]);
|
|
212
|
+
|
|
213
|
+
const results = prepareSecretsForAllApps(secrets, sniffedApps);
|
|
214
|
+
|
|
215
|
+
expect(results.size).toBe(1);
|
|
216
|
+
expect(results.has('api')).toBe(true);
|
|
217
|
+
expect(results.has('web')).toBe(false);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should generate unique master keys per app', () => {
|
|
221
|
+
const sniffedApps = new Map<string, SniffedEnvironment>([
|
|
222
|
+
['api', { appName: 'api', requiredEnvVars: ['DATABASE_URL'] }],
|
|
223
|
+
['auth', { appName: 'auth', requiredEnvVars: ['DATABASE_URL'] }],
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
const results = prepareSecretsForAllApps(secrets, sniffedApps);
|
|
227
|
+
|
|
228
|
+
const apiKey = results.get('api')?.masterKey;
|
|
229
|
+
const authKey = results.get('auth')?.masterKey;
|
|
230
|
+
|
|
231
|
+
expect(apiKey).not.toBe(authKey);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('generateSecretsReport', () => {
|
|
236
|
+
it('should generate report for apps with and without secrets', () => {
|
|
237
|
+
const sniffedApps = new Map<string, SniffedEnvironment>([
|
|
238
|
+
['api', { appName: 'api', requiredEnvVars: ['DATABASE_URL'] }],
|
|
239
|
+
['auth', { appName: 'auth', requiredEnvVars: ['DATABASE_URL'] }],
|
|
240
|
+
['web', { appName: 'web', requiredEnvVars: [] }],
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
const encryptedApps = new Map([
|
|
244
|
+
[
|
|
245
|
+
'api',
|
|
246
|
+
{
|
|
247
|
+
appName: 'api',
|
|
248
|
+
payload: { encrypted: '', iv: '', masterKey: '' },
|
|
249
|
+
masterKey: 'key1',
|
|
250
|
+
secretCount: 1,
|
|
251
|
+
missingSecrets: [],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
[
|
|
255
|
+
'auth',
|
|
256
|
+
{
|
|
257
|
+
appName: 'auth',
|
|
258
|
+
payload: { encrypted: '', iv: '', masterKey: '' },
|
|
259
|
+
masterKey: 'key2',
|
|
260
|
+
secretCount: 1,
|
|
261
|
+
missingSecrets: ['MISSING_VAR'],
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const report = generateSecretsReport(encryptedApps, sniffedApps);
|
|
267
|
+
|
|
268
|
+
expect(report.totalApps).toBe(3);
|
|
269
|
+
expect(report.appsWithSecrets).toEqual(['api', 'auth']);
|
|
270
|
+
expect(report.appsWithoutSecrets).toEqual(['web']);
|
|
271
|
+
expect(report.appsWithMissingSecrets).toEqual([
|
|
272
|
+
{ appName: 'auth', missing: ['MISSING_VAR'] },
|
|
273
|
+
]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('should handle all apps having secrets', () => {
|
|
277
|
+
const sniffedApps = new Map<string, SniffedEnvironment>([
|
|
278
|
+
['api', { appName: 'api', requiredEnvVars: ['DATABASE_URL'] }],
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
const encryptedApps = new Map([
|
|
282
|
+
[
|
|
283
|
+
'api',
|
|
284
|
+
{
|
|
285
|
+
appName: 'api',
|
|
286
|
+
payload: { encrypted: '', iv: '', masterKey: '' },
|
|
287
|
+
masterKey: 'key1',
|
|
288
|
+
secretCount: 1,
|
|
289
|
+
missingSecrets: [],
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
]);
|
|
293
|
+
|
|
294
|
+
const report = generateSecretsReport(encryptedApps, sniffedApps);
|
|
295
|
+
|
|
296
|
+
expect(report.appsWithSecrets).toEqual(['api']);
|
|
297
|
+
expect(report.appsWithoutSecrets).toEqual([]);
|
|
298
|
+
expect(report.appsWithMissingSecrets).toEqual([]);
|
|
299
|
+
});
|
|
300
|
+
});
|