@contrast/agent 5.1.0 → 5.2.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.
@@ -21,13 +21,19 @@ export default function checkImportVsLoaderVsNodeVersion() {
21
21
  // allow testing to ignore these restrictions
22
22
  const noValidate = process.env.CSI_EXPOSE_CORE === 'no-validate';
23
23
  let flag;
24
+ const { execArgv, version, env: { NODE_OPTIONS } } = process;
24
25
 
25
- const { execArgv, version } = process;
26
26
  // eslint-disable-next-line newline-per-chained-call
27
27
  const [major, minor] = version.slice(1).split('.').map(Number);
28
- for (let i = 0; i < execArgv.length; i++) {
29
- const loader = execArgv[i];
30
- const agent = execArgv[i + 1];
28
+
29
+ const _execArgs = [
30
+ ...(NODE_OPTIONS?.split?.(/\s+/).map((v) => v.trim()) || []),
31
+ ...execArgv,
32
+ ]
33
+
34
+ for (let i = 0; i < _execArgs.length; i++) {
35
+ const loader = _execArgs[i];
36
+ const agent = _execArgs[i + 1];
31
37
  if (['--import', '--loader', '--experimental-loader'].includes(loader) && agent?.startsWith('@contrast/agent')) {
32
38
  flag = loader;
33
39
  if (noValidate) {
package/lib/esm-hooks.mjs CHANGED
@@ -28,229 +28,233 @@ if (process.env.CSI_HOOKS_LOG) {
28
28
  const [major, minor] = process.versions.node.split('.').map(it => +it);
29
29
  const isLT16_12 = major < 16 || (major === 16 && minor < 12);
30
30
 
31
+
32
+ // from esmock: git@github.com:iambumblehead/esmock.git
33
+ //
34
+ // new versions of node: when multiple loaders are used and context
35
+ // is passed to nextResolve, the process crashes in a recursive call
36
+ // see: /esmock/issues/#48
37
+ //
38
+ // old versions of node: if context.parentURL is defined, and context
39
+ // is not passed to nextResolve, the tests fail
40
+ //
41
+ // later versions of node v16 include 'node-addons'
42
+ async function protectedNextResolve(nextResolve, specifier, context) {
43
+ if (context.parentURL) {
44
+ if (context.conditions.at(-1) === 'node-addons' || context.importAssertions || isLT16_12) {
45
+ return nextResolve(specifier, context);
46
+ }
47
+ }
48
+
49
+ return nextResolve(specifier);
50
+ }
51
+
31
52
  // import.meta.resolve('node-fetch') v20 became synchronous. not all that useful for now because
32
53
  // of the significant break in behavior. but the function is great - it resolves a specifier to the
33
54
  // file that would be loaded.
34
55
  // file:///home/bruce/github/csi/rasp-v3/node_modules/node-fetch/src/index.js
35
56
 
36
57
  const { default: core } = await import('./initialize.mjs');
37
- const { esmHooks: { mappings, fixPath, getFileType } } = core;
38
-
39
- const initialize = Module.register && async function(data) {
40
- if (data?.port) {
41
- data.port.on('message', _msg => {
42
- // we don't currently send messages to the loader thread but when we do,
43
- // this is where they will show up.
44
- //console.log('ESM HOOK -> INIT -> MESSAGE', _msg);
45
- });
46
- data.port.postMessage({ type: 'keep-alive', data: 'hello from esm-hooks' });
47
- data.port.unref();
48
- // this is running in the loader thread. save thread info because it can't
49
- // be set from the main thread.
50
- if (W.isMainThread) {
51
- throw new Error('initialize() called from main thread.');
58
+ let load, resolve, initialize;
59
+
60
+ if (core) {
61
+ const { esmHooks: { mappings, fixPath, getFileType } } = core;
62
+ initialize = Module.register && async function(data) {
63
+ if (data?.port) {
64
+ data.port.on('message', _msg => {
65
+ // we don't currently send messages to the loader thread but when we do,
66
+ // this is where they will show up.
67
+ //console.log('ESM HOOK -> INIT -> MESSAGE', _msg);
68
+ });
69
+ data.port.postMessage({ type: 'keep-alive', data: 'hello from esm-hooks' });
70
+ data.port.unref();
71
+ // this is running in the loader thread. save thread info because it can't
72
+ // be set from the main thread.
73
+ if (W.isMainThread) {
74
+ throw new Error('initialize() called from main thread.');
75
+ }
76
+ core.threadInfo.isMainThread = false;
77
+ core.threadInfo.threadId = W.threadId;
78
+ core.threadInfo.port = data.port;
79
+ // the loader thread's post() sends via the port.
80
+ core.threadInfo.post = (type, data) => {
81
+ core.threadInfo.port.postMessage({ type, data });
82
+ };
52
83
  }
53
- core.threadInfo.isMainThread = false;
54
- core.threadInfo.threadId = W.threadId;
55
- core.threadInfo.port = data.port;
56
- // the loader thread's post() sends via the port.
57
- core.threadInfo.post = (type, data) => {
58
- core.threadInfo.port.postMessage({ type, data });
59
- };
60
- }
61
84
 
62
- log && console.log('ESM HOOK -> INIT');
63
- // it's not clear to me why Module is present when initialize() is being
64
- // executed in the loader thread. but it is. (code here originally loaded
65
- // Module via createRequire(), but that is not necessary.)
66
- const originalRequire = Module.prototype.require;
67
- const originalCompile = Module.prototype._compile;
68
- const originalExtensions = Module._extensions['.js'];
69
- const originalLoad = Module._load;
70
-
71
- // Module needs to be patched in the loader thread too. initialize() runs in
72
- // that context.
73
- Module.prototype.require = function(moduleId) {
74
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> require(${moduleId})`);
75
- return originalRequire.call(this, moduleId);
76
- };
85
+ log && console.log('ESM HOOK -> INIT');
86
+ // it's not clear to me why Module is present when initialize() is being
87
+ // executed in the loader thread. but it is. (code here originally loaded
88
+ // Module via createRequire(), but that is not necessary.)
89
+ const originalRequire = Module.prototype.require;
90
+ const originalCompile = Module.prototype._compile;
91
+ const originalExtensions = Module._extensions['.js'];
92
+ const originalLoad = Module._load;
93
+
94
+ // Module needs to be patched in the loader thread too. initialize() runs in
95
+ // that context.
96
+ Module.prototype.require = function(moduleId) {
97
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> require(${moduleId})`);
98
+ return originalRequire.call(this, moduleId);
99
+ };
77
100
 
78
- Module.prototype._compile = function(code, filename) {
79
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _compile(${filename})`);
80
- return originalCompile.call(this, code, filename);
81
- };
101
+ Module.prototype._compile = function(code, filename) {
102
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _compile(${filename})`);
103
+ return originalCompile.call(this, code, filename);
104
+ };
82
105
 
83
- Module._extensions['.js'] = function(module, filename) {
84
- (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _extensions[".js"]: ${filename}`);
85
- return originalExtensions.call(this, module, filename);
86
- };
106
+ Module._extensions['.js'] = function(module, filename) {
107
+ (log & logRequireAll) && console.log(`CJS(init ${W.threadId}) -> _extensions[".js"]: ${filename}`);
108
+ return originalExtensions.call(this, module, filename);
109
+ };
87
110
 
88
- Module._load = function(request, parent, isMain) {
89
- (log & logLoad) && console.log(`CJS(init ${W.threadId}) -> _load(${request})`);
90
- return originalLoad.call(this, request, parent, isMain);
111
+ Module._load = function(request, parent, isMain) {
112
+ (log & logLoad) && console.log(`CJS(init ${W.threadId}) -> _load(${request})`);
113
+ return originalLoad.call(this, request, parent, isMain);
114
+ };
91
115
  };
92
- };
93
116
 
94
- const resolve = async function(specifier, context, nextResolve) {
95
- (log & logResolve) && console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier}`);
96
- let isFlaggedToPatch = false;
97
- if (context.parentURL) {
98
- isFlaggedToPatch = context.parentURL.endsWith('csi-flag=', context.parentURL.length - 1);
99
- }
117
+ resolve = async function(specifier, context, nextResolve) {
118
+ (log & logResolve) && console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier}`);
119
+ let isFlaggedToPatch = false;
120
+ if (context.parentURL) {
121
+ isFlaggedToPatch = context.parentURL.endsWith('csi-flag=', context.parentURL.length - 1);
122
+ }
100
123
 
101
- // this needs to be generalized so it uses the execArgv value. the idea is to
102
- // capture when the application actually starts. is this needed? i don't think so
103
- // because the loaders aren't instantiated until esm-loader.mjs returns/register()s
104
- // the hooks. i am leaving the comment here as a breadcrumb in case we need to
105
- // capture the application start time. but we could 1) look at process.argv, 2)
106
- // find the first argument, and 3) notice when that's loaded and capture that
107
- // "now we are in the user's application".
108
- //
109
- //if (false) debugger;
110
- //if (log & logApplication) {
111
- // const RE = /test-integration-servers\/express.m?js(\?.+)?$/;
112
- // if (specifier.match(RE) || context.parentURL?.match(RE)) {
113
- // console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier} from ${context.parentURL}`);
114
- // }
115
- //}
116
-
117
- // this works for most of what we need to patch because they are not esm-native
118
- // modules. but for esm-native modules, e.g., node-fetch, this won't work. they
119
- // will need to be patched in the loader thread.
120
- //
121
- // We could walk up the parent chain to see if we should interpret this specifier
122
- // as a module or commonjs, but it seems more straightforward to just always
123
- // redirect. walking up the parent chain would let us avoid redirecting commonjs
124
- // files. we could also call nextResolve() and let node tell us type of the file
125
- // is but that would potentially create problems when other hooks, e.g., datadog,
126
- // are in place.
127
- //
128
- // also we could check to see if this module's already been patched and skip this.
129
- // not sure of that though; it might get loaded as a module, not a commonjs file,
130
- // and that would be a problem. so when we start patching esm-native modules, we'll
131
- // need a second "already patched" weakmap.
132
- if (!isFlaggedToPatch && specifier in mappings) {
133
- // eslint-disable-next-line prefer-const
134
- let { url, format, target } = mappings[specifier];
135
- // set flag to module or commonjs. i'm not sure this is needed but am keeping it
136
- // in place until we have to implement esm-native module rewrites/wrapping. this
137
- // is the point at which resolve() communicates to load().
124
+ // this needs to be generalized so it uses the execArgv value. the idea is to
125
+ // capture when the application actually starts. is this needed? i don't think so
126
+ // because the loaders aren't instantiated until esm-loader.mjs returns/register()s
127
+ // the hooks. i am leaving the comment here as a breadcrumb in case we need to
128
+ // capture the application start time. but we could 1) look at process.argv, 2)
129
+ // find the first argument, and 3) notice when that's loaded and capture that
130
+ // "now we are in the user's application".
131
+ //
132
+ //if (false) debugger;
133
+ //if (log & logApplication) {
134
+ // const RE = /test-integration-servers\/express.m?js(\?.+)?$/;
135
+ // if (specifier.match(RE) || context.parentURL?.match(RE)) {
136
+ // console.log(`ESM HOOK(${W.threadId}) -> RESOLVE -> ${specifier} from ${context.parentURL}`);
137
+ // }
138
+ //}
139
+
140
+ // this works for most of what we need to patch because they are not esm-native
141
+ // modules. but for esm-native modules, e.g., node-fetch, this won't work. they
142
+ // will need to be patched in the loader thread.
138
143
  //
139
- // builtin's are probably the most likely to be loaded, so they're first.
140
- // some tweaks might be needed when we start to patch esm-native modules
141
- // in esm-native-code:
142
- // https://nodejs.org/docs/latest-v20.x/api/esm.html#builtin-modules
143
- // https://nodejs.org/docs/latest-v20.x/api/module.html#modulesyncbuiltinesmexports
144
- if (target === 'builtin') {
145
- url = `${url.href}?csi-flag=m`;
146
- format = 'module';
147
- } else if (target === 'commonjs') {
148
- url = `${url.href}?csi-flag=c`;
149
- format = getFileType(url) || format || 'commonjs';
150
- } else if (target === 'module') {
151
- url = `${url.href}?csi-flag=m`;
152
- format = getFileType(url) || format || 'module';
153
- } else {
154
- throw new Error(`unexpected target ${target} for ${specifier}`);
144
+ // We could walk up the parent chain to see if we should interpret this specifier
145
+ // as a module or commonjs, but it seems more straightforward to just always
146
+ // redirect. walking up the parent chain would let us avoid redirecting commonjs
147
+ // files. we could also call nextResolve() and let node tell us type of the file
148
+ // is but that would potentially create problems when other hooks, e.g., datadog,
149
+ // are in place.
150
+ //
151
+ // also we could check to see if this module's already been patched and skip this.
152
+ // not sure of that though; it might get loaded as a module, not a commonjs file,
153
+ // and that would be a problem. so when we start patching esm-native modules, we'll
154
+ // need a second "already patched" weakmap.
155
+ if (!isFlaggedToPatch && specifier in mappings) {
156
+ // eslint-disable-next-line prefer-const
157
+ let { url, format, target } = mappings[specifier];
158
+ // set flag to module or commonjs. i'm not sure this is needed but am keeping it
159
+ // in place until we have to implement esm-native module rewrites/wrapping. this
160
+ // is the point at which resolve() communicates to load().
161
+ //
162
+ // builtin's are probably the most likely to be loaded, so they're first.
163
+ // some tweaks might be needed when we start to patch esm-native modules
164
+ // in esm-native-code:
165
+ // https://nodejs.org/docs/latest-v20.x/api/esm.html#builtin-modules
166
+ // https://nodejs.org/docs/latest-v20.x/api/module.html#modulesyncbuiltinesmexports
167
+ if (target === 'builtin') {
168
+ url = `${url.href}?csi-flag=m`;
169
+ format = 'module';
170
+ } else if (target === 'commonjs') {
171
+ url = `${url.href}?csi-flag=c`;
172
+ format = getFileType(url) || format || 'commonjs';
173
+ } else if (target === 'module') {
174
+ url = `${url.href}?csi-flag=m`;
175
+ format = getFileType(url) || format || 'module';
176
+ } else {
177
+ throw new Error(`unexpected target ${target} for ${specifier}`);
178
+ }
179
+
180
+ return {
181
+ url,
182
+ format,
183
+ shortCircuit: true,
184
+ };
155
185
  }
156
186
 
157
- return {
158
- url,
159
- format,
160
- shortCircuit: true,
161
- };
162
- }
163
-
164
- return protectedNextResolve(nextResolve, specifier, context);
165
- };
166
-
167
- // readFile is a live binding. we need to capture it so it won't be
168
- // altered by any patching later.
169
- const readFile = rf;
170
-
171
- const load = async function(url, context, nextLoad) {
172
- (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD ${url}`);
187
+ return protectedNextResolve(nextResolve, specifier, context);
188
+ };
173
189
 
174
- const urlObject = new URL(url);
190
+ // readFile is a live binding. we need to capture it so it won't be
191
+ // altered by any patching later.
192
+ const readFile = rf;
175
193
 
176
- if (urlObject.searchParams.has('csi-flag')) {
177
- // target type will, i think, be used to determine if this will require extra
178
- // processing of some sort for esm-native modules.
179
- // eslint-disable-next-line no-unused-vars
180
- const targetType = urlObject.searchParams.get('csi-flag');
181
- const { pathname } = urlObject;
194
+ load = async function(url, context, nextLoad) {
195
+ (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD ${url}`);
182
196
 
183
- (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD -> CSI FLAG ${pathname}`);
197
+ const urlObject = new URL(url);
184
198
 
185
- const source = await readFile(fixPath(pathname), 'utf8');
186
- return {
187
- source,
188
- format: 'module',
189
- shortCircuit: true,
190
- };
191
- }
199
+ if (urlObject.searchParams.has('csi-flag')) {
200
+ // target type will, i think, be used to determine if this will require extra
201
+ // processing of some sort for esm-native modules.
202
+ // eslint-disable-next-line no-unused-vars
203
+ const targetType = urlObject.searchParams.get('csi-flag');
204
+ const { pathname } = urlObject;
192
205
 
193
- if (urlObject.pathname.endsWith('.node')) {
194
- const metaUrl = JSON.stringify(url);
195
- const addon = urlObject.pathname;
196
- return {
197
- source: `require("node:module").createRequire(${metaUrl})("${addon}")`,
198
- format: 'commonjs',
199
- shortCircuit: true,
200
- };
201
- }
206
+ (log & logLoad) && console.log(`ESM HOOK(${W.threadId}) -> LOAD -> CSI FLAG ${pathname}`);
202
207
 
203
- // if it's not a builtin, a .node addon, or a flagged file, it needs to be
204
- // rewritten if it's a module.
205
- if (urlObject.pathname.match(/(.js|.mjs|.cjs)$/)) {
206
- // if it's not a module it will be rewritten by the require hooks.
207
- const type = getFileType(urlObject);
208
- if (type !== 'module') {
209
- return nextLoad(url, context);
208
+ const source = await readFile(fixPath(pathname), 'utf8');
209
+ return {
210
+ source,
211
+ format: 'module',
212
+ shortCircuit: true,
213
+ };
210
214
  }
211
215
 
212
- const filename = fixPath(urlObject.pathname);
213
- const source = await readFile(filename, 'utf8');
214
-
215
- const rewriteOptions = {
216
- filename,
217
- isModule: type === 'module',
218
- inject: true,
219
- wrap: type !== 'module', // cannot wrap modules
220
- };
221
-
222
- core.threadInfo.post('rewrite', rewriteOptions);
223
-
224
- const result = core.rewriter.rewrite(source, rewriteOptions);
225
-
226
- return {
227
- source: result.code,
228
- format: type || 'commonjs', // don't know what else to do here. log?
229
- shortCircuit: true,
230
- };
231
- }
232
- // this never gets called, so hmmm.
233
- return nextLoad(url, context);
234
- };
235
-
236
- // from esmock: git@github.com:iambumblehead/esmock.git
237
- //
238
- // new versions of node: when multiple loaders are used and context
239
- // is passed to nextResolve, the process crashes in a recursive call
240
- // see: /esmock/issues/#48
241
- //
242
- // old versions of node: if context.parentURL is defined, and context
243
- // is not passed to nextResolve, the tests fail
244
- //
245
- // later versions of node v16 include 'node-addons'
246
- async function protectedNextResolve(nextResolve, specifier, context) {
247
- if (context.parentURL) {
248
- if (context.conditions.at(-1) === 'node-addons' || context.importAssertions || isLT16_12) {
249
- return nextResolve(specifier, context);
216
+ if (urlObject.pathname.endsWith('.node')) {
217
+ const metaUrl = JSON.stringify(url);
218
+ const addon = urlObject.pathname;
219
+ return {
220
+ source: `require("node:module").createRequire(${metaUrl})("${addon}")`,
221
+ format: 'commonjs',
222
+ shortCircuit: true,
223
+ };
250
224
  }
251
- }
252
225
 
253
- return nextResolve(specifier);
226
+ // if it's not a builtin, a .node addon, or a flagged file, it needs to be
227
+ // rewritten if it's a module.
228
+ if (urlObject.pathname.match(/(.js|.mjs|.cjs)$/)) {
229
+ // if it's not a module it will be rewritten by the require hooks.
230
+ const type = getFileType(urlObject);
231
+ if (type !== 'module') {
232
+ return nextLoad(url, context);
233
+ }
234
+
235
+ const filename = fixPath(urlObject.pathname);
236
+ const source = await readFile(filename, 'utf8');
237
+
238
+ const rewriteOptions = {
239
+ filename,
240
+ isModule: type === 'module',
241
+ inject: true,
242
+ wrap: type !== 'module', // cannot wrap modules
243
+ };
244
+
245
+ core.threadInfo.post('rewrite', rewriteOptions);
246
+
247
+ const result = core.rewriter.rewrite(source, rewriteOptions);
248
+
249
+ return {
250
+ source: result.code,
251
+ format: type || 'commonjs', // don't know what else to do here. log?
252
+ shortCircuit: true,
253
+ };
254
+ }
255
+ // this never gets called, so hmmm.
256
+ return nextLoad(url, context);
257
+ };
254
258
  }
255
259
 
256
260
  export {
@@ -16,7 +16,7 @@
16
16
  import Module from 'node:module';
17
17
  import W from 'node:worker_threads';
18
18
  import EventEmitter from 'node:events';
19
-
19
+ import * as hooks from './esm-hooks.mjs';
20
20
  import checkImportVsLoaderVsNodeVersion from './check-flag-vs-node-version.mjs';
21
21
 
22
22
  // might need to get exclude function here as opposed to deep within initialize
@@ -37,6 +37,7 @@ if (process.env.CSI_HOOKS_LOG) {
37
37
 
38
38
  // verify that we're running with the correct flag for the version of node.
39
39
  const { flag, msg } = checkImportVsLoaderVsNodeVersion();
40
+
40
41
  if (msg) {
41
42
  console.error(msg);
42
43
  throw new Error(msg);
@@ -50,11 +51,11 @@ if (msg) {
50
51
  // Module. I don't think this will be called more than once, but the flag is
51
52
  // in place just in case.
52
53
  //
53
- let core;
54
54
 
55
- if (!core) {
56
- core = (await import('./initialize.mjs')).default;
55
+ const core = (await import('./initialize.mjs')).default;
56
+ let load, resolve;
57
57
 
58
+ if (core) {
58
59
  // most of this can be removed; it's here for debugging. but considering the relatively
59
60
  // low cost compared with loading a module, i'm leaving it in.
60
61
  (log & logRequireAll) && console.log('ESM-LOADER executing CJS Module patching');
@@ -85,75 +86,76 @@ if (!core) {
85
86
  (log & logLoad) && console.log(`CJS(${W.threadId}) -> _load() ${request}`);
86
87
  return originalLoad.call(this, request, parent, isMain);
87
88
  };
88
- }
89
89
 
90
- core.threadInfo.syncEmitter = new EventEmitter();
91
- // abstract how notifications are posted so that the non-loader
92
- // code is not coupled to the implementation. the loader-thread
93
- // complement of this is in esm-hooks.
94
- //
95
- // specifically, this is the main thread and post() directly emits
96
- // the event while in the loader thread post() uses post.postMessage()
97
- // to communicate back to this (the main) thread.
98
- core.threadInfo.post = (type, data) => {
99
- core.threadInfo.syncEmitter.emit(type, data);
100
- };
90
+ core.threadInfo.syncEmitter = new EventEmitter();
91
+ // abstract how notifications are posted so that the non-loader
92
+ // code is not coupled to the implementation. the loader-thread
93
+ // complement of this is in esm-hooks.
94
+ //
95
+ // specifically, this is the main thread and post() directly emits
96
+ // the event while in the loader thread post() uses post.postMessage()
97
+ // to communicate back to this (the main) thread.
98
+ core.threadInfo.post = (type, data) => {
99
+ core.threadInfo.syncEmitter.emit(type, data);
100
+ };
101
101
 
102
- //
103
- // setup ESM hooks
104
- //
105
- // if register exists this is 20.6.0 or later. we do not support
106
- // node 20 prior to 20.6.0 (or 20.9.0 when 20 became LTS).
107
- // news flash: backported register to node 18.19.0, so checking register
108
- // is no longer enough.
109
- //
110
- if (Module.register && flag === '--import') {
111
- // this file should never be executed in the loader thread; this file creates
112
- // the loader thread by calling register(). the loader thread must create its
113
- // own copy of threadInfo and insert it into core.
114
- if (!core.threadInfo.isMainThread) {
115
- // only get an error in CI on node v18.
116
- throw new Error('esm-loader.mjs should not be executed in the loader thread');
117
- }
118
- const { MessageChannel } = await import('node:worker_threads');
119
- const { port1, port2 } = new MessageChannel();
120
-
121
-
122
- core.threadInfo.port = port1;
123
- // messages received on the port are re-emitted as standard node events. the
124
- // body must contain the type; data can be undefined.
125
- core.threadInfo.missingTypeCount = 0;
126
- core.threadInfo.port.on('message', (body) => {
127
- const { type, data } = body;
128
- if (type) {
129
- core.threadInfo.syncEmitter.emit(type, data);
130
- } else {
131
- core.threadInfo.missingTypeCount += 1;
102
+ //
103
+ // setup ESM hooks
104
+ //
105
+ // if register exists this is 20.6.0 or later. we do not support
106
+ // node 20 prior to 20.6.0 (or 20.9.0 when 20 became LTS).
107
+ // news flash: backported register to node 18.19.0, so checking register
108
+ // is no longer enough.
109
+ //
110
+ if (Module.register && flag === '--import') {
111
+ // this file should never be executed in the loader thread; this file creates
112
+ // the loader thread by calling register(). the loader thread must create its
113
+ // own copy of threadInfo and insert it into core.
114
+ if (!core.threadInfo.isMainThread) {
115
+ // only get an error in CI on node v18.
116
+ throw new Error('esm-loader.mjs should not be executed in the loader thread');
132
117
  }
133
- });
134
- core.threadInfo.port.unref();
135
-
136
- // record the URL of the entry point.
137
- core.threadInfo.url = import.meta.url;
138
-
139
- // get relative URL
140
- const url = new URL('./esm-hooks.mjs', import.meta.url);
141
- await Module.register(url.href, import.meta.url, { data: { port: port2 }, transferList: [port2] });
142
-
143
- // we only need to do this if there is a background thread.
144
- // The esmHooks component of the main agent will send TS settings update to the loader agent via the port.
145
- // To get the loader agent components to update we just need to forward the settings using `.messages` emitter.
146
- core.messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
147
- core.threadInfo.port.postMessage({
148
- type: Event.SERVER_SETTINGS_UPDATE,
149
- ...msg,
118
+ const { MessageChannel } = await import('node:worker_threads');
119
+ const { port1, port2 } = new MessageChannel();
120
+
121
+
122
+ core.threadInfo.port = port1;
123
+ // messages received on the port are re-emitted as standard node events. the
124
+ // body must contain the type; data can be undefined.
125
+ core.threadInfo.missingTypeCount = 0;
126
+ core.threadInfo.port.on('message', (body) => {
127
+ const { type, data } = body;
128
+ if (type) {
129
+ core.threadInfo.syncEmitter.emit(type, data);
130
+ } else {
131
+ core.threadInfo.missingTypeCount += 1;
132
+ }
150
133
  });
151
- });
134
+ core.threadInfo.port.unref();
135
+
136
+ // record the URL of the entry point.
137
+ core.threadInfo.url = import.meta.url;
138
+
139
+ // get relative URL
140
+ const url = new URL('./esm-hooks.mjs', import.meta.url);
141
+ await Module.register(url.href, import.meta.url, { data: { port: port2 }, transferList: [port2] });
142
+
143
+ // we only need to do this if there is a background thread.
144
+ // The esmHooks component of the main agent will send TS settings update to the loader agent via the port.
145
+ // To get the loader agent components to update we just need to forward the settings using `.messages` emitter.
146
+ core.messages.on(Event.SERVER_SETTINGS_UPDATE, (msg) => {
147
+ core.threadInfo.port.postMessage({
148
+ type: Event.SERVER_SETTINGS_UPDATE,
149
+ ...msg,
150
+ });
151
+ });
152
+ }
153
+ // it's not possible to conditionally export, but exporting undefined
154
+ // values is close.
155
+
156
+
157
+ const finalHooks = (Module.register && flag === '--import') ? {} : hooks;
158
+ ({ load, resolve } = finalHooks);
152
159
  }
153
160
 
154
- // it's not possible to conditionally export, but exporting undefined
155
- // values is close.
156
- import * as hooks from './esm-hooks.mjs';
157
- const finalHooks = (Module.register && flag === '--import') ? {} : hooks;
158
- const { load, resolve } = finalHooks;
159
161
  export { load, resolve };
@@ -74,11 +74,11 @@ if (!core) {
74
74
 
75
75
  core = await loadModules({ core, options: {} });
76
76
 
77
- core = await startAgent({ core, options: { executor, installOrder } });
77
+ if (core) core = await startAgent({ core, options: { executor, installOrder } });
78
78
 
79
79
  // self identification
80
80
  // for communications between the main and loader thread (if present)
81
- core.threadInfo = {
81
+ if (core) core.threadInfo = {
82
82
  isMainThread,
83
83
  threadId,
84
84
  port: undefined, // filled in if there's a loader thread
@@ -89,10 +89,8 @@ if (!core) {
89
89
  const coreKey = Symbol.for('contrast:core');
90
90
  global[coreKey] = core;
91
91
  }
92
-
93
92
  }
94
93
 
95
-
96
94
  export default core;
97
95
 
98
96
  async function executor(core) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/agent",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Assess and Protect agents for Node.js",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -21,13 +21,13 @@
21
21
  "test": "../scripts/test.sh"
22
22
  },
23
23
  "dependencies": {
24
- "@contrast/agentify": "1.19.3",
25
- "@contrast/architecture-components": "1.15.0",
26
- "@contrast/assess": "1.23.0",
27
- "@contrast/library-analysis": "1.16.0",
28
- "@contrast/protect": "1.31.1",
29
- "@contrast/route-coverage": "1.15.1",
30
- "@contrast/telemetry": "1.3.2",
24
+ "@contrast/agentify": "1.20.0",
25
+ "@contrast/architecture-components": "1.16.0",
26
+ "@contrast/assess": "1.24.0",
27
+ "@contrast/library-analysis": "1.17.0",
28
+ "@contrast/protect": "1.32.0",
29
+ "@contrast/route-coverage": "1.16.0",
30
+ "@contrast/telemetry": "1.4.0",
31
31
  "semver": "^7.3.7"
32
32
  }
33
33
  }