@verifik/mcp 0.0.0-stage → 0.1.2
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/LICENSE +21 -0
- package/README.md +543 -2
- package/data/field-enrichment.json +452 -0
- package/docs/field-formats.md +148 -0
- package/lib/catalog.js +183 -0
- package/lib/client-features.js +64 -0
- package/lib/config.js +75 -0
- package/lib/description.js +67 -0
- package/lib/error-response.js +210 -0
- package/lib/field-enrichment.js +103 -0
- package/lib/field-normalize.js +65 -0
- package/lib/filters.js +53 -0
- package/lib/meta-tools.js +128 -0
- package/lib/proxy.js +224 -0
- package/lib/tool-naming.js +51 -0
- package/lib/tool-schema.js +245 -0
- package/lib/url-builder.js +58 -0
- package/lib/validate-args.js +230 -0
- package/package.json +60 -5
- package/server.js +140 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/** Fields where common punctuation (dots, dashes, slashes, spaces) may appear but length limits apply to alphanumeric content. */
|
|
4
|
+
const PUNCTUATION_TOLERANT_FIELDS = new Set([
|
|
5
|
+
"documentNumber",
|
|
6
|
+
"plate",
|
|
7
|
+
"card",
|
|
8
|
+
"serial",
|
|
9
|
+
"citizenIdentifier",
|
|
10
|
+
"emissionNumber",
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Remove common formatting characters used in ID numbers and plates.
|
|
15
|
+
* @param {unknown} value
|
|
16
|
+
*/
|
|
17
|
+
const stripFormatting = (value) => {
|
|
18
|
+
return String(value).replace(/[.\-\s/]/g, "");
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} fieldName
|
|
23
|
+
*/
|
|
24
|
+
const isPunctuationTolerantField = (fieldName) => {
|
|
25
|
+
return PUNCTUATION_TOLERANT_FIELDS.has(fieldName);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Value used for minLength/maxLength validation (punctuation ignored for tolerant fields).
|
|
30
|
+
* @param {string} fieldName
|
|
31
|
+
* @param {unknown} value
|
|
32
|
+
*/
|
|
33
|
+
const valueForLengthCheck = (fieldName, value) => {
|
|
34
|
+
const text = String(value);
|
|
35
|
+
if (!isPunctuationTolerantField(fieldName)) return text;
|
|
36
|
+
return stripFormatting(text);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalize request params: trim plate whitespace; leave document numbers as entered.
|
|
41
|
+
* @param {Record<string, unknown>} params
|
|
42
|
+
*/
|
|
43
|
+
const normalizeRequestParams = (params) => {
|
|
44
|
+
if (!params || typeof params !== "object") return {};
|
|
45
|
+
|
|
46
|
+
const normalized = { ...params };
|
|
47
|
+
|
|
48
|
+
for (const [field, value] of Object.entries(normalized)) {
|
|
49
|
+
if (value == null || value === "") continue;
|
|
50
|
+
|
|
51
|
+
if (field === "plate" || field === "card") {
|
|
52
|
+
normalized[field] = String(value).replace(/\s+/g, "").trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return normalized;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
PUNCTUATION_TOLERANT_FIELDS,
|
|
61
|
+
stripFormatting,
|
|
62
|
+
isPunctuationTolerantField,
|
|
63
|
+
valueForLengthCheck,
|
|
64
|
+
normalizeRequestParams,
|
|
65
|
+
};
|
package/lib/filters.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {import("./types").AppFeature} feature
|
|
5
|
+
* @param {import("./types").McpConfig} config
|
|
6
|
+
* @param {Set<string>|null} grantedFeatureCodes
|
|
7
|
+
*/
|
|
8
|
+
const isEligibleFeature = (feature, config, grantedFeatureCodes = null) => {
|
|
9
|
+
if (!feature?.code || !feature?.url) return false;
|
|
10
|
+
if (config.smartCheckOnly && !feature.smartCheckEnabled) return false;
|
|
11
|
+
if (config.codeAllowlist.length && !config.codeAllowlist.includes(feature.code)) return false;
|
|
12
|
+
if (config.countryFilter.length && !config.countryFilter.includes(feature.country)) return false;
|
|
13
|
+
if (config.baseCategoryFilter.length && !config.baseCategoryFilter.includes(feature.baseCategory)) return false;
|
|
14
|
+
|
|
15
|
+
if (feature.requiresClientFeature && grantedFeatureCodes && !grantedFeatureCodes.has(feature.code)) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return true;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @param {import("./types").AppFeature[]} features
|
|
24
|
+
* @param {import("./types").McpConfig} config
|
|
25
|
+
* @param {Set<string>|null} [grantedFeatureCodes]
|
|
26
|
+
*/
|
|
27
|
+
const filterFeatures = (features, config, grantedFeatureCodes = null) => {
|
|
28
|
+
if (!Array.isArray(features)) return [];
|
|
29
|
+
return features.filter((feature) => isEligibleFeature(feature, config, grantedFeatureCodes));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {import("./types").AppFeature} feature
|
|
34
|
+
*/
|
|
35
|
+
const toFeatureSummary = (feature) => ({
|
|
36
|
+
code: feature.code,
|
|
37
|
+
name: feature.name,
|
|
38
|
+
description: feature.description,
|
|
39
|
+
country: feature.country,
|
|
40
|
+
baseCategory: feature.baseCategory,
|
|
41
|
+
url: feature.url,
|
|
42
|
+
method: feature.method || "GET",
|
|
43
|
+
smartCheckEnabled: Boolean(feature.smartCheckEnabled),
|
|
44
|
+
price: feature.price,
|
|
45
|
+
smartCheckPrice: feature.smartCheckPrice,
|
|
46
|
+
dependencyCount: Array.isArray(feature.dependencies) ? feature.dependencies.length : 0,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
isEligibleFeature,
|
|
51
|
+
filterFeatures,
|
|
52
|
+
toFeatureSummary,
|
|
53
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { toFeatureSummary } = require("./filters");
|
|
4
|
+
const { buildInputSchema } = require("./tool-schema");
|
|
5
|
+
|
|
6
|
+
const LIST_CATALOG_TOOL = "verifik_list_catalog";
|
|
7
|
+
const GET_FEATURE_TOOL = "verifik_get_feature";
|
|
8
|
+
|
|
9
|
+
const META_TOOL_DEFINITIONS = [
|
|
10
|
+
{
|
|
11
|
+
name: LIST_CATALOG_TOOL,
|
|
12
|
+
description:
|
|
13
|
+
"List SmartCheck / Database Screening features available to this client without charging credits.",
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
country: {
|
|
18
|
+
type: "string",
|
|
19
|
+
description: "Optional comma-separated country filter (e.g. Colombia,world).",
|
|
20
|
+
},
|
|
21
|
+
baseCategory: {
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Optional comma-separated baseCategory filter.",
|
|
24
|
+
},
|
|
25
|
+
code: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional comma-separated feature code allowlist.",
|
|
28
|
+
},
|
|
29
|
+
smartCheckOnly: {
|
|
30
|
+
type: "boolean",
|
|
31
|
+
description: "When true, only return smartCheckEnabled features. Defaults to server config.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
additionalProperties: false,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: GET_FEATURE_TOOL,
|
|
39
|
+
description: "Get full metadata and dependency schema for one AppFeature by code (no credit charge).",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
code: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "AppFeature code (e.g. colombia_api_identity_lookup).",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["code"],
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const META_TOOL_NAMES = new Set(META_TOOL_DEFINITIONS.map((tool) => tool.name));
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import("./catalog").CatalogCache} catalogCache
|
|
58
|
+
* @param {Record<string, unknown>} args
|
|
59
|
+
*/
|
|
60
|
+
const handleListCatalog = async (catalogCache, args = {}) => {
|
|
61
|
+
const state = await catalogCache.getState();
|
|
62
|
+
const countryFilter = parseOptionalList(args.country);
|
|
63
|
+
const baseCategoryFilter = parseOptionalList(args.baseCategory);
|
|
64
|
+
const codeFilter = parseOptionalList(args.code);
|
|
65
|
+
const smartCheckOnly =
|
|
66
|
+
args.smartCheckOnly === undefined ? undefined : Boolean(args.smartCheckOnly);
|
|
67
|
+
|
|
68
|
+
const features = state.features.filter((feature) => {
|
|
69
|
+
if (smartCheckOnly === true && !feature.smartCheckEnabled) return false;
|
|
70
|
+
if (countryFilter.length && !countryFilter.includes(feature.country)) return false;
|
|
71
|
+
if (baseCategoryFilter.length && !baseCategoryFilter.includes(feature.baseCategory)) return false;
|
|
72
|
+
if (codeFilter.length && !codeFilter.includes(feature.code)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
loadedAt: state.loadedAt,
|
|
78
|
+
total: features.length,
|
|
79
|
+
features: features.map(toFeatureSummary),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @param {import("./catalog").CatalogCache} catalogCache
|
|
85
|
+
* @param {Record<string, unknown>} args
|
|
86
|
+
*/
|
|
87
|
+
const handleGetFeature = async (catalogCache, args = {}) => {
|
|
88
|
+
const code = String(args.code || "").trim();
|
|
89
|
+
|
|
90
|
+
if (!code) {
|
|
91
|
+
throw new Error("code is required");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const feature = catalogCache.findFeatureByCode(code);
|
|
95
|
+
|
|
96
|
+
if (!feature) {
|
|
97
|
+
throw new Error(`feature_not_found:${code}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const toolName = catalogCache.state?.registry?.featureCodeToToolName?.get(code) || null;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
...toFeatureSummary(feature),
|
|
104
|
+
toolName,
|
|
105
|
+
dependencies: feature.dependencies || [],
|
|
106
|
+
inputSchema: buildInputSchema(feature),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {unknown} value
|
|
112
|
+
*/
|
|
113
|
+
const parseOptionalList = (value) => {
|
|
114
|
+
if (value == null || value === "") return [];
|
|
115
|
+
return String(value)
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((entry) => entry.trim())
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
LIST_CATALOG_TOOL,
|
|
123
|
+
GET_FEATURE_TOOL,
|
|
124
|
+
META_TOOL_DEFINITIONS,
|
|
125
|
+
META_TOOL_NAMES,
|
|
126
|
+
handleListCatalog,
|
|
127
|
+
handleGetFeature,
|
|
128
|
+
};
|
package/lib/proxy.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
buildFeatureRequestUrl,
|
|
5
|
+
normalizeHttpMethod,
|
|
6
|
+
sanitizeRequestParams,
|
|
7
|
+
splitRequestPayload,
|
|
8
|
+
} = require("./url-builder");
|
|
9
|
+
const { getRequestOverride, getParameterAliases } = require("./field-enrichment");
|
|
10
|
+
const { normalizeRequestParams } = require("./field-normalize");
|
|
11
|
+
const {
|
|
12
|
+
extractCreditsCharged,
|
|
13
|
+
buildProxyErrorPayload,
|
|
14
|
+
sanitizeResponseData,
|
|
15
|
+
classifyUpstreamError,
|
|
16
|
+
SERVICE_UNAVAILABLE_MESSAGE,
|
|
17
|
+
} = require("./error-response");
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import("./types").AppFeature} feature
|
|
21
|
+
*/
|
|
22
|
+
const resolveFeatureRequest = (feature) => {
|
|
23
|
+
const override = getRequestOverride(feature.code);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
method: override?.method || feature.method,
|
|
27
|
+
url: override?.url || feature.url,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} featureCode
|
|
33
|
+
* @param {Record<string, unknown>} params
|
|
34
|
+
*/
|
|
35
|
+
const applyParameterAliases = (featureCode, params) => {
|
|
36
|
+
const aliases = getParameterAliases(featureCode);
|
|
37
|
+
const mapped = { ...params };
|
|
38
|
+
|
|
39
|
+
for (const [from, to] of Object.entries(aliases)) {
|
|
40
|
+
if (mapped[from] !== undefined) {
|
|
41
|
+
mapped[to] = mapped[from];
|
|
42
|
+
delete mapped[from];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return mapped;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import("./types").AppFeature} feature
|
|
51
|
+
* @param {Record<string, unknown>} args
|
|
52
|
+
* @param {import("./types").McpConfig} config
|
|
53
|
+
*/
|
|
54
|
+
const invokeFeature = async (feature, args, config) => {
|
|
55
|
+
const requestMeta = resolveFeatureRequest(feature);
|
|
56
|
+
const method = normalizeHttpMethod(requestMeta.method);
|
|
57
|
+
const url = buildFeatureRequestUrl(config.apiBase, requestMeta.url);
|
|
58
|
+
const params = applyParameterAliases(
|
|
59
|
+
feature.code,
|
|
60
|
+
normalizeRequestParams(sanitizeRequestParams(args))
|
|
61
|
+
);
|
|
62
|
+
const { query, body } = splitRequestPayload(method, params);
|
|
63
|
+
|
|
64
|
+
const requestInit = {
|
|
65
|
+
method,
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${config.token}`,
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
},
|
|
70
|
+
signal: AbortSignal.timeout(config.requestTimeoutMs),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (body !== undefined) {
|
|
74
|
+
requestInit.headers["Content-Type"] = "application/json";
|
|
75
|
+
requestInit.body = JSON.stringify({ ...body, includeCost: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const requestUrl = query ? appendQueryParams(url, { ...query, includeCost: "true" }) : url;
|
|
79
|
+
const startedAt = Date.now();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const response = await fetch(requestUrl, requestInit);
|
|
83
|
+
const responseBody = await readResponseBody(response);
|
|
84
|
+
const durationMs = Date.now() - startedAt;
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
ok: response.ok,
|
|
88
|
+
status: response.status,
|
|
89
|
+
statusText: response.statusText,
|
|
90
|
+
body: responseBody,
|
|
91
|
+
durationMs,
|
|
92
|
+
request: {
|
|
93
|
+
method,
|
|
94
|
+
url: requestUrl,
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
const durationMs = Date.now() - startedAt;
|
|
99
|
+
|
|
100
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
status: 504,
|
|
104
|
+
statusText: "Gateway Timeout",
|
|
105
|
+
body: null,
|
|
106
|
+
durationMs,
|
|
107
|
+
timedOut: true,
|
|
108
|
+
request: {
|
|
109
|
+
method,
|
|
110
|
+
url: requestUrl,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* @param {string} url
|
|
121
|
+
* @param {Record<string, unknown>} query
|
|
122
|
+
*/
|
|
123
|
+
const appendQueryParams = (url, query) => {
|
|
124
|
+
const target = new URL(url);
|
|
125
|
+
|
|
126
|
+
for (const [key, value] of Object.entries(query)) {
|
|
127
|
+
if (Array.isArray(value)) {
|
|
128
|
+
for (const entry of value) target.searchParams.append(key, String(entry));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
target.searchParams.set(key, String(value));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return target.toString();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {Response} response
|
|
140
|
+
*/
|
|
141
|
+
const readResponseBody = async (response) => {
|
|
142
|
+
const contentType = response.headers.get("content-type") || "";
|
|
143
|
+
|
|
144
|
+
if (contentType.includes("application/json")) {
|
|
145
|
+
return response.json();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return response.text();
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {{ status: number, statusText?: string, body: unknown, durationMs?: number, request?: { method: string, url: string }, timedOut?: boolean }} result
|
|
153
|
+
*/
|
|
154
|
+
const formatProxyResult = (result) => {
|
|
155
|
+
if (result.timedOut) {
|
|
156
|
+
const payload = {
|
|
157
|
+
ok: false,
|
|
158
|
+
httpStatus: result.status,
|
|
159
|
+
statusText: result.statusText,
|
|
160
|
+
durationMs: result.durationMs,
|
|
161
|
+
error: {
|
|
162
|
+
code: "ServiceUnavailable",
|
|
163
|
+
message: SERVICE_UNAVAILABLE_MESSAGE,
|
|
164
|
+
retryable: true,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
text: JSON.stringify(payload, null, 2),
|
|
170
|
+
structuredContent: payload,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const creditsCharged = extractCreditsCharged(result.body);
|
|
175
|
+
const data = result.ok ? sanitizeResponseData(result.body) : undefined;
|
|
176
|
+
|
|
177
|
+
const payload = {
|
|
178
|
+
ok: result.ok,
|
|
179
|
+
httpStatus: result.status,
|
|
180
|
+
statusText: result.statusText,
|
|
181
|
+
durationMs: result.durationMs,
|
|
182
|
+
...(creditsCharged != null ? { creditsCharged } : {}),
|
|
183
|
+
...(result.ok ? { data } : {}),
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
Object.assign(payload, buildProxyErrorPayload(result));
|
|
188
|
+
delete payload.data;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
text: JSON.stringify(payload, null, 2),
|
|
193
|
+
structuredContent: payload,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} message
|
|
199
|
+
*/
|
|
200
|
+
const formatValidationError = (message) => {
|
|
201
|
+
const payload = {
|
|
202
|
+
ok: false,
|
|
203
|
+
error: {
|
|
204
|
+
code: "ValidationError",
|
|
205
|
+
message,
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
text: JSON.stringify(payload, null, 2),
|
|
211
|
+
structuredContent: payload,
|
|
212
|
+
};
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
module.exports = {
|
|
216
|
+
invokeFeature,
|
|
217
|
+
formatProxyResult,
|
|
218
|
+
formatValidationError,
|
|
219
|
+
appendQueryParams,
|
|
220
|
+
applyParameterAliases,
|
|
221
|
+
resolveFeatureRequest,
|
|
222
|
+
extractCreditsCharged,
|
|
223
|
+
classifyUpstreamError,
|
|
224
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { createHash } = require("node:crypto");
|
|
4
|
+
const { getToolNameOverride } = require("./field-enrichment");
|
|
5
|
+
|
|
6
|
+
const TOOL_NAME_REGEX = /^[a-z][a-z0-9_]{0,127}$/;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a catalog feature code into a stable MCP tool name.
|
|
10
|
+
* @param {string} code
|
|
11
|
+
*/
|
|
12
|
+
const normalizeToolName = (code) => {
|
|
13
|
+
const override = getToolNameOverride(code);
|
|
14
|
+
if (override) return override;
|
|
15
|
+
|
|
16
|
+
const raw = String(code || "")
|
|
17
|
+
.trim()
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/-/g, "_")
|
|
20
|
+
.replace(/[^a-z0-9_]+/g, "_")
|
|
21
|
+
.replace(/_+/g, "_")
|
|
22
|
+
.replace(/^_+|_+$/g, "");
|
|
23
|
+
|
|
24
|
+
if (!raw) return "verifik_feature";
|
|
25
|
+
|
|
26
|
+
if (raw.length <= 128 && TOOL_NAME_REGEX.test(raw)) return raw;
|
|
27
|
+
|
|
28
|
+
const hash = createHash("sha1").update(String(code)).digest("hex").slice(0, 8);
|
|
29
|
+
const prefix = raw.slice(0, 119).replace(/_+$/g, "");
|
|
30
|
+
return `${prefix}_${hash}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {import("./types").AppFeature[]} features
|
|
35
|
+
*/
|
|
36
|
+
const buildToolNameMapping = (features) => {
|
|
37
|
+
const mapping = new Map();
|
|
38
|
+
|
|
39
|
+
for (const feature of features) {
|
|
40
|
+
if (!feature?.code) continue;
|
|
41
|
+
mapping.set(feature.code, normalizeToolName(feature.code));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return mapping;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
normalizeToolName,
|
|
49
|
+
buildToolNameMapping,
|
|
50
|
+
TOOL_NAME_REGEX,
|
|
51
|
+
};
|