@trustify-da/trustify-da-javascript-client 0.3.0-ea.e660b02 → 0.3.0-ea.efdc87d
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/README.md +199 -11
- package/dist/package.json +29 -25
- package/dist/src/analysis.d.ts +50 -22
- package/dist/src/analysis.js +70 -18
- package/dist/src/cli.js +188 -6
- package/dist/src/config.d.ts +120 -0
- package/dist/src/config.js +260 -0
- package/dist/src/cyclone_dx_sbom.d.ts +13 -0
- package/dist/src/cyclone_dx_sbom.js +39 -1
- package/dist/src/index.d.ts +27 -22
- package/dist/src/index.js +27 -38
- package/dist/src/license/index.d.ts +4 -4
- package/dist/src/license/index.js +13 -7
- package/dist/src/license/license_utils.js +4 -1
- package/dist/src/license/licenses_api.d.ts +3 -3
- package/dist/src/license/licenses_api.js +1 -1
- package/dist/src/oci_image/images.d.ts +1 -1
- package/dist/src/oci_image/images.js +15 -1
- package/dist/src/oci_image/utils.d.ts +5 -5
- package/dist/src/package_version.d.ts +8 -0
- package/dist/src/package_version.js +31 -0
- package/dist/src/provider.d.ts +2 -2
- package/dist/src/provider.js +3 -1
- package/dist/src/providers/base_java.d.ts +55 -2
- package/dist/src/providers/base_java.js +64 -17
- package/dist/src/providers/base_javascript.d.ts +63 -9
- package/dist/src/providers/base_javascript.js +106 -4
- package/dist/src/providers/base_pyproject.d.ts +5 -5
- package/dist/src/providers/containerfile_parser.d.ts +5 -0
- package/dist/src/providers/containerfile_parser.js +20 -0
- package/dist/src/providers/golang_gomodules.d.ts +1 -1
- package/dist/src/providers/golang_gomodules.js +49 -7
- package/dist/src/providers/java_gradle.d.ts +48 -0
- package/dist/src/providers/java_gradle.js +196 -18
- package/dist/src/providers/java_gradle_groovy.d.ts +1 -1
- package/dist/src/providers/java_gradle_kotlin.d.ts +1 -1
- package/dist/src/providers/java_maven.d.ts +29 -9
- package/dist/src/providers/java_maven.js +104 -10
- package/dist/src/providers/javascript_bun.d.ts +12 -0
- package/dist/src/providers/javascript_bun.js +42 -1
- package/dist/src/providers/javascript_npm.d.ts +19 -1
- package/dist/src/providers/javascript_npm.js +40 -1
- package/dist/src/providers/javascript_pnpm.d.ts +13 -0
- package/dist/src/providers/javascript_pnpm.js +47 -1
- package/dist/src/providers/javascript_yarn.d.ts +12 -0
- package/dist/src/providers/javascript_yarn.js +75 -2
- package/dist/src/providers/manifest.js +6 -3
- package/dist/src/providers/oci_dockerfile.d.ts +51 -0
- package/dist/src/providers/oci_dockerfile.js +177 -0
- package/dist/src/providers/processors/yarn_berry_processor.d.ts +6 -2
- package/dist/src/providers/processors/yarn_berry_processor.js +3 -2
- package/dist/src/providers/processors/yarn_classic_processor.d.ts +6 -2
- package/dist/src/providers/processors/yarn_classic_processor.js +8 -6
- package/dist/src/providers/python_controller.js +4 -1
- package/dist/src/providers/python_poetry.d.ts +1 -1
- package/dist/src/providers/python_uv.d.ts +1 -1
- package/dist/src/providers/requirements_parser.js +1 -1
- package/dist/src/providers/rust_cargo.d.ts +1 -1
- package/dist/src/providers/rust_cargo.js +70 -29
- package/dist/src/providers/tree-sitter-containerfile.wasm +0 -0
- package/dist/src/remediate.d.ts +120 -0
- package/dist/src/remediate.js +253 -0
- package/dist/src/remediation.d.ts +58 -0
- package/dist/src/remediation.js +425 -0
- package/dist/src/remediation_report.d.ts +40 -0
- package/dist/src/remediation_report.js +159 -0
- package/dist/src/sbom.d.ts +11 -0
- package/dist/src/sbom.js +10 -0
- package/dist/src/tools.d.ts +11 -13
- package/dist/src/tools.js +34 -13
- package/dist/src/updaters/maven_updater.d.ts +58 -0
- package/dist/src/updaters/maven_updater.js +345 -0
- package/dist/src/updaters/toml_updater.d.ts +62 -0
- package/dist/src/updaters/toml_updater.js +276 -0
- package/dist/src/workspace.d.ts +2 -1
- package/dist/src/workspace.js +2 -1
- package/package.json +30 -26
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import analysis from './analysis.js';
|
|
4
|
+
import { availableProviders, match } from './provider.js';
|
|
5
|
+
import { extractRemediations } from './remediation.js';
|
|
6
|
+
import { mavenChangeKey, updateMavenVersions } from './updaters/maven_updater.js';
|
|
7
|
+
import { tomlChangeKey, updateTomlVersions } from './updaters/toml_updater.js';
|
|
8
|
+
import { selectTrustifyDABackend } from './index.js';
|
|
9
|
+
// Mirrors DEFAULT_WORKSPACE_DISCOVERY_IGNORE in workspace.js
|
|
10
|
+
const SKIP_DIRS = new Set(['node_modules', '.git']);
|
|
11
|
+
/**
|
|
12
|
+
* A single version change requested from an updater: bump `groupId:artifactId` to `newVersion`.
|
|
13
|
+
* The input side of every updater; each updater's output side (its `applied` entries) is its own
|
|
14
|
+
* type — see {@link AppliedChange}.
|
|
15
|
+
* @typedef {{groupId: string, artifactId: string, newVersion: string}} VersionChangeRequest
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* One entry from an updater's `applied` list, describing where and how a version was changed.
|
|
19
|
+
* Owned by the updater layer: the union of each updater's applied-entry shape. `changeKey`
|
|
20
|
+
* (per {@link ManifestType}) turns one of these into a stable edit-site identifier.
|
|
21
|
+
* @typedef {import('./updaters/maven_updater.js').MavenAppliedChange
|
|
22
|
+
* | import('./updaters/toml_updater.js').TomlAppliedChange} AppliedChange
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* The result of running an updater over a manifest's raw content.
|
|
26
|
+
* @typedef {{content: string, applied: AppliedChange[], skipped: Array<{groupId: string, artifactId: string, newVersion: string, reason: string}>}} UpdaterResult
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* A supported manifest type and the operations that act on it.
|
|
30
|
+
* `changeKey` builds a stable edit-site key from a single `applied` entry, so callers can detect
|
|
31
|
+
* inseparable remediations (same key => same commit/PR).
|
|
32
|
+
* @typedef {{
|
|
33
|
+
* test: (basename: string) => boolean,
|
|
34
|
+
* updater: (content: string, versionChanges: VersionChangeRequest[]) => UpdaterResult,
|
|
35
|
+
* label: ('maven'|'toml'),
|
|
36
|
+
* changeKey: (manifestPath: string, applied: AppliedChange) => string
|
|
37
|
+
* }} ManifestType
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* An isolated, single-dependency edit to one manifest file.
|
|
41
|
+
* - `after` is the *original* manifest content with only this dependency's fix applied, so a caller
|
|
42
|
+
* can create an isolated commit by writing `after` to `path` on a branch cut from the base.
|
|
43
|
+
* - `changeKey` is a stable identifier for the underlying edit site. Two remediations that share a
|
|
44
|
+
* `changeKey` are inseparable (e.g. two Maven deps whose versions resolve to the same `${property}`,
|
|
45
|
+
* or two Gradle libraries sharing one `version.ref`) and MUST land in the same commit/PR — the
|
|
46
|
+
* caller should union their CVEs/advisories.
|
|
47
|
+
* @typedef {{ path: string, after: string, changeKey: string }} DependencyFix
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* A single applicable remediation, as produced by `extractRemediations` and enriched by
|
|
51
|
+
* `runRemediation` with the originating manifest path(s) and (optionally) per-dependency changes.
|
|
52
|
+
* @typedef {{
|
|
53
|
+
* purl: string,
|
|
54
|
+
* groupId: string,
|
|
55
|
+
* artifactId: string,
|
|
56
|
+
* currentVersion: string,
|
|
57
|
+
* fixedInVersion: string,
|
|
58
|
+
* fixedInPurl: string,
|
|
59
|
+
* provider: string,
|
|
60
|
+
* source: string,
|
|
61
|
+
* advisories: Array<{id: string, url: string}>,
|
|
62
|
+
* severity: string,
|
|
63
|
+
* cves: string[],
|
|
64
|
+
* files: string[],
|
|
65
|
+
* changes?: DependencyFix[]
|
|
66
|
+
* }} Remediation
|
|
67
|
+
*/
|
|
68
|
+
/** @type {ManifestType[]} */
|
|
69
|
+
const MANIFEST_TYPES = [
|
|
70
|
+
{
|
|
71
|
+
test: (basename) => basename === 'pom.xml',
|
|
72
|
+
updater: updateMavenVersions,
|
|
73
|
+
label: 'maven',
|
|
74
|
+
changeKey: mavenChangeKey
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
test: (basename) => basename.endsWith('.versions.toml') || basename === 'libs.versions.toml',
|
|
78
|
+
updater: updateTomlVersions,
|
|
79
|
+
label: 'toml',
|
|
80
|
+
changeKey: tomlChangeKey
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
/**
|
|
84
|
+
* Returns the manifest type descriptor for a given filename, or null if unsupported.
|
|
85
|
+
* @param {string} basename - the file name to check
|
|
86
|
+
* @returns {ManifestType|null}
|
|
87
|
+
*/
|
|
88
|
+
function getManifestType(basename) {
|
|
89
|
+
return MANIFEST_TYPES.find(t => t.test(basename)) || null;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolves a target path to the supported manifest files it contains: the single file
|
|
93
|
+
* if `targetPath` is a supported manifest, or every supported manifest discovered
|
|
94
|
+
* recursively if it's a directory. This is the single source of truth for what
|
|
95
|
+
* `runRemediation` will scan, so callers (e.g. the CLI) can reuse it to tell
|
|
96
|
+
* "no manifests here" apart from "manifests, but nothing to fix".
|
|
97
|
+
* @param {string} targetPath - path to a manifest file or directory
|
|
98
|
+
* @returns {string[]} absolute paths to supported manifests (empty for an empty directory)
|
|
99
|
+
* @throws if the path does not exist, or is a file of an unsupported manifest type
|
|
100
|
+
*/
|
|
101
|
+
export function findManifests(targetPath) {
|
|
102
|
+
const resolvedPath = path.resolve(targetPath);
|
|
103
|
+
let stat;
|
|
104
|
+
try {
|
|
105
|
+
stat = fs.statSync(resolvedPath);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
throw new Error(`Path not found: ${resolvedPath}`);
|
|
109
|
+
}
|
|
110
|
+
// A single file must itself be a supported manifest.
|
|
111
|
+
if (!stat.isDirectory()) {
|
|
112
|
+
const basename = path.basename(resolvedPath);
|
|
113
|
+
if (!getManifestType(basename)) {
|
|
114
|
+
throw new Error(`Unsupported manifest type: ${basename}`);
|
|
115
|
+
}
|
|
116
|
+
return [resolvedPath];
|
|
117
|
+
}
|
|
118
|
+
// A directory yields every supported manifest found recursively beneath it.
|
|
119
|
+
const manifests = [];
|
|
120
|
+
function walk(dir) {
|
|
121
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
122
|
+
if (SKIP_DIRS.has(entry.name)) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const fullPath = path.join(dir, entry.name);
|
|
126
|
+
if (entry.isDirectory()) {
|
|
127
|
+
walk(fullPath);
|
|
128
|
+
}
|
|
129
|
+
else if (getManifestType(entry.name)) {
|
|
130
|
+
manifests.push(fullPath);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
walk(resolvedPath);
|
|
135
|
+
return manifests;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Orchestrates the full remediation pipeline for a single manifest or directory:
|
|
139
|
+
* discover manifests → scan via DA backend → extract remediations → apply or preview.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} targetPath - path to a manifest file or directory
|
|
142
|
+
* @param {object} [options]
|
|
143
|
+
* @param {boolean} [options.dryRun=false] - preview changes without modifying files (applies by default)
|
|
144
|
+
* @param {string} [options.providers] - comma-separated provider list
|
|
145
|
+
* @param {string} [options.sources] - comma-separated source list
|
|
146
|
+
* @param {string} [options.backendUrl] - Trustify DA backend URL
|
|
147
|
+
* @param {boolean} [options.perDependencyChanges=false] - when true, each remediation is populated with
|
|
148
|
+
* a `changes` array describing the isolated, single-dependency edit (see {@link DependencyFix}). This lets
|
|
149
|
+
* callers create one commit/PR per dependency without attributing diff hunks themselves.
|
|
150
|
+
* @returns {Promise<{exitCode: number, output: string, remediations: Remediation[], manifests: string[], appliedFiles: string[]}>}
|
|
151
|
+
* exitCode is 2 for a dry-run that found remediations (nothing written), 0 otherwise. `remediations`
|
|
152
|
+
* is the structured, per-manifest list of applicable updates — each entry carries the originating
|
|
153
|
+
* manifest path(s) in `files` so callers can group and create per-dependency changes. `appliedFiles`
|
|
154
|
+
* lists only the manifests actually written to disk (empty on a dry-run), so callers can report a
|
|
155
|
+
* truthful "updated N files" count without conflating "had remediations" with "was written".
|
|
156
|
+
*/
|
|
157
|
+
export async function runRemediation(targetPath, options = {}) {
|
|
158
|
+
const { dryRun = false, providers, sources, perDependencyChanges = false, backendUrl } = options;
|
|
159
|
+
const manifestPaths = findManifests(targetPath);
|
|
160
|
+
if (manifestPaths.length === 0) {
|
|
161
|
+
return { exitCode: 0, remediations: [], manifests: manifestPaths, appliedFiles: [] };
|
|
162
|
+
}
|
|
163
|
+
const opts = {};
|
|
164
|
+
if (backendUrl !== undefined) {
|
|
165
|
+
opts.TRUSTIFY_DA_BACKEND_URL = backendUrl;
|
|
166
|
+
}
|
|
167
|
+
if (providers !== undefined) {
|
|
168
|
+
opts.TRUSTIFY_DA_PROVIDERS = providers;
|
|
169
|
+
}
|
|
170
|
+
if (sources !== undefined) {
|
|
171
|
+
opts.TRUSTIFY_DA_SOURCES = sources;
|
|
172
|
+
}
|
|
173
|
+
const url = selectTrustifyDABackend(opts);
|
|
174
|
+
const allRemediations = [];
|
|
175
|
+
const appliedFiles = [];
|
|
176
|
+
for (const manifestPath of manifestPaths) {
|
|
177
|
+
const basename = path.basename(manifestPath);
|
|
178
|
+
const manifestType = getManifestType(basename);
|
|
179
|
+
if (!manifestType) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
let provider;
|
|
183
|
+
try {
|
|
184
|
+
provider = match(manifestPath, availableProviders, opts);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const analysisReport = await analysis.requestStack(provider, manifestPath, url, false, opts);
|
|
190
|
+
const remediations = extractRemediations(analysisReport, {
|
|
191
|
+
providerPriority: providers ? providers.split(',').map(p => p.trim()).filter(Boolean) : undefined,
|
|
192
|
+
});
|
|
193
|
+
if (remediations.length === 0) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
// Tag each remediation with the manifest it came from so callers can group
|
|
197
|
+
// changes per dependency across a multi-manifest workspace.
|
|
198
|
+
for (const remediation of remediations) {
|
|
199
|
+
remediation.files = [manifestPath];
|
|
200
|
+
}
|
|
201
|
+
// Read the pristine manifest once. Both the atomic apply and the per-dependency
|
|
202
|
+
// change computation must diff against the *original* content.
|
|
203
|
+
const needsContent = perDependencyChanges || !dryRun;
|
|
204
|
+
const originalContent = needsContent ? fs.readFileSync(manifestPath, 'utf-8') : null;
|
|
205
|
+
if (perDependencyChanges) {
|
|
206
|
+
// With per-dependency changes every returned remediation must carry an isolated
|
|
207
|
+
// edit. A dependency the updater cannot locate in this manifest — e.g. a vulnerable
|
|
208
|
+
// *transitive* dependency surfaced by analysis but not declared here — yields no
|
|
209
|
+
// applied change, so it is dropped rather than returned with an absent `changes`
|
|
210
|
+
// array (which would crash callers that iterate `remediation.changes` to build PRs).
|
|
211
|
+
const applicable = [];
|
|
212
|
+
for (const remediation of remediations) {
|
|
213
|
+
const isolated = manifestType.updater(originalContent, [{
|
|
214
|
+
groupId: remediation.groupId,
|
|
215
|
+
artifactId: remediation.artifactId,
|
|
216
|
+
newVersion: remediation.fixedInVersion,
|
|
217
|
+
}]);
|
|
218
|
+
if (isolated.applied.length === 0) {
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
remediation.changes = [{
|
|
222
|
+
path: manifestPath,
|
|
223
|
+
after: isolated.content,
|
|
224
|
+
changeKey: manifestType.changeKey(manifestPath, isolated.applied[0])
|
|
225
|
+
}];
|
|
226
|
+
applicable.push(remediation);
|
|
227
|
+
}
|
|
228
|
+
allRemediations.push(...applicable);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
allRemediations.push(...remediations);
|
|
232
|
+
}
|
|
233
|
+
if (!dryRun) {
|
|
234
|
+
// Disk receives the union of every dependency's fix. When
|
|
235
|
+
// perDependencyChanges is also set, each remediation's `changes[].after`
|
|
236
|
+
// deliberately stays isolated (single-dep) for branch-per-dep workflows.
|
|
237
|
+
const result = manifestType.updater(originalContent, remediations.map(r => ({
|
|
238
|
+
groupId: r.groupId,
|
|
239
|
+
artifactId: r.artifactId,
|
|
240
|
+
newVersion: r.fixedInVersion,
|
|
241
|
+
})));
|
|
242
|
+
if (result.applied.length > 0) {
|
|
243
|
+
fs.writeFileSync(manifestPath, result.content, 'utf-8');
|
|
244
|
+
appliedFiles.push(manifestPath);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (allRemediations.length === 0) {
|
|
249
|
+
return { exitCode: 0, remediations: [], manifests: manifestPaths, appliedFiles };
|
|
250
|
+
}
|
|
251
|
+
// Dry-run signals "changes available but not written" via exit code 2.
|
|
252
|
+
return { exitCode: dryRun ? 2 : 0, remediations: allRemediations, manifests: manifestPaths, appliedFiles };
|
|
253
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts actionable remediation instructions from a DA AnalysisReport response.
|
|
3
|
+
*
|
|
4
|
+
* Walks the provider/source/dependency/issue tree, collects fixedIn and trustedContent
|
|
5
|
+
* remediation data, applies provider priority resolution, and uses the configured
|
|
6
|
+
* version selection strategy to resolve conflicts. Dependencies with no remediation
|
|
7
|
+
* data are skipped.
|
|
8
|
+
*
|
|
9
|
+
* @param {import('@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts').AnalysisReport} analysisReport - raw DA AnalysisReport JSON response
|
|
10
|
+
* @param {object} [options] - extraction options
|
|
11
|
+
* @param {string[]} [options.providerPriority] - provider names in descending priority order.
|
|
12
|
+
* The first entry has the highest priority. Providers not listed share the lowest priority.
|
|
13
|
+
* When omitted or empty, all providers are treated equally and the highest fix version wins.
|
|
14
|
+
* @param {VersionStrategy} [options.versionStrategy] - version selection strategy with selectVersion
|
|
15
|
+
* and resolveConflict methods. Defaults to closestCoverageStrategy.
|
|
16
|
+
* @returns {Array<{purl: string, groupId: string, artifactId: string, currentVersion: string, fixedInVersion: string, fixedInPurl: string, provider: string, source: string, advisories: Array<{id: string, url: string}>, severity: string, cves: string[]}>}
|
|
17
|
+
*/
|
|
18
|
+
export function extractRemediations(analysisReport: import("@trustify-da/trustify-da-api-model/model/v5/AnalysisReport.ts").AnalysisReport, options?: {
|
|
19
|
+
providerPriority?: string[] | undefined;
|
|
20
|
+
versionStrategy?: VersionStrategy | undefined;
|
|
21
|
+
}): Array<{
|
|
22
|
+
purl: string;
|
|
23
|
+
groupId: string;
|
|
24
|
+
artifactId: string;
|
|
25
|
+
currentVersion: string;
|
|
26
|
+
fixedInVersion: string;
|
|
27
|
+
fixedInPurl: string;
|
|
28
|
+
provider: string;
|
|
29
|
+
source: string;
|
|
30
|
+
advisories: Array<{
|
|
31
|
+
id: string;
|
|
32
|
+
url: string;
|
|
33
|
+
}>;
|
|
34
|
+
severity: string;
|
|
35
|
+
cves: string[];
|
|
36
|
+
}>;
|
|
37
|
+
/**
|
|
38
|
+
* @typedef {{selectVersion: (fixedInVersions: string[], currentVersion: string) => string, resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => 'existing'|'candidate'}} VersionStrategy
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Version selection strategy that prefers the closest compatible version
|
|
42
|
+
* within the same major version stream. Falls back to the lowest cross-major
|
|
43
|
+
* version when no same-major option exists.
|
|
44
|
+
* @type {VersionStrategy}
|
|
45
|
+
*/
|
|
46
|
+
export const closestCoverageStrategy: VersionStrategy;
|
|
47
|
+
/**
|
|
48
|
+
* Version selection strategy that always picks the highest version regardless
|
|
49
|
+
* of major version distance. Guarantees maximum CVE coverage but may produce
|
|
50
|
+
* large version jumps. This is the original behavior before pluggable strategies.
|
|
51
|
+
* @type {VersionStrategy}
|
|
52
|
+
*/
|
|
53
|
+
export const highestStrategy: VersionStrategy;
|
|
54
|
+
export const SEVERITY_ORDER: string[];
|
|
55
|
+
export type VersionStrategy = {
|
|
56
|
+
selectVersion: (fixedInVersions: string[], currentVersion: string) => string;
|
|
57
|
+
resolveConflict: (existing: ConflictCandidate, candidate: ConflictCandidate) => "existing" | "candidate";
|
|
58
|
+
};
|