@elastic/synthetics 1.24.4 → 1.26.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 (119) hide show
  1. package/README.md +67 -0
  2. package/dist/cli.js +3 -1
  3. package/dist/cli.js.map +1 -1
  4. package/dist/common_types.d.ts +25 -0
  5. package/dist/common_types.d.ts.map +1 -1
  6. package/dist/core/certs.d.ts +55 -0
  7. package/dist/core/certs.d.ts.map +1 -0
  8. package/dist/core/certs.js +111 -0
  9. package/dist/core/certs.js.map +1 -0
  10. package/dist/core/gatherer.d.ts +4 -4
  11. package/dist/core/gatherer.d.ts.map +1 -1
  12. package/dist/core/gatherer.js +44 -7
  13. package/dist/core/gatherer.js.map +1 -1
  14. package/dist/core/globals.js +1 -1
  15. package/dist/core/index.d.ts +2 -1
  16. package/dist/core/index.d.ts.map +1 -1
  17. package/dist/core/index.js +16 -1
  18. package/dist/core/index.js.map +1 -1
  19. package/dist/core/runner.d.ts +1 -1
  20. package/dist/core/runner.d.ts.map +1 -1
  21. package/dist/core/runner.js +87 -58
  22. package/dist/core/runner.js.map +1 -1
  23. package/dist/dsl/api-journey.d.ts +62 -0
  24. package/dist/dsl/api-journey.d.ts.map +1 -0
  25. package/dist/dsl/api-journey.js +55 -0
  26. package/dist/dsl/api-journey.js.map +1 -0
  27. package/dist/dsl/index.d.ts +1 -0
  28. package/dist/dsl/index.d.ts.map +1 -1
  29. package/dist/dsl/index.js +1 -0
  30. package/dist/dsl/index.js.map +1 -1
  31. package/dist/dsl/journey.d.ts +10 -2
  32. package/dist/dsl/journey.d.ts.map +1 -1
  33. package/dist/dsl/journey.js +11 -2
  34. package/dist/dsl/journey.js.map +1 -1
  35. package/dist/dsl/monitor.d.ts +2 -1
  36. package/dist/dsl/monitor.d.ts.map +1 -1
  37. package/dist/dsl/monitor.js.map +1 -1
  38. package/dist/index.d.ts +2 -2
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +2 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/loader.d.ts.map +1 -1
  43. package/dist/loader.js +66 -1
  44. package/dist/loader.js.map +1 -1
  45. package/dist/network-timings.d.ts +55 -0
  46. package/dist/network-timings.d.ts.map +1 -0
  47. package/dist/network-timings.js +107 -0
  48. package/dist/network-timings.js.map +1 -0
  49. package/dist/options.d.ts +1 -0
  50. package/dist/options.d.ts.map +1 -1
  51. package/dist/options.js +16 -0
  52. package/dist/options.js.map +1 -1
  53. package/dist/plugins/api-network.d.ts +39 -0
  54. package/dist/plugins/api-network.d.ts.map +1 -0
  55. package/dist/plugins/api-network.js +235 -0
  56. package/dist/plugins/api-network.js.map +1 -0
  57. package/dist/plugins/network.d.ts +0 -6
  58. package/dist/plugins/network.d.ts.map +1 -1
  59. package/dist/plugins/network.js +5 -70
  60. package/dist/plugins/network.js.map +1 -1
  61. package/dist/plugins/plugin-manager.d.ts +11 -4
  62. package/dist/plugins/plugin-manager.d.ts.map +1 -1
  63. package/dist/plugins/plugin-manager.js +36 -16
  64. package/dist/plugins/plugin-manager.js.map +1 -1
  65. package/dist/push/index.d.ts.map +1 -1
  66. package/dist/push/index.js +4 -0
  67. package/dist/push/index.js.map +1 -1
  68. package/dist/push/monitor.d.ts.map +1 -1
  69. package/dist/push/monitor.js +1 -1
  70. package/dist/push/monitor.js.map +1 -1
  71. package/dist/push/run-local.d.ts.map +1 -1
  72. package/dist/push/run-local.js +15 -3
  73. package/dist/push/run-local.js.map +1 -1
  74. package/dist/push/utils.d.ts +7 -0
  75. package/dist/push/utils.d.ts.map +1 -1
  76. package/dist/push/utils.js +15 -1
  77. package/dist/push/utils.js.map +1 -1
  78. package/dist/reporters/base.d.ts +2 -2
  79. package/dist/reporters/base.d.ts.map +1 -1
  80. package/dist/reporters/base.js.map +1 -1
  81. package/dist/reporters/index.d.ts +2 -2
  82. package/dist/reporters/index.d.ts.map +1 -1
  83. package/dist/reporters/index.js.map +1 -1
  84. package/dist/reporters/json.d.ts +6 -2
  85. package/dist/reporters/json.d.ts.map +1 -1
  86. package/dist/reporters/json.js +68 -33
  87. package/dist/reporters/json.js.map +1 -1
  88. package/package.json +11 -21
  89. package/src/cli.ts +3 -0
  90. package/src/common_types.ts +34 -0
  91. package/src/core/certs.ts +122 -0
  92. package/src/core/gatherer.ts +63 -10
  93. package/src/core/globals.ts +1 -1
  94. package/src/core/index.ts +28 -0
  95. package/src/core/runner.ts +94 -67
  96. package/src/dsl/api-journey.ts +93 -0
  97. package/src/dsl/index.ts +1 -0
  98. package/src/dsl/journey.ts +21 -9
  99. package/src/dsl/monitor.ts +2 -0
  100. package/src/index.ts +6 -0
  101. package/src/loader.ts +78 -2
  102. package/src/network-timings.ts +130 -0
  103. package/src/options.ts +25 -0
  104. package/src/plugins/api-network.ts +252 -0
  105. package/src/plugins/network.ts +5 -87
  106. package/src/plugins/plugin-manager.ts +60 -19
  107. package/src/push/index.ts +11 -0
  108. package/src/push/monitor.ts +6 -2
  109. package/src/push/run-local.ts +25 -15
  110. package/src/push/utils.ts +15 -0
  111. package/src/reporters/base.ts +2 -1
  112. package/src/reporters/index.ts +2 -1
  113. package/src/reporters/json.ts +74 -41
  114. package/templates/README.md +3 -1
  115. package/templates/journeys/advanced-api-example-helpers.ts +79 -0
  116. package/templates/journeys/advanced-api-example.journey.ts +52 -0
  117. package/templates/journeys/api-example.journey.ts +35 -0
  118. package/templates/synthetics.config.ts +2 -0
  119. package/.husky/install.mjs +0 -7
