@newrelic/preflight 1.6.12 → 1.6.14

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/dist/config.d.ts CHANGED
@@ -6,6 +6,8 @@ import type { PersonalAlertThresholds } from './alerts/types.js';
6
6
  export interface McpServerConfig {
7
7
  readonly licenseKey?: string;
8
8
  readonly accountId?: string;
9
+ /** Config-file schema version. Defaults to 1; see CURRENT_CONFIG_VERSION. */
10
+ readonly configVersion: number;
9
11
  readonly appName: string;
10
12
  readonly developer: string;
11
13
  readonly teamId: string | null;
@@ -35,29 +37,37 @@ export interface McpServerConfig {
35
37
  /** Default: 90. `null` disables retention (only reachable via an explicit `null` in config.json). */
36
38
  readonly retainSessionsDays: number | null;
37
39
  readonly personalAlertThresholds: PersonalAlertThresholds;
38
- readonly otlpEndpoint: string | null;
39
- readonly otlpHeaders: Readonly<Record<string, string>>;
40
- readonly transport: 'nr-events-api' | 'otlp' | 'both';
41
40
  readonly mode: 'cloud' | 'local' | 'both';
42
41
  readonly platformTarget?: PlatformTarget;
43
- /** Enable the local OTLP/HTTP receiver. Default: false. */
44
- readonly otlpReceiverEnabled: boolean;
45
- /** Port for the local OTLP/HTTP receiver. Default: 4318. */
46
- readonly otlpReceiverPort: number;
47
- /** Bind address for the local OTLP/HTTP receiver. Default: '127.0.0.1'. */
48
- readonly otlpReceiverBindAddress: string;
49
42
  /**
50
- * OTLP forward endpoint where to relay received spans.
51
- * Defaults to New Relic US OTLP endpoint when licenseKey is present.
52
- * Set to null to disable forwarding (receive and enrich only, then drop).
43
+ * OTLP-related config, grouped to match the `dashboard`/`alerts` nesting
44
+ * precedent. The config-file schema also still accepts the legacy flat
45
+ * top-level keys (`otlpEndpoint`, `otlpReceiverPort`, etc.) for backward
46
+ * compatibility — see `pickOtlpValue()` in loadMcpConfig().
53
47
  */
54
- readonly otlpForwardEndpoint: string | null;
55
- /**
56
- * HTTP headers added to every OTLP forward request (e.g. `{ 'api-key': licenseKey }`).
57
- * Defaults to `{ 'api-key': licenseKey }` when licenseKey is present.
58
- * Configurable via NR_AI_OTLP_FORWARD_HEADERS (comma-separated key=value pairs).
59
- */
60
- readonly otlpForwardHeaders: Readonly<Record<string, string>>;
48
+ readonly otlp: {
49
+ readonly endpoint: string | null;
50
+ readonly headers: Readonly<Record<string, string>>;
51
+ readonly transport: 'nr-events-api' | 'otlp' | 'both';
52
+ /** Enable the local OTLP/HTTP receiver. Default: false. */
53
+ readonly receiverEnabled: boolean;
54
+ /** Port for the local OTLP/HTTP receiver. Default: 4318. */
55
+ readonly receiverPort: number;
56
+ /** Bind address for the local OTLP/HTTP receiver. Default: '127.0.0.1'. */
57
+ readonly receiverBindAddress: string;
58
+ /**
59
+ * OTLP forward endpoint — where to relay received spans.
60
+ * Defaults to New Relic US OTLP endpoint when licenseKey is present.
61
+ * Set to null to disable forwarding (receive and enrich only, then drop).
62
+ */
63
+ readonly forwardEndpoint: string | null;
64
+ /**
65
+ * HTTP headers added to every OTLP forward request (e.g. `{ 'api-key': licenseKey }`).
66
+ * Defaults to `{ 'api-key': licenseKey }` when licenseKey is present.
67
+ * Configurable via NR_AI_OTLP_FORWARD_HEADERS (comma-separated key=value pairs).
68
+ */
69
+ readonly forwardHeaders: Readonly<Record<string, string>>;
70
+ };
61
71
  readonly dashboard: {
62
72
  readonly port: number;
63
73
  readonly host: string;
@@ -79,6 +89,7 @@ export interface McpServerConfig {
79
89
  export declare const DEFAULT_STORAGE_PATH: string;
80
90
  export type PlatformTarget = 'native' | 'wsl-windows-cc' | 'wsl-linux-cc';
81
91
  export declare const ConfigFileSchema: z.ZodObject<{
92
+ configVersion: z.ZodOptional<z.ZodNumber>;
82
93
  licenseKey: z.ZodOptional<z.ZodString>;
83
94
  accountId: z.ZodOptional<z.ZodString>;
84
95
  appName: z.ZodOptional<z.ZodString>;
@@ -117,6 +128,11 @@ export declare const ConfigFileSchema: z.ZodObject<{
117
128
  otlp: "otlp";
118
129
  both: "both";
119
130
  }>>;
131
+ otlpReceiverEnabled: z.ZodOptional<z.ZodBoolean>;
132
+ otlpReceiverPort: z.ZodOptional<z.ZodNumber>;
133
+ otlpReceiverBindAddress: z.ZodOptional<z.ZodString>;
134
+ otlpForwardEndpoint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
135
+ otlpForwardHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
120
136
  mode: z.ZodOptional<z.ZodEnum<{
121
137
  both: "both";
122
138
  cloud: "cloud";
@@ -127,11 +143,20 @@ export declare const ConfigFileSchema: z.ZodObject<{
127
143
  "wsl-windows-cc": "wsl-windows-cc";
128
144
  "wsl-linux-cc": "wsl-linux-cc";
129
145
  }>>;
130
- otlpReceiverEnabled: z.ZodOptional<z.ZodBoolean>;
131
- otlpReceiverPort: z.ZodOptional<z.ZodNumber>;
132
- otlpReceiverBindAddress: z.ZodOptional<z.ZodString>;
133
- otlpForwardEndpoint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
134
- otlpForwardHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
146
+ otlp: z.ZodOptional<z.ZodObject<{
147
+ endpoint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
148
+ headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
149
+ transport: z.ZodOptional<z.ZodEnum<{
150
+ "nr-events-api": "nr-events-api";
151
+ otlp: "otlp";
152
+ both: "both";
153
+ }>>;
154
+ receiverEnabled: z.ZodOptional<z.ZodBoolean>;
155
+ receiverPort: z.ZodOptional<z.ZodNumber>;
156
+ receiverBindAddress: z.ZodOptional<z.ZodString>;
157
+ forwardEndpoint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
158
+ forwardHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
159
+ }, z.core.$loose>>;
135
160
  alerts: z.ZodOptional<z.ZodObject<{
136
161
  personal: z.ZodOptional<z.ZodObject<{
137
162
  dailyCostUsd: z.ZodOptional<z.ZodNumber>;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/config.js CHANGED
@@ -13,8 +13,17 @@ export const DEFAULT_STORAGE_PATH = resolve(homedir(), '.newrelic-preflight');
13
13
  // retainSessionsDays at all — an explicit `null` in config.json still means
14
14
  // "retention disabled" (see the resolution logic below).
15
15
  const DEFAULT_RETAIN_SESSIONS_DAYS = 90;
16
+ /**
17
+ * Bump when a change to config.json's shape is non-additive (a field is
18
+ * renamed, moved, or removed) so a future migration path has something to
19
+ * branch on. Every change to date has been additive — including the `otlp`
20
+ * nested object below, which was introduced as an accept-both addition
21
+ * rather than a breaking rename — so this has never moved past 1.
22
+ */
23
+ const CURRENT_CONFIG_VERSION = 1;
16
24
  export const ConfigFileSchema = z
17
25
  .object({
26
+ configVersion: z.number().int().optional(),
18
27
  licenseKey: z.string().optional(),
19
28
  accountId: z.string().optional(),
20
29
  appName: z.string().optional(),
@@ -41,16 +50,32 @@ export const ConfigFileSchema = z
41
50
  digestWebhookUrl: z.string().nullable().optional(),
42
51
  digestSchedule: z.string().optional(),
43
52
  retainSessionsDays: z.number().nullable().optional(),
53
+ // Legacy flat OTLP fields — deprecated in favor of the nested `otlp`
54
+ // object below, but still accepted for backward compatibility (using
55
+ // one logs a deprecation warning; see pickOtlpValue() in loadMcpConfig).
44
56
  otlpEndpoint: z.string().nullable().optional(),
45
57
  otlpHeaders: z.record(z.string(), z.string()).optional(),
46
58
  transport: z.enum(['nr-events-api', 'otlp', 'both']).optional(),
47
- mode: z.enum(['cloud', 'local', 'both']).optional(),
48
- platformTarget: z.enum(['native', 'wsl-windows-cc', 'wsl-linux-cc']).optional(),
49
59
  otlpReceiverEnabled: z.boolean().optional(),
50
60
  otlpReceiverPort: z.number().optional(),
51
61
  otlpReceiverBindAddress: z.string().optional(),
52
62
  otlpForwardEndpoint: z.string().nullable().optional(),
53
63
  otlpForwardHeaders: z.record(z.string(), z.string()).optional(),
64
+ mode: z.enum(['cloud', 'local', 'both']).optional(),
65
+ platformTarget: z.enum(['native', 'wsl-windows-cc', 'wsl-linux-cc']).optional(),
66
+ otlp: z
67
+ .object({
68
+ endpoint: z.string().nullable().optional(),
69
+ headers: z.record(z.string(), z.string()).optional(),
70
+ transport: z.enum(['nr-events-api', 'otlp', 'both']).optional(),
71
+ receiverEnabled: z.boolean().optional(),
72
+ receiverPort: z.number().optional(),
73
+ receiverBindAddress: z.string().optional(),
74
+ forwardEndpoint: z.string().nullable().optional(),
75
+ forwardHeaders: z.record(z.string(), z.string()).optional(),
76
+ })
77
+ .passthrough()
78
+ .optional(),
54
79
  alerts: z
55
80
  .object({
56
81
  personal: z
@@ -165,18 +190,25 @@ function envBool(key, defaultValue) {
165
190
  return false;
166
191
  return defaultValue;
167
192
  }
193
+ // Applied to every return path of envInt (not just the parsed-env-var one) so
194
+ // a bounds-checked call site is protected uniformly regardless of whether the
195
+ // value actually came from the env var or from its (often config-file- or
196
+ // CLI-derived) `defaultValue` argument.
197
+ function clampNumber(value, bounds) {
198
+ if (bounds?.min !== undefined && value < bounds.min)
199
+ return bounds.min;
200
+ if (bounds?.max !== undefined && value > bounds.max)
201
+ return bounds.max;
202
+ return value;
203
+ }
168
204
  function envInt(key, defaultValue, bounds) {
169
205
  const val = process.env[key];
170
206
  if (val === undefined)
171
- return defaultValue;
207
+ return clampNumber(defaultValue, bounds);
172
208
  const parsed = parseInt(val, 10);
173
209
  if (Number.isNaN(parsed))
174
- return defaultValue;
175
- if (bounds?.min !== undefined && parsed < bounds.min)
176
- return bounds.min;
177
- if (bounds?.max !== undefined && parsed > bounds.max)
178
- return bounds.max;
179
- return parsed;
210
+ return clampNumber(defaultValue, bounds);
211
+ return clampNumber(parsed, bounds);
180
212
  }
181
213
  function envLogLevel(key, defaultValue) {
182
214
  const val = process.env[key]?.toLowerCase();
@@ -259,6 +291,20 @@ function parseOtlpHeaders(headerString) {
259
291
  }
260
292
  return result;
261
293
  }
294
+ // Resolves a config-file OTLP field: the new nested `otlp.X` value wins when
295
+ // present; otherwise falls back to the legacy flat top-level key, recording
296
+ // its name in `used` so the caller can warn about exactly the legacy keys
297
+ // that were actually consulted (not merely present-but-shadowed by a nested
298
+ // value for the same field).
299
+ function pickOtlpValue(nested, flat, legacyKey, used) {
300
+ if (nested !== undefined)
301
+ return nested;
302
+ if (flat !== undefined) {
303
+ used.push(legacyKey);
304
+ return flat;
305
+ }
306
+ return undefined;
307
+ }
262
308
  /**
263
309
  * Validate the alerts.rulesPath value. The path is read from the user's
264
310
  * config file or NR_AI_ALERTS_RULES_PATH env var; both are user-controlled
@@ -298,6 +344,21 @@ function validateRulesPath(rawPath, storagePath) {
298
344
  }
299
345
  return resolved;
300
346
  }
347
+ // Config-file-sourced numeric fields (budgets, retention) validate positivity
348
+ // only on the env-var path today — a negative/zero value in config.json
349
+ // silently disables the guardrail with no warning, unlike every other
350
+ // malformed-input path in this file (see validateRulesPath, dashboard.host,
351
+ // parseProxyUpstreams). Callers decide their own fallback value; this only
352
+ // validates and logs.
353
+ function validateFilePositiveNumber(value, fieldName) {
354
+ if (Number.isFinite(value) && value > 0)
355
+ return true;
356
+ logger.warn(`${fieldName} in config file must be a positive number — ignoring value`, {
357
+ fieldName,
358
+ value,
359
+ });
360
+ return false;
361
+ }
301
362
  function parseProxyUpstreams(envValue, fileValue) {
302
363
  // Env var takes precedence (JSON string)
303
364
  if (envValue) {
@@ -367,6 +428,16 @@ export function loadMcpConfig(cliOptions) {
367
428
  'antiPatternCountMax',
368
429
  ]);
369
430
  const knownDashboardKeys = new Set(['port', 'host', 'openOnStart']);
431
+ const knownOtlpKeys = new Set([
432
+ 'endpoint',
433
+ 'headers',
434
+ 'transport',
435
+ 'receiverEnabled',
436
+ 'receiverPort',
437
+ 'receiverBindAddress',
438
+ 'forwardEndpoint',
439
+ 'forwardHeaders',
440
+ ]);
370
441
  if (file.alerts && typeof file.alerts === 'object') {
371
442
  const alerts = file.alerts;
372
443
  const unknownAlertsKeys = Object.keys(alerts).filter((k) => !knownAlertsKeys.has(k));
@@ -397,6 +468,15 @@ export function loadMcpConfig(cliOptions) {
397
468
  });
398
469
  }
399
470
  }
471
+ if (file.otlp && typeof file.otlp === 'object') {
472
+ const unknownOtlpKeys = Object.keys(file.otlp).filter((k) => !knownOtlpKeys.has(k));
473
+ if (unknownOtlpKeys.length > 0) {
474
+ logger.warn('Unknown keys in otlp config (ignored)', {
475
+ unknownFields: unknownOtlpKeys,
476
+ path: configFilePath,
477
+ });
478
+ }
479
+ }
400
480
  // --- Resolve mode early so we can gate licenseKey/accountId requirements ---
401
481
  // File mode is already validated by the zod schema in loadConfigFile.
402
482
  const VALID_MODES = ['cloud', 'local', 'both'];
@@ -446,6 +526,7 @@ export function loadMcpConfig(cliOptions) {
446
526
  const config = {
447
527
  licenseKey,
448
528
  accountId,
529
+ configVersion: typeof file.configVersion === 'number' ? file.configVersion : CURRENT_CONFIG_VERSION,
449
530
  appName: process.env.NEW_RELIC_AI_MCP_APP_NAME ??
450
531
  (typeof file.appName === 'string' ? file.appName : 'preflight'),
451
532
  model: process.env.NEW_RELIC_AI_MODEL ??
@@ -476,7 +557,11 @@ export function loadMcpConfig(cliOptions) {
476
557
  if (Number.isFinite(v) && v > 0)
477
558
  return v;
478
559
  }
479
- return typeof file.sessionBudgetUsd === 'number' ? file.sessionBudgetUsd : null;
560
+ if (typeof file.sessionBudgetUsd !== 'number')
561
+ return null;
562
+ return validateFilePositiveNumber(file.sessionBudgetUsd, 'sessionBudgetUsd')
563
+ ? file.sessionBudgetUsd
564
+ : null;
480
565
  })(),
481
566
  dailyBudgetUsd: (() => {
482
567
  const raw = process.env.NEW_RELIC_AI_DAILY_BUDGET_USD;
@@ -485,7 +570,11 @@ export function loadMcpConfig(cliOptions) {
485
570
  if (Number.isFinite(v) && v > 0)
486
571
  return v;
487
572
  }
488
- return typeof file.dailyBudgetUsd === 'number' ? file.dailyBudgetUsd : null;
573
+ if (typeof file.dailyBudgetUsd !== 'number')
574
+ return null;
575
+ return validateFilePositiveNumber(file.dailyBudgetUsd, 'dailyBudgetUsd')
576
+ ? file.dailyBudgetUsd
577
+ : null;
489
578
  })(),
490
579
  weeklyBudgetUsd: (() => {
491
580
  const raw = process.env.NEW_RELIC_AI_WEEKLY_BUDGET_USD;
@@ -494,13 +583,20 @@ export function loadMcpConfig(cliOptions) {
494
583
  if (Number.isFinite(v) && v > 0)
495
584
  return v;
496
585
  }
497
- return typeof file.weeklyBudgetUsd === 'number' ? file.weeklyBudgetUsd : null;
586
+ if (typeof file.weeklyBudgetUsd !== 'number')
587
+ return null;
588
+ return validateFilePositiveNumber(file.weeklyBudgetUsd, 'weeklyBudgetUsd')
589
+ ? file.weeklyBudgetUsd
590
+ : null;
498
591
  })(),
499
- port: cliOptions?.port ??
592
+ // clampNumber wraps the whole expression, not just envInt's own bounds
593
+ // param, because cliOptions?.port short-circuits envInt (and its
594
+ // clamping) entirely when a CLI value is supplied.
595
+ port: clampNumber(cliOptions?.port ??
500
596
  envInt('NEW_RELIC_AI_MCP_PORT', typeof file.port === 'number' ? file.port : 9847, {
501
597
  min: 1,
502
598
  max: 65535,
503
- }),
599
+ }), { min: 1, max: 65535 }),
504
600
  logLevel: cliOptions?.logLevel ??
505
601
  envLogLevel('NEW_RELIC_AI_MCP_LOG_LEVEL', typeof file.logLevel === 'string' &&
506
602
  ['debug', 'info', 'warn', 'error'].includes(file.logLevel)
@@ -521,89 +617,128 @@ export function loadMcpConfig(cliOptions) {
521
617
  if (Number.isFinite(v) && v > 0)
522
618
  return v;
523
619
  }
524
- if (typeof file.retainSessionsDays === 'number')
525
- return file.retainSessionsDays;
620
+ if (typeof file.retainSessionsDays === 'number') {
621
+ // An invalid (non-positive) value falls back to the default rather
622
+ // than null — null already has a distinct, intentional meaning
623
+ // (explicit opt-out, handled below); a typo'd negative number is a
624
+ // mistake, not an opt-out.
625
+ return validateFilePositiveNumber(file.retainSessionsDays, 'retainSessionsDays')
626
+ ? file.retainSessionsDays
627
+ : DEFAULT_RETAIN_SESSIONS_DAYS;
628
+ }
526
629
  // Explicit `null` in the config file is a deliberate opt-out — distinct
527
630
  // from the key being absent entirely, which falls through to the default.
528
631
  if (file.retainSessionsDays === null)
529
632
  return null;
530
633
  return DEFAULT_RETAIN_SESSIONS_DAYS;
531
634
  })(),
532
- otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
533
- (typeof file.otlpEndpoint === 'string' ? file.otlpEndpoint : null),
534
- otlpHeaders: (() => {
535
- const envValue = process.env.OTEL_EXPORTER_OTLP_HEADERS;
536
- if (envValue)
537
- return parseOtlpHeaders(envValue);
538
- return typeof file.otlpHeaders === 'object' && file.otlpHeaders !== null
539
- ? file.otlpHeaders
540
- : {};
541
- })(),
542
- transport: process.env.NEW_RELIC_AI_TRANSPORT === 'otlp'
543
- ? 'otlp'
544
- : process.env.NEW_RELIC_AI_TRANSPORT === 'both'
545
- ? 'both'
546
- : typeof file.transport === 'string' &&
547
- (file.transport === 'otlp' || file.transport === 'both')
548
- ? file.transport
549
- : 'nr-events-api',
550
635
  mode,
551
- otlpReceiverEnabled: envBool('NR_AI_OTLP_RECEIVER_ENABLED', typeof file.otlpReceiverEnabled === 'boolean' ? file.otlpReceiverEnabled : false),
552
- otlpReceiverPort: envInt('NR_AI_OTLP_RECEIVER_PORT', typeof file.otlpReceiverPort === 'number' ? file.otlpReceiverPort : 4318, { min: 1, max: 65535 }),
553
- otlpReceiverBindAddress: (() => {
554
- const envVal = process.env.NR_AI_OTLP_RECEIVER_BIND_ADDRESS;
555
- if (envVal !== undefined && envVal !== '')
556
- return envVal;
557
- if (typeof file.otlpReceiverBindAddress === 'string' && file.otlpReceiverBindAddress !== '') {
558
- return file.otlpReceiverBindAddress;
559
- }
560
- return '127.0.0.1';
561
- })(),
562
- otlpForwardEndpoint: (() => {
563
- const envVal = process.env.NR_AI_OTLP_FORWARD_ENDPOINT;
564
- // Default to the NR OTLP endpoint only when not in local mode. Local users
565
- // may still set the value explicitly via env or config file (e.g. to point
566
- // at a self-hosted collector); we just don't synthesize a NR default for them.
567
- const defaultEndpoint = mode !== 'local' && licenseKey !== undefined ? 'https://otlp.nr-data.net' : null;
568
- const endpoint = envVal !== undefined
569
- ? envVal || null
570
- : typeof file.otlpForwardEndpoint === 'string'
571
- ? file.otlpForwardEndpoint || null
572
- : defaultEndpoint;
573
- if (endpoint === null)
574
- return null;
575
- try {
576
- const url = new URL(endpoint);
577
- if (url.protocol === 'https:')
578
- return endpoint;
579
- if (url.protocol === 'http:') {
580
- logger.warn('OTLP forward endpoint uses http:// instead of https:// TLS not enabled', {
636
+ // The 8 OTLP-related fields are grouped here (matching the dashboard/alerts
637
+ // nesting precedent). Each still accepts the legacy flat top-level key for
638
+ // backward compatibility — pickOtlpValue() prefers the nested `otlp.X`
639
+ // value and records which legacy keys were actually consulted so exactly
640
+ // those can be named in a single deprecation warning below.
641
+ otlp: (() => {
642
+ const fileOtlp = file.otlp ?? {};
643
+ const usedLegacyOtlpKeys = [];
644
+ const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
645
+ pickOtlpValue(typeof fileOtlp.endpoint === 'string' ? fileOtlp.endpoint : undefined, typeof file.otlpEndpoint === 'string' ? file.otlpEndpoint : undefined, 'otlpEndpoint', usedLegacyOtlpKeys) ??
646
+ null;
647
+ const headers = (() => {
648
+ const envValue = process.env.OTEL_EXPORTER_OTLP_HEADERS;
649
+ if (envValue)
650
+ return parseOtlpHeaders(envValue);
651
+ return (pickOtlpValue(typeof fileOtlp.headers === 'object' && fileOtlp.headers !== null
652
+ ? fileOtlp.headers
653
+ : undefined, typeof file.otlpHeaders === 'object' && file.otlpHeaders !== null
654
+ ? file.otlpHeaders
655
+ : undefined, 'otlpHeaders', usedLegacyOtlpKeys) ?? {});
656
+ })();
657
+ const transportValue = process.env.NEW_RELIC_AI_TRANSPORT === 'otlp'
658
+ ? 'otlp'
659
+ : process.env.NEW_RELIC_AI_TRANSPORT === 'both'
660
+ ? 'both'
661
+ : (pickOtlpValue(fileOtlp.transport === 'otlp' || fileOtlp.transport === 'both'
662
+ ? fileOtlp.transport
663
+ : undefined, file.transport === 'otlp' || file.transport === 'both' ? file.transport : undefined, 'transport', usedLegacyOtlpKeys) ?? 'nr-events-api');
664
+ const receiverEnabled = envBool('NR_AI_OTLP_RECEIVER_ENABLED', pickOtlpValue(typeof fileOtlp.receiverEnabled === 'boolean' ? fileOtlp.receiverEnabled : undefined, typeof file.otlpReceiverEnabled === 'boolean' ? file.otlpReceiverEnabled : undefined, 'otlpReceiverEnabled', usedLegacyOtlpKeys) ?? false);
665
+ const receiverPort = envInt('NR_AI_OTLP_RECEIVER_PORT', pickOtlpValue(typeof fileOtlp.receiverPort === 'number' ? fileOtlp.receiverPort : undefined, typeof file.otlpReceiverPort === 'number' ? file.otlpReceiverPort : undefined, 'otlpReceiverPort', usedLegacyOtlpKeys) ?? 4318, { min: 1, max: 65535 });
666
+ const receiverBindAddress = (() => {
667
+ const envVal = process.env.NR_AI_OTLP_RECEIVER_BIND_ADDRESS;
668
+ if (envVal !== undefined && envVal !== '')
669
+ return envVal;
670
+ return (pickOtlpValue(typeof fileOtlp.receiverBindAddress === 'string' && fileOtlp.receiverBindAddress !== ''
671
+ ? fileOtlp.receiverBindAddress
672
+ : undefined, typeof file.otlpReceiverBindAddress === 'string' && file.otlpReceiverBindAddress !== ''
673
+ ? file.otlpReceiverBindAddress
674
+ : undefined, 'otlpReceiverBindAddress', usedLegacyOtlpKeys) ?? '127.0.0.1');
675
+ })();
676
+ const forwardEndpoint = (() => {
677
+ const envVal = process.env.NR_AI_OTLP_FORWARD_ENDPOINT;
678
+ // Default to the NR OTLP endpoint only when not in local mode. Local users
679
+ // may still set the value explicitly via env or config file (e.g. to point
680
+ // at a self-hosted collector); we just don't synthesize a NR default for them.
681
+ const defaultEndpoint = mode !== 'local' && licenseKey !== undefined ? 'https://otlp.nr-data.net' : null;
682
+ const fileValue = pickOtlpValue(typeof fileOtlp.forwardEndpoint === 'string' ? fileOtlp.forwardEndpoint : undefined, typeof file.otlpForwardEndpoint === 'string' ? file.otlpForwardEndpoint : undefined, 'otlpForwardEndpoint', usedLegacyOtlpKeys);
683
+ const endpoint = envVal !== undefined
684
+ ? envVal || null
685
+ : fileValue !== undefined
686
+ ? fileValue || null
687
+ : defaultEndpoint;
688
+ if (endpoint === null)
689
+ return null;
690
+ try {
691
+ const url = new URL(endpoint);
692
+ if (url.protocol === 'https:')
693
+ return endpoint;
694
+ if (url.protocol === 'http:') {
695
+ logger.warn('OTLP forward endpoint uses http:// instead of https:// — TLS not enabled', { endpoint });
696
+ return endpoint;
697
+ }
698
+ logger.warn('OTLP forward endpoint has unexpected protocol', {
581
699
  endpoint,
700
+ protocol: url.protocol,
582
701
  });
583
702
  return endpoint;
584
703
  }
585
- logger.warn('OTLP forward endpoint has unexpected protocol', {
586
- endpoint,
587
- protocol: url.protocol,
588
- });
589
- return endpoint;
590
- }
591
- catch (_err) {
592
- logger.error('OTLP forward endpoint is not a valid URL', { endpoint });
593
- return null;
594
- }
595
- })(),
596
- otlpForwardHeaders: (() => {
597
- const envValue = process.env.NR_AI_OTLP_FORWARD_HEADERS;
598
- if (envValue !== undefined)
599
- return parseOtlpHeaders(envValue);
600
- if (typeof file.otlpForwardHeaders === 'object' && file.otlpForwardHeaders !== null) {
601
- return file.otlpForwardHeaders;
704
+ catch (_err) {
705
+ logger.error('OTLP forward endpoint is not a valid URL', { endpoint });
706
+ return null;
707
+ }
708
+ })();
709
+ const forwardHeaders = (() => {
710
+ const envValue = process.env.NR_AI_OTLP_FORWARD_HEADERS;
711
+ if (envValue !== undefined)
712
+ return parseOtlpHeaders(envValue);
713
+ const fileValue = pickOtlpValue(typeof fileOtlp.forwardHeaders === 'object' && fileOtlp.forwardHeaders !== null
714
+ ? fileOtlp.forwardHeaders
715
+ : undefined, typeof file.otlpForwardHeaders === 'object' && file.otlpForwardHeaders !== null
716
+ ? file.otlpForwardHeaders
717
+ : undefined, 'otlpForwardHeaders', usedLegacyOtlpKeys);
718
+ if (fileValue !== undefined)
719
+ return fileValue;
720
+ // Don't synthesize a NR api-key header default in local mode — the licenseKey
721
+ // may be present in the env from another tool, and we must not leak it to a
722
+ // forward target the user didn't explicitly configure.
723
+ return mode !== 'local' && licenseKey !== undefined ? { 'api-key': licenseKey } : {};
724
+ })();
725
+ if (usedLegacyOtlpKeys.length > 0) {
726
+ logger.warn('Config file uses deprecated flat OTLP fields — move these under a nested "otlp" object',
727
+ // Field name deliberately avoids "key"/"keys" — the logger's redact()
728
+ // treats any key-shaped property name as secret-bearing and would
729
+ // replace the whole array with '***' (see SECRET_KEY_RE).
730
+ { legacyFieldNames: usedLegacyOtlpKeys, path: configFilePath });
602
731
  }
603
- // Don't synthesize a NR api-key header default in local mode — the licenseKey
604
- // may be present in the env from another tool, and we must not leak it to a
605
- // forward target the user didn't explicitly configure.
606
- return mode !== 'local' && licenseKey !== undefined ? { 'api-key': licenseKey } : {};
732
+ return {
733
+ endpoint,
734
+ headers,
735
+ transport: transportValue,
736
+ receiverEnabled,
737
+ receiverPort,
738
+ receiverBindAddress,
739
+ forwardEndpoint,
740
+ forwardHeaders,
741
+ };
607
742
  })(),
608
743
  personalAlertThresholds: (() => {
609
744
  const fileThresholds = file.alerts?.personal;