kadi-deploy 0.19.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/.env.example +6 -0
- package/.prettierrc +6 -0
- package/README.md +589 -0
- package/agent.json +23 -0
- package/index.js +11 -0
- package/package.json +42 -0
- package/quick-command.txt +92 -0
- package/scripts/preflight.js +458 -0
- package/scripts/preflight.sh +300 -0
- package/src/cli/bid-selector.ts +222 -0
- package/src/cli/colors.ts +216 -0
- package/src/cli/index.ts +11 -0
- package/src/cli/prompts.ts +190 -0
- package/src/cli/spinners.ts +165 -0
- package/src/commands/deploy-local.ts +475 -0
- package/src/commands/deploy.ts +1342 -0
- package/src/commands/down.ts +679 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/lock.ts +571 -0
- package/src/config/agent-loader.ts +177 -0
- package/src/config/index.ts +9 -0
- package/src/display/deployment-info.ts +220 -0
- package/src/display/pricing.ts +137 -0
- package/src/display/resources.ts +234 -0
- package/src/enhanced-registry-manager.ts +892 -0
- package/src/index.ts +307 -0
- package/src/infrastructure/registry.ts +269 -0
- package/src/schemas/profiles.ts +529 -0
- package/src/secrets/broker-urls.ts +109 -0
- package/src/secrets/handshake.ts +407 -0
- package/src/secrets/index.ts +69 -0
- package/src/secrets/inject-env.ts +171 -0
- package/src/secrets/nonce.ts +31 -0
- package/src/secrets/normalize.ts +204 -0
- package/src/secrets/prepare.ts +152 -0
- package/src/secrets/validate.ts +243 -0
- package/src/secrets/vault.ts +80 -0
- package/src/types/akash.ts +116 -0
- package/src/types/container-registry-ability.d.ts +158 -0
- package/src/types/external.ts +49 -0
- package/src/types.ts +211 -0
- package/src/utils/akt-price.ts +74 -0
- package/tests/agent-loader.test.ts +239 -0
- package/tests/autonomous.test.ts +244 -0
- package/tests/down.test.ts +1143 -0
- package/tests/lock.test.ts +1148 -0
- package/tests/nonce.test.ts +34 -0
- package/tests/normalize.test.ts +270 -0
- package/tests/secrets-schema.test.ts +301 -0
- package/tests/types.test.ts +198 -0
- package/tsconfig.json +18 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nonce Generation Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for cryptographic nonce generation used in secrets injection.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { generateNonce } from '../src/secrets/nonce.js';
|
|
9
|
+
|
|
10
|
+
describe('generateNonce', () => {
|
|
11
|
+
it('generates a hex string of default length', () => {
|
|
12
|
+
const nonce = generateNonce();
|
|
13
|
+
expect(nonce).toMatch(/^[0-9a-f]+$/);
|
|
14
|
+
// 32 bytes = 64 hex chars
|
|
15
|
+
expect(nonce).toHaveLength(64);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('generates a hex string of custom length', () => {
|
|
19
|
+
const nonce = generateNonce(16);
|
|
20
|
+
expect(nonce).toMatch(/^[0-9a-f]+$/);
|
|
21
|
+
// 16 bytes = 32 hex chars
|
|
22
|
+
expect(nonce).toHaveLength(32);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('generates unique nonces on each call', () => {
|
|
26
|
+
const nonces = new Set(Array.from({ length: 100 }, () => generateNonce()));
|
|
27
|
+
expect(nonces.size).toBe(100);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('generates a 1-byte nonce', () => {
|
|
31
|
+
const nonce = generateNonce(1);
|
|
32
|
+
expect(nonce).toHaveLength(2);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets Normalization Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies normalizeSecrets() correctly handles legacy single-vault,
|
|
5
|
+
* multi-vault, and edge-case configurations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import {
|
|
10
|
+
normalizeSecrets,
|
|
11
|
+
hasAnySecrets,
|
|
12
|
+
allRequiredKeys,
|
|
13
|
+
allOptionalKeys,
|
|
14
|
+
allSecretKeys,
|
|
15
|
+
buildVaultSourcesEnv,
|
|
16
|
+
type NormalizedSecrets,
|
|
17
|
+
type NormalizedVaultSource,
|
|
18
|
+
type RawSecretsConfig,
|
|
19
|
+
} from '../src/secrets/normalize.js';
|
|
20
|
+
|
|
21
|
+
// ─────────────────────────────────────────────────────────
|
|
22
|
+
// normalizeSecrets
|
|
23
|
+
// ─────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
describe('normalizeSecrets', () => {
|
|
26
|
+
describe('returns null for falsy input', () => {
|
|
27
|
+
it('returns null for undefined', () => {
|
|
28
|
+
expect(normalizeSecrets(undefined)).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns null for null', () => {
|
|
32
|
+
expect(normalizeSecrets(null)).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('legacy single-vault format', () => {
|
|
37
|
+
it('normalizes minimal vault-only config', () => {
|
|
38
|
+
const result = normalizeSecrets({ vault: 'my-app' });
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
sources: [{ vault: 'my-app', required: [], optional: [] }],
|
|
41
|
+
delivery: 'env',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('normalizes with required keys', () => {
|
|
46
|
+
const result = normalizeSecrets({
|
|
47
|
+
vault: 'dev',
|
|
48
|
+
required: ['API_KEY', 'DB_URL'],
|
|
49
|
+
});
|
|
50
|
+
expect(result).toEqual({
|
|
51
|
+
sources: [{ vault: 'dev', required: ['API_KEY', 'DB_URL'], optional: [] }],
|
|
52
|
+
delivery: 'env',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('normalizes with optional keys', () => {
|
|
57
|
+
const result = normalizeSecrets({
|
|
58
|
+
vault: 'dev',
|
|
59
|
+
optional: ['DEBUG_KEY'],
|
|
60
|
+
});
|
|
61
|
+
expect(result).toEqual({
|
|
62
|
+
sources: [{ vault: 'dev', required: [], optional: ['DEBUG_KEY'] }],
|
|
63
|
+
delivery: 'env',
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('normalizes with required, optional, and delivery', () => {
|
|
68
|
+
const result = normalizeSecrets({
|
|
69
|
+
vault: 'production',
|
|
70
|
+
required: ['API_KEY'],
|
|
71
|
+
optional: ['OBS_KEY'],
|
|
72
|
+
delivery: 'broker',
|
|
73
|
+
});
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
sources: [{
|
|
76
|
+
vault: 'production',
|
|
77
|
+
required: ['API_KEY'],
|
|
78
|
+
optional: ['OBS_KEY'],
|
|
79
|
+
}],
|
|
80
|
+
delivery: 'broker',
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('defaults delivery to env', () => {
|
|
85
|
+
const result = normalizeSecrets({ vault: 'test' });
|
|
86
|
+
expect(result!.delivery).toBe('env');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('multi-vault format', () => {
|
|
91
|
+
it('normalizes single-element vaults array', () => {
|
|
92
|
+
const result = normalizeSecrets({
|
|
93
|
+
vaults: [{ vault: 'app', required: ['KEY'] }],
|
|
94
|
+
});
|
|
95
|
+
expect(result).toEqual({
|
|
96
|
+
sources: [{ vault: 'app', required: ['KEY'], optional: [] }],
|
|
97
|
+
delivery: 'env',
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('normalizes multiple vaults', () => {
|
|
102
|
+
const result = normalizeSecrets({
|
|
103
|
+
vaults: [
|
|
104
|
+
{ vault: 'app', required: ['API_KEY'], optional: ['DEBUG'] },
|
|
105
|
+
{ vault: 'infra', required: ['TUNNEL_TOKEN'] },
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
sources: [
|
|
110
|
+
{ vault: 'app', required: ['API_KEY'], optional: ['DEBUG'] },
|
|
111
|
+
{ vault: 'infra', required: ['TUNNEL_TOKEN'], optional: [] },
|
|
112
|
+
],
|
|
113
|
+
delivery: 'env',
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('respects broker delivery mode', () => {
|
|
118
|
+
const result = normalizeSecrets({
|
|
119
|
+
vaults: [
|
|
120
|
+
{ vault: 'v1', required: ['A'] },
|
|
121
|
+
{ vault: 'v2', required: ['B'] },
|
|
122
|
+
],
|
|
123
|
+
delivery: 'broker',
|
|
124
|
+
});
|
|
125
|
+
expect(result!.delivery).toBe('broker');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('defaults optional to empty array', () => {
|
|
129
|
+
const result = normalizeSecrets({
|
|
130
|
+
vaults: [{ vault: 'x', required: ['K'] }],
|
|
131
|
+
});
|
|
132
|
+
expect(result!.sources[0].optional).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('defaults required to empty array', () => {
|
|
136
|
+
const result = normalizeSecrets({
|
|
137
|
+
vaults: [{ vault: 'x', optional: ['K'] }],
|
|
138
|
+
});
|
|
139
|
+
expect(result!.sources[0].required).toEqual([]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ─────────────────────────────────────────────────────────
|
|
145
|
+
// hasAnySecrets
|
|
146
|
+
// ─────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
describe('hasAnySecrets', () => {
|
|
149
|
+
it('returns false for null', () => {
|
|
150
|
+
expect(hasAnySecrets(null)).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns false when all sources have empty keys', () => {
|
|
154
|
+
const normalized: NormalizedSecrets = {
|
|
155
|
+
sources: [
|
|
156
|
+
{ vault: 'a', required: [], optional: [] },
|
|
157
|
+
{ vault: 'b', required: [], optional: [] },
|
|
158
|
+
],
|
|
159
|
+
delivery: 'env',
|
|
160
|
+
};
|
|
161
|
+
expect(hasAnySecrets(normalized)).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('returns true when any source has required keys', () => {
|
|
165
|
+
const normalized: NormalizedSecrets = {
|
|
166
|
+
sources: [
|
|
167
|
+
{ vault: 'a', required: [], optional: [] },
|
|
168
|
+
{ vault: 'b', required: ['KEY'], optional: [] },
|
|
169
|
+
],
|
|
170
|
+
delivery: 'env',
|
|
171
|
+
};
|
|
172
|
+
expect(hasAnySecrets(normalized)).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('returns true when any source has optional keys', () => {
|
|
176
|
+
const normalized: NormalizedSecrets = {
|
|
177
|
+
sources: [
|
|
178
|
+
{ vault: 'a', required: [], optional: ['OPT'] },
|
|
179
|
+
],
|
|
180
|
+
delivery: 'env',
|
|
181
|
+
};
|
|
182
|
+
expect(hasAnySecrets(normalized)).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ─────────────────────────────────────────────────────────
|
|
187
|
+
// allRequiredKeys / allOptionalKeys / allSecretKeys
|
|
188
|
+
// ─────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
describe('key aggregation helpers', () => {
|
|
191
|
+
const normalized: NormalizedSecrets = {
|
|
192
|
+
sources: [
|
|
193
|
+
{ vault: 'app', required: ['A', 'B'], optional: ['X'] },
|
|
194
|
+
{ vault: 'infra', required: ['C'], optional: ['Y', 'Z'] },
|
|
195
|
+
],
|
|
196
|
+
delivery: 'env',
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
it('allRequiredKeys collects from all sources', () => {
|
|
200
|
+
expect(allRequiredKeys(normalized)).toEqual(['A', 'B', 'C']);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('allOptionalKeys collects from all sources', () => {
|
|
204
|
+
expect(allOptionalKeys(normalized)).toEqual(['X', 'Y', 'Z']);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('allSecretKeys returns required + optional', () => {
|
|
208
|
+
expect(allSecretKeys(normalized)).toEqual(['A', 'B', 'C', 'X', 'Y', 'Z']);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('works for single-source configs', () => {
|
|
212
|
+
const single: NormalizedSecrets = {
|
|
213
|
+
sources: [{ vault: 'v', required: ['R'], optional: ['O'] }],
|
|
214
|
+
delivery: 'env',
|
|
215
|
+
};
|
|
216
|
+
expect(allSecretKeys(single)).toEqual(['R', 'O']);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ─────────────────────────────────────────────────────────
|
|
221
|
+
// buildVaultSourcesEnv
|
|
222
|
+
// ─────────────────────────────────────────────────────────
|
|
223
|
+
|
|
224
|
+
describe('buildVaultSourcesEnv', () => {
|
|
225
|
+
it('returns null for single-vault (no KADI_VAULT_SOURCES needed)', () => {
|
|
226
|
+
const sources: NormalizedVaultSource[] = [
|
|
227
|
+
{ vault: 'app', required: ['KEY'], optional: [] },
|
|
228
|
+
];
|
|
229
|
+
expect(buildVaultSourcesEnv(sources)).toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('returns null for empty sources', () => {
|
|
233
|
+
expect(buildVaultSourcesEnv([])).toBeNull();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('returns JSON string for multi-vault', () => {
|
|
237
|
+
const sources: NormalizedVaultSource[] = [
|
|
238
|
+
{ vault: 'app', required: ['API_KEY'], optional: [] },
|
|
239
|
+
{ vault: 'infra', required: ['TOKEN'], optional: ['OBS'] },
|
|
240
|
+
];
|
|
241
|
+
const result = buildVaultSourcesEnv(sources);
|
|
242
|
+
expect(result).not.toBeNull();
|
|
243
|
+
|
|
244
|
+
const parsed = JSON.parse(result!);
|
|
245
|
+
expect(parsed).toEqual([
|
|
246
|
+
{ vault: 'app', keys: ['API_KEY'] },
|
|
247
|
+
{ vault: 'infra', keys: ['TOKEN', 'OBS'] },
|
|
248
|
+
]);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('combines required and optional into keys array', () => {
|
|
252
|
+
const sources: NormalizedVaultSource[] = [
|
|
253
|
+
{ vault: 'a', required: ['R1', 'R2'], optional: ['O1'] },
|
|
254
|
+
{ vault: 'b', required: [], optional: ['O2', 'O3'] },
|
|
255
|
+
];
|
|
256
|
+
const parsed = JSON.parse(buildVaultSourcesEnv(sources)!);
|
|
257
|
+
expect(parsed[0].keys).toEqual(['R1', 'R2', 'O1']);
|
|
258
|
+
expect(parsed[1].keys).toEqual(['O2', 'O3']);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('returns valid JSON that can be round-tripped', () => {
|
|
262
|
+
const sources: NormalizedVaultSource[] = [
|
|
263
|
+
{ vault: 'v1', required: ['A'], optional: [] },
|
|
264
|
+
{ vault: 'v2', required: ['B'], optional: ['C'] },
|
|
265
|
+
];
|
|
266
|
+
const json = buildVaultSourcesEnv(sources)!;
|
|
267
|
+
expect(() => JSON.parse(json)).not.toThrow();
|
|
268
|
+
expect(JSON.stringify(JSON.parse(json))).toBe(json);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Validation Tests for Multi-Vault Secrets
|
|
3
|
+
*
|
|
4
|
+
* Verifies that the SecretsSchema in profiles.ts correctly validates
|
|
5
|
+
* both legacy single-vault and new multi-vault formats, and rejects
|
|
6
|
+
* invalid configurations.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect } from 'vitest';
|
|
10
|
+
import { AkashProfileSchema, LocalProfileSchema, validateProfile } from '../src/schemas/profiles.js';
|
|
11
|
+
|
|
12
|
+
// ─────────────────────────────────────────────────────────
|
|
13
|
+
// Helpers
|
|
14
|
+
// ─────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
/** Minimal valid Akash profile (no secrets) */
|
|
17
|
+
function akashBase(secrets?: unknown) {
|
|
18
|
+
return {
|
|
19
|
+
target: 'akash' as const,
|
|
20
|
+
network: 'testnet' as const,
|
|
21
|
+
services: {
|
|
22
|
+
app: {
|
|
23
|
+
image: 'nginx:alpine',
|
|
24
|
+
expose: [{ port: 80, as: 80, to: [{ global: true }] }],
|
|
25
|
+
resources: { cpu: 0.5, memory: '512Mi' },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
...(secrets !== undefined ? { secrets } : {}),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Minimal valid Local profile (no secrets) */
|
|
33
|
+
function localBase(secrets?: unknown) {
|
|
34
|
+
return {
|
|
35
|
+
target: 'local' as const,
|
|
36
|
+
engine: 'docker' as const,
|
|
37
|
+
services: {
|
|
38
|
+
app: {
|
|
39
|
+
image: 'nginx:alpine',
|
|
40
|
+
expose: [{ port: 80, as: 80 }],
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
...(secrets !== undefined ? { secrets } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─────────────────────────────────────────────────────────
|
|
48
|
+
// Legacy single-vault format
|
|
49
|
+
// ─────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
describe('SecretsSchema — legacy single-vault', () => {
|
|
52
|
+
it('accepts vault with required keys', () => {
|
|
53
|
+
const result = AkashProfileSchema.safeParse(
|
|
54
|
+
akashBase({ vault: 'my-app', required: ['API_KEY'] })
|
|
55
|
+
);
|
|
56
|
+
expect(result.success).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('accepts vault with optional keys', () => {
|
|
60
|
+
const result = AkashProfileSchema.safeParse(
|
|
61
|
+
akashBase({ vault: 'my-app', optional: ['DEBUG_KEY'] })
|
|
62
|
+
);
|
|
63
|
+
expect(result.success).toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts vault with both required and optional', () => {
|
|
67
|
+
const result = AkashProfileSchema.safeParse(
|
|
68
|
+
akashBase({
|
|
69
|
+
vault: 'my-app',
|
|
70
|
+
required: ['API_KEY'],
|
|
71
|
+
optional: ['DEBUG_KEY'],
|
|
72
|
+
delivery: 'broker',
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
expect(result.success).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('accepts vault-only (no keys)', () => {
|
|
79
|
+
const result = AkashProfileSchema.safeParse(
|
|
80
|
+
akashBase({ vault: 'dev' })
|
|
81
|
+
);
|
|
82
|
+
expect(result.success).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('accepts env delivery mode', () => {
|
|
86
|
+
const result = AkashProfileSchema.safeParse(
|
|
87
|
+
akashBase({ vault: 'dev', delivery: 'env' })
|
|
88
|
+
);
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('accepts broker delivery mode', () => {
|
|
93
|
+
const result = AkashProfileSchema.safeParse(
|
|
94
|
+
akashBase({ vault: 'dev', delivery: 'broker' })
|
|
95
|
+
);
|
|
96
|
+
expect(result.success).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('rejects empty vault name', () => {
|
|
100
|
+
const result = AkashProfileSchema.safeParse(
|
|
101
|
+
akashBase({ vault: '' })
|
|
102
|
+
);
|
|
103
|
+
expect(result.success).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('rejects vault name with spaces', () => {
|
|
107
|
+
const result = AkashProfileSchema.safeParse(
|
|
108
|
+
akashBase({ vault: 'my app' })
|
|
109
|
+
);
|
|
110
|
+
expect(result.success).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('rejects invalid delivery mode', () => {
|
|
114
|
+
const result = AkashProfileSchema.safeParse(
|
|
115
|
+
akashBase({ vault: 'dev', delivery: 'pigeon' })
|
|
116
|
+
);
|
|
117
|
+
expect(result.success).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ─────────────────────────────────────────────────────────
|
|
122
|
+
// New multi-vault format
|
|
123
|
+
// ─────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
describe('SecretsSchema — multi-vault', () => {
|
|
126
|
+
it('accepts single-element vaults array', () => {
|
|
127
|
+
const result = AkashProfileSchema.safeParse(
|
|
128
|
+
akashBase({
|
|
129
|
+
vaults: [{ vault: 'app', required: ['KEY'] }],
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
expect(result.success).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('accepts multiple vaults', () => {
|
|
136
|
+
const result = AkashProfileSchema.safeParse(
|
|
137
|
+
akashBase({
|
|
138
|
+
vaults: [
|
|
139
|
+
{ vault: 'app', required: ['API_KEY'] },
|
|
140
|
+
{ vault: 'infra', required: ['TUNNEL_TOKEN'], optional: ['OBS_KEY'] },
|
|
141
|
+
],
|
|
142
|
+
})
|
|
143
|
+
);
|
|
144
|
+
expect(result.success).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('accepts multi-vault with broker delivery', () => {
|
|
148
|
+
const result = AkashProfileSchema.safeParse(
|
|
149
|
+
akashBase({
|
|
150
|
+
vaults: [
|
|
151
|
+
{ vault: 'v1', required: ['A'] },
|
|
152
|
+
{ vault: 'v2', required: ['B'] },
|
|
153
|
+
],
|
|
154
|
+
delivery: 'broker',
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('accepts vault with only optional keys', () => {
|
|
161
|
+
const result = AkashProfileSchema.safeParse(
|
|
162
|
+
akashBase({
|
|
163
|
+
vaults: [{ vault: 'debug', optional: ['LOG_LEVEL'] }],
|
|
164
|
+
})
|
|
165
|
+
);
|
|
166
|
+
expect(result.success).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('rejects empty vaults array', () => {
|
|
170
|
+
const result = AkashProfileSchema.safeParse(
|
|
171
|
+
akashBase({ vaults: [] })
|
|
172
|
+
);
|
|
173
|
+
expect(result.success).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('rejects vault entry with empty vault name', () => {
|
|
177
|
+
const result = AkashProfileSchema.safeParse(
|
|
178
|
+
akashBase({
|
|
179
|
+
vaults: [{ vault: '', required: ['KEY'] }],
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
expect(result.success).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('rejects vault entry with invalid vault name', () => {
|
|
186
|
+
const result = AkashProfileSchema.safeParse(
|
|
187
|
+
akashBase({
|
|
188
|
+
vaults: [{ vault: 'has space', required: ['KEY'] }],
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
expect(result.success).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('rejects invalid delivery mode in multi-vault', () => {
|
|
195
|
+
const result = AkashProfileSchema.safeParse(
|
|
196
|
+
akashBase({
|
|
197
|
+
vaults: [{ vault: 'app', required: ['KEY'] }],
|
|
198
|
+
delivery: 'carrier-pigeon',
|
|
199
|
+
})
|
|
200
|
+
);
|
|
201
|
+
expect(result.success).toBe(false);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────
|
|
206
|
+
// Profile works with both local and akash
|
|
207
|
+
// ─────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
describe('SecretsSchema — local profiles', () => {
|
|
210
|
+
it('accepts legacy format on local profile', () => {
|
|
211
|
+
const result = LocalProfileSchema.safeParse(
|
|
212
|
+
localBase({ vault: 'dev', required: ['KEY'] })
|
|
213
|
+
);
|
|
214
|
+
expect(result.success).toBe(true);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('accepts multi-vault format on local profile', () => {
|
|
218
|
+
const result = LocalProfileSchema.safeParse(
|
|
219
|
+
localBase({
|
|
220
|
+
vaults: [
|
|
221
|
+
{ vault: 'app', required: ['API_KEY'] },
|
|
222
|
+
{ vault: 'infra', optional: ['TOKEN'] },
|
|
223
|
+
],
|
|
224
|
+
delivery: 'env',
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
expect(result.success).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// ─────────────────────────────────────────────────────────
|
|
232
|
+
// validateProfile integration
|
|
233
|
+
// ─────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
describe('validateProfile — secrets integration', () => {
|
|
236
|
+
it('validates akash profile with multi-vault secrets', () => {
|
|
237
|
+
const profile = validateProfile(
|
|
238
|
+
akashBase({
|
|
239
|
+
vaults: [
|
|
240
|
+
{ vault: 'my-agent', required: ['API_KEY', 'API_URL'] },
|
|
241
|
+
{ vault: 'infra', required: ['TUNNEL_TOKEN'] },
|
|
242
|
+
],
|
|
243
|
+
delivery: 'broker',
|
|
244
|
+
}),
|
|
245
|
+
'production'
|
|
246
|
+
);
|
|
247
|
+
expect(profile.target).toBe('akash');
|
|
248
|
+
expect(profile.secrets).toBeDefined();
|
|
249
|
+
expect((profile.secrets as any).vaults).toHaveLength(2);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('validates akash profile with legacy secrets', () => {
|
|
253
|
+
const profile = validateProfile(
|
|
254
|
+
akashBase({
|
|
255
|
+
vault: 'my-agent',
|
|
256
|
+
required: ['API_KEY'],
|
|
257
|
+
delivery: 'env',
|
|
258
|
+
}),
|
|
259
|
+
'production'
|
|
260
|
+
);
|
|
261
|
+
expect(profile.secrets).toBeDefined();
|
|
262
|
+
expect((profile.secrets as any).vault).toBe('my-agent');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('throws on invalid secrets configuration', () => {
|
|
266
|
+
expect(() =>
|
|
267
|
+
validateProfile(
|
|
268
|
+
akashBase({ vaults: [] }),
|
|
269
|
+
'bad-profile'
|
|
270
|
+
)
|
|
271
|
+
).toThrow(/validation failed/i);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('validates profile without secrets', () => {
|
|
275
|
+
const profile = validateProfile(akashBase(), 'no-secrets');
|
|
276
|
+
expect(profile.secrets).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// ─────────────────────────────────────────────────────────
|
|
281
|
+
// Vault name validation
|
|
282
|
+
// ─────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
describe('VaultNameSchema validation', () => {
|
|
285
|
+
const validNames = ['my-app', 'prod_vault', 'v1', 'ABC123', 'under_score-dash'];
|
|
286
|
+
const invalidNames = ['', 'has space', 'has.dot', 'has/slash', 'has@symbol'];
|
|
287
|
+
|
|
288
|
+
it.each(validNames)('accepts valid vault name: %s', (name) => {
|
|
289
|
+
const result = AkashProfileSchema.safeParse(
|
|
290
|
+
akashBase({ vault: name })
|
|
291
|
+
);
|
|
292
|
+
expect(result.success).toBe(true);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it.each(invalidNames)('rejects invalid vault name: %s', (name) => {
|
|
296
|
+
const result = AkashProfileSchema.safeParse(
|
|
297
|
+
akashBase({ vault: name })
|
|
298
|
+
);
|
|
299
|
+
expect(result.success).toBe(false);
|
|
300
|
+
});
|
|
301
|
+
});
|