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,407 @@
1
+ /**
2
+ * Secret Handshake for Broker Delivery Mode
3
+ *
4
+ * After deployment, the deployer stays connected to the broker and waits
5
+ * for the deployed agent to request secrets. This module handles:
6
+ *
7
+ * 1. Connecting to broker
8
+ * 2. Subscribing to secrets.request channel
9
+ * 3. Verifying the nonce matches what was injected
10
+ * 4. Prompting user for approval (or auto-approving)
11
+ * 5. Encrypting secrets with agent's public key (E2E encryption)
12
+ * 6. Sharing encrypted secrets via broker pub/sub
13
+ *
14
+ * ## Encryption
15
+ *
16
+ * Secrets are encrypted using sealed boxes (X25519 + XSalsa20-Poly1305).
17
+ * The agent's Ed25519 public key is converted to X25519 for encryption.
18
+ * Only the agent can decrypt using its private key.
19
+ *
20
+ * @module secrets/handshake
21
+ */
22
+
23
+ import { KadiClient, convertToEncryptionKey, type BrokerEvent } from '@kadi.build/core';
24
+ import nacl from 'tweetnacl';
25
+ // @ts-expect-error - tweetnacl-sealedbox-js doesn't have type definitions
26
+ import sealedbox from 'tweetnacl-sealedbox-js';
27
+
28
+ /**
29
+ * Secret request from deployed agent
30
+ */
31
+ export interface SecretRequest {
32
+ /** Nonce injected during deployment (for verification) */
33
+ nonce: string;
34
+ /** Agent's unique ID */
35
+ agentId: string;
36
+ /** Agent's public key (for future E2E encryption) */
37
+ publicKey?: string;
38
+ /** List of required secrets */
39
+ required: string[];
40
+ /** List of optional secrets */
41
+ optional?: string[];
42
+ }
43
+
44
+ /**
45
+ * Options for waiting for secret requests
46
+ */
47
+ export interface WaitForSecretsOptions {
48
+ /** Broker URL to connect to */
49
+ brokerUrl: string;
50
+ /** Expected nonce (must match request) */
51
+ expectedNonce: string;
52
+ /** Timeout in milliseconds (default: 5 minutes) */
53
+ timeout?: number;
54
+ /** Logger for status updates */
55
+ logger?: {
56
+ log: (msg: string) => void;
57
+ error: (msg: string) => void;
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Result of waiting for secrets
63
+ */
64
+ export interface WaitForSecretsResult {
65
+ success: boolean;
66
+ /** The verified request (if successful) */
67
+ request?: SecretRequest;
68
+ /** Error message (if failed) */
69
+ error?: string;
70
+ /** Whether timeout occurred */
71
+ timedOut?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Options for sharing secrets
76
+ */
77
+ export interface ShareSecretsOptions {
78
+ /** Broker URL */
79
+ brokerUrl: string;
80
+ /** Agent ID to share with */
81
+ agentId: string;
82
+ /** Agent's Ed25519 public key (base64 SPKI DER format) for encryption */
83
+ agentPublicKey: string;
84
+ /** Secrets to share (key-value pairs) */
85
+ secrets: Record<string, string>;
86
+ /** Logger for status updates */
87
+ logger?: {
88
+ log: (msg: string) => void;
89
+ error: (msg: string) => void;
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Wait for deployed agent to request secrets
95
+ *
96
+ * Connects to broker, subscribes to secrets.request, and waits for
97
+ * a request with matching nonce.
98
+ *
99
+ * @param options - Configuration options
100
+ * @returns Result with verified request or error
101
+ */
102
+ export async function waitForSecretRequest(
103
+ options: WaitForSecretsOptions
104
+ ): Promise<WaitForSecretsResult> {
105
+ const {
106
+ brokerUrl,
107
+ expectedNonce,
108
+ timeout = 5 * 60 * 1000, // 5 minutes default
109
+ logger = { log: console.log, error: console.error },
110
+ } = options;
111
+
112
+ // Create a minimal KadiClient for broker connection
113
+ const client = new KadiClient({
114
+ name: 'kadi-deploy-handshake',
115
+ version: '1.0.0',
116
+ brokers: {
117
+ default: { url: brokerUrl },
118
+ },
119
+ });
120
+
121
+ return new Promise(async (resolve) => {
122
+ let resolved = false;
123
+ let timeoutHandle: NodeJS.Timeout | undefined;
124
+
125
+ // Cleanup function
126
+ const cleanup = async () => {
127
+ if (timeoutHandle) {
128
+ clearTimeout(timeoutHandle);
129
+ }
130
+ try {
131
+ await client.disconnect();
132
+ } catch {
133
+ // Ignore disconnect errors
134
+ }
135
+ };
136
+
137
+ // Set timeout
138
+ timeoutHandle = setTimeout(async () => {
139
+ if (!resolved) {
140
+ resolved = true;
141
+ await cleanup();
142
+ resolve({
143
+ success: false,
144
+ timedOut: true,
145
+ error: `Timeout waiting for agent to request secrets (${timeout / 1000}s)`,
146
+ });
147
+ }
148
+ }, timeout);
149
+
150
+ try {
151
+ // Connect to broker
152
+ logger.log('Connecting to broker...');
153
+ await client.connect();
154
+ logger.log('Connected. Waiting for agent to request secrets...');
155
+
156
+ // Subscribe to secret requests
157
+ await client.subscribe('secrets.request', async (event: BrokerEvent) => {
158
+ if (resolved) return;
159
+
160
+ // The actual request payload is in event.data
161
+ const request = event.data as SecretRequest;
162
+
163
+ // Verify nonce
164
+ if (request.nonce !== expectedNonce) {
165
+ logger.log(`Received request with invalid nonce (ignoring)`);
166
+ return;
167
+ }
168
+
169
+ // Valid request - capture and return it
170
+ // Caller will handle approval and sharing
171
+ resolved = true;
172
+ await cleanup();
173
+ resolve({
174
+ success: true,
175
+ request,
176
+ });
177
+ });
178
+ } catch (err) {
179
+ if (!resolved) {
180
+ resolved = true;
181
+ await cleanup();
182
+ resolve({
183
+ success: false,
184
+ error: `Failed to connect to broker: ${(err as Error).message}`,
185
+ });
186
+ }
187
+ }
188
+ });
189
+ }
190
+
191
+ /**
192
+ * Share secrets with deployed agent via broker
193
+ *
194
+ * Encrypts secrets using the agent's public key (sealed box) and publishes
195
+ * them to the agent's approval channel. Only the agent can decrypt.
196
+ *
197
+ * ## Encryption Flow
198
+ *
199
+ * 1. Convert agent's Ed25519 public key to X25519 (encryption key)
200
+ * 2. Serialize secrets as JSON
201
+ * 3. Encrypt with sealed box (X25519 + XSalsa20-Poly1305)
202
+ * 4. Send base64-encoded encrypted blob via broker
203
+ *
204
+ * @param options - Configuration options
205
+ */
206
+ export async function shareSecrets(options: ShareSecretsOptions): Promise<void> {
207
+ const {
208
+ brokerUrl,
209
+ agentId,
210
+ agentPublicKey,
211
+ secrets,
212
+ logger = { log: console.log, error: console.error },
213
+ } = options;
214
+
215
+ // Step 1: Convert agent's Ed25519 public key to X25519 encryption key
216
+ // This uses kadi-core's convertToEncryptionKey which handles the
217
+ // Ed25519 -> X25519 conversion (both use Curve25519 underneath)
218
+ const encryptionKey = convertToEncryptionKey(agentPublicKey);
219
+
220
+ // Step 2: Serialize secrets as JSON
221
+ const secretsJson = JSON.stringify(secrets);
222
+ const secretsBytes = new TextEncoder().encode(secretsJson);
223
+
224
+ // Step 3: Encrypt with sealed box
225
+ // Sealed box = anonymous encryption where only recipient can decrypt
226
+ // Uses ephemeral keypair internally, so sender cannot decrypt their own message
227
+ const encrypted = sealedbox.seal(secretsBytes, encryptionKey);
228
+
229
+ // Step 4: Encode as base64 for transport
230
+ const encryptedBase64 = Buffer.from(encrypted).toString('base64');
231
+
232
+ // Create a minimal KadiClient for broker connection
233
+ const client = new KadiClient({
234
+ name: 'kadi-deploy-share',
235
+ version: '1.0.0',
236
+ brokers: {
237
+ default: { url: brokerUrl },
238
+ },
239
+ });
240
+
241
+ try {
242
+ // Connect to broker
243
+ await client.connect();
244
+
245
+ // Publish encrypted secrets to agent's response channel
246
+ // Channel format: secrets.response.{agentId}
247
+ const channel = `secrets.response.${agentId}`;
248
+
249
+ await client.publish(channel, {
250
+ status: 'approved',
251
+ encrypted: encryptedBase64,
252
+ sharedAt: new Date().toISOString(),
253
+ });
254
+
255
+ logger.log('Encrypted secrets shared successfully');
256
+ } finally {
257
+ try {
258
+ await client.disconnect();
259
+ } catch {
260
+ // Ignore disconnect errors
261
+ }
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Options for sending rejection
267
+ */
268
+ export interface SendRejectionOptions {
269
+ /** Broker URL */
270
+ brokerUrl: string;
271
+ /** Agent ID to notify */
272
+ agentId: string;
273
+ /** Reason for rejection */
274
+ reason: string;
275
+ /** Logger for status updates */
276
+ logger?: {
277
+ log: (msg: string) => void;
278
+ error: (msg: string) => void;
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Send rejection to deployed agent via broker
284
+ *
285
+ * Notifies the agent that secrets will not be shared, allowing it to
286
+ * fail immediately instead of waiting for timeout.
287
+ *
288
+ * @param options - Configuration options
289
+ */
290
+ export async function sendRejection(options: SendRejectionOptions): Promise<void> {
291
+ const {
292
+ brokerUrl,
293
+ agentId,
294
+ reason,
295
+ logger = { log: console.log, error: console.error },
296
+ } = options;
297
+
298
+ // Create a minimal KadiClient for broker connection
299
+ const client = new KadiClient({
300
+ name: 'kadi-deploy-reject',
301
+ version: '1.0.0',
302
+ brokers: {
303
+ default: { url: brokerUrl },
304
+ },
305
+ });
306
+
307
+ try {
308
+ // Connect to broker
309
+ await client.connect();
310
+
311
+ // Publish rejection to agent's response channel
312
+ const channel = `secrets.response.${agentId}`;
313
+
314
+ await client.publish(channel, {
315
+ status: 'rejected',
316
+ reason,
317
+ rejectedAt: new Date().toISOString(),
318
+ });
319
+
320
+ logger.log('Rejection sent to agent');
321
+ } finally {
322
+ try {
323
+ await client.disconnect();
324
+ } catch {
325
+ // Ignore disconnect errors
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Complete secret handshake flow
332
+ *
333
+ * Convenience function that combines waiting and sharing.
334
+ *
335
+ * @param options - Configuration options
336
+ * @param getSecrets - Function to retrieve secret values by name
337
+ */
338
+ export async function performSecretHandshake(
339
+ options: WaitForSecretsOptions & {
340
+ /** Function to get secret value by name */
341
+ getSecret: (name: string) => Promise<string | null>;
342
+ }
343
+ ): Promise<{ success: boolean; error?: string }> {
344
+ const { getSecret, ...waitOptions } = options;
345
+ const logger = options.logger || { log: console.log, error: console.error };
346
+
347
+ // Step 1: Wait for agent to request secrets
348
+ const waitResult = await waitForSecretRequest(waitOptions);
349
+
350
+ if (!waitResult.success || !waitResult.request) {
351
+ return {
352
+ success: false,
353
+ error: waitResult.error,
354
+ };
355
+ }
356
+
357
+ const request = waitResult.request;
358
+
359
+ // Step 2: Retrieve secrets from local vault
360
+ const secrets: Record<string, string> = {};
361
+ const missingSecrets: string[] = [];
362
+
363
+ // Get required secrets
364
+ for (const name of request.required) {
365
+ const value = await getSecret(name);
366
+ if (value !== null) {
367
+ secrets[name] = value;
368
+ } else {
369
+ missingSecrets.push(name);
370
+ }
371
+ }
372
+
373
+ // Get optional secrets (no error if missing)
374
+ for (const name of request.optional || []) {
375
+ const value = await getSecret(name);
376
+ if (value !== null) {
377
+ secrets[name] = value;
378
+ }
379
+ }
380
+
381
+ // Check for missing required secrets
382
+ if (missingSecrets.length > 0) {
383
+ return {
384
+ success: false,
385
+ error: `Missing required secrets: ${missingSecrets.join(', ')}`,
386
+ };
387
+ }
388
+
389
+ // Step 3: Verify agent provided public key for encryption
390
+ if (!request.publicKey) {
391
+ return {
392
+ success: false,
393
+ error: 'Agent did not provide public key for encryption',
394
+ };
395
+ }
396
+
397
+ // Step 4: Share encrypted secrets with agent
398
+ await shareSecrets({
399
+ brokerUrl: options.brokerUrl,
400
+ agentId: request.agentId,
401
+ agentPublicKey: request.publicKey,
402
+ secrets,
403
+ logger,
404
+ });
405
+
406
+ return { success: true };
407
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Secrets Module
3
+ *
4
+ * Handles secrets validation and injection for secure secret
5
+ * sharing with deployed agents.
6
+ *
7
+ * @module secrets
8
+ */
9
+
10
+ // Validation (Phase 2.2)
11
+ export {
12
+ validateSecrets,
13
+ validateSecretsOrFail,
14
+ formatMissingSecretsError,
15
+ formatMissingOptionalWarning,
16
+ type SecretsValidationResult,
17
+ } from './validate.js';
18
+
19
+ // Nonce generation (Phase 2.3)
20
+ export { generateNonce } from './nonce.js';
21
+
22
+ // Broker URL resolution (Phase 2.3)
23
+ export {
24
+ resolveBrokerUrls,
25
+ formatMissingBrokerUrlsError,
26
+ type BrokerUrlsResult,
27
+ } from './broker-urls.js';
28
+
29
+ // Secrets injection
30
+ export {
31
+ injectSecretsEnv,
32
+ type SecretsInjectionEnv,
33
+ } from './inject-env.js';
34
+
35
+ // Secret handshake (Phase 2.4)
36
+ export {
37
+ waitForSecretRequest,
38
+ shareSecrets,
39
+ sendRejection,
40
+ performSecretHandshake,
41
+ type SecretRequest,
42
+ type WaitForSecretsOptions,
43
+ type WaitForSecretsResult,
44
+ type ShareSecretsOptions,
45
+ type SendRejectionOptions,
46
+ } from './handshake.js';
47
+
48
+ // Local vault helpers
49
+ export { readSecretFromCli, readSecretsFromCli } from './vault.js';
50
+
51
+ // Secrets preparation (shared logic for deploy commands)
52
+ export {
53
+ prepareSecretsForDeployment,
54
+ type PrepareSecretsOptions,
55
+ type PrepareSecretsResult,
56
+ } from './prepare.js';
57
+
58
+ // Secrets normalization (multi-vault support)
59
+ export {
60
+ normalizeSecrets,
61
+ hasAnySecrets,
62
+ allRequiredKeys,
63
+ allOptionalKeys,
64
+ allSecretKeys,
65
+ buildVaultSourcesEnv,
66
+ type NormalizedVaultSource,
67
+ type NormalizedSecrets,
68
+ type RawSecretsConfig,
69
+ } from './normalize.js';
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Secrets Injection for Deployment
3
+ *
4
+ * Injects KADI environment variables into all services in a deploy profile.
5
+ * Supports two delivery modes:
6
+ *
7
+ * - "env" (default): Secrets injected as plain environment variables.
8
+ * Works with any container. Secrets are visible in SDL/compose.
9
+ *
10
+ * - "broker": E2E encrypted handshake via broker. Requires KADI CLI or SDK
11
+ * in the container. Secrets never appear in SDL/compose.
12
+ *
13
+ * Injected variables (both modes):
14
+ * - KADI_DEPLOY_NONCE: Cryptographic nonce to verify request authenticity
15
+ * - KADI_REQUIRED_SECRETS: Comma-separated list of secrets the agent needs
16
+ * - KADI_SECRET_DELIVERY: Delivery mode ("env" or "broker")
17
+ * - KADI_VAULT_SOURCES: JSON array mapping vaults to keys (only for multi-vault)
18
+ *
19
+ * Additional variables for "broker" mode:
20
+ * - KADI_BROKER_URLS: Comma-separated broker URLs
21
+ * - KADI_RENDEZVOUS_BROKER: The broker URL where secrets will be delivered
22
+ *
23
+ * Additional variables for "env" mode:
24
+ * - The actual secret values (e.g., API_KEY=xxx, DB_URL=xxx)
25
+ *
26
+ * @module secrets/inject-env
27
+ */
28
+
29
+ import type { Profile } from '../schemas/profiles.js';
30
+ import { buildVaultSourcesEnv, type NormalizedVaultSource } from './normalize.js';
31
+
32
+ /**
33
+ * Secret delivery mode
34
+ */
35
+ export type DeliveryMode = 'env' | 'broker';
36
+
37
+ /**
38
+ * Environment variables to inject into deployed containers
39
+ */
40
+ export interface SecretsInjectionEnv {
41
+ /** Comma-separated broker URLs (required for broker mode) */
42
+ brokerUrls?: string;
43
+ /** Cryptographic nonce for verification */
44
+ nonce: string;
45
+ /** Comma-separated required secret names */
46
+ requiredSecrets: string[];
47
+ /** Comma-separated optional secret names */
48
+ optionalSecrets: string[];
49
+ /** Delivery mode */
50
+ delivery: DeliveryMode;
51
+ /** Actual secret values (only for env mode) */
52
+ secrets?: Record<string, string>;
53
+ /** Vault sources for multi-vault routing (optional; omit for legacy single-vault) */
54
+ vaultSources?: NormalizedVaultSource[];
55
+ }
56
+
57
+ /**
58
+ * Normalize service env to array format
59
+ *
60
+ * The profile schema allows env as either:
61
+ * - Array of strings: ["KEY=value", "KEY2=value2"]
62
+ * - Object: { KEY: "value", KEY2: "value2" }
63
+ *
64
+ * This normalizes to array format for consistent handling.
65
+ *
66
+ * @param env - Service env in either format
67
+ * @returns Array of "KEY=value" strings
68
+ */
69
+ function normalizeEnvToArray(env: string[] | Record<string, string> | undefined): string[] {
70
+ if (!env) {
71
+ return [];
72
+ }
73
+
74
+ if (Array.isArray(env)) {
75
+ return [...env];
76
+ }
77
+
78
+ // Convert object to array
79
+ return Object.entries(env).map(([key, value]) => `${key}=${value}`);
80
+ }
81
+
82
+ /**
83
+ * Inject environment variables for secret delivery into a deploy profile
84
+ *
85
+ * Mutates the profile in place, adding KADI_* env vars to all services.
86
+ * Works with both Akash and Local profiles.
87
+ *
88
+ * @param profile - Deployment profile (will be mutated)
89
+ * @param env - Environment variables to inject
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * // Broker mode (E2E encrypted)
94
+ * injectSecretsEnv(profile, {
95
+ * brokerUrls: 'ws://broker:8080/kadi',
96
+ * nonce: generateNonce(),
97
+ * requiredSecrets: ['API_KEY', 'DB_URL'],
98
+ * optionalSecrets: [],
99
+ * delivery: 'broker',
100
+ * });
101
+ *
102
+ * // Env mode (plain env vars)
103
+ * injectSecretsEnv(profile, {
104
+ * nonce: generateNonce(),
105
+ * requiredSecrets: ['API_KEY', 'DB_URL'],
106
+ * optionalSecrets: [],
107
+ * delivery: 'env',
108
+ * secrets: { API_KEY: 'xxx', DB_URL: 'postgres://...' },
109
+ * });
110
+ *
111
+ * // Multi-vault broker mode
112
+ * injectSecretsEnv(profile, {
113
+ * brokerUrls: 'ws://broker:8080/kadi',
114
+ * nonce: generateNonce(),
115
+ * requiredSecrets: ['API_KEY', 'TUNNEL_TOKEN'],
116
+ * optionalSecrets: [],
117
+ * delivery: 'broker',
118
+ * vaultSources: [
119
+ * { vault: 'app', required: ['API_KEY'], optional: [] },
120
+ * { vault: 'infra', required: ['TUNNEL_TOKEN'], optional: [] },
121
+ * ],
122
+ * });
123
+ * ```
124
+ */
125
+ export function injectSecretsEnv(
126
+ profile: Profile,
127
+ env: SecretsInjectionEnv
128
+ ): void {
129
+ const kadiEnvVars: string[] = [
130
+ `KADI_DEPLOY_NONCE=${env.nonce}`,
131
+ `KADI_SECRET_DELIVERY=${env.delivery}`,
132
+ ];
133
+
134
+ // Add secrets list
135
+ const allSecrets = [...env.requiredSecrets, ...env.optionalSecrets];
136
+ if (allSecrets.length > 0) {
137
+ kadiEnvVars.push(`KADI_REQUIRED_SECRETS=${allSecrets.join(',')}`);
138
+ }
139
+
140
+ // Multi-vault: inject KADI_VAULT_SOURCES so the receiver knows which vault each key belongs to
141
+ if (env.vaultSources) {
142
+ const vaultSourcesJson = buildVaultSourcesEnv(env.vaultSources);
143
+ if (vaultSourcesJson) {
144
+ kadiEnvVars.push(`KADI_VAULT_SOURCES=${vaultSourcesJson}`);
145
+ }
146
+ }
147
+
148
+ // Mode-specific env vars
149
+ if (env.delivery === 'broker') {
150
+ if (!env.brokerUrls) {
151
+ throw new Error('brokerUrls is required for broker delivery mode');
152
+ }
153
+ const rendezvousBroker = env.brokerUrls.split(',')[0]!;
154
+ kadiEnvVars.push(`KADI_BROKER_URLS=${env.brokerUrls}`);
155
+ kadiEnvVars.push(`KADI_RENDEZVOUS_BROKER=${rendezvousBroker}`);
156
+ } else if (env.delivery === 'env' && env.secrets) {
157
+ // Inject actual secret values
158
+ for (const [key, value] of Object.entries(env.secrets)) {
159
+ kadiEnvVars.push(`${key}=${value}`);
160
+ }
161
+ }
162
+
163
+ // Inject into all services
164
+ for (const serviceName of Object.keys(profile.services)) {
165
+ const service = profile.services[serviceName];
166
+
167
+ // Normalize existing env to array and add KADI vars
168
+ const existingEnv = normalizeEnvToArray(service.env);
169
+ service.env = [...existingEnv, ...kadiEnvVars];
170
+ }
171
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Nonce Generation for Secrets Injection
3
+ *
4
+ * Generates cryptographically secure nonces used to verify that
5
+ * secret requests come from the deployed agent (not an impersonator).
6
+ *
7
+ * @module secrets/nonce
8
+ */
9
+
10
+ import { randomBytes } from 'node:crypto';
11
+
12
+ /**
13
+ * Generate a cryptographically secure nonce
14
+ *
15
+ * The nonce is:
16
+ * 1. Injected into the container via KADI_DEPLOY_NONCE env var
17
+ * 2. Included in the agent's secret request message
18
+ * 3. Verified by the deployer before sharing secrets
19
+ *
20
+ * @param length - Number of random bytes (default: 32 = 256 bits)
21
+ * @returns Hex-encoded nonce string
22
+ *
23
+ * @example
24
+ * ```typescript
25
+ * const nonce = generateNonce();
26
+ * // nonce = "a1b2c3d4e5f6..." (64 hex characters)
27
+ * ```
28
+ */
29
+ export function generateNonce(length: number = 32): string {
30
+ return randomBytes(length).toString('hex');
31
+ }