@newrelic/preflight 1.6.11 → 1.6.13

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;
@@ -32,31 +34,40 @@ export interface McpServerConfig {
32
34
  readonly nrApiKey: string | null;
33
35
  readonly digestWebhookUrl: string | null;
34
36
  readonly digestSchedule: string;
37
+ /** Default: 90. `null` disables retention (only reachable via an explicit `null` in config.json). */
35
38
  readonly retainSessionsDays: number | null;
36
39
  readonly personalAlertThresholds: PersonalAlertThresholds;
37
- readonly otlpEndpoint: string | null;
38
- readonly otlpHeaders: Readonly<Record<string, string>>;
39
- readonly transport: 'nr-events-api' | 'otlp' | 'both';
40
40
  readonly mode: 'cloud' | 'local' | 'both';
41
41
  readonly platformTarget?: PlatformTarget;
42
- /** Enable the local OTLP/HTTP receiver. Default: false. */
43
- readonly otlpReceiverEnabled: boolean;
44
- /** Port for the local OTLP/HTTP receiver. Default: 4318. */
45
- readonly otlpReceiverPort: number;
46
- /** Bind address for the local OTLP/HTTP receiver. Default: '127.0.0.1'. */
47
- readonly otlpReceiverBindAddress: string;
48
42
  /**
49
- * OTLP forward endpoint where to relay received spans.
50
- * Defaults to New Relic US OTLP endpoint when licenseKey is present.
51
- * 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().
52
47
  */
53
- readonly otlpForwardEndpoint: string | null;
54
- /**
55
- * HTTP headers added to every OTLP forward request (e.g. `{ 'api-key': licenseKey }`).
56
- * Defaults to `{ 'api-key': licenseKey }` when licenseKey is present.
57
- * Configurable via NR_AI_OTLP_FORWARD_HEADERS (comma-separated key=value pairs).
58
- */
59
- 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
+ };
60
71
  readonly dashboard: {
61
72
  readonly port: number;
62
73
  readonly host: string;
@@ -78,6 +89,7 @@ export interface McpServerConfig {
78
89
  export declare const DEFAULT_STORAGE_PATH: string;
79
90
  export type PlatformTarget = 'native' | 'wsl-windows-cc' | 'wsl-linux-cc';
80
91
  export declare const ConfigFileSchema: z.ZodObject<{
92
+ configVersion: z.ZodOptional<z.ZodNumber>;
81
93
  licenseKey: z.ZodOptional<z.ZodString>;
82
94
  accountId: z.ZodOptional<z.ZodString>;
83
95
  appName: z.ZodOptional<z.ZodString>;
@@ -116,6 +128,11 @@ export declare const ConfigFileSchema: z.ZodObject<{
116
128
  otlp: "otlp";
117
129
  both: "both";
118
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>>;
119
136
  mode: z.ZodOptional<z.ZodEnum<{
120
137
  both: "both";
121
138
  cloud: "cloud";
@@ -126,11 +143,20 @@ export declare const ConfigFileSchema: z.ZodObject<{
126
143
  "wsl-windows-cc": "wsl-windows-cc";
127
144
  "wsl-linux-cc": "wsl-linux-cc";
128
145
  }>>;
129
- otlpReceiverEnabled: z.ZodOptional<z.ZodBoolean>;
130
- otlpReceiverPort: z.ZodOptional<z.ZodNumber>;
131
- otlpReceiverBindAddress: z.ZodOptional<z.ZodString>;
132
- otlpForwardEndpoint: z.ZodOptional<z.ZodNullable<z.ZodString>>;
133
- 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>>;
134
160
  alerts: z.ZodOptional<z.ZodObject<{
135
161
  personal: z.ZodOptional<z.ZodObject<{
136
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
@@ -9,8 +9,21 @@ import { REDACTION_PATTERNS as DEFAULT_REDACTION_PATTERNS } from './redaction-pa
9
9
  import { resolveRecordContent } from './record-content-gate.js';
10
10
  const logger = createLogger('mcp-config');
11
11
  export const DEFAULT_STORAGE_PATH = resolve(homedir(), '.newrelic-preflight');
12
+ // Applied only when neither the env var nor the config file specify
13
+ // retainSessionsDays at all — an explicit `null` in config.json still means
14
+ // "retention disabled" (see the resolution logic below).
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;
12
24
  export const ConfigFileSchema = z
13
25
  .object({
26
+ configVersion: z.number().int().optional(),
14
27
  licenseKey: z.string().optional(),
15
28
  accountId: z.string().optional(),
16
29
  appName: z.string().optional(),
@@ -37,16 +50,32 @@ export const ConfigFileSchema = z
37
50
  digestWebhookUrl: z.string().nullable().optional(),
38
51
  digestSchedule: z.string().optional(),
39
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).
40
56
  otlpEndpoint: z.string().nullable().optional(),
41
57
  otlpHeaders: z.record(z.string(), z.string()).optional(),
42
58
  transport: z.enum(['nr-events-api', 'otlp', 'both']).optional(),
43
- mode: z.enum(['cloud', 'local', 'both']).optional(),
44
- platformTarget: z.enum(['native', 'wsl-windows-cc', 'wsl-linux-cc']).optional(),
45
59
  otlpReceiverEnabled: z.boolean().optional(),
46
60
  otlpReceiverPort: z.number().optional(),
47
61
  otlpReceiverBindAddress: z.string().optional(),
48
62
  otlpForwardEndpoint: z.string().nullable().optional(),
49
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(),
50
79
  alerts: z
51
80
  .object({
52
81
  personal: z
@@ -161,18 +190,25 @@ function envBool(key, defaultValue) {
161
190
  return false;
162
191
  return defaultValue;
163
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
+ }
164
204
  function envInt(key, defaultValue, bounds) {
165
205
  const val = process.env[key];
166
206
  if (val === undefined)
167
- return defaultValue;
207
+ return clampNumber(defaultValue, bounds);
168
208
  const parsed = parseInt(val, 10);
169
209
  if (Number.isNaN(parsed))
170
- return defaultValue;
171
- if (bounds?.min !== undefined && parsed < bounds.min)
172
- return bounds.min;
173
- if (bounds?.max !== undefined && parsed > bounds.max)
174
- return bounds.max;
175
- return parsed;
210
+ return clampNumber(defaultValue, bounds);
211
+ return clampNumber(parsed, bounds);
176
212
  }
177
213
  function envLogLevel(key, defaultValue) {
178
214
  const val = process.env[key]?.toLowerCase();
@@ -255,6 +291,20 @@ function parseOtlpHeaders(headerString) {
255
291
  }
256
292
  return result;
257
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
+ }
258
308
  /**
259
309
  * Validate the alerts.rulesPath value. The path is read from the user's
260
310
  * config file or NR_AI_ALERTS_RULES_PATH env var; both are user-controlled
@@ -294,6 +344,21 @@ function validateRulesPath(rawPath, storagePath) {
294
344
  }
295
345
  return resolved;
296
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
+ }
297
362
  function parseProxyUpstreams(envValue, fileValue) {
298
363
  // Env var takes precedence (JSON string)
299
364
  if (envValue) {
@@ -363,6 +428,16 @@ export function loadMcpConfig(cliOptions) {
363
428
  'antiPatternCountMax',
364
429
  ]);
365
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
+ ]);
366
441
  if (file.alerts && typeof file.alerts === 'object') {
367
442
  const alerts = file.alerts;
368
443
  const unknownAlertsKeys = Object.keys(alerts).filter((k) => !knownAlertsKeys.has(k));
@@ -393,6 +468,15 @@ export function loadMcpConfig(cliOptions) {
393
468
  });
394
469
  }
395
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
+ }
396
480
  // --- Resolve mode early so we can gate licenseKey/accountId requirements ---
397
481
  // File mode is already validated by the zod schema in loadConfigFile.
398
482
  const VALID_MODES = ['cloud', 'local', 'both'];
@@ -442,6 +526,7 @@ export function loadMcpConfig(cliOptions) {
442
526
  const config = {
443
527
  licenseKey,
444
528
  accountId,
529
+ configVersion: typeof file.configVersion === 'number' ? file.configVersion : CURRENT_CONFIG_VERSION,
445
530
  appName: process.env.NEW_RELIC_AI_MCP_APP_NAME ??
446
531
  (typeof file.appName === 'string' ? file.appName : 'preflight'),
447
532
  model: process.env.NEW_RELIC_AI_MODEL ??
@@ -472,7 +557,11 @@ export function loadMcpConfig(cliOptions) {
472
557
  if (Number.isFinite(v) && v > 0)
473
558
  return v;
474
559
  }
475
- 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;
476
565
  })(),
477
566
  dailyBudgetUsd: (() => {
478
567
  const raw = process.env.NEW_RELIC_AI_DAILY_BUDGET_USD;
@@ -481,7 +570,11 @@ export function loadMcpConfig(cliOptions) {
481
570
  if (Number.isFinite(v) && v > 0)
482
571
  return v;
483
572
  }
484
- 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;
485
578
  })(),
486
579
  weeklyBudgetUsd: (() => {
487
580
  const raw = process.env.NEW_RELIC_AI_WEEKLY_BUDGET_USD;
@@ -490,13 +583,20 @@ export function loadMcpConfig(cliOptions) {
490
583
  if (Number.isFinite(v) && v > 0)
491
584
  return v;
492
585
  }
493
- 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;
494
591
  })(),
495
- 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 ??
496
596
  envInt('NEW_RELIC_AI_MCP_PORT', typeof file.port === 'number' ? file.port : 9847, {
497
597
  min: 1,
498
598
  max: 65535,
499
- }),
599
+ }), { min: 1, max: 65535 }),
500
600
  logLevel: cliOptions?.logLevel ??
501
601
  envLogLevel('NEW_RELIC_AI_MCP_LOG_LEVEL', typeof file.logLevel === 'string' &&
502
602
  ['debug', 'info', 'warn', 'error'].includes(file.logLevel)
@@ -517,83 +617,128 @@ export function loadMcpConfig(cliOptions) {
517
617
  if (Number.isFinite(v) && v > 0)
518
618
  return v;
519
619
  }
520
- return typeof file.retainSessionsDays === 'number' ? file.retainSessionsDays : null;
521
- })(),
522
- otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
523
- (typeof file.otlpEndpoint === 'string' ? file.otlpEndpoint : null),
524
- otlpHeaders: (() => {
525
- const envValue = process.env.OTEL_EXPORTER_OTLP_HEADERS;
526
- if (envValue)
527
- return parseOtlpHeaders(envValue);
528
- return typeof file.otlpHeaders === 'object' && file.otlpHeaders !== null
529
- ? file.otlpHeaders
530
- : {};
531
- })(),
532
- transport: process.env.NEW_RELIC_AI_TRANSPORT === 'otlp'
533
- ? 'otlp'
534
- : process.env.NEW_RELIC_AI_TRANSPORT === 'both'
535
- ? 'both'
536
- : typeof file.transport === 'string' &&
537
- (file.transport === 'otlp' || file.transport === 'both')
538
- ? file.transport
539
- : 'nr-events-api',
540
- mode,
541
- otlpReceiverEnabled: envBool('NR_AI_OTLP_RECEIVER_ENABLED', typeof file.otlpReceiverEnabled === 'boolean' ? file.otlpReceiverEnabled : false),
542
- otlpReceiverPort: envInt('NR_AI_OTLP_RECEIVER_PORT', typeof file.otlpReceiverPort === 'number' ? file.otlpReceiverPort : 4318, { min: 1, max: 65535 }),
543
- otlpReceiverBindAddress: (() => {
544
- const envVal = process.env.NR_AI_OTLP_RECEIVER_BIND_ADDRESS;
545
- if (envVal !== undefined && envVal !== '')
546
- return envVal;
547
- if (typeof file.otlpReceiverBindAddress === 'string' && file.otlpReceiverBindAddress !== '') {
548
- return file.otlpReceiverBindAddress;
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;
549
628
  }
550
- return '127.0.0.1';
551
- })(),
552
- otlpForwardEndpoint: (() => {
553
- const envVal = process.env.NR_AI_OTLP_FORWARD_ENDPOINT;
554
- // Default to the NR OTLP endpoint only when not in local mode. Local users
555
- // may still set the value explicitly via env or config file (e.g. to point
556
- // at a self-hosted collector); we just don't synthesize a NR default for them.
557
- const defaultEndpoint = mode !== 'local' && licenseKey !== undefined ? 'https://otlp.nr-data.net' : null;
558
- const endpoint = envVal !== undefined
559
- ? envVal || null
560
- : typeof file.otlpForwardEndpoint === 'string'
561
- ? file.otlpForwardEndpoint || null
562
- : defaultEndpoint;
563
- if (endpoint === null)
629
+ // Explicit `null` in the config file is a deliberate opt-out — distinct
630
+ // from the key being absent entirely, which falls through to the default.
631
+ if (file.retainSessionsDays === null)
564
632
  return null;
565
- try {
566
- const url = new URL(endpoint);
567
- if (url.protocol === 'https:')
568
- return endpoint;
569
- if (url.protocol === 'http:') {
570
- logger.warn('OTLP forward endpoint uses http:// instead of https:// — TLS not enabled', {
633
+ return DEFAULT_RETAIN_SESSIONS_DAYS;
634
+ })(),
635
+ mode,
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', {
571
699
  endpoint,
700
+ protocol: url.protocol,
572
701
  });
573
702
  return endpoint;
574
703
  }
575
- logger.warn('OTLP forward endpoint has unexpected protocol', {
576
- endpoint,
577
- protocol: url.protocol,
578
- });
579
- return endpoint;
580
- }
581
- catch (_err) {
582
- logger.error('OTLP forward endpoint is not a valid URL', { endpoint });
583
- return null;
584
- }
585
- })(),
586
- otlpForwardHeaders: (() => {
587
- const envValue = process.env.NR_AI_OTLP_FORWARD_HEADERS;
588
- if (envValue !== undefined)
589
- return parseOtlpHeaders(envValue);
590
- if (typeof file.otlpForwardHeaders === 'object' && file.otlpForwardHeaders !== null) {
591
- 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 });
592
731
  }
593
- // Don't synthesize a NR api-key header default in local mode — the licenseKey
594
- // may be present in the env from another tool, and we must not leak it to a
595
- // forward target the user didn't explicitly configure.
596
- 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
+ };
597
742
  })(),
598
743
  personalAlertThresholds: (() => {
599
744
  const fileThresholds = file.alerts?.personal;