@@ -0,0 +1,93 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present, Elastic NV
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ * THE SOFTWARE.
23
+ *
24
+ */
25
+
26
+ import { APIRequestContext } from 'playwright-core';
27
+ import { Location, Params } from '../common_types';
28
+
29
+ import { Journey } from './journey';
30
+ import { Monitor, MonitorConfig } from './monitor';
31
+ import { RunnerInfo } from '../core/runner';
32
+
33
+ export type APIJourneyOptions = {
34
+ name: string;
35
+ id?: string;
36
+ tags?: string[];
37
+ };
38
+
39
+ export type APIJourneyCallbackOpts = {
40
+ params: Params;
41
+ request: APIRequestContext;
42
+ info: RunnerInfo;
43
+ };
44
+ export type APIJourneyCallback = (options: APIJourneyCallbackOpts) => void;
45
+
46
+ type APIJourneyType = (
47
+ options: string | APIJourneyOptions,
48
+ callback: APIJourneyCallback
49
+ ) => APIJourney;
50
+
51
+ /**
52
+ * Same step-based model as a browser `Journey` but without a Chromium
53
+ * context. `type === 'api'` lets the runner/gatherer/plugin-manager skip
54
+ * browser-only setup.
55
+ */
56
+ export class APIJourney extends Journey {
57
+ constructor(
58
+ options: APIJourneyOptions | string,
59
+ cb: APIJourneyCallback,
60
+ location?: Location
61
+ ) {
62
+ const opts =
63
+ typeof options === 'string'
64
+ ? { name: options, id: options }
65
+ : { ...options };
66
+ // Runner narrows callback args by `journey.type`, so the cast is sound.
67
+ super({ ...opts, type: 'api' }, cb as any, location);
68
+ }
69
+
70
+ // Register as an `api` project monitor on `synthetics push`.
71
+ override _updateMonitor(config: MonitorConfig) {
72
+ this._setMonitor(
73
+ new Monitor({
74
+ name: this.name,
75
+ id: this.id,
76
+ type: 'api',
77
+ tags: this.tags ?? [],
78
+ ...config,
79
+ })
80
+ );
81
+ }
82
+ }
83
+
84
+ export type APIJourneyWithAnnotations = APIJourneyType & {
85
+ /**
86
+ * Skip this journey and all its steps
87
+ */
88
+ skip: APIJourneyType;
89
+ /**
90
+ * Run only this journey and skip rest of the journeys
91
+ */
92
+ only: APIJourneyType;
93
+ };
package/src/dsl/index.ts CHANGED
@@ -24,4 +24,5 @@
24
24
  */
