@aifabrix/builder 2.50.0 → 2.51.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 (103) hide show
  1. package/.cursor/plans/157.1-security-quality-violations-by-type.csv +374 -0
  2. package/.cursor/rules/assistant-driven-integration-build.mdc +40 -0
  3. package/integration/hubspot-test/create-hubspot.js +1 -16
  4. package/lib/api/evidence-kinds.api.js +128 -0
  5. package/lib/api/pipeline.api.js +5 -1
  6. package/lib/api/resource-types.api.js +4 -4
  7. package/lib/api/roles.api.js +121 -0
  8. package/lib/api/types/evidence-kinds.types.js +42 -0
  9. package/lib/api/types/roles.types.js +36 -0
  10. package/lib/app/certification-verify-rows.js +1 -1
  11. package/lib/app/show.js +2 -2
  12. package/lib/certification/merge-certification-from-artifact.js +1 -1
  13. package/lib/channels/channel-artifact-generator.js +2 -1
  14. package/lib/cli/index.js +4 -0
  15. package/lib/cli/setup-app.js +22 -5
  16. package/lib/cli/setup-dev.js +23 -7
  17. package/lib/cli/setup-utility-help-after.js +12 -0
  18. package/lib/cli/setup-utility-validate.js +41 -2
  19. package/lib/commands/deploy-gate-failure-details.js +21 -0
  20. package/lib/commands/deploy-gate-orchestrator.js +64 -18
  21. package/lib/commands/dev-cli-handlers.js +6 -3
  22. package/lib/commands/dev-init.js +7 -109
  23. package/lib/commands/dev-refresh.js +197 -0
  24. package/lib/commands/evidence-kind.js +428 -0
  25. package/lib/commands/governance-command-action.js +4 -2
  26. package/lib/commands/repair-openapi-export.js +1 -1
  27. package/lib/commands/repair-openapi-sync.js +14 -6
  28. package/lib/commands/resource-type.js +101 -18
  29. package/lib/commands/role.js +452 -0
  30. package/lib/commands/setup.js +1 -1
  31. package/lib/commands/validate-json.js +230 -0
  32. package/lib/commands/verify-operations-step-plan.js +78 -40
  33. package/lib/core/audit-logger.js +1 -1
  34. package/lib/core/secrets-load.js +1 -3
  35. package/lib/datasource/capability/relate-operations.js +1 -1
  36. package/lib/generator/helpers.js +13 -5
  37. package/lib/generator/split-variables.js +9 -3
  38. package/lib/infrastructure/helpers-docker-check.js +6 -0
  39. package/lib/infrastructure/helpers.js +1 -1
  40. package/lib/programmatic/auth-config.js +3 -2
  41. package/lib/programmatic/bearer-auth.js +1 -1
  42. package/lib/programmatic/enterprise-mcp-client.js +18 -2
  43. package/lib/programmatic/manifest-section-registry.js +18 -1
  44. package/lib/programmatic/openapi-result-schemas.js +28 -11
  45. package/lib/programmatic/openapi-schema-descriptions-core.js +5 -2
  46. package/lib/programmatic/openapi-schema-helpers.js +16 -1
  47. package/lib/programmatic/openapi-spec.js +1 -1
  48. package/lib/programmatic/run-repository-emcp.js +11 -3
  49. package/lib/programmatic/run-template-workspace.js +14 -1
  50. package/lib/resolvers/evidence-kind-file.js +230 -0
  51. package/lib/resolvers/resource-type-file.js +36 -1
  52. package/lib/resolvers/role-file.js +227 -0
  53. package/lib/schema/application-schema.json +22 -2
  54. package/lib/schema/external-datasource.schema.json +85 -2
  55. package/lib/schema/external-system.schema.json +16 -2
  56. package/lib/schema/infra.parameter.yaml +25 -1
  57. package/lib/schema/type/credential-field-registry.json +19 -3
  58. package/lib/utils/api.js +8 -2
  59. package/lib/utils/app-register-config.js +4 -3
  60. package/lib/utils/cli-sync-options.js +59 -3
  61. package/lib/utils/cli-utils.js +11 -3
  62. package/lib/utils/dev-ca-install.js +5 -6
  63. package/lib/utils/dev-config.js +2 -2
  64. package/lib/utils/dev-list-sort.js +145 -0
  65. package/lib/utils/dev-platform-tls.js +0 -63
  66. package/lib/utils/docker-build.js +46 -27
  67. package/lib/utils/docker-daemon-tls-ca.js +1 -1
  68. package/lib/utils/docker-exec.js +91 -6
  69. package/lib/utils/env-copy.js +1 -3
  70. package/lib/utils/error-formatters/error-parser.js +0 -3
  71. package/lib/utils/external-env-template.js +1 -1
  72. package/lib/utils/external-system-readiness-compact-blocks.js +28 -1
  73. package/lib/utils/health-check-request.js +2 -3
  74. package/lib/utils/help-builder.js +3 -0
  75. package/lib/utils/image-name.js +22 -4
  76. package/lib/utils/logger.js +37 -4
  77. package/lib/utils/manifest-source-emit.js +1 -1
  78. package/lib/utils/paths.js +32 -4
  79. package/lib/utils/pipeline-upload-auth-retry.js +4 -0
  80. package/lib/utils/postgres-platform-bootstrap.js +24 -17
  81. package/lib/utils/postgres-wipe.js +5 -2
  82. package/lib/utils/remote-dev-auth.js +39 -4
  83. package/lib/utils/resolve-docker-image-ref.js +36 -6
  84. package/lib/utils/token-manager.js +16 -0
  85. package/lib/utils/upload-sync-options.js +17 -3
  86. package/lib/validation/openapi-contract-surface-validator.js +4 -0
  87. package/lib/validation/validator.js +6 -5
  88. package/package.json +34 -12
  89. package/scripts/export-builder-api-evidence.js +2 -2
  90. package/scripts/lib/plan-actor.js +2 -2
  91. package/scripts/lib/plan-meta.js +1 -1
  92. package/scripts/lib/plan-parse.js +1 -1
  93. package/scripts/lib/plan-prevalidate-gates.js +12 -1
  94. package/scripts/lib/plan-run-report.js +13 -2
  95. package/scripts/pnpm/lib/resolve-aifabrix-bin.mjs +10 -10
  96. package/templates/applications/dataplane/application.yaml +26 -4
  97. package/templates/applications/dataplane/env.template +28 -9
  98. package/templates/applications/dataplane/rbac.yaml +64 -2
  99. package/templates/applications/miso-controller/application.yaml +10 -1
  100. package/templates/applications/miso-controller/env.template +10 -7
  101. package/templates/applications/miso-controller/rbac.yaml +72 -0
  102. package/templates/channels/slack/integration/slack-system.json +60 -2
  103. package/templates/python/requirements.txt +1 -1
