dd-trace 6.4.0 → 6.6.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 (185) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/init.js +18 -0
  52. package/initialize.mjs +1 -1
  53. package/loader-hook.mjs +28 -18
  54. package/openfeature.d.ts +1 -0
  55. package/openfeature.js +4 -0
  56. package/package.json +11 -8
  57. package/packages/datadog-instrumentations/src/ai.js +8 -2
  58. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  59. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  60. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  61. package/packages/datadog-instrumentations/src/cucumber.js +31 -19
  62. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  63. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  64. package/packages/datadog-instrumentations/src/helpers/channel.js +74 -0
  65. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  66. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  67. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  68. package/packages/datadog-instrumentations/src/jest.js +44 -17
  69. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  70. package/packages/datadog-instrumentations/src/mocha/main.js +79 -32
  71. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  72. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  73. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  74. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  75. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  76. package/packages/datadog-instrumentations/src/next.js +24 -0
  77. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  78. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  79. package/packages/datadog-instrumentations/src/pg.js +2 -2
  80. package/packages/datadog-instrumentations/src/playwright.js +162 -46
  81. package/packages/datadog-instrumentations/src/process.js +3 -0
  82. package/packages/datadog-instrumentations/src/router.js +18 -8
  83. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  84. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +3 -3
  85. package/packages/datadog-instrumentations/src/vitest-main.js +14 -28
  86. package/packages/datadog-instrumentations/src/vitest-util.js +0 -7
  87. package/packages/datadog-instrumentations/src/vitest-worker.js +5 -8
  88. package/packages/datadog-instrumentations/src/ws.js +2 -1
  89. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  90. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  91. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -2
  92. package/packages/datadog-plugin-cucumber/src/index.js +3 -2
  93. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -57
  94. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  95. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  96. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  97. package/packages/datadog-plugin-graphql/src/execute.js +87 -45
  98. package/packages/datadog-plugin-graphql/src/validate.js +2 -2
  99. package/packages/datadog-plugin-http2/src/client.js +1 -1
  100. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  101. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  102. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  103. package/packages/datadog-plugin-mocha/src/index.js +2 -1
  104. package/packages/datadog-plugin-playwright/src/index.js +181 -65
  105. package/packages/datadog-plugin-router/src/index.js +11 -2
  106. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  107. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  108. package/packages/datadog-plugin-ws/src/close.js +2 -1
  109. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  110. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  111. package/packages/datadog-plugin-ws/src/server.js +2 -1
  112. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  113. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  114. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  115. package/packages/dd-trace/src/azure_metadata.js +2 -1
  116. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  117. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  118. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  119. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  122. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +41 -3
  124. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -2
  125. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +1 -1
  126. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  127. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  128. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  129. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  131. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +168 -8
  132. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  133. package/packages/dd-trace/src/config/generated-config-types.d.ts +16 -0
  134. package/packages/dd-trace/src/config/helper.js +1 -0
  135. package/packages/dd-trace/src/config/index.js +10 -0
  136. package/packages/dd-trace/src/config/supported-configurations.json +66 -0
  137. package/packages/dd-trace/src/exporter.js +2 -0
  138. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  139. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  140. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  141. package/packages/dd-trace/src/feature-registry.js +10 -3
  142. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  143. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  144. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  145. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  146. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  147. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  148. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  149. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  150. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  151. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  152. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  153. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  154. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  155. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  156. package/packages/dd-trace/src/llmobs/tagger.js +68 -0
  157. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  158. package/packages/dd-trace/src/llmobs/util.js +32 -0
  159. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  160. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  161. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  162. package/packages/dd-trace/src/openfeature/index.js +0 -2
  163. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  164. package/packages/dd-trace/src/openfeature/register.js +16 -27
  165. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  166. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  167. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  168. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  169. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  170. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  171. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  172. package/packages/dd-trace/src/plugins/util/test.js +56 -4
  173. package/packages/dd-trace/src/profiling/profilers/space.js +1 -1
  174. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -0
  175. package/packages/dd-trace/src/proxy.js +84 -7
  176. package/packages/dd-trace/src/span_processor.js +5 -0
  177. package/packages/dd-trace/src/standalone/index.js +0 -14
  178. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  179. package/packages/dd-trace/src/util.js +26 -0
  180. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  181. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  182. package/vendor/dist/protobufjs/index.js +1 -1
  183. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  184. package/vendor/dist/shell-quote/index.js +1 -1
  185. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,480 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable eslint-rules/eslint-process-env */