25
25
 
26
26
  export * from './journey';
27
+ export * from './api-journey';
27
28
  export * from './step';
@@ -50,7 +50,7 @@ export type JourneyOptions = {
50
50
 
51
51
  type HookType = 'before' | 'after';
52
52
  export type Hooks = Record<HookType, Array<HooksCallback>>;
53
- type JourneyCallbackOpts = {
53
+ export type JourneyCallbackOpts = {
54
54
  page: Page;
55
55
  context: BrowserContext;
56
56
  browser: Browser;
@@ -62,6 +62,7 @@ type JourneyCallbackOpts = {
62
62
  export type JourneyCallback = (options: JourneyCallbackOpts) => void;
63
63
 
64
64
  export class Journey {
65
+ readonly type: 'browser' | 'api' = 'browser';
65
66
  readonly name: string;
66
67
  readonly id?: string;
67
68
  readonly tags?: string[];
@@ -78,10 +79,11 @@ export class Journey {
78
79
  error?: Error;
79
80
 
80
81
  constructor(
81
- options: JourneyOptions,
82
+ options: JourneyOptions & { type?: 'browser' | 'api' },
82
83
  cb: JourneyCallback,
83
84
  location?: Location
84
85
  ) {
86
+ this.type = options.type ?? 'browser';
85
87
  this.name = options.name;
86
88
  this.id = options.id || options.name;
87
89
  this.tags = options.tags;
@@ -144,13 +146,23 @@ export class Journey {
144
146
  /**
145
147
  * Use defaults values from journey for monitor object (id, name and tags)
146
148
  */
147
- this.#monitor = new Monitor({
148
- name: this.name,
149
- id: this.id,
150
- type: 'browser',
151
- tags: this.tags ?? [],
152
- ...config,
153
- });
149
+ this._setMonitor(
150
+ new Monitor({
151
+ name: this.name,
152
+ id: this.id,
153
+ type: 'browser',
154
+ tags: this.tags ?? [],
155
+ ...config,
156
+ })
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Subclasses use this to swap in a monitor with a different default
162
+ * `type` while reusing the source-location and name-filter wiring.
163
+ */
164
+ protected _setMonitor(monitor: Monitor) {
165
+ this.#monitor = monitor;
154
166
  this.#monitor.setSource(this.location);
155
167
  this.#monitor.setFilter({ match: this.name });
156
168
  }
@@ -32,6 +32,7 @@ import {
32
32
  ScreenshotOptions,
33
33
  Params,
34
34
  PlaywrightOptions,
35
+ CertificateErrorSpkiAllowlist,
35
36
  } from '../common_types';
36
37
  import { indent, isMatch } from '../helpers';
37
38
  import { LocationsMap } from '../locations/public-locations';
@@ -72,6 +73,7 @@ export type MonitorConfig = {
72
73
  screenshot?: ScreenshotOptions;
73
74
  params?: Params;
74
75
  playwrightOptions?: PlaywrightOptions;
76
+ certificateErrorSpkiAllowlist?: CertificateErrorSpkiAllowlist;
75
77
  alert?: AlertConfig;
76
78
  /**
77
79
  * By default, the monitor will be retested on failure
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ export async function run(options: RunOptions) {
35
35
  */
36
36
  export {
37
37
  journey,
38
+ apiJourney,
38
39
  step,
39
40
  monitor,
40
41
  beforeAll,
@@ -82,6 +83,11 @@ export type {
82
83
  JourneyWithAnnotations,
83
84
  JourneyOptions,
84
85
  StepWithAnnotations,
86
+ APIJourney,
87
+ APIJourneyCallback,
88
+ APIJourneyCallbackOpts,
89
+ APIJourneyOptions,
90
+ APIJourneyWithAnnotations,
85
91
  } from './dsl';
86
92
 
87
93
  export type {
package/src/loader.ts CHANGED
@@ -24,7 +24,10 @@
24
24
  */
25
25
 
26
26
  import { stdin, cwd } from 'process';
27
- import { extname, resolve } from 'path';
27
+ import { extname, resolve, join } from 'path';
28
+ import { tmpdir } from 'os';
29
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
30
+ import Module from 'module';
28
31
  import { CliArgs } from './common_types';
29
32
  import { step, journey } from './core';
30
33
  import { log } from './core/logger';
@@ -83,7 +86,21 @@ export async function loadTestFiles(options: CliArgs, args: string[]) {
83
86
  requireSuites(suites);
84
87
  }
85
88
 
86
- const loadInlineScript = source => {
89
+ /**
90
+ * A leading `import`/`export` opts the inline source into module loading;
91
+ * anything else is treated as legacy statements with injected locals.
92
+ * Keying off `import`/`export` (not a `journey(` regex) avoids false
93
+ * matches inside strings/comments that would strip the implicit injection.
94
+ */
95
+ const isModuleInlineSource = (source: string): boolean => {
96
+ return /^\s*(?:import|export)\b/m.test(source);
97
+ };
98
+
99
+ const loadInlineScript = (source: string) => {
100
+ if (isModuleInlineSource(source)) {
101
+ loadInlineModule(source);
102
+ return;
103
+ }
87
104
  const scriptFn = new Function(
88
105
  'step',
89
106
  'page',
@@ -109,6 +126,65 @@ const loadInlineScript = source => {
109
126
  });
110
127
  };
111
128
 
129
+ const SYNTHETICS_PKG_ROOT = resolve(__dirname, '..');
130
+ let syntheticsAliasInstalled = false;
131
+
132
+ /**
133
+ * Alias `@elastic/synthetics` (and subpaths) to this package so an inline
134
+ * module materialised in a temp dir — whose ancestors may lack a
135
+ * `node_modules` entry — resolves to the currently running code.
136
+ */
137
+ const installSyntheticsRequireAlias = () => {
138
+ if (syntheticsAliasInstalled) return;
139
+ syntheticsAliasInstalled = true;
140
+ const PKG = '@elastic/synthetics';
141
+ const ModuleInternal = Module as unknown as {
142
+ _resolveFilename: (
143
+ request: string,
144
+ parent: NodeModule,
145
+ isMain?: boolean,
146
+ options?: Record<string, unknown>
147
+ ) => string;
148
+ };
149
+ const originalResolve = ModuleInternal._resolveFilename;
150
+ ModuleInternal._resolveFilename = function (
151
+ request,
152
+ parent,
153
+ isMain,
154
+ options
155
+ ) {
156
+ if (request === PKG || request.startsWith(`${PKG}/`)) {
157
+ const subpath = request === PKG ? '' : request.slice(PKG.length + 1);
158
+ const aliased = subpath
159
+ ? join(SYNTHETICS_PKG_ROOT, subpath)
160
+ : SYNTHETICS_PKG_ROOT;
161
+ return originalResolve.call(this, aliased, parent, isMain, options);
162
+ }
163
+ return originalResolve.call(this, request, parent, isMain, options);
164
+ };
165
+ };
166
+
167
+ /**
168
+ * Materialise a self-contained inline module to disk and `require` it. The
169
+ * esbuild transform hook rewrites ESM → CJS, so the script registers its
170
+ * journeys on import instead of being wrapped in an implicit journey.
171
+ */
172
+ const loadInlineModule = (source: string) => {
173
+ installSyntheticsRequireAlias();
174
+ const dir = mkdtempSync(join(tmpdir(), 'synthetics-inline-'));
175
+ // Best-effort cleanup so long-lived hosts don't accumulate temp dirs.
176
+ process.once('exit', () => {
177
+ try {
178
+ rmSync(dir, { recursive: true, force: true });
179
+ } catch {
180
+ /* best-effort */
181
+ }
182
+ });
183
+ const file = join(dir, 'inline.journey.ts');
184
+ writeFileSync(file, source);
185
+ require(file);
186
+ };
187
+
112
188
  /**
113
189
  * Read the input from STDIN and run it as inline journey
114
190
  */
@@ -0,0 +1,130 @@
1
+ /**
2
+ * MIT License
3
+ *
4
+ * Copyright (c) 2020-present, Elastic NV
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in
14
+ * all copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ * THE SOFTWARE.
23
+ *
24
+ */
25
+
26
+ import { Request } from 'playwright-core';
27
+ import { NetworkInfo } from './common_types';
28
+
29
+ /**
30
+ * Resource Timing shape shared by `Request.timing()` and (>= 1.62)
31
+ * `APIResponse.timing()`. Values are milliseconds relative to `startTime`,
32
+ * `-1` when a phase did not happen or is unknown (e.g. reused keep-alive
33
+ * socket, HAR replay).
34
+ */
35
+ export type ResourceTiming = ReturnType<Request['timing']>;
36
+
37
+ export const roundMilliSecs = (value: number): number => {
38
+ return Math.floor(value * 1000) / 1000;
39
+ };
40
+
41
+ /**
42
+ * First positive number in the list, used to derive the `blocked` phase from
43
+ * whichever Resource Timing marker fired first.
44
+ */
45
+ export const firstPositive = (numbers: number[]): number | null => {
46
+ for (let i = 0; i < numbers.length; ++i) {
47
+ if (numbers[i] > 0) {
48
+ return numbers[i];
49
+ }
50
+ }
51
+ return null;
52
+ };
53
+
54
+ /**
55
+ * Maps a Playwright Resource Timing object into the per-phase `NetworkInfo`
56
+ * timings. `total` is left at `-1` for the caller to fill via `calcTotalTime`
57
+ * once `loadEndTime`/`requestSentTime` are known. `receive` resolves to `-1`
58
+ * until `responseEnd` is available (browser journeys learn it on
59
+ * `requestfinished`); API journeys have the full timing up front.
60
+ */
61
+ export function getResourceTimings(
62
+ rtiming: ResourceTiming
63
+ ): NetworkInfo['timings'] {
64
+ const blocked =
65
+ roundMilliSecs(
66
+ firstPositive([
67
+ rtiming.domainLookupStart,
68
+ rtiming.connectStart,
69
+ rtiming.requestStart,
70
+ ])
71
+ ) || -1;
72
+ const dns =
73
+ rtiming.domainLookupEnd !== -1
74
+ ? roundMilliSecs(rtiming.domainLookupEnd - rtiming.domainLookupStart)
75
+ : -1;
76
+ const connect =
77
+ rtiming.connectEnd !== -1
78
+ ? roundMilliSecs(rtiming.connectEnd - rtiming.connectStart)
79
+ : -1;
80
+ const ssl =
81
+ rtiming.secureConnectionStart !== -1
82
+ ? roundMilliSecs(rtiming.connectEnd - rtiming.secureConnectionStart)
83
+ : -1;
84
+ const wait =
85
+ rtiming.responseStart !== -1
86
+ ? roundMilliSecs(rtiming.responseStart - rtiming.requestStart)
87
+ : -1;
88
+ const receive =
89
+ rtiming.responseEnd !== -1
90
+ ? roundMilliSecs(rtiming.responseEnd - rtiming.responseStart)
91
+ : -1;
92
+
93
+ return {
94
+ blocked,
95
+ dns,
96
+ ssl,
97
+ connect,
98
+ send: 0, // not exposed via the Resource Timing API
99
+ wait,
100
+ receive,
101
+ total: -1,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * Returns the `total` phase by summing the positive phases. Falls back to the
107
+ * wall-clock span between `requestSentTime` and `loadEndTime` when Resource
108
+ * Timing data is unavailable (`startTime <= 0`, e.g. HAR replay or aborted
109
+ * requests). Pure — the caller assigns the result onto `timings.total`.
110
+ */
111
+ export function calcTotalTime(
112
+ entry: NetworkInfo,
113
+ rtiming: ResourceTiming
114
+ ): number {
115
+ if (rtiming.startTime <= 0) {
116
+ const end =
117
+ entry.loadEndTime || entry.responseReceivedTime || entry.requestSentTime;
118
+ const total = roundMilliSecs((end - entry.requestSentTime) * 1000);
119
+ return total <= 0 ? -1 : total;
120
+ }
121
+
122
+ const { timings } = entry;
123
+ return [
124
+ timings.blocked,
125
+ timings.dns,
126
+ timings.connect,
127
+ timings.wait,
128
+ timings.receive,
129
+ ].reduce((pre, cur) => ((cur || -1) > 0 ? cur + pre : pre), 0);
130
+ }
package/src/options.ts CHANGED
@@ -28,6 +28,7 @@ import { createOption } from 'commander';
28
28
  import { readConfig } from './config';
29
29
  import type { CliArgs, RunOptions } from './common_types';
30
30
  import { isFile, THROTTLING_WARNING_MSG, warn } from './helpers';
31
+ import { normalizeCertificateErrorSpkiAllowlist } from './core/certs';
31
32
  import { readFileSync } from 'fs';
32
33
 
33
34
  type Mode = 'run' | 'push';
@@ -103,6 +104,24 @@ export async function normalizeOptions(
103
104
  merge(config?.proxy ?? {}, cliArgs?.proxy || {})
104
105
  );
105
106
 
107
+ /**
108
+ * Merge PEM certificates from the Synthetics config and the CLI. Each entry
109
+ * can be inline PEM content or a path to a PEM file that we resolve here so
110
+ * downstream consumers only ever deal with PEM strings used to build the
111
+ * Chromium SPKI certificate-error allowlist.
112
+ */
113
+ const certificateErrorSpkiAllowlist = [
114
+ ...normalizeCertificateErrorSpkiAllowlist(
115
+ config.certificateErrorSpkiAllowlist
116
+ ),
117
+ ...normalizeCertificateErrorSpkiAllowlist(
118
+ cliArgs.certificateErrorSpkiAllowlist
119
+ ),
120
+ ].map(entry => (isFile(entry) ? readFileSync(entry, 'utf-8') : entry));
121
+ options.certificateErrorSpkiAllowlist = certificateErrorSpkiAllowlist.length
122
+ ? certificateErrorSpkiAllowlist
123
+ : undefined;
124
+
106
125
  /**
107
126
  * Merge default options based on the mode of operation whether we are running tests locally
108
127
  * or pushing the project monitors
@@ -265,6 +284,11 @@ export function getCommonCommandOpts() {
265
284
  "List of Kibana's Maintenance Windows IDs assigned by default. More information on https://www.elastic.co/docs/explore-analyze/alerts-cases/alerts/maintenance-windows."
266
285
  );
267
286
 
287
+ const certificateErrorSpkiAllowlist = createOption(
288
+ '--certificate-error-spki-allowlist <pathOrPem...>',
289
+ "One or more PEM certificates, each provided as inline content or a path to a PEM file. Their SPKI hashes are used to bypass Chromium certificate errors for matching presented certificates; this does not add a CA to Chromium's trust store."
290
+ );
291
+
268
292
  return {
269
293
  auth,
270
294
  authMandatory,
@@ -276,6 +300,7 @@ export function getCommonCommandOpts() {
276
300
  match,
277
301
  fields,
278
302
  maintenanceWindows,
303
+ certificateErrorSpkiAllowlist,
279
304
  };
280
305
  }
281
306