@@ -7,12 +7,12 @@
7
7
  "key":"external-datasource-schema",
8
8
  "name":"External Data Source Configuration Schema",
9
9
  "description":"JSON schema for validating ExternalDataSource configuration files",
10
- "version":"2.9.2",
10
+ "version":"2.9.4",
11
11
  "type":"schema",
12
12
  "category":"integration",
13
13
  "author":"AI Fabrix Team",
14
14
  "createdAt":"2024-01-01T00:00:00Z",
15
- "updatedAt":"2026-06-16T00:00:00Z",
15
+ "updatedAt":"2026-07-18T00:00:00Z",
16
16
  "compatibility":{
17
17
  "minVersion":"1.0.0",
18
18
  "maxVersion":"3.0.0",
@@ -31,6 +31,22 @@
31
31
  "external-system.schema.json"
32
32
  ],
33
33
  "changelog":[
34
+ {
35
+ "version":"2.9.4",
36
+ "date":"2026-07-18T00:00:00Z",
37
+ "changes":[
38
+ "Removed root statusMaps (superseded by dimensions.*.map {dimensionValue, rawValue} only — 346 §7.1)."
39
+ ],
40
+ "breaking":true
41
+ },
42
+ {
43
+ "version":"2.9.3",
44
+ "date":"2026-07-18T00:00:00Z",
45
+ "changes":[
46
+ "Added optional dimensionBinding.map array of {dimensionValue, rawValue} for static Dimension raw→enterprise translation on normalize (346 §7.1)."
47
+ ],
48
+ "breaking":false
49
+ },
34
50
  {
35
51
  "version":"2.9.2",
36
52
  "date":"2026-07-02T00:00:00Z",
@@ -1514,6 +1530,10 @@
1514
1530
  "type":"boolean",
1515
1531
  "description":"Filterability hint for metadata-driven query planning."
1516
1532
  },
1533
+ "observer":{
1534
+ "type":"boolean",
1535
+ "description":"When true, a committed change to this normalized first-level scalar field is eligible for Enterprise Runtime observation wake-up."
1536
+ },
1517
1537
  "nullable":{
1518
1538
  "type":"boolean"
1519
1539
  },
@@ -1694,6 +1714,45 @@
1694
1714
  }
