@simplens/onboard 1.0.8 → 1.0.10

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.
@@ -1,87 +0,0 @@
1
- /**
2
- * Application configuration constants
3
- */
4
-
5
- /**
6
- * Service port mappings
7
- */
8
- export const SERVICE_PORTS = {
9
- API: 3000,
10
- DASHBOARD: 3002,
11
- GRAFANA: 3001,
12
- KAFKA_UI: 8080,
13
- MONGO: 27017,
14
- KAFKA: 9092,
15
- REDIS: 6379,
16
- LOKI: 3100,
17
- } as const;
18
-
19
- /**
20
- * Health check configuration
21
- */
22
- export const HEALTH_CHECK = {
23
- MAX_RETRIES: 30,
24
- RETRY_DELAY_MS: 2000,
25
- TIMEOUT_MS: 60000,
26
- } as const;
27
-
28
- /**
29
- * Docker command timeouts
30
- */
31
- export const DOCKER_TIMEOUTS = {
32
- START_MS: 30000,
33
- STOP_MS: 15000,
34
- BUILD_MS: 300000,
35
- } as const;
36
-
37
- /**
38
- * Critical environment variables that always need user input
39
- */
40
- export const CRITICAL_ENV_KEYS = [
41
- 'NS_API_KEY',
42
- 'MONGO_URI',
43
- 'BROKERS',
44
- 'REDIS_URL',
45
- 'AUTH_SECRET',
46
- 'ADMIN_PASSWORD',
47
- 'CORE_VERSION',
48
- 'DASHBOARD_VERSION',
49
- ];
50
-
51
- /**
52
- * Validation constraints
53
- */
54
- export const VALIDATION = {
55
- MIN_PASSWORD_LENGTH: 8,
56
- MIN_API_KEY_LENGTH: 8,
57
- PORT_MIN: 1,
58
- PORT_MAX: 65535,
59
- } as const;
60
-
61
- /**
62
- * File paths
63
- */
64
- export const FILES = {
65
- DOCKER_COMPOSE_INFRA: 'docker-compose.infra.yaml',
66
- DOCKER_COMPOSE_APP: 'docker-compose.yaml',
67
- ENV_FILE: '.env',
68
- CONFIG_FILE: 'simplens.config.yaml',
69
- ERROR_LOG: 'onboard-error.log',
70
- } as const;
71
-
72
- /**
73
- * URL templates for service access
74
- */
75
- export function getServiceURL(service: keyof typeof SERVICE_PORTS, host: string = 'localhost'): string {
76
- return `http://${host}:${SERVICE_PORTS[service]}`;
77
- }
78
-
79
- /**
80
- * Docker compose file paths
81
- */
82
- export const DOCKER_COMPOSE_COMMANDS = {
83
- UP: ['up', '-d'],
84
- DOWN: ['down'],
85
- LOGS: ['logs', '-f'],
86
- PS: ['ps'],
87
- } as const;
@@ -1 +0,0 @@
1
- export * from './constants.js';
package/src/env-config.ts DELETED
@@ -1,503 +0,0 @@
1
- import { writeFile, appendFile, logInfo, logSuccess, logWarning } from './utils.js';
2
- import { text, password } from '@clack/prompts';
3
- import { handleCancel } from './ui.js';
4
- import path from 'path';
5
- import crypto from 'crypto';
6
- import { CRITICAL_ENV_KEYS } from './config/constants.js';
7
- import type { EnvVariable } from './types/domain.js';
8
-
9
- export const DEFAULT_BASE_PATH = '';
10
- const AUTO_GENERATE_ON_EMPTY_KEYS = ['NS_API_KEY', 'AUTH_SECRET', 'CORE_VERSION', 'DASHBOARD_VERSION'] as const;
11
-
12
- function shouldAutoGenerateOnEmpty(key: string, fullMode: boolean): boolean {
13
- return !fullMode && AUTO_GENERATE_ON_EMPTY_KEYS.includes(key as (typeof AUTO_GENERATE_ON_EMPTY_KEYS)[number]);
14
- }
15
-
16
- function resolveEnvAnswer(key: string, answer: string | symbol, fullMode: boolean): string {
17
- const value = typeof answer === 'string' ? answer : '';
18
- if (!value.trim() && shouldAutoGenerateOnEmpty(key, fullMode)) {
19
- return generateDefaultValue(key);
20
- }
21
- return value;
22
- }
23
-
24
- /**
25
- * Generate a secure random string for credentials
26
- */
27
- export function generateSecureRandom(length: number = 32): string {
28
- return crypto.randomBytes(length).toString('base64').slice(0, length);
29
- }
30
-
31
- /**
32
- * Generate default value for a critical environment variable
33
- */
34
- export function generateDefaultValue(key: string): string {
35
- if (key === 'NS_API_KEY') {
36
- return `ns_${generateSecureRandom(48)}`;
37
- }
38
- if (key === 'AUTH_SECRET') {
39
- return generateSecureRandom(64);
40
- }
41
- if (key === 'ADMIN_PASSWORD') {
42
- return `Admin${generateSecureRandom(16)}`;
43
- }
44
- if (key === 'CORE_VERSION' || key ==='DASHBOARD_VERSION') {
45
- return `latest`;
46
- }
47
- return '';
48
- }
49
-
50
- /**
51
- * Validate BASE_PATH value.
52
- * Accepts:
53
- * - Empty value for root path
54
- * - Slash-prefixed lowercase segments (e.g. /dashboard, /admin/v1)
55
- */
56
- export function validateBasePath(input: string): true | string {
57
- const value = input.trim();
58
-
59
- if (!value) {
60
- return true;
61
- }
62
-
63
- if (!value.startsWith('/')) {
64
- return 'Base path must start with / (example: /dashboard)';
65
- }
66
-
67
- if (value.endsWith('/')) {
68
- return 'Base path must not end with /';
69
- }
70
-
71
- if (!/^\/[a-z0-9-]+(?:\/[a-z0-9-]+)*$/.test(value)) {
72
- return 'Use lowercase letters, numbers, hyphens, and "/" separators only';
73
- }
74
-
75
- return true;
76
- }
77
-
78
- /**
79
- * Normalize BASE_PATH for consistent downstream use.
80
- */
81
- export function normalizeBasePath(input: string): string {
82
- return input.trim();
83
- }
84
-
85
- /**
86
- * Prompt BASE_PATH once at the beginning of onboarding.
87
- */
88
- export async function promptBasePath(defaultValue: string = DEFAULT_BASE_PATH): Promise<string> {
89
- const result = await text({
90
- message: 'BASE_PATH for dashboard (leave empty for root, example: /dashboard):',
91
- placeholder: defaultValue || 'leave empty for root',
92
- defaultValue,
93
- validate: (value: string | undefined) => {
94
- const v = validateBasePath(value ?? '');
95
- return v === true ? undefined : v;
96
- },
97
- withGuide: true,
98
- });
99
-
100
- handleCancel(result);
101
- return normalizeBasePath(result as string);
102
- }
103
-
104
- /**
105
- * Load and parse .env.example from embedded template
106
- */
107
- export async function loadEnvExample(): Promise<EnvVariable[]> {
108
- // Embedded .env template - always available regardless of installation
109
- const envTemplate = `
110
- NODE_ENV=production
111
- # ============================================
112
- # API SERVER
113
- # ============================================
114
- NS_API_KEY=
115
- PORT=3000
116
- MAX_BATCH_REQ_LIMIT=1000
117
-
118
- # ============================================
119
- # DATABASE
120
- # ============================================
121
- MONGO_URI=
122
-
123
- # ============================================
124
- # KAFKA
125
- # ============================================
126
- BROKERS=
127
-
128
- # Kafka Topic Partitions (Core Topics)
129
- DELAYED_PARTITION=1
130
- NOTIFICATION_STATUS_PARTITION=1
131
-
132
- # ============================================
133
- # REDIS
134
- # ============================================
135
- REDIS_URL=
136
-
137
- # ============================================
138
- # PLUGIN SYSTEM
139
- # ============================================
140
- SIMPLENS_CONFIG_PATH=./simplens.config.yaml
141
- PROCESSOR_CHANNEL=all
142
-
143
- # ============================================
144
- # BACKGROUND WORKER
145
- # ============================================
146
- OUTBOX_POLL_INTERVAL_MS=5000
147
- OUTBOX_CLEANUP_INTERVAL_MS=60000
148
- OUTBOX_BATCH_SIZE=100
149
- OUTBOX_RETENTION_MS=300000
150
- OUTBOX_CLAIM_TIMEOUT_MS=30000
151
-
152
- # ============================================
153
- # RETRY & IDEMPOTENCY
154
- # ============================================
155
- IDEMPOTENCY_TTL_SECONDS=86400
156
- MAX_RETRY_COUNT=5
157
- PROCESSING_TTL_SECONDS=120
158
-
159
- # ============================================
160
- # DELAYED NOTIFICATIONS
161
- # ============================================
162
- DELAYED_POLL_INTERVAL_MS=1000
163
- DELAYED_BATCH_SIZE=10
164
- MAX_POLLER_RETRIES=3
165
-
166
- # ============================================
167
- # RECOVERY SERVICE
168
- # ============================================
169
- RECOVERY_POLL_INTERVAL_MS=60000
170
- PROCESSING_STUCK_THRESHOLD_MS=300000
171
- PENDING_STUCK_THRESHOLD_MS=300000
172
- RECOVERY_BATCH_SIZE=50
173
- RECOVERY_CLAIM_TIMEOUT_MS=60000
174
-
175
- # ============================================
176
- # CLEANUP
177
- # ============================================
178
- CLEANUP_RESOLVED_ALERTS_RETENTION_MS=86400000
179
- CLEANUP_PROCESSED_STATUS_OUTBOX_RETENTION_MS=86400000
180
-
181
- # ============================================
182
- # LOGGING
183
- # ============================================
184
- LOKI_URL=
185
- LOG_LEVEL=info
186
- LOG_TO_FILE=true
187
-
188
- # ============================================
189
- # DOCKER IMAGE VERSIONS
190
- # ============================================
191
- CORE_VERSION=latest
192
- DASHBOARD_VERSION=latest
193
-
194
- # ============================================
195
- # ADMIN DASHBOARD
196
- # ============================================
197
- AUTH_SECRET=
198
- ADMIN_USERNAME=admin
199
- ADMIN_PASSWORD=
200
- AUTH_TRUST_HOST=true
201
- API_BASE_URL=http://api:3000
202
- WEBHOOK_HOST=dashboard
203
- WEBHOOK_PORT=3002
204
- BASE_PATH=
205
- DASHBOARD_PORT=3002
206
- `;
207
-
208
- return parseEnvContent(envTemplate);
209
- }
210
-
211
- /**
212
- * Parse .env content into structured format
213
- */
214
- function parseEnvContent(content: string): EnvVariable[] {
215
- const lines = content.split('\n');
216
- const variables: EnvVariable[] = [];
217
- let currentComment = '';
218
-
219
- for (const line of lines) {
220
- const trimmed = line.trim();
221
-
222
- // Capture comments as descriptions
223
- if (trimmed.startsWith('#') && !trimmed.includes('====')) {
224
- currentComment = trimmed.replace(/^#\s*/, '');
225
- continue;
226
- }
227
-
228
- // Skip empty lines and section dividers
229
- if (!trimmed || trimmed.includes('====')) {
230
- currentComment = '';
231
- continue;
232
- }
233
-
234
- // Parse key=value pairs
235
- const match = trimmed.match(/^([A-Z_]+)=(.*)$/);
236
- if (match) {
237
- const [, key, value] = match;
238
- variables.push({
239
- key,
240
- value: value || '',
241
- description: currentComment || undefined,
242
- required: CRITICAL_ENV_KEYS.includes(key) || !value,
243
- });
244
- currentComment = '';
245
- }
246
- }
247
-
248
- return variables;
249
- }
250
-
251
- /**
252
- * Prompts user for environment variable values based on the selected mode.
253
- *
254
- * @param mode - 'default' prompts only for critical vars, 'interactive' prompts for all
255
- * @param infraServices - List of selected infrastructure service IDs
256
- * @param basePath - BASE_PATH value already collected
257
- * @param fullMode - If true, auto-generate critical values without prompting
258
- * @returns Map of environment variable keys to values
259
- */
260
- export async function promptEnvVariables(
261
- mode: 'default' | 'interactive',
262
- infraServices: string[],
263
- basePath: string = DEFAULT_BASE_PATH,
264
- fullMode: boolean = false,
265
- envOverrides: Partial<Record<'CORE_VERSION' | 'DASHBOARD_VERSION', string>> = {}
266
- ): Promise<Map<string, string>> {
267
- logInfo('Configuring environment variables...');
268
-
269
- const envVars = await loadEnvExample();
270
- const result = new Map<string, string>();
271
- const normalizedBasePath = normalizeBasePath(basePath);
272
- const basePathLabel = normalizedBasePath || '(root)';
273
- logInfo(`BASE_PATH selected: ${basePathLabel}`);
274
-
275
- // Auto-fill infra connection URLs based on selected services using Docker service names
276
- const autoInfraUrls: Record<string, string> = {
277
- MONGO_URI: infraServices.includes('mongo')
278
- ? `mongodb://mongo:27017/simplens?replicaSet=rs0`
279
- : '',
280
- BROKERS: infraServices.includes('kafka')
281
- ? 'kafka:9093'
282
- : '',
283
- REDIS_URL: infraServices.includes('redis')
284
- ? 'redis://redis:6379'
285
- : '',
286
- LOKI_URL: infraServices.includes('loki')
287
- ? 'http://loki:3100'
288
- : '',
289
- };
290
-
291
- if (mode === 'default') {
292
- // Use defaults, only prompt for critical values
293
- for (const envVar of envVars) {
294
- if (fullMode && envVar.key in envOverrides) {
295
- const overrideValue = envOverrides[envVar.key as keyof typeof envOverrides];
296
- if (overrideValue) {
297
- result.set(envVar.key, overrideValue);
298
- continue;
299
- }
300
- }
301
-
302
- // Use auto-filled infra URLs
303
- if (autoInfraUrls[envVar.key]) {
304
- result.set(envVar.key, autoInfraUrls[envVar.key]);
305
- continue;
306
- }
307
-
308
- // BASE_PATH is collected upfront in onboarding flow
309
- if (envVar.key === 'BASE_PATH') {
310
- result.set(envVar.key, normalizedBasePath);
311
- continue;
312
- }
313
-
314
- // Use default value if available
315
- if (envVar.value && !CRITICAL_ENV_KEYS.includes(envVar.key)) {
316
- result.set(envVar.key, envVar.value);
317
- continue;
318
- }
319
-
320
- // Prompt for critical values (only if not auto-filled)
321
- if (CRITICAL_ENV_KEYS.includes(envVar.key)) {
322
- if (fullMode) {
323
- // In full mode, auto-generate critical values
324
- const defaultValue = generateDefaultValue(envVar.key);
325
- if (defaultValue) {
326
- result.set(envVar.key, defaultValue);
327
- }
328
- } else {
329
- // Show changelog info for version variables
330
- if (envVar.key === 'CORE_VERSION' || envVar.key === 'DASHBOARD_VERSION') {
331
- logInfo('ℹ️ Visit https://simplens.in/changelog for version information');
332
- }
333
-
334
- const promptMessage = `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`;
335
- const isPasswordField = envVar.key.includes('PASSWORD');
336
-
337
- let answer: string | symbol;
338
- if (isPasswordField) {
339
- answer = await password({
340
- message: promptMessage,
341
- validate: (input: string | undefined) => {
342
- if (shouldAutoGenerateOnEmpty(envVar.key, fullMode) && !input) {
343
- return undefined;
344
- }
345
- if (!input && envVar.required) {
346
- return `${envVar.key} is required`;
347
- }
348
- return undefined;
349
- },
350
- });
351
- } else {
352
- answer = await text({
353
- message: promptMessage,
354
- placeholder: getSuggestedValue(envVar.key) || undefined,
355
- defaultValue: generateDefaultValue(envVar.key) || undefined,
356
- validate: (input: string | undefined) => {
357
- if (shouldAutoGenerateOnEmpty(envVar.key, fullMode) && !input) {
358
- return undefined;
359
- }
360
- if (!input && envVar.required) {
361
- return `${envVar.key} is required`;
362
- }
363
- return undefined;
364
- },
365
- });
366
- }
367
-
368
- handleCancel(answer);
369
- result.set(envVar.key, resolveEnvAnswer(envVar.key, answer, fullMode));
370
- }
371
- }
372
- }
373
- } else {
374
- // Interactive mode: prompt for everything
375
- logInfo('Interactive mode: You will be prompted for each environment variable.');
376
-
377
- for (const envVar of envVars) {
378
- if (fullMode && envVar.key in envOverrides) {
379
- const overrideValue = envOverrides[envVar.key as keyof typeof envOverrides];
380
- if (overrideValue) {
381
- result.set(envVar.key, overrideValue);
382
- continue;
383
- }
384
- }
385
-
386
- const defaultValue = autoInfraUrls[envVar.key] || envVar.value || getSuggestedValue(envVar.key);
387
-
388
- // BASE_PATH is collected upfront in onboarding flow
389
- if (envVar.key === 'BASE_PATH') {
390
- result.set(envVar.key, normalizedBasePath);
391
- continue;
392
- }
393
-
394
- // Show changelog info for version variables
395
- if (envVar.key === 'CORE_VERSION' || envVar.key === 'DASHBOARD_VERSION') {
396
- logInfo('ℹ️ Visit https://simplens.in/changelog for version information');
397
- }
398
-
399
- const promptMessage = `${envVar.key}${envVar.description ? ` (${envVar.description})` : ''}:`;
400
- const isPasswordField = envVar.key.includes('PASSWORD');
401
-
402
- let answer: string | symbol;
403
- if (isPasswordField) {
404
- answer = await password({
405
- message: promptMessage,
406
- validate: (input: string | undefined) => {
407
- if (shouldAutoGenerateOnEmpty(envVar.key, fullMode) && !input) {
408
- return undefined;
409
- }
410
- if (!input && envVar.required) {
411
- return `${envVar.key} is required`;
412
- }
413
- return undefined;
414
- },
415
- });
416
- } else {
417
- answer = await text({
418
- message: promptMessage,
419
- placeholder: defaultValue || undefined,
420
- defaultValue: defaultValue || undefined,
421
- validate: (input: string | undefined) => {
422
- if (shouldAutoGenerateOnEmpty(envVar.key, fullMode) && !input) {
423
- return undefined;
424
- }
425
- if (!input && envVar.required) {
426
- return `${envVar.key} is required`;
427
- }
428
- return undefined;
429
- },
430
- });
431
- }
432
-
433
- handleCancel(answer);
434
- result.set(envVar.key, resolveEnvAnswer(envVar.key, answer, fullMode));
435
- }
436
- }
437
-
438
- logSuccess('Environment variables configured');
439
- return result;
440
- }
441
-
442
- /**
443
- * Get suggested value for specific keys
444
- */
445
- function getSuggestedValue(key: string): string {
446
- if (key === 'NS_API_KEY' || key === 'AUTH_SECRET') {
447
- return `Replace with: openssl rand -base64 32 (or) Press <Enter> to auto-generate value`;
448
- }
449
- if (key === 'NODE_ENV') {
450
- return 'production';
451
- }
452
- if (key === 'ADMIN_USERNAME') {
453
- return 'admin';
454
- }
455
- if (key === 'CORE_VERSION' || key === 'DASHBOARD_VERSION') {
456
- return 'latest';
457
- }
458
- return '';
459
- }
460
-
461
- /**
462
- * Generate .env file from variables
463
- */
464
- export async function generateEnvFile(
465
- targetDir: string,
466
- envVars: Map<string, string>
467
- ): Promise<void> {
468
- const envPath = path.join(targetDir, '.env');
469
-
470
- let content = '# SimpleNS Environment Configuration\n';
471
- content += '# Generated by @simplens/onboard\n\n';
472
-
473
- for (const [key, value] of envVars.entries()) {
474
- content += `${key}=${value}\n`;
475
- }
476
-
477
- await writeFile(envPath, content);
478
- logSuccess('Generated .env file');
479
- }
480
-
481
- /**
482
- * Append plugin credentials to .env file
483
- */
484
- export async function appendPluginEnv(
485
- targetDir: string,
486
- pluginEnvVars: Map<string, string>
487
- ): Promise<void> {
488
- // Only append if there are actually credentials to add
489
- if (pluginEnvVars.size === 0) {
490
- logInfo('No plugin credentials to add');
491
- return;
492
- }
493
-
494
- const envPath = path.join(targetDir, '.env');
495
-
496
- let content = '\n# Plugin Credentials\n';
497
- for (const [key, value] of pluginEnvVars.entries()) {
498
- content += `${key}=${value}\n`;
499
- }
500
-
501
- await appendFile(envPath, content);
502
- logSuccess('Added plugin credentials to .env');
503
- }