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.
Files changed (52) hide show
  1. package/.env.example +6 -0
  2. package/.prettierrc +6 -0
  3. package/README.md +589 -0
  4. package/agent.json +23 -0
  5. package/index.js +11 -0
  6. package/package.json +42 -0
  7. package/quick-command.txt +92 -0
  8. package/scripts/preflight.js +458 -0
  9. package/scripts/preflight.sh +300 -0
  10. package/src/cli/bid-selector.ts +222 -0
  11. package/src/cli/colors.ts +216 -0
  12. package/src/cli/index.ts +11 -0
  13. package/src/cli/prompts.ts +190 -0
  14. package/src/cli/spinners.ts +165 -0
  15. package/src/commands/deploy-local.ts +475 -0
  16. package/src/commands/deploy.ts +1342 -0
  17. package/src/commands/down.ts +679 -0
  18. package/src/commands/index.ts +10 -0
  19. package/src/commands/lock.ts +571 -0
  20. package/src/config/agent-loader.ts +177 -0
  21. package/src/config/index.ts +9 -0
  22. package/src/display/deployment-info.ts +220 -0
  23. package/src/display/pricing.ts +137 -0
  24. package/src/display/resources.ts +234 -0
  25. package/src/enhanced-registry-manager.ts +892 -0
  26. package/src/index.ts +307 -0
  27. package/src/infrastructure/registry.ts +269 -0
  28. package/src/schemas/profiles.ts +529 -0
  29. package/src/secrets/broker-urls.ts +109 -0
  30. package/src/secrets/handshake.ts +407 -0
  31. package/src/secrets/index.ts +69 -0
  32. package/src/secrets/inject-env.ts +171 -0
  33. package/src/secrets/nonce.ts +31 -0
  34. package/src/secrets/normalize.ts +204 -0
  35. package/src/secrets/prepare.ts +152 -0
  36. package/src/secrets/validate.ts +243 -0
  37. package/src/secrets/vault.ts +80 -0
  38. package/src/types/akash.ts +116 -0
  39. package/src/types/container-registry-ability.d.ts +158 -0
  40. package/src/types/external.ts +49 -0
  41. package/src/types.ts +211 -0
  42. package/src/utils/akt-price.ts +74 -0
  43. package/tests/agent-loader.test.ts +239 -0
  44. package/tests/autonomous.test.ts +244 -0
  45. package/tests/down.test.ts +1143 -0
  46. package/tests/lock.test.ts +1148 -0
  47. package/tests/nonce.test.ts +34 -0
  48. package/tests/normalize.test.ts +270 -0
  49. package/tests/secrets-schema.test.ts +301 -0
  50. package/tests/types.test.ts +198 -0
  51. package/tsconfig.json +18 -0
  52. 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
+ });