1695
1715
  }
1696
1716
  },
1717
+ {
1718
+ "if":{
1719
+ "properties":{
1720
+ "observer":{
1721
+ "const":true
1722
+ }
1723
+ },
1724
+ "required":[
1725
+ "observer"
1726
+ ]
1727
+ },
1728
+ "then":{
1729
+ "properties":{
1730
+ "type":{
1731
+ "type":"string",
1732
+ "enum":[
1733
+ "string",
1734
+ "number",
1735
+ "integer",
1736
+ "boolean"
1737
+ ]
1738
+ }
1739
+ },
1740
+ "not":{
1741
+ "anyOf":[
1742
+ {
1743
+ "required":[
1744
+ "properties"
1745
+ ]
1746
+ },
1747
+ {
1748
+ "required":[
1749
+ "items"
1750
+ ]
1751
+ }
1752
+ ]
1753
+ }
1754
+ }
1755
+ },
1697
1756
  {
1698
1757
  "if":{
1699
1758
  "required":[
@@ -1845,6 +1904,30 @@
1845
1904
  "type":"boolean",
1846
1905
  "default":true,
1847
1906
  "description":"When false, this dimension binding is not used for protection/RSS dimension enforcement or governance visibility checks. Catalog binding and metadata materialization may still apply. Use object form to disable; string shorthand bindings are always enforcement-enabled."
1907
+ },
1908
+ "map":{
1909
+ "type":"array",
1910
+ "description":"Optional raw→enterprise value map for static Dimensions. Each entry has only dimensionValue and rawValue. Datasource and field are implied by this binding. CIP normalize persists dimensionValue for ABAC/MCP.",
1911
+ "items":{
1912
+ "type":"object",
1913
+ "additionalProperties":false,
1914
+ "required":[
1915
+ "dimensionValue",
1916
+ "rawValue"
1917
+ ],
1918
+ "properties":{
1919
+ "dimensionValue":{
1920
+ "type":"string",
1921
+ "minLength":1,
1922
+ "description":"Enterprise Dimension catalog value."
1923
+ },
1924
+ "rawValue":{
1925
+ "type":"string",
1926
+ "minLength":1,
1927
+ "description":"Connected-system token for this binding's field."
1928
+ }
1929
+ }
1930
+ }
1848
1931
  }
1849
1932
  },
1850
1933
  "oneOf":[
@@ -7,12 +7,12 @@
7
7
  "key": "external-system-schema",
8
8
  "name": "External System Configuration Schema",
9
9
  "description": "JSON schema for validating ExternalSystem configuration files",
10
- "version": "1.13.0",
10
+ "version": "1.13.1",
11
11
  "type": "schema",
12
12
  "category": "integration",
13
13
  "author": "AI Fabrix Team",
14
14
  "createdAt": "2024-01-01T00:00:00Z",
15
- "updatedAt": "2026-06-15T00:00:00Z",
15
+ "updatedAt": "2026-07-22T00:00:00Z",
16
16
  "compatibility": {
17
17
  "minVersion": "1.4.0",
18
18
  "maxVersion": "2.0.0",
@@ -27,6 +27,14 @@
27
27
  ],
28
28
  "dependencies": [],
29
29
  "changelog": [
30
+ {
31
+ "version": "1.13.1",
32
+ "date": "2026-07-22T00:00:00Z",
33
+ "changes": [
34
+ "Added optional configuration[].category for portal UI grouping labels (parity with application-schema)."
35
+ ],
36
+ "breaking": false
37
+ },
30
38
  {
31
39
  "version": "1.13.0",
32
40
  "date": "2026-07-02T00:00:00Z",
@@ -1163,6 +1171,12 @@
1163
1171
  "required": {
1164
1172
  "type": "boolean"
1165
1173
  },
1174
+ "category": {
1175
+ "type": "string",
1176
+ "description": "Optional UI grouping label for portal/deployment wizard Configuration tab",
1177
+ "minLength": 1,
1178
+ "maxLength": 100
1179
+ },
1166
1180
  "portalInput": {
1167
1181
  "type": "object",
1168
1182
  "required": [
@@ -204,6 +204,19 @@ parameters:
204
204
  notes: >-
205
205
  JWT signing secret; generated on first ensure. Azure Bicep typically uses `${prefix}-secrets-jwtKeyVault`.
206
206
 
207
+ # LICENSE_JWT=kv://miso-controller-jwt-licenseKeyVault — local default skips Mori/JWT license checks.
208
+ # Controller ignores DEVELOPMENT when DEPLOYMENT=azure.
209
+ - key: miso-controller-jwt-licenseKeyVault
210
+ scope: app
211
+ generator:
212
+ type: literal
213
+ value: 'DEVELOPMENT'
214
+ ensureOn: [upInfra, resolveApp]
215
+ azure:
216
+ notes: >-
217
+ Local/dev default DEVELOPMENT disables license validation (no Mori/JWT required).
218
+ Ignored when DEPLOYMENT=azure. Override with a real offline license JWT (or empty for Mori) in non-dev.
219
+
207
220
  # Exact entry so this is not caught by *KeyVault → randomBytes32 (invalid email).
208
221
  - key: miso-controller-admin-emailKeyVault
209
222
  scope: app
@@ -327,7 +340,18 @@ parameters:
327
340
  vaultSecretName: dataplane-client-secretKeyVault
328
341
  notes: Per-app OAuth client secret for dataplane; generated on first ensure.
329
342
 
330
- # Mori / telemetry: optional locally (LICENSE_JWT=DEVELOPMENT skips Mori); empty until integrated.
343
+ # Mori / telemetry: optional locally (LICENSE_JWT via miso-controller-jwt-licenseKeyVault=DEVELOPMENT skips Mori).
344
+ # MORI_AUTH_METHOD=kv://mori-controller-auth-methodKeyVault — local default apiKey.
345
+ - key: mori-controller-auth-methodKeyVault
346
+ scope: infra
347
+ generator:
348
+ type: literal
349
+ value: 'apiKey'
350
+ ensureOn: [upInfra, resolveApp]
351
+ azure:
352
+ notes: >-
353
+ Local/dev default apiKey. Override when using Mori basic auth or other methods.
354
+
331
355
  - key: mori-controller-api-keyKeyVault
332
356
  scope: infra
333
357
  generator:
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 6,
2
+ "version": 8,
3
3
  "semantics": {
4
4
  "visibility.show": "When false, the field name is omitted from default portal field lists, embedded credential form field lists, and from public API credentialConfig read models (GET/list with decrypt=false), in addition to any omission driven by encrypted. GET with decrypt=true returns the full decrypted config for authorized callers and does not apply this visibility filter.",
5
5
  "visibility.showValue": "UI-only: whether stored values may be shown versus placeholders in form flows that honor this flag. Does not control encryption at rest or public API stripping.",
@@ -114,10 +114,12 @@
114
114
  "display": {
115
115
  "label": "Tenant ID",
116
116
  "control": "text",
117
- "enabled": true
117
+ "enabled": true,
118
+ "placeholder": "Azure AD directory (tenant) ID",
119
+ "description": "Used for Microsoft identity platforms. May also appear in tokenUrl as {{tenantId}}."
118
120
  },
119
121
  "visibility": {
120
- "show": false,
122
+ "show": true,
121
123
  "showValue": true
122
124
  },
123
125
  "encrypted": false
@@ -149,6 +151,20 @@
149
151
  "showValue": false
150
152
  },
151
153
  "encrypted": true
154
+ },
155
+ {
156
+ "name": "accessTokenExpiresAt",
157
+ "required": false,
158
+ "display": {
159
+ "label": "Access Token Expires At",
160
+ "control": "text",
161
+ "enabled": false
162
+ },
163
+ "visibility": {
164
+ "show": false,
165
+ "showValue": false
166
+ },
167
+ "encrypted": false
152
168
  }
153
169
  ]
154
170
  },
package/lib/utils/api.js CHANGED
@@ -426,8 +426,14 @@ async function authenticatedApiCall(url, options = {}, tokenOrAuthConfig) {
426
426
  headers
427
427
  });
428
428
 
429
- // Only attempt device token refresh on 401 when user Bearer token was used (not for client-token)
430
- if (!response.success && response.status === 401 && authType !== 'client-token') {
429
+ // Only attempt device token refresh on 401 when user Bearer token was used (not for client-token).
430
+ // Token-forward (builder-api): never refresh from mount device store — keep the caller JWT.
431
+ if (
432
+ !response.success &&
433
+ response.status === 401 &&
434
+ authType !== 'client-token' &&
435
+ !(typeof tokenOrAuthConfig === 'object' && tokenOrAuthConfig?.preserveBearer)
436
+ ) {
431
437
  try {
432
438
  const { forceRefreshDeviceToken } = require('./token-manager');
433
439
  const refreshedToken = await forceRefreshDeviceToken(authControllerUrl || extractControllerUrl(url));
@@ -228,11 +228,11 @@ async function extractExternalAppConfiguration(appKey, variables, appKeyFromFile
228
228
  * @returns {Object} Webapp configuration
229
229
  */
230
230
  function extractWebappConfiguration(variables, appKeyFromFile, displayName, description, options) {
231
- const appType = variables.build?.language === 'typescript' ? 'webapp' : 'service';
231
+ const appType = variables.app?.type || 'webapp';
232
232
  const registryMode = variables.image?.registryMode || 'external';
233
233
  const port = options.port ?? getContainerPort(variables);
234
234
  const localPort = port;
235
- const language = variables.build?.language || 'typescript';
235
+ const language = variables.build?.language || variables.app?.language || 'typescript';
236
236
  const image = buildImageReference(variables, appKeyFromFile);
237
237
  const url = variables.app?.url || variables.deployment?.dataplaneUrl || variables.deployment?.appUrl || null;
238
238
 
@@ -252,7 +252,8 @@ function extractWebappConfiguration(variables, appKeyFromFile, displayName, desc
252
252
 
253
253
  async function extractAppConfiguration(variables, appKey, options) {
254
254
  const appKeyFromFile = variables.app?.key || appKey;
255
- const displayName = variables.app?.name || options.name || appKey;
255
+ const displayName =
256
+ variables.app?.displayName || variables.app?.name || options.name || appKey;
256
257
  const description = variables.app?.description || '';
257
258
 
258
259
  if (variables.app?.type === 'external') {
@@ -6,9 +6,60 @@
6
6
 
7
7
  'use strict';
8
8
 
9
+ /**
10
+ * True when `node` looks like a Commander command (or a test double), not a plain opts bag.
11
+ *
12
+ * @param {*} node
13
+ * @returns {boolean}
14
+ */
15
+ function isCommanderLike(node) {
16
+ if (!node || typeof node !== 'object') {
17
+ return false;
18
+ }
19
+ return (
20
+ Array.isArray(node.rawArgs) ||
21
+ typeof node.opts === 'function' ||
22
+ (Object.prototype.hasOwnProperty.call(node, 'parent') &&
23
+ (node.parent === null ||
24
+ node.parent === undefined ||
25
+ typeof node.parent === 'object'))
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Collect argv-like arrays from a Commander command and its parents.
31
+ * Subcommand `rawArgs` is often empty; the flag lives on the root command.
32
+ * Safely no-ops for null/undefined or plain options objects (not Command-like).
33
+ *
34
+ * @param {import('commander').Command|null|undefined} cmd
35
+ * @returns {string[]}
36
+ */
37
+ function collectCommanderRawArgs(cmd) {
38
+ if (!isCommanderLike(cmd)) {
39
+ return [];
40
+ }
41
+ const chunks = [];
42
+ const seen = new Set();
43
+ let current = cmd;
44
+ while (isCommanderLike(current)) {
45
+ if (seen.has(current)) {
46
+ break;
47
+ }
48
+ seen.add(current);
49
+ if (Array.isArray(current.rawArgs) && current.rawArgs.length > 0) {
50
+ chunks.push(...current.rawArgs);
51
+ }
52
+ current = current.parent;
53
+ }
54
+ return chunks;
55
+ }
56
+
9
57
  /**
10
58
  * True when the user passed `--no-publish` or set `noPublish: true` (programmatic).
11
59
  *
60
+ * Commander maps `.option('--no-publish', …)` to `publish: false` (default `true`),
61
+ * not `noPublish: true`. Detect that shape as well as programmatic `noPublish`.
62
+ *
12
63
  * @param {Object|null|undefined} options - Commander-parsed options
13
64
  * @param {import('commander').Command} [cmd] - Commander command (for rawArgs fallback)
14
65
  * @returns {boolean}
@@ -20,10 +71,15 @@ function cliOptsSkipPublish(options, cmd) {
20
71
  if (options.noPublish === true) {
21
72
  return true;
22
73
  }
23
- const raw = cmd && Array.isArray(cmd.rawArgs) ? cmd.rawArgs : [];
24
- return raw.includes('--no-publish');
74
+ // Commander `--no-X` → property `X` defaults true; flag sets it false
75
+ if (options.publish === false) {
76
+ return true;
77
+ }
78
+ return collectCommanderRawArgs(cmd).includes('--no-publish');
25
79
  }
26
80
 
27
81
  module.exports = {
28
- cliOptsSkipPublish
82
+ cliOptsSkipPublish,
83
+ collectCommanderRawArgs,
84
+ isCommanderLike
29
85
  };
@@ -219,9 +219,14 @@ function formatAzureError(errorMsg) {
219
219
  if (errorMsg.includes('Docker build failed') || errorMsg.includes('Build failed: Docker')) {
220
220
  return null;
221
221
  }
222
- // Only match ACR-specific authentication errors, not general authentication failures
223
- if (errorMsg.includes('ACR') || errorMsg.includes('azurecr.io') ||
224
- (errorMsg.includes('authenticate') && (errorMsg.includes('registry') || errorMsg.includes('container')))) {
222
+ const lowerMsg = String(errorMsg || '').toLowerCase();
223
+ const hasAcrDomain = /\b[a-z0-9.-]+\.azurecr\.io\b/i.test(errorMsg);
224
+ const hasAcrKeyword = /\bacr\b/.test(lowerMsg);
225
+ const hasAuthAndRegistryContext =
226
+ /\bauthenticate\b/.test(lowerMsg) &&
227
+ (/\bregistry\b/.test(lowerMsg) || /\bcontainer\b/.test(lowerMsg));
228
+ // Only match ACR-specific authentication errors, not general authentication failures.
229
+ if (hasAcrKeyword || hasAcrDomain || hasAuthAndRegistryContext) {
225
230
  return [
226
231
  ' Azure Container Registry authentication failed.',
227
232
  ' Run: az acr login --name <registry-name>',
@@ -414,6 +419,9 @@ function isAuthenticationError(error) {
414
419
  * @param {string} command - Command that failed
415
420
  */
416
421
  function handleCommandError(error, command) {
422
+ if (error && error.compactGateAbortLogged === true) {
423
+ return;
424
+ }
417
425
  const errorMessages = formatError(error);
418
426
  const skipDoctor = isMissingSecretsErrorMessage(error && error.message);
419
427
  logError(command, errorMessages, { skipDoctor });
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Dev CA Install – SSL untrusted detection, fetch CA from Builder Server, install into OS trust store.
3
- * Used by `aifabrix dev init` when the server certificate is self-signed. Only /install-ca uses
4
- * rejectUnauthorized: false; all other requests use default TLS verification.
3
+ * Used by `aifabrix dev init` when the server certificate is self-signed.
4
+ * Requests keep TLS verification enabled by default.
5
5
  *
6
6
  * @fileoverview CA install utilities for development Builder Server
7
7
  * @author AI Fabrix Team
@@ -163,7 +163,7 @@ function finalizeInstallCaResponse(res, absoluteUrl, chunks, resolve, reject) {
163
163
  }
164
164
 
165
165
  /**
166
- * GET absolute install-ca URL with insecure TLS; follow redirects (resolve relative Location).
166
+ * GET absolute install-ca URL with TLS verification enabled; follow redirects.
167
167
  * @param {string} absoluteUrl - Full https URL
168
168
  * @param {number} redirectCount - Redirect depth
169
169
  * @returns {Promise<Buffer>} CA PEM
@@ -179,10 +179,9 @@ function fetchInstallCaAbsolute(absoluteUrl, redirectCount) {
179
179
  reject(new Error('install-ca requires https URL'));
180
180
  return;
181
181
  }
182
- const agent = new https.Agent({ rejectUnauthorized: false });
183
182
  const req = https.get(
184
183
  absoluteUrl,
185
- { agent },
184
+ {},
186
185
  (res) => {
187
186
  if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
188
187
  res.destroy();
@@ -208,7 +207,7 @@ function fetchInstallCaAbsolute(absoluteUrl, redirectCount) {
208
207
 
209
208
  /**
210
209
  * Fetch CA PEM from Builder Server via GET {baseUrl}/install-ca.
211
- * Uses rejectUnauthorized: false only for this endpoint (dev setup).
210
+ * Uses default TLS verification for this endpoint.
212
211
  * @param {string} baseUrl - Builder Server base URL (no trailing slash)
213
212
  * @returns {Promise<Buffer>} CA certificate PEM
214
213
  */
@@ -44,8 +44,8 @@ function getDevPorts(developerId) {
44
44
  throw new Error('Developer ID must be a positive number');
45
45
  }
46
46
 
47
- // Handle invalids
48
- if (developerId === undefined || developerId === null || Number.isNaN(developerId)) {
47
+ // Handle invalid numeric values
48
+ if (Number.isNaN(developerId)) {
49
49
  throw new Error('Developer ID must be a positive number');
50
50
  }
51
51
  if (developerId < 0 || !Number.isInteger(developerId)) {
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @fileoverview Client-side sort for aifabrix dev list (--sort field / -field).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const ALLOWED_SORT_FIELDS = Object.freeze(['id', 'name', 'email', 'until']);
8
+
9
+ /**
10
+ * @param {unknown} id
11
+ * @returns {string}
12
+ */
13
+ function normalizeIdKey(id) {
14
+ const trimmed = String(id ?? '').trim();
15
+ if (!/^\d+$/.test(trimmed)) {
16
+ return trimmed;
17
+ }
18
+ return String(parseInt(trimmed, 10)).padStart(2, '0');
19
+ }
20
+
21
+ /**
22
+ * @param {unknown} a
23
+ * @param {unknown} b
24
+ * @returns {number}
25
+ */
26
+ function compareIds(a, b) {
27
+ const sa = String(a ?? '').trim();
28
+ const sb = String(b ?? '').trim();
29
+ if (/^\d+$/.test(sa) && /^\d+$/.test(sb)) {
30
+ return parseInt(sa, 10) - parseInt(sb, 10);
31
+ }
32
+ return normalizeIdKey(sa).localeCompare(normalizeIdKey(sb), undefined, { sensitivity: 'base' });
33
+ }
34
+
35
+ /**
36
+ * @param {string} a
37
+ * @param {string} b
38
+ * @returns {number}
39
+ */
40
+ function compareText(a, b) {
41
+ return a.localeCompare(b, undefined, { sensitivity: 'base' });
42
+ }
43
+
44
+ /**
45
+ * @param {Object} user
46
+ * @returns {string}
47
+ */
48
+ function untilValue(user) {
49
+ const raw = user && user.certificateValidNotAfter;
50
+ return typeof raw === 'string' ? raw.trim() : '';
51
+ }
52
+
53
+ /**
54
+ * Parse --sort option. Default: name ascending.
55
+ * @param {string|undefined|null} sortOption
56
+ * @returns {{ field: string, descending: boolean }}
57
+ * @throws {Error} When field is empty or not allowed
58
+ */
59
+ function parseSortSpec(sortOption) {
60
+ if (sortOption === undefined || sortOption === null) {
61
+ return { field: 'name', descending: false };
62
+ }
63
+ const trimmed = String(sortOption).trim();
64
+ if (!trimmed) {
65
+ throw new Error(
66
+ 'Invalid --sort value "". Allowed: id, name, email, until (prefix - for descending).'
67
+ );
68
+ }
69
+ let descending = false;
70
+ let fieldPart = trimmed;
71
+ if (fieldPart.startsWith('-')) {
72
+ descending = true;
73
+ fieldPart = fieldPart.slice(1).trim();
74
+ }
75
+ if (!fieldPart) {
76
+ throw new Error(
77
+ 'Invalid --sort value "-". Allowed: id, name, email, until (prefix - for descending).'
78
+ );
79
+ }
80
+ const field = fieldPart.toLowerCase();
81
+ if (!ALLOWED_SORT_FIELDS.includes(field)) {
82
+ throw new Error(
83
+ `Invalid --sort value "${trimmed}". Allowed: id, name, email, until (prefix - for descending).`
84
+ );
85
+ }
86
+ return { field, descending };
87
+ }
88
+
89
+ /**
90
+ * @param {Object} a
91
+ * @param {Object} b
92
+ * @param {string} field
93
+ * @returns {number}
94
+ */
95
+ function compareByField(a, b, field) {
96
+ if (field === 'id') {
97
+ return compareIds(a.id, b.id);
98
+ }
99
+ if (field === 'name') {
100
+ return compareText(String(a.name || ''), String(b.name || ''));
101
+ }
102
+ if (field === 'email') {
103
+ return compareText(String(a.email || ''), String(b.email || ''));
104
+ }
105
+ const ua = untilValue(a);
106
+ const ub = untilValue(b);
107
+ if (!ua && !ub) {
108
+ return 0;
109
+ }
110
+ if (!ua) {
111
+ return 1;
112
+ }
113
+ if (!ub) {
114
+ return -1;
115
+ }
116
+ return compareText(ua, ub);
117
+ }
118
+
119
+ /**
120
+ * Sort developer users without mutating the input array.
121
+ * @param {Object[]} users
122
+ * @param {string|undefined|null} sortOption
123
+ * @returns {Object[]}
124
+ */
125
+ function sortDevUsers(users, sortOption) {
126
+ const { field, descending } = parseSortSpec(sortOption);
127
+ const list = Array.isArray(users) ? [...users] : [];
128
+ list.sort((a, b) => {
129
+ let cmp = compareByField(a, b, field);
130
+ if (descending) {
131
+ cmp = -cmp;
132
+ }
133
+ if (cmp !== 0) {
134
+ return cmp;
135
+ }
136
+ return compareIds(a.id, b.id);
137
+ });
138
+ return list;
139
+ }
140
+
141
+ module.exports = {
142
+ ALLOWED_SORT_FIELDS,
143
+ parseSortSpec,
144
+ sortDevUsers
145
+ };
@@ -8,7 +8,6 @@
8
8
 
9
9
  'use strict';
10
10
 
11
- const https = require('https');
12
11
  const config = require('../core/config');
13
12
  const { isLocalhostPublicSchemeHostname } = require('./localhost-public-host');
14
13
 
@@ -68,65 +67,6 @@ async function shouldAllowInsecureDevPlatformTls(url) {
68
67
  }
69
68
  }
70
69
 
71
- /**
72
- * Minimal fetch for self-signed dev platform HTTPS (Node global fetch cannot set rejectUnauthorized).
73
- *
74
- * @param {string} url
75
- * @param {Object} options
76
- * @returns {Promise<{ status: number, statusText: string, headers: { get: Function }, text: Function, json: Function }>}
77
- */
78
- function nodeHttpsFetchInsecure(url, options = {}) {
79
- return new Promise((resolve, reject) => {
80
- const u = new URL(url);
81
- const method = String(options.method || 'GET').toUpperCase();
82
- const headers = { ...(options.headers || {}) };
83
- const body = options.body;
84
- const reqOpts = {
85
- hostname: u.hostname,
86
- port: u.port || 443,
87
- path: `${u.pathname}${u.search}`,
88
- method,
89
- headers,
90
- rejectUnauthorized: false
91
- };
92
-
93
- const req = https.request(reqOpts, (res) => {
94
- const chunks = [];
95
- res.on('data', (chunk) => chunks.push(chunk));
96
- res.on('end', () => {
97
- const raw = Buffer.concat(chunks).toString('utf8');
98
- const status = res.statusCode || 0;
99
- resolve({
100
- ok: status >= 200 && status < 300,
101
- status,
102
- statusText: res.statusMessage || '',
103
- headers: {
104
- get: (name) => res.headers[String(name || '').toLowerCase()] || null
105
- },
106
- text: async() => raw,
107
- json: async() => JSON.parse(raw)
108
- });
109
- });
110
- });
111
-
112
- req.on('error', reject);
113
- if (options.signal) {
114
- const onAbort = () => {
115
- req.destroy(new Error('The operation was aborted'));
116
- };
117
- if (options.signal.aborted) {
118
- onAbort();
119
- return;
120
- }
121
- options.signal.addEventListener('abort', onAbort, { once: true });
122
- }
123
- if (body !== undefined && body !== null) {
124
- req.write(typeof body === 'string' ? body : String(body));
125
- }
126
- req.end();
127
- });
128
- }
129
-
130
70
  /**
131
71
  * @async
132
72
  * @param {string} url
@@ -134,9 +74,6 @@ function nodeHttpsFetchInsecure(url, options = {}) {
134
74
  * @returns {Promise<Response>}
135
75
  */
136
76
  async function fetchWithOptionalDevPlatformTls(url, options = {}) {
137
- if (await shouldAllowInsecureDevPlatformTls(url)) {
138
- return nodeHttpsFetchInsecure(url, options);
139
- }
140
77
  return fetch(url, options);
141
78
  }
142
79