4
+
5
+ const crypto = require('node:crypto')
6
+ const fs = require('node:fs')
7
+ const path = require('node:path')
8
+
9
+ const { bindApprovedExecutable, getApprovedExecutable } = require('./executable-approval')
10
+ const { getCiWiringCommand, getLocalValidationCommand } = require('./local-command')
11
+
12
+ /**
13
+ * Returns an executable that is unavailable for a structured command.
14
+ *
15
+ * @param {object} command manifest command
16
+ * @returns {string|undefined} unavailable executable
17
+ */
18
+ function getUnavailableExecutable (command) {
19
+ for (const executableCommand of getExecutableCommands(command)) {
20
+ const executable = getExecutable(executableCommand)
21
+ if (executable && !resolveExecutable(executable, executableCommand)) return executable
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Reads the executable used to start a structured command.
27
+ *
28
+ * @param {object} command manifest command
29
+ * @returns {string|undefined} command executable
30
+ */
31
+ function getExecutable (command) {
32
+ if (!command?.usesShell) return command?.argv?.[0]
33
+ if (typeof command.shell === 'string' && command.shell.trim()) return command.shell.trim()
34
+ return process.platform === 'win32'
35
+ ? process.env.ComSpec || process.env.COMSPEC || 'cmd.exe'
36
+ : process.env.SHELL || '/bin/sh'
37
+ }
38
+
39
+ /**
40
+ * Resolves an executable from the command working directory or PATH.
41
+ *
42
+ * @param {string} executable executable name or path
43
+ * @param {object} command manifest command
44
+ * @returns {boolean} whether the executable can be resolved
45
+ */
46
+ function resolveExecutable (executable, command) {
47
+ if (isExplicitExecutablePath(executable)) {
48
+ return isExecutable(path.resolve(command.cwd, executable))
49
+ }
50
+
51
+ const environmentPath = getEnvironmentPath(command)
52
+ const extensions = getExecutableExtensions()
53
+
54
+ for (const directory of environmentPath.split(path.delimiter)) {
55
+ if (!directory) continue
56
+ const resolvedDirectory = path.resolve(command.cwd, directory)
57
+ for (const extension of extensions) {
58
+ const filename = path.join(resolvedDirectory, `${executable}${extension}`)
59
+ if (isExecutable(filename)) return true
60
+ }
61
+ }
62
+ return false
63
+ }
64
+
65
+ /**
66
+ * Resolves the filesystem path used for a structured command executable.
67
+ *
68
+ * @param {object} command manifest command
69
+ * @returns {string|undefined} resolved executable path
70
+ */
71
+ function getResolvedExecutable (command) {
72
+ const executable = getExecutable(command)
73
+ if (!executable) return
74
+
75
+ if (isExplicitExecutablePath(executable)) {
76
+ const filename = path.resolve(command.cwd, executable)
77
+ return isExecutable(filename) ? filename : undefined
78
+ }
79
+
80
+ const environmentPath = getEnvironmentPath(command)
81
+ const extensions = getExecutableExtensions()
82
+
83
+ for (const directory of environmentPath.split(path.delimiter)) {
84
+ if (!directory) continue
85
+ const resolvedDirectory = path.resolve(command.cwd, directory)
86
+ for (const extension of extensions) {
87
+ const filename = path.join(resolvedDirectory, `${executable}${extension}`)
88
+ if (isExecutable(filename)) return filename
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Returns the PATH used by a command, preserving an explicitly empty value.
95
+ *
96
+ * @param {object} command manifest command
97
+ * @returns {string} command PATH
98
+ */
99
+ function getEnvironmentPath (command) {
100
+ if (Object.hasOwn(command.env || {}, 'PATH')) return command.env.PATH || ''
101
+ return process.env.PATH || ''
102
+ }
103
+
104
+ /**
105
+ * Binds every executable selected by an approvable manifest command to its canonical file identity.
106
+ *
107
+ * @param {object} manifest loaded manifest
108
+ * @returns {object[]} sorted executable identities included in approval material
109
+ */
110
+ function bindManifestExecutables (manifest) {
111
+ const identities = []
112
+ const identitiesByPath = new Map()
113
+ for (const [label, command, sourceCommand] of getManifestCommands(manifest)) {
114
+ const identity = getCommandExecutableIdentity(command, identitiesByPath)
115
+ if (!identity) continue
116
+ bindApprovedExecutable(command, identity)
117
+ if (sourceCommand !== command) bindApprovedExecutable(sourceCommand, identity)
118
+ identities.push({ label, ...identity })
119
+ }
120
+ return identities.sort((left, right) => left.label.localeCompare(right.label))
121
+ }
122
+
123
+ /**
124
+ * Verifies and returns the canonical executable and approved invocation name used to spawn it.
125
+ *
126
+ * @param {object} command command about to execute
127
+ * @param {{requireApproval?: boolean}} [options] verification options
128
+ * @returns {{argv0: string, path: string}} approved launch identity
129
+ */
130
+ function getExecutableForSpawn (command, options = {}) {
131
+ const approved = getApprovedExecutable(command)
132
+ const current = getCommandExecutableIdentity(command)
133
+ if (!approved) {
134
+ if (options.requireApproval) {
135
+ throw new Error('The selected command executable was not covered by the approved execution plan.')
136
+ }
137
+ if (!current) throw new Error(`Command executable is unavailable: ${getExecutable(command)}`)
138
+ return getLaunchIdentity(current)
139
+ }
140
+ if (!areExecutableIdentitiesEqual(current, approved)) {
141
+ throw new Error(
142
+ 'The selected command executable changed after approval. Render and approve a fresh execution plan.'
143
+ )
144
+ }
145
+ return getLaunchIdentity(approved)
146
+ }
147
+
148
+ /**
149
+ * Resolves a command launcher and every executable delegated through env wrappers.
150
+ *
151
+ * @param {object} command manifest command
152
+ * @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
153
+ * @returns {{delegated?: object[], invocationPath: string, path: string, sha256: string}|undefined} identity tree
154
+ */
155
+ function getCommandExecutableIdentity (command, identitiesByPath) {
156
+ const identities = []
157
+ for (const executableCommand of getExecutableCommands(command)) {
158
+ const identity = getExecutableIdentity(executableCommand, identitiesByPath)
159
+ if (!identity) return
160
+ identities.push(identity)
161
+ }
162
+
163
+ const [launcher, ...delegated] = identities
164
+ return delegated.length === 0 ? launcher : { ...launcher, delegated }
165
+ }
166
+
167
+ /**
168
+ * Checks whether the launcher and delegated executable identities still match approval.
169
+ *
170
+ * @param {object|undefined} current current identity tree
171
+ * @param {object|undefined} approved approved identity tree
172
+ * @returns {boolean} whether every executable matches
173
+ */
174
+ function areExecutableIdentitiesEqual (current, approved) {
175
+ if (!current || !approved) return false
176
+ const currentIdentities = [current, ...(current.delegated || [])]
177
+ const approvedIdentities = [approved, ...(approved.delegated || [])]
178
+ if (currentIdentities.length !== approvedIdentities.length) return false
179
+
180
+ return currentIdentities.every((identity, index) => {
181
+ const expected = approvedIdentities[index]
182
+ return identity.invocationPath === expected.invocationPath && identity.path === expected.path &&
183
+ identity.sha256 === expected.sha256
184
+ })
185
+ }
186
+
187
+ /**
188
+ * Expands nested env wrappers into the commands whose executables they delegate to.
189
+ *
190
+ * @param {object} command manifest command
191
+ * @returns {object[]} launcher followed by delegated commands
192
+ */
193
+ function getExecutableCommands (command) {
194
+ const commands = [command]
195
+ let current = command
196
+ while (!current.usesShell && isEnvExecutable(current.argv?.[0])) {
197
+ current = getEnvDelegatedCommand(current)
198
+ commands.push(current)
199
+ }
200
+ return commands
201
+ }
202
+
203
+ /**
204
+ * Returns the command executed by an env wrapper with its effective PATH and working directory.
205
+ *
206
+ * @param {object} command env wrapper command
207
+ * @returns {object} delegated command
208
+ */
209
+ function getEnvDelegatedCommand (command) {
210
+ const parsed = parseArgv(command.argv)
211
+ if (parsed.unsupportedEnvOption) {
212
+ throw new Error(
213
+ `Cannot approve env-wrapped command because option "${parsed.unsupportedEnvOption}" prevents reliable ` +
214
+ 'executable fingerprinting.'
215
+ )
216
+ }
217
+ if (parsed.commandIndex >= command.argv.length) {
218
+ throw new Error('Cannot approve env-wrapped command because it does not identify a delegated executable.')
219
+ }
220
+ const delegatedExecutable = command.argv[parsed.commandIndex]
221
+ const requiresPathLookup = !isExplicitExecutablePath(delegatedExecutable)
222
+ if (requiresPathLookup && parsed.unsetEnvNames.some(name => name.toUpperCase() === 'PATH')) {
223
+ throw new Error('Cannot approve env-wrapped command because it removes PATH before selecting its executable.')
224
+ }
225
+
226
+ const env = parsed.ignoreEnvironment ? { ...parsed.prefixEnv } : { ...command.env, ...parsed.prefixEnv }
227
+ if (requiresPathLookup && parsed.ignoreEnvironment && !Object.hasOwn(env, 'PATH')) {
228
+ throw new Error(
229
+ 'Cannot approve env-wrapped command because it clears the environment without declaring an explicit PATH.'
230
+ )
231
+ }
232
+
233
+ return {
234
+ ...command,
235
+ argv: command.argv.slice(parsed.commandIndex),
236
+ cwd: parsed.workingDirectory ? path.resolve(command.cwd, parsed.workingDirectory) : command.cwd,
237
+ env,
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Selects the verified path and invocation name used to launch an executable.
243
+ *
244
+ * @param {{invocationPath: string, path: string}} identity verified executable identity
245
+ * @returns {{argv0: string, path: string}} executable launch identity
246
+ */
247
+ function getLaunchIdentity (identity) {
248
+ return {
249
+ argv0: identity.invocationPath,
250
+ // Windows package-manager shims rely on their invoked path. The canonical target is still verified above.
251
+ path: process.platform === 'win32' ? identity.invocationPath : identity.path,
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Resolves one command executable to a stable canonical path and content digest.
257
+ *
258
+ * @param {object} command manifest command
259
+ * @param {Map<string, object>} [identitiesByPath] identities already hashed during this approval pass
260
+ * @returns {{invocationPath: string, path: string, sha256: string}|undefined} executable identity
261
+ */
262
+ function getExecutableIdentity (command, identitiesByPath) {
263
+ const resolved = getResolvedExecutable(command)
264
+ if (!resolved) return
265
+ const canonicalPath = fs.realpathSync(resolved)
266
+ const cached = identitiesByPath?.get(canonicalPath)
267
+ if (cached) return { invocationPath: resolved, ...cached }
268
+ const stat = fs.statSync(canonicalPath)
269
+ if (!stat.isFile()) return
270
+ const canonicalIdentity = {
271
+ path: canonicalPath,
272
+ sha256: crypto.createHash('sha256').update(fs.readFileSync(canonicalPath)).digest('hex'),
273
+ }
274
+ identitiesByPath?.set(canonicalPath, canonicalIdentity)
275
+ return { invocationPath: resolved, ...canonicalIdentity }
276
+ }
277
+
278
+ /**
279
+ * Enumerates every executable-bearing manifest command with a stable approval label.
280
+ *
281
+ * @param {object} manifest loaded manifest
282
+ * @returns {Array<[string, object]>} labeled commands
283
+ */
284
+ function getManifestCommands (manifest) {
285
+ const commands = []
286
+ for (const framework of manifest.frameworks || []) {
287
+ const prefix = `framework:${framework.id}`
288
+ const basicSource = framework.existingTestCommand
289
+ if (basicSource) {
290
+ commands.push([`${prefix}:basic-reporting`, getLocalValidationCommand(framework, basicSource), basicSource])
291
+ }
292
+ if (framework.ciWiringCommand) {
293
+ commands.push([`${prefix}:ci-wiring`, getCiWiringCommand(framework), framework.ciWiringCommand])
294
+ }
295
+ for (const [index, command] of (framework.setup?.commands || []).entries()) {
296
+ commands.push([`${prefix}:setup:${index}`, command, command])
297
+ }
298
+ for (const [index, scenario] of (framework.generatedTestStrategy?.scenarios || []).entries()) {
299
+ if (scenario.runCommand) {
300
+ commands.push([
301
+ `${prefix}:generated:${index}`,
302
+ getLocalValidationCommand(framework, scenario.runCommand),
303
+ scenario.runCommand,
304
+ ])
305
+ }
306
+ }
307
+ }
308
+ return commands
309
+ }
310
+
311
+ /**
312
+ * Detects explicit executable paths using platform path syntax.
313
+ *
314
+ * @param {string} executable executable name or path
315
+ * @param {string} [platform] target platform
316
+ * @returns {boolean} whether the value is a path rather than a PATH name
317
+ */
318
+ function isExplicitExecutablePath (executable, platform = process.platform) {
319
+ return path.isAbsolute(executable) || executable.includes('/') || (platform === 'win32' && executable.includes('\\'))
320
+ }
321
+
322
+ function getExecutableExtensions () {
323
+ if (process.platform !== 'win32') return ['']
324
+ return ['', ...(process.env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')]
325
+ }
326
+
327
+ /**
328
+ * Checks whether a filesystem entry can be executed.
329
+ *
330
+ * @param {string} filename executable candidate
331
+ * @returns {boolean} whether the candidate is executable
332
+ */
333
+ function isExecutable (filename) {
334
+ try {
335
+ fs.accessSync(filename, fs.constants.X_OK)
336
+ return true
337
+ } catch {
338
+ return false
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Parses structured env wrappers and runtime plumbing without executing them.
344
+ *
345
+ * @param {string[]} argv command arguments
346
+ * @returns {object} parsed wrapper details
347
+ */
348
+ function parseArgv (argv) {
349
+ const result = {
350
+ ignoreEnvironment: false,
351
+ prefixAssignments: [],
352
+ prefixEnv: {},
353
+ unsetEnvNames: [],
354
+ commandIndex: 0,
355
+ corepackIndex: -1,
356
+ pathAdjusted: false,
357
+ unsupportedEnvOption: undefined,
358
+ workingDirectory: undefined,
359
+ }
360
+
361
+ if (!Array.isArray(argv) || argv.length === 0) return result
362
+
363
+ let index = 0
364
+ if (isEnvExecutable(argv[index])) {
365
+ index++
366
+ while (index < argv.length) {
367
+ const option = argv[index]
368
+ if (option === '--') {
369
+ index++
370
+ break
371
+ }
372
+ if (option === '-' || option === '-i' || option === '--ignore-environment') {
373
+ result.ignoreEnvironment = true
374
+ index++
375
+ continue
376
+ }
377
+ if (option === '-u' || option === '--unset') {
378
+ if (typeof argv[index + 1] === 'string') result.unsetEnvNames.push(argv[index + 1])
379
+ index += 2
380
+ continue
381
+ }
382
+ const unsetMatch = /^(?:-u|--unset=)(.+)$/.exec(option)
383
+ if (unsetMatch) {
384
+ result.unsetEnvNames.push(unsetMatch[1])
385
+ index++
386
+ continue
387
+ }
388
+ if (option === '-C' || option === '--chdir') {
389
+ if (typeof argv[index + 1] !== 'string') {
390
+ result.unsupportedEnvOption = option
391
+ break
392
+ }
393
+ result.workingDirectory = argv[index + 1]
394
+ index += 2
395
+ continue
396
+ }
397
+ const chdirMatch = /^(?:-C(.+)|--chdir=(.+))$/.exec(option)
398
+ if (chdirMatch) {
399
+ result.workingDirectory = chdirMatch[1] || chdirMatch[2]
400
+ index++
401
+ continue
402
+ }
403
+ if (option === '-S' || option === '--split-string' || /^(?:-S.+|--split-string=.+)$/.test(option)) {
404
+ result.unsupportedEnvOption = option
405
+ break
406
+ }
407
+ if (isSupportedEnvFlag(option)) {
408
+ index++
409
+ continue
410
+ }
411
+ if (option.startsWith('-')) {
412
+ result.unsupportedEnvOption = option
413
+ break
414
+ }
415
+ if (!isEnvAssignment(option)) break
416
+
417
+ const assignment = argv[index]
418
+ const equalsIndex = assignment.indexOf('=')
419
+ const name = assignment.slice(0, equalsIndex)
420
+ const value = assignment.slice(equalsIndex + 1)
421
+ result.prefixEnv[name] = value
422
+
423
+ if (name === 'PATH') {
424
+ result.pathAdjusted = true
425
+ } else {
426
+ result.prefixAssignments.push(assignment)
427
+ }
428
+ index++
429
+ }
430
+ }
431
+
432
+ result.commandIndex = index
433
+
434
+ if (isNodeExecutable(argv[index]) && isCorepackScript(argv[index + 1]) && argv[index + 2]) {
435
+ result.corepackIndex = index + 1
436
+ }
437
+
438
+ return result
439
+ }
440
+
441
+ function isSupportedEnvFlag (option) {
442
+ return /^(?:-0|-v|--null|--debug|--help|--version|--list-signal-handling)$/.test(option) ||
443
+ /^--(?:block|default|ignore)-signal(?:=.*)?$/.test(option)
444
+ }
445
+
446
+ function isEnvExecutable (value) {
447
+ const name = getExecutableName(value)
448
+ return name === 'env' || name === 'env.exe'
449
+ }
450
+
451
+ function isEnvAssignment (value) {
452
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(value)
453
+ }
454
+
455
+ function isNodeExecutable (value = '') {
456
+ const name = getExecutableName(value)
457
+ return name === 'node' || name === 'node.exe'
458
+ }
459
+
460
+ function isCorepackScript (value = '') {
461
+ const name = getExecutableName(value)
462
+ return name === 'corepack' || name === 'corepack.exe' || name === 'corepack.js'
463
+ }
464
+
465
+ function getExecutableName (value = '') {
466
+ return String(value).split(/[\\/]/).pop().toLowerCase()
467
+ }
468
+
469
+ module.exports = {
470
+ bindManifestExecutables,
471
+ getApprovedExecutable,
472
+ getExecutableForSpawn,
473
+ getManifestCommands,
474
+ getResolvedExecutable,
475
+ getUnavailableExecutable,
476
+ isEnvExecutable,
477
+ isExplicitExecutablePath,
478
+ isNodeExecutable,
479
+ parseArgv,
480
+ }
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const { hasUnsafeInvisibleCharacter, sanitizeString } = require('./redaction')
4
+
5
+ const MAX_GENERATED_FILES = 8
6
+ const MAX_GENERATED_FILE_BYTES = 32 * 1024
7
+ const MAX_GENERATED_FILE_LINES = 256
8
+ const MAX_GENERATED_LINE_BYTES = 4096
9
+
10
+ /**
11
+ * Returns why generated source is unsafe to write and execute.
12
+ *
13
+ * @param {unknown} contentLines generated source lines
14
+ * @returns {string|undefined} policy violation
15
+ */
16
+ function getGeneratedFileContentError (contentLines) {
17
+ if (!Array.isArray(contentLines) || contentLines.some(line => typeof line !== 'string')) return
18
+ if (contentLines.length > MAX_GENERATED_FILE_LINES) {
19
+ return `must contain at most ${MAX_GENERATED_FILE_LINES} lines`
20
+ }
21
+
22
+ let totalBytes = 1
23
+ for (const line of contentLines) {
24
+ if (line.includes('\n') || line.includes('\r') || hasUnsafeControlCharacter(line)) {
25
+ return 'must use one printable source line per contentLines entry'
26
+ }
27
+ if (Buffer.byteLength(line) > MAX_GENERATED_LINE_BYTES) {
28
+ return `must not contain a line larger than ${MAX_GENERATED_LINE_BYTES} bytes`
29
+ }
30
+ if (sanitizeString(line) !== line) {
31
+ return 'must contain only synthetic source and no secret-like values'
32
+ }
33
+ totalBytes += Buffer.byteLength(line) + 1
34
+ }
35
+
36
+ if (totalBytes > MAX_GENERATED_FILE_BYTES) {
37
+ return `must be at most ${MAX_GENERATED_FILE_BYTES} bytes`
38
+ }
39
+
40
+ const source = contentLines.join('\n')
41
+ if (/\bwriteFileSync\s*\(/.test(source) && /\bnew\s+URL\s*\(/.test(source)) {
42
+ return 'must derive generated state-file paths from fileURLToPath(import.meta.url) without new URL because ' +
43
+ 'browser-like test environments may replace the global URL constructor'
44
+ }
45
+ }
46
+
47
+ function hasUnsafeControlCharacter (value) {
48
+ if (hasUnsafeInvisibleCharacter(value)) return true
49
+
50
+ for (const character of value) {
51
+ const code = character.charCodeAt(0)
52
+ if (code <= 0x08 || code === 0x0B || code === 0x0C || (code >= 0x0E && code <= 0x1F) ||
53
+ code === 0x7F) {
54
+ return true
55
+ }
56
+ }
57
+ return false
58
+ }
59
+
60
+ module.exports = {
61
+ MAX_GENERATED_FILES,
62
+ getGeneratedFileContentError,
63
+ }