newo 1.5.0 → 1.5.2
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 +17 -6
- package/CHANGELOG.md +91 -0
- package/README.md +502 -105
- package/dist/akb.d.ts +1 -1
- package/dist/akb.js +21 -17
- package/dist/api.d.ts +3 -2
- package/dist/api.js +24 -21
- package/dist/auth.d.ts +5 -5
- package/dist/auth.js +332 -75
- package/dist/cli.js +225 -29
- package/dist/customer.d.ts +23 -0
- package/dist/customer.js +87 -0
- package/dist/customerAsync.d.ts +22 -0
- package/dist/customerAsync.js +67 -0
- package/dist/customerInit.d.ts +10 -0
- package/dist/customerInit.js +78 -0
- package/dist/env.d.ts +33 -0
- package/dist/env.js +82 -0
- package/dist/fsutil.d.ts +14 -6
- package/dist/fsutil.js +35 -12
- package/dist/hash.d.ts +2 -2
- package/dist/hash.js +31 -8
- package/dist/sync.d.ts +5 -5
- package/dist/sync.js +91 -52
- package/dist/types.d.ts +76 -53
- package/package.json +16 -9
- package/src/akb.ts +23 -18
- package/src/api.ts +27 -24
- package/src/auth.ts +367 -94
- package/src/cli.ts +234 -33
- package/src/customer.ts +102 -0
- package/src/customerAsync.ts +78 -0
- package/src/customerInit.ts +97 -0
- package/src/env.ts +118 -0
- package/src/fsutil.ts +43 -11
- package/src/hash.ts +29 -8
- package/src/sync.ts +105 -54
- package/src/types.ts +82 -54
package/src/cli.ts
CHANGED
|
@@ -4,56 +4,239 @@ import dotenv from 'dotenv';
|
|
|
4
4
|
import { makeClient, getProjectMeta, importAkbArticle } from './api.js';
|
|
5
5
|
import { pullAll, pushChanged, status } from './sync.js';
|
|
6
6
|
import { parseAkbFile, prepareArticlesForImport } from './akb.js';
|
|
7
|
+
import { initializeEnvironment, ENV, EnvValidationError } from './env.js';
|
|
8
|
+
import { parseCustomerConfigAsync, listCustomers, getCustomer, getDefaultCustomer, validateCustomerConfig } from './customerAsync.js';
|
|
9
|
+
import { getValidAccessToken } from './auth.js';
|
|
7
10
|
import path from 'path';
|
|
8
|
-
import type {
|
|
11
|
+
import type { CliArgs, NewoApiError, CustomerConfig } from './types.js';
|
|
12
|
+
|
|
13
|
+
// Enhanced error logging for CLI
|
|
14
|
+
function logCliError(level: 'error' | 'warn' | 'info', message: string, meta?: Record<string, unknown>): void {
|
|
15
|
+
const timestamp = new Date().toISOString();
|
|
16
|
+
const logEntry = {
|
|
17
|
+
timestamp,
|
|
18
|
+
level,
|
|
19
|
+
module: 'cli',
|
|
20
|
+
message,
|
|
21
|
+
...meta
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// Only log JSON format in verbose mode, otherwise use clean user messages
|
|
25
|
+
const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
26
|
+
|
|
27
|
+
if (verbose) {
|
|
28
|
+
if (level === 'error') {
|
|
29
|
+
console.error(JSON.stringify(logEntry));
|
|
30
|
+
} else if (level === 'warn') {
|
|
31
|
+
console.warn(JSON.stringify(logEntry));
|
|
32
|
+
} else {
|
|
33
|
+
console.log(JSON.stringify(logEntry));
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
// Clean user-facing messages
|
|
37
|
+
if (level === 'error') {
|
|
38
|
+
console.error(`❌ ${message}`);
|
|
39
|
+
} else if (level === 'warn') {
|
|
40
|
+
console.warn(`⚠️ ${message}`);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`ℹ️ ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Enhanced error handling with user-friendly messages
|
|
48
|
+
function handleCliError(error: unknown, operation: string = 'operation'): never {
|
|
49
|
+
const verbose = process.argv.includes('--verbose') || process.argv.includes('-v');
|
|
50
|
+
|
|
51
|
+
if (error instanceof Error) {
|
|
52
|
+
// Authentication errors
|
|
53
|
+
if (error.message.includes('API key') || error.message.includes('Authentication failed')) {
|
|
54
|
+
logCliError('error', 'Authentication failed. Please check your API key configuration.');
|
|
55
|
+
if (!verbose) {
|
|
56
|
+
console.error('\n💡 Troubleshooting tips:');
|
|
57
|
+
console.error(' • Verify your API key is correct in .env file');
|
|
58
|
+
console.error(' • For multi-customer setup, check NEWO_CUSTOMER_<IDN>_API_KEY');
|
|
59
|
+
console.error(' • Run with --verbose for detailed error information');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Network errors
|
|
63
|
+
else if (error.message.includes('Network timeout') || error.message.includes('ENOTFOUND') || error.message.includes('ECONNREFUSED')) {
|
|
64
|
+
logCliError('error', 'Network connection failed. Please check your internet connection.');
|
|
65
|
+
if (!verbose) {
|
|
66
|
+
console.error('\n💡 Troubleshooting tips:');
|
|
67
|
+
console.error(' • Check your internet connection');
|
|
68
|
+
console.error(' • Verify NEWO_BASE_URL is correct');
|
|
69
|
+
console.error(' • Try again in a few moments');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Environment configuration errors
|
|
73
|
+
else if (error instanceof EnvValidationError || error.message.includes('not set')) {
|
|
74
|
+
logCliError('error', 'Configuration error. Please check your environment setup.');
|
|
75
|
+
if (!verbose) {
|
|
76
|
+
console.error('\n💡 Setup help:');
|
|
77
|
+
console.error(' • Copy .env.example to .env and configure your settings');
|
|
78
|
+
console.error(' • Run "newo --help" to see configuration examples');
|
|
79
|
+
console.error(' • Check the README for detailed setup instructions');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// File system errors
|
|
83
|
+
else if (error.message.includes('ENOENT') || error.message.includes('EACCES')) {
|
|
84
|
+
logCliError('error', 'File system error. Please check file permissions and paths.');
|
|
85
|
+
}
|
|
86
|
+
// Rate limiting
|
|
87
|
+
else if (error.message.includes('Rate limit exceeded')) {
|
|
88
|
+
logCliError('error', 'Rate limit exceeded. Please wait before trying again.');
|
|
89
|
+
}
|
|
90
|
+
// General API errors
|
|
91
|
+
else if (error.message.includes('response') || error.message.includes('status')) {
|
|
92
|
+
logCliError('error', `API error during ${operation}. Please try again or contact support.`);
|
|
93
|
+
}
|
|
94
|
+
// Unknown errors
|
|
95
|
+
else {
|
|
96
|
+
logCliError('error', `Unexpected error during ${operation}: ${error.message}`);
|
|
97
|
+
if (!verbose) {
|
|
98
|
+
console.error('\n💡 For more details, run the command with --verbose flag');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (verbose) {
|
|
103
|
+
logCliError('error', 'Full error details', {
|
|
104
|
+
operation,
|
|
105
|
+
errorType: error.constructor.name,
|
|
106
|
+
stack: error.stack?.split('\n').slice(0, 5).join('\n') // First 5 lines of stack
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
logCliError('error', `Unknown error during ${operation}: ${String(error)}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
9
115
|
|
|
10
116
|
dotenv.config();
|
|
11
|
-
const { NEWO_PROJECT_ID } = process.env as NewoEnvironment;
|
|
12
117
|
|
|
13
118
|
async function main(): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
// Initialize and validate environment at startup
|
|
121
|
+
initializeEnvironment();
|
|
122
|
+
} catch (error: unknown) {
|
|
123
|
+
if (error instanceof EnvValidationError) {
|
|
124
|
+
console.error('Environment validation failed:', error.message);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
|
|
14
130
|
const args = minimist(process.argv.slice(2)) as CliArgs;
|
|
15
131
|
const cmd = args._[0];
|
|
16
132
|
const verbose = Boolean(args.verbose || args.v);
|
|
133
|
+
|
|
134
|
+
// Parse customer configuration (async for API key array support)
|
|
135
|
+
let customerConfig;
|
|
136
|
+
try {
|
|
137
|
+
customerConfig = await parseCustomerConfigAsync(ENV as any, verbose);
|
|
138
|
+
validateCustomerConfig(customerConfig);
|
|
139
|
+
} catch (error: unknown) {
|
|
140
|
+
logCliError('error', 'Failed to parse customer configuration');
|
|
141
|
+
if (error instanceof Error) {
|
|
142
|
+
logCliError('error', error.message);
|
|
143
|
+
}
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle customer selection
|
|
148
|
+
let selectedCustomer: CustomerConfig;
|
|
149
|
+
|
|
150
|
+
if (cmd === 'list-customers') {
|
|
151
|
+
const customers = listCustomers(customerConfig);
|
|
152
|
+
console.log('Available customers:');
|
|
153
|
+
for (const customerIdn of customers) {
|
|
154
|
+
const isDefault = customerConfig.defaultCustomer === customerIdn;
|
|
155
|
+
console.log(` ${customerIdn}${isDefault ? ' (default)' : ''}`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args.customer) {
|
|
161
|
+
const customer = getCustomer(customerConfig, args.customer as string);
|
|
162
|
+
if (!customer) {
|
|
163
|
+
console.error(`Unknown customer: ${args.customer}`);
|
|
164
|
+
console.error(`Available customers: ${listCustomers(customerConfig).join(', ')}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
selectedCustomer = customer;
|
|
168
|
+
} else {
|
|
169
|
+
try {
|
|
170
|
+
selectedCustomer = getDefaultCustomer(customerConfig);
|
|
171
|
+
} catch (error: unknown) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
console.error(message);
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
17
177
|
|
|
18
178
|
if (!cmd || ['help', '-h', '--help'].includes(cmd)) {
|
|
19
|
-
console.log(`NEWO CLI
|
|
179
|
+
console.log(`NEWO CLI - Multi-Customer Support
|
|
20
180
|
Usage:
|
|
21
|
-
newo pull
|
|
22
|
-
newo push
|
|
23
|
-
newo status
|
|
24
|
-
newo
|
|
25
|
-
newo
|
|
181
|
+
newo pull [--customer <idn>] # download projects -> ./newo_customers/<idn>/projects/
|
|
182
|
+
newo push [--customer <idn>] # upload modified *.guidance/*.jinja back to NEWO
|
|
183
|
+
newo status [--customer <idn>] # show modified files
|
|
184
|
+
newo list-customers # list available customers
|
|
185
|
+
newo meta [--customer <idn>] # get project metadata (debug)
|
|
186
|
+
newo import-akb <file> <persona_id> [--customer <idn>] # import AKB articles from file
|
|
26
187
|
|
|
27
188
|
Flags:
|
|
189
|
+
--customer <idn> # specify customer (if not set, uses default)
|
|
28
190
|
--verbose, -v # enable detailed logging
|
|
29
191
|
|
|
30
|
-
|
|
31
|
-
NEWO_BASE_URL
|
|
192
|
+
Environment Variables:
|
|
193
|
+
NEWO_BASE_URL # NEWO API base URL (default: https://app.newo.ai)
|
|
194
|
+
NEWO_CUSTOMER_<IDN>_API_KEY # API key for customer <IDN>
|
|
195
|
+
NEWO_CUSTOMER_<IDN>_PROJECT_ID # Optional: specific project ID for customer
|
|
196
|
+
NEWO_DEFAULT_CUSTOMER # Optional: default customer to use
|
|
197
|
+
|
|
198
|
+
Multi-Customer Examples:
|
|
199
|
+
# Configure customers in .env:
|
|
200
|
+
NEWO_CUSTOMER_acme_API_KEY=your_acme_api_key
|
|
201
|
+
NEWO_CUSTOMER_globex_API_KEY=your_globex_api_key
|
|
202
|
+
NEWO_DEFAULT_CUSTOMER=acme
|
|
203
|
+
|
|
204
|
+
# Commands:
|
|
205
|
+
newo pull --customer acme # Pull projects for Acme
|
|
206
|
+
newo push --customer globex # Push changes for Globex
|
|
207
|
+
newo status # Status for default customer
|
|
32
208
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
209
|
+
File Structure:
|
|
210
|
+
newo_customers/
|
|
211
|
+
├── acme/
|
|
212
|
+
│ └── projects/
|
|
213
|
+
│ └── project1/
|
|
214
|
+
└── globex/
|
|
215
|
+
└── projects/
|
|
216
|
+
└── project2/
|
|
39
217
|
`);
|
|
40
218
|
return;
|
|
41
219
|
}
|
|
42
220
|
|
|
43
|
-
|
|
221
|
+
// Get access token for the selected customer
|
|
222
|
+
const accessToken = await getValidAccessToken(selectedCustomer);
|
|
223
|
+
const client = await makeClient(verbose, accessToken);
|
|
44
224
|
|
|
45
225
|
if (cmd === 'pull') {
|
|
46
|
-
//
|
|
47
|
-
|
|
226
|
+
// Use customer-specific project ID if set, otherwise pull all projects
|
|
227
|
+
const projectId = selectedCustomer.projectId || null;
|
|
228
|
+
await pullAll(client, selectedCustomer, projectId, verbose);
|
|
48
229
|
} else if (cmd === 'push') {
|
|
49
|
-
await pushChanged(client, verbose);
|
|
230
|
+
await pushChanged(client, selectedCustomer, verbose);
|
|
50
231
|
} else if (cmd === 'status') {
|
|
51
|
-
await status(verbose);
|
|
232
|
+
await status(selectedCustomer, verbose);
|
|
52
233
|
} else if (cmd === 'meta') {
|
|
53
|
-
if (!
|
|
54
|
-
|
|
234
|
+
if (!selectedCustomer.projectId) {
|
|
235
|
+
console.error(`No project ID configured for customer ${selectedCustomer.idn}`);
|
|
236
|
+
console.error(`Set NEWO_CUSTOMER_${selectedCustomer.idn.toUpperCase()}_PROJECT_ID in your .env file`);
|
|
237
|
+
process.exit(1);
|
|
55
238
|
}
|
|
56
|
-
const meta = await getProjectMeta(client,
|
|
239
|
+
const meta = await getProjectMeta(client, selectedCustomer.projectId);
|
|
57
240
|
console.log(JSON.stringify(meta, null, 2));
|
|
58
241
|
} else if (cmd === 'import-akb') {
|
|
59
242
|
const akbFile = args._[1];
|
|
@@ -69,7 +252,7 @@ Notes:
|
|
|
69
252
|
|
|
70
253
|
try {
|
|
71
254
|
if (verbose) console.log(`📖 Parsing AKB file: ${filePath}`);
|
|
72
|
-
const articles = parseAkbFile(filePath);
|
|
255
|
+
const articles = await parseAkbFile(filePath);
|
|
73
256
|
console.log(`✓ Parsed ${articles.length} articles from ${akbFile}`);
|
|
74
257
|
|
|
75
258
|
if (verbose) console.log(`🔧 Preparing articles for persona: ${personaId}`);
|
|
@@ -88,9 +271,13 @@ Notes:
|
|
|
88
271
|
await importAkbArticle(client, article);
|
|
89
272
|
successCount++;
|
|
90
273
|
if (!verbose) process.stdout.write('.');
|
|
91
|
-
} catch (error) {
|
|
274
|
+
} catch (error: unknown) {
|
|
92
275
|
errorCount++;
|
|
93
|
-
const errorMessage =
|
|
276
|
+
const errorMessage = error instanceof Error && 'response' in error
|
|
277
|
+
? (error as NewoApiError)?.response?.data
|
|
278
|
+
: error instanceof Error
|
|
279
|
+
? error.message
|
|
280
|
+
: String(error);
|
|
94
281
|
console.error(`\n❌ Failed to import ${article.topic_name}:`, errorMessage);
|
|
95
282
|
}
|
|
96
283
|
}
|
|
@@ -98,8 +285,9 @@ Notes:
|
|
|
98
285
|
if (!verbose) console.log(''); // new line after dots
|
|
99
286
|
console.log(`✅ Import complete: ${successCount} successful, ${errorCount} failed`);
|
|
100
287
|
|
|
101
|
-
} catch (error) {
|
|
102
|
-
|
|
288
|
+
} catch (error: unknown) {
|
|
289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
290
|
+
console.error('❌ AKB import failed:', message);
|
|
103
291
|
process.exit(1);
|
|
104
292
|
}
|
|
105
293
|
} else {
|
|
@@ -108,8 +296,21 @@ Notes:
|
|
|
108
296
|
}
|
|
109
297
|
}
|
|
110
298
|
|
|
111
|
-
main().catch((error:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
299
|
+
main().catch((error: unknown) => {
|
|
300
|
+
// Determine operation context from command line args
|
|
301
|
+
const args = process.argv.slice(2);
|
|
302
|
+
const cmd = args.find(arg => !arg.startsWith('-')) || 'unknown command';
|
|
303
|
+
|
|
304
|
+
// Handle API errors with specific data
|
|
305
|
+
if (error instanceof Error && 'response' in error) {
|
|
306
|
+
const apiError = error as NewoApiError;
|
|
307
|
+
const responseData = apiError.response?.data;
|
|
308
|
+
const status = apiError.response?.status;
|
|
309
|
+
|
|
310
|
+
if (responseData && status) {
|
|
311
|
+
logCliError('error', `API error (${status}): ${JSON.stringify(responseData)}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
handleCliError(error, cmd);
|
|
115
316
|
});
|
package/src/customer.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { NewoEnvironment, CustomerConfig, MultiCustomerConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse environment variables to extract customer configurations
|
|
5
|
+
* Supports both array-based (NEWO_API_KEYS) and individual customer configs
|
|
6
|
+
*/
|
|
7
|
+
export function parseCustomerConfig(env: NewoEnvironment): MultiCustomerConfig {
|
|
8
|
+
const customers: Record<string, CustomerConfig> = {};
|
|
9
|
+
|
|
10
|
+
// Parse customer-specific API keys
|
|
11
|
+
// Format: NEWO_CUSTOMER_[IDN]_API_KEY=api_key
|
|
12
|
+
for (const [key, value] of Object.entries(env)) {
|
|
13
|
+
if (key.startsWith('NEWO_CUSTOMER_') && key.endsWith('_API_KEY') && value) {
|
|
14
|
+
const idn = key.slice('NEWO_CUSTOMER_'.length, -'_API_KEY'.length).toLowerCase();
|
|
15
|
+
|
|
16
|
+
if (!customers[idn]) {
|
|
17
|
+
customers[idn] = { idn, apiKey: value };
|
|
18
|
+
} else {
|
|
19
|
+
customers[idn].apiKey = value;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Check for corresponding project ID
|
|
23
|
+
const projectIdKey = `NEWO_CUSTOMER_${idn.toUpperCase()}_PROJECT_ID`;
|
|
24
|
+
if (env[projectIdKey]) {
|
|
25
|
+
customers[idn].projectId = env[projectIdKey];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check for legacy single customer mode
|
|
31
|
+
if (env.NEWO_API_KEY && Object.keys(customers).length === 0) {
|
|
32
|
+
customers['default'] = {
|
|
33
|
+
idn: 'default',
|
|
34
|
+
apiKey: env.NEWO_API_KEY,
|
|
35
|
+
projectId: env.NEWO_PROJECT_ID
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
customers,
|
|
41
|
+
defaultCustomer: env.NEWO_DEFAULT_CUSTOMER || (Object.keys(customers).length === 1 ? Object.keys(customers)[0] : undefined)
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* List all available customer IDNs
|
|
47
|
+
*/
|
|
48
|
+
export function listCustomers(config: MultiCustomerConfig): string[] {
|
|
49
|
+
return Object.keys(config.customers).sort();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get customer configuration by IDN
|
|
54
|
+
*/
|
|
55
|
+
export function getCustomer(config: MultiCustomerConfig, customerIdn: string): CustomerConfig | null {
|
|
56
|
+
return config.customers[customerIdn] || null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get default customer or throw error if none
|
|
61
|
+
*/
|
|
62
|
+
export function getDefaultCustomer(config: MultiCustomerConfig): CustomerConfig {
|
|
63
|
+
if (config.defaultCustomer) {
|
|
64
|
+
const customer = getCustomer(config, config.defaultCustomer);
|
|
65
|
+
if (customer) return customer;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const customerIdns = listCustomers(config);
|
|
69
|
+
if (customerIdns.length === 1) {
|
|
70
|
+
const firstCustomerIdn = customerIdns[0];
|
|
71
|
+
if (firstCustomerIdn) {
|
|
72
|
+
return config.customers[firstCustomerIdn]!;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (customerIdns.length === 0) {
|
|
77
|
+
throw new Error('No customers configured. Please set NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Multiple customers configured but no default specified. Available: ${customerIdns.join(', ')}. ` +
|
|
82
|
+
`Set NEWO_DEFAULT_CUSTOMER or use --customer flag.`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validate customer configuration
|
|
88
|
+
*/
|
|
89
|
+
export function validateCustomerConfig(config: MultiCustomerConfig): void {
|
|
90
|
+
const customers = listCustomers(config);
|
|
91
|
+
|
|
92
|
+
if (customers.length === 0) {
|
|
93
|
+
throw new Error('No customers configured. Please set NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const customerIdn of customers) {
|
|
97
|
+
const customer = config.customers[customerIdn]!;
|
|
98
|
+
if (!customer.apiKey) {
|
|
99
|
+
throw new Error(`Customer ${customerIdn} missing API key`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { NewoEnvironment, CustomerConfig, MultiCustomerConfig } from './types.js';
|
|
2
|
+
import { initializeCustomersFromApiKeys, usesArrayBasedConfig } from './customerInit.js';
|
|
3
|
+
import { parseCustomerConfig } from './customer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Async version of customer configuration parsing that supports API key array initialization
|
|
7
|
+
*/
|
|
8
|
+
export async function parseCustomerConfigAsync(env: NewoEnvironment, verbose: boolean = false): Promise<MultiCustomerConfig> {
|
|
9
|
+
|
|
10
|
+
// If using array-based config, initialize from API keys
|
|
11
|
+
if (usesArrayBasedConfig(env)) {
|
|
12
|
+
if (verbose) console.log('📝 Using array-based API key configuration');
|
|
13
|
+
return await initializeCustomersFromApiKeys(env, verbose);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Fall back to synchronous individual customer parsing
|
|
17
|
+
if (verbose) console.log('📝 Using individual customer configuration');
|
|
18
|
+
return parseCustomerConfig(env);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* List all available customer IDNs
|
|
23
|
+
*/
|
|
24
|
+
export function listCustomers(config: MultiCustomerConfig): string[] {
|
|
25
|
+
return Object.keys(config.customers).sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get customer configuration by IDN
|
|
30
|
+
*/
|
|
31
|
+
export function getCustomer(config: MultiCustomerConfig, customerIdn: string): CustomerConfig | null {
|
|
32
|
+
return config.customers[customerIdn] || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get default customer or throw error if none
|
|
37
|
+
*/
|
|
38
|
+
export function getDefaultCustomer(config: MultiCustomerConfig): CustomerConfig {
|
|
39
|
+
if (config.defaultCustomer) {
|
|
40
|
+
const customer = getCustomer(config, config.defaultCustomer);
|
|
41
|
+
if (customer) return customer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const customerIdns = listCustomers(config);
|
|
45
|
+
if (customerIdns.length === 1) {
|
|
46
|
+
const firstCustomerIdn = customerIdns[0];
|
|
47
|
+
if (firstCustomerIdn) {
|
|
48
|
+
return config.customers[firstCustomerIdn]!;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (customerIdns.length === 0) {
|
|
53
|
+
throw new Error('No customers configured. Please set NEWO_API_KEYS or NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Multiple customers configured but no default specified. Available: ${customerIdns.join(', ')}. ` +
|
|
58
|
+
`Set NEWO_DEFAULT_CUSTOMER or use --customer flag.`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate customer configuration
|
|
64
|
+
*/
|
|
65
|
+
export function validateCustomerConfig(config: MultiCustomerConfig): void {
|
|
66
|
+
const customers = listCustomers(config);
|
|
67
|
+
|
|
68
|
+
if (customers.length === 0) {
|
|
69
|
+
throw new Error('No customers configured. Please set NEWO_API_KEYS or NEWO_CUSTOMER_[IDN]_API_KEY in your .env file.');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const customerIdn of customers) {
|
|
73
|
+
const customer = config.customers[customerIdn]!;
|
|
74
|
+
if (!customer.apiKey) {
|
|
75
|
+
throw new Error(`Customer ${customerIdn} missing API key`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getCustomerProfile, makeClient } from './api.js';
|
|
2
|
+
import { exchangeApiKeyForToken } from './auth.js';
|
|
3
|
+
import type { NewoEnvironment, ApiKeyConfig, CustomerConfig, MultiCustomerConfig } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize customer configurations from API keys array
|
|
7
|
+
*/
|
|
8
|
+
export async function initializeCustomersFromApiKeys(
|
|
9
|
+
env: NewoEnvironment,
|
|
10
|
+
verbose: boolean = false
|
|
11
|
+
): Promise<MultiCustomerConfig> {
|
|
12
|
+
if (!env.NEWO_API_KEYS) {
|
|
13
|
+
throw new Error('NEWO_API_KEYS not set. Provide API keys array in .env file.');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let apiKeyConfigs: (string | ApiKeyConfig)[];
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
apiKeyConfigs = JSON.parse(env.NEWO_API_KEYS);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw new Error(`Invalid NEWO_API_KEYS format. Must be valid JSON array: ${error}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(apiKeyConfigs)) {
|
|
25
|
+
throw new Error('NEWO_API_KEYS must be an array');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const customers: Record<string, CustomerConfig> = {};
|
|
29
|
+
|
|
30
|
+
if (verbose) console.log(`🔍 Initializing ${apiKeyConfigs.length} API keys...`);
|
|
31
|
+
|
|
32
|
+
for (const [index, keyConfig] of apiKeyConfigs.entries()) {
|
|
33
|
+
try {
|
|
34
|
+
// Normalize config
|
|
35
|
+
const apiKey = typeof keyConfig === 'string' ? keyConfig : keyConfig.key;
|
|
36
|
+
const projectId = typeof keyConfig === 'object' ? keyConfig.project_id : undefined;
|
|
37
|
+
|
|
38
|
+
if (verbose) console.log(` [${index + 1}/${apiKeyConfigs.length}] Exchanging API key for token...`);
|
|
39
|
+
|
|
40
|
+
// Create temporary customer config for token exchange
|
|
41
|
+
const tempCustomer: CustomerConfig = {
|
|
42
|
+
idn: 'temp',
|
|
43
|
+
apiKey,
|
|
44
|
+
projectId
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Exchange API key for token
|
|
48
|
+
const tokens = await exchangeApiKeyForToken(tempCustomer);
|
|
49
|
+
|
|
50
|
+
if (verbose) console.log(` [${index + 1}/${apiKeyConfigs.length}] Getting customer profile...`);
|
|
51
|
+
|
|
52
|
+
// Create client with token
|
|
53
|
+
const client = await makeClient(verbose, tokens.access_token);
|
|
54
|
+
|
|
55
|
+
// Get customer profile to extract IDN
|
|
56
|
+
const profile = await getCustomerProfile(client);
|
|
57
|
+
|
|
58
|
+
if (verbose) {
|
|
59
|
+
console.log(` [${index + 1}/${apiKeyConfigs.length}] ✓ Customer: ${profile.idn} (${profile.organization_name})`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Store customer config with real IDN
|
|
63
|
+
customers[profile.idn] = {
|
|
64
|
+
idn: profile.idn,
|
|
65
|
+
apiKey,
|
|
66
|
+
projectId
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
} catch (error) {
|
|
70
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
71
|
+
console.error(` [${index + 1}/${apiKeyConfigs.length}] ❌ Failed to initialize API key: ${message}`);
|
|
72
|
+
// Continue with other keys rather than failing entirely
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const customerIdns = Object.keys(customers);
|
|
77
|
+
|
|
78
|
+
if (customerIdns.length === 0) {
|
|
79
|
+
throw new Error('No valid API keys found. Check your NEWO_API_KEYS configuration.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (verbose) {
|
|
83
|
+
console.log(`✅ Initialized ${customerIdns.length} customers: ${customerIdns.join(', ')}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
customers,
|
|
88
|
+
defaultCustomer: env.NEWO_DEFAULT_CUSTOMER || (customerIdns.length === 1 ? customerIdns[0] : undefined)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if environment uses array-based configuration
|
|
94
|
+
*/
|
|
95
|
+
export function usesArrayBasedConfig(env: NewoEnvironment): boolean {
|
|
96
|
+
return Boolean(env.NEWO_API_KEYS);
|
|
97
|
+
}
|