@aifabrix/builder 2.32.1 → 2.32.3

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.
@@ -10,45 +10,116 @@
10
10
  */
11
11
 
12
12
  /**
13
- * Formats a single validation error into a developer-friendly message
14
- *
15
- * @function formatSingleError
16
- * @param {Object} error - Raw validation error from Ajv
17
- * @returns {string} Formatted error message
13
+ * Maps common regex patterns to human-readable descriptions
14
+ * @type {Object.<string, string>}
18
15
  */
19
- function formatSingleError(error) {
20
- // Handle empty or missing instancePath - use 'Configuration' for root level errors
16
+ const PATTERN_DESCRIPTIONS = {
17
+ '^[a-z0-9-]+$': 'lowercase letters, numbers, and hyphens only',
18
+ '^[a-z0-9-:]+$': 'lowercase letters, numbers, hyphens, and colons only (e.g., "entity:action")',
19
+ '^[a-z-]+$': 'lowercase letters and hyphens only',
20
+ '^[A-Z_][A-Z0-9_]*$': 'uppercase letters, numbers, and underscores (must start with letter or underscore)',
21
+ '^[a-zA-Z0-9_-]+$': 'letters, numbers, hyphens, and underscores only',
22
+ '^(http|https)://.*$': 'valid HTTP or HTTPS URL',
23
+ '^/[a-z0-9/-]*$': 'URL path starting with / (lowercase letters, numbers, hyphens, slashes)'
24
+ };
25
+
26
+ /**
27
+ * Gets a human-readable description of a regex pattern
28
+ * @function getPatternDescription
29
+ * @param {string} pattern - The regex pattern
30
+ * @returns {string} Human-readable description
31
+ */
32
+ function getPatternDescription(pattern) {
33
+ return PATTERN_DESCRIPTIONS[pattern] || `must match pattern: ${pattern}`;
34
+ }
35
+
36
+ /**
37
+ * Extracts the field name from an error's instancePath
38
+ * @function getFieldName
39
+ * @param {Object} error - Validation error object
40
+ * @returns {string} Formatted field name
41
+ */
42
+ function getFieldName(error) {
21
43
  const instancePath = error.instancePath || '';
22
44
  const path = instancePath ? instancePath.slice(1) : '';
23
- const field = path ? `Field "${path}"` : 'Configuration';
45
+ return path ? `Field "${path}"` : 'Configuration';
46
+ }
24
47
 
25
- // Check if params exists before accessing it
26
- const errorMessages = {
27
- required: error.params?.missingProperty
28
- ? `${field}: Missing required property "${error.params.missingProperty}"`
48
+ /**
49
+ * Formats a pattern validation error with the actual invalid value
50
+ * @function formatPatternError
51
+ * @param {string} field - Field name
52
+ * @param {Object} error - Validation error object
53
+ * @returns {string} Formatted error message
54
+ */
55
+ function formatPatternError(field, error) {
56
+ const invalidValue = error.data !== undefined ? `"${error.data}"` : 'value';
57
+ const patternDesc = getPatternDescription(error.params?.pattern);
58
+ return `${field}: Invalid value ${invalidValue} - ${patternDesc}`;
59
+ }
60
+
61
+ /**
62
+ * Creates error message formatters for each validation keyword
63
+ * @function createKeywordFormatters
64
+ * @param {string} field - Field name
65
+ * @param {Object} error - Validation error object
66
+ * @returns {Object} Object mapping keywords to formatted messages
67
+ */
68
+ function createKeywordFormatters(field, error) {
69
+ const params = error.params || {};
70
+
71
+ return {
72
+ required: params.missingProperty
73
+ ? `${field}: Missing required property "${params.missingProperty}"`
29
74
  : `${field}: Missing required property`,
30
- type: error.params?.type
31
- ? `${field}: Expected ${error.params.type}, got ${typeof error.data}`
75
+
76
+ type: params.type
77
+ ? `${field}: Expected ${params.type}, got ${typeof error.data}`
32
78
  : `${field}: Type error`,
33
- minimum: error.params?.limit
34
- ? `${field}: Value must be at least ${error.params.limit}`
79
+
80
+ minimum: params.limit !== undefined
81
+ ? `${field}: Value must be at least ${params.limit}`
35
82
  : `${field}: Value below minimum`,
36
- maximum: error.params?.limit
37
- ? `${field}: Value must be at most ${error.params.limit}`
83
+
84
+ maximum: params.limit !== undefined
85
+ ? `${field}: Value must be at most ${params.limit}`
38
86
  : `${field}: Value above maximum`,
39
- minLength: error.params?.limit
40
- ? `${field}: Must be at least ${error.params.limit} characters`
87
+
88
+ minLength: params.limit !== undefined
89
+ ? `${field}: Must be at least ${params.limit} characters`
41
90
  : `${field}: Too short`,
42
- maxLength: error.params?.limit
43
- ? `${field}: Must be at most ${error.params.limit} characters`
91
+
92
+ maxLength: params.limit !== undefined
93
+ ? `${field}: Must be at most ${params.limit} characters`
44
94
  : `${field}: Too long`,
45
- pattern: `${field}: Invalid format`,
46
- enum: error.params?.allowedValues && error.params.allowedValues.length > 0
47
- ? `${field}: Must be one of: ${error.params.allowedValues.join(', ')}`
95
+
96
+ enum: params.allowedValues && params.allowedValues.length > 0
97
+ ? `${field}: Must be one of: ${params.allowedValues.join(', ')}`
48
98
  : `${field}: Must be one of: unknown`
49
99
  };
100
+ }
50
101
 
51
- return errorMessages[error.keyword] || `${field}: ${error.message || 'Validation error'}`;
102
+ /**
103
+ * Formats a single validation error into a developer-friendly message
104
+ *
105
+ * @function formatSingleError
106
+ * @param {Object} error - Raw validation error from Ajv
107
+ * @returns {string} Formatted error message
108
+ */
109
+ function formatSingleError(error) {
110
+ const field = getFieldName(error);
111
+
112
+ // Handle pattern errors with special formatting
113
+ if (error.keyword === 'pattern') {
114
+ return formatPatternError(field, error);
115
+ }
116
+
117
+ // Use object lookup for keyword-specific messages
118
+ const formatters = createKeywordFormatters(field, error);
119
+ const message = formatters[error.keyword];
120
+
121
+ // Return keyword message or fallback to generic message
122
+ return message || `${field}: ${error.message || 'Validation error'}`;
52
123
  }
53
124
 
54
125
  /**
@@ -73,6 +144,7 @@ function formatValidationErrors(errors) {
73
144
 
74
145
  module.exports = {
75
146
  formatSingleError,
76
- formatValidationErrors
147
+ formatValidationErrors,
148
+ getPatternDescription,
149
+ PATTERN_DESCRIPTIONS
77
150
  };
78
-
@@ -357,6 +357,64 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
357
357
  throw new Error('Invalid authentication type');
358
358
  }
359
359
 
360
+ /**
361
+ * Force refresh device token for controller (regardless of local expiry time)
362
+ * Used when server returns 401 even though local token hasn't expired
363
+ * @param {string} controllerUrl - Controller URL
364
+ * @returns {Promise<{token: string, controller: string}|null>} Token and controller URL, or null if not available
365
+ */
366
+ async function forceRefreshDeviceToken(controllerUrl) {
367
+ // Try to get existing token to get refresh token
368
+ const tokenInfo = await getDeviceToken(controllerUrl);
369
+
370
+ if (!tokenInfo) {
371
+ return null;
372
+ }
373
+
374
+ // Must have refresh token to force refresh
375
+ if (!tokenInfo.refreshToken) {
376
+ logger.warn('Cannot refresh: no refresh token available. Please login again using: aifabrix login');
377
+ return null;
378
+ }
379
+
380
+ try {
381
+ const refreshed = await refreshDeviceToken(controllerUrl, tokenInfo.refreshToken);
382
+ return {
383
+ token: refreshed.token,
384
+ controller: controllerUrl
385
+ };
386
+ } catch (error) {
387
+ const errorMessage = error.message || String(error);
388
+ if (errorMessage.includes('Refresh token has expired')) {
389
+ logger.warn(`Refresh token expired: ${errorMessage}`);
390
+ } else {
391
+ logger.warn(`Failed to refresh device token: ${errorMessage}`);
392
+ }
393
+ return null;
394
+ }
395
+ }
396
+
397
+ /**
398
+ * Get device-only authentication for services that require user-level authentication.
399
+ * Used for interactive commands like wizard that don't support client credentials.
400
+ * @async
401
+ * @function getDeviceOnlyAuth
402
+ * @param {string} controllerUrl - Controller URL
403
+ * @returns {Promise<Object>} Auth config with device token
404
+ * @throws {Error} If device token is not available
405
+ */
406
+ async function getDeviceOnlyAuth(controllerUrl) {
407
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
408
+ if (deviceToken && deviceToken.token) {
409
+ return {
410
+ type: 'bearer',
411
+ token: deviceToken.token,
412
+ controller: deviceToken.controller
413
+ };
414
+ }
415
+ throw new Error('Device token authentication required. Run "aifabrix login" to authenticate.');
416
+ }
417
+
360
418
  module.exports = {
361
419
  getDeviceToken,
362
420
  getClientToken,
@@ -367,7 +425,9 @@ module.exports = {
367
425
  loadClientCredentials,
368
426
  getOrRefreshClientToken,
369
427
  getOrRefreshDeviceToken,
428
+ forceRefreshDeviceToken,
370
429
  getDeploymentAuth,
430
+ getDeviceOnlyAuth,
371
431
  extractClientCredentials
372
432
  };
373
433
 
@@ -296,7 +296,8 @@ function validateDeploymentJson(deployment) {
296
296
  };
297
297
  }
298
298
 
299
- const ajv = new Ajv({ allErrors: true, strict: false });
299
+ // verbose: true includes the actual data value in error objects for better error messages
300
+ const ajv = new Ajv({ allErrors: true, strict: false, verbose: true });
300
301
  // Register external schemas with their $id (GitHub raw URLs)
301
302
  // Create copies to avoid modifying the original schemas
302
303
  const externalSystemSchemaCopy = { ...externalSystemSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.32.1",
3
+ "version": "2.32.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -18,8 +18,8 @@ npm install -g @aifabrix/builder
18
18
  # Check your environment
19
19
  aifabrix doctor
20
20
 
21
- # Login to controller
22
- aifabrix login --method device --environment dev --offline --controller http://localhost:3000
21
+ # Login to controller (offline tokens are default)
22
+ aifabrix login --method device --environment dev --controller http://localhost:3000
23
23
 
24
24
  # Register your application (gets you credentials automatically)
25
25
  aifabrix app register {{appName}} --environment dev
@@ -39,7 +39,7 @@
39
39
  "name": "{{name}}",
40
40
  "value": "{{value}}",
41
41
  "description": "{{description}}"{{#if Groups}},
42
- "Groups": [{{#each Groups}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]{{/if}}
42
+ "groups": [{{#each Groups}}"{{this}}"{{#unless @last}}, {{/unless}}{{/each}}]{{/if}}
43
43
  }{{#unless @last}},{{/unless}}
44
44
  {{/each}}
45
45
  ]{{/if}}{{#if permissions}},