@endday/search-mcp 1.0.0 → 1.0.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.
Files changed (84) hide show
  1. package/dist/index.js +4724 -0
  2. package/{mcp → dist}/search-mcp.js +1 -2
  3. package/package.json +14 -14
  4. package/data/blocklist.generated.js +0 -2
  5. package/envs.js +0 -129
  6. package/index.js +0 -6
  7. package/src/content/extract.impl.js +0 -228
  8. package/src/content/extract.js +0 -1
  9. package/src/content/fetch.impl.js +0 -400
  10. package/src/content/fetch.js +0 -1
  11. package/src/core/crypto.js +0 -7
  12. package/src/core/errors.impl.js +0 -52
  13. package/src/core/errors.js +0 -1
  14. package/src/core/html.impl.js +0 -69
  15. package/src/core/html.js +0 -1
  16. package/src/mcp/config.js +0 -75
  17. package/src/mcp/format.js +0 -44
  18. package/src/mcp/index.js +0 -10
  19. package/src/mcp/local/content.js +0 -26
  20. package/src/mcp/local/search.js +0 -233
  21. package/src/mcp/schemas.js +0 -132
  22. package/src/mcp/server.js +0 -97
  23. package/src/mcp/tools/content.js +0 -31
  24. package/src/mcp/tools/jinaContent.js +0 -38
  25. package/src/mcp/tools/newsSearch.js +0 -22
  26. package/src/mcp/tools/webSearch.js +0 -57
  27. package/src/platform/auth.impl.js +0 -166
  28. package/src/platform/auth.js +0 -1
  29. package/src/platform/cache.impl.js +0 -166
  30. package/src/platform/cache.js +0 -1
  31. package/src/platform/health.impl.js +0 -133
  32. package/src/platform/health.js +0 -1
  33. package/src/platform/http.impl.js +0 -108
  34. package/src/platform/http.js +0 -1
  35. package/src/platform/logger.impl.js +0 -51
  36. package/src/platform/logger.js +0 -1
  37. package/src/platform/metrics.impl.js +0 -43
  38. package/src/platform/metrics.js +0 -1
  39. package/src/platform/nodeHttpClient.js +0 -104
  40. package/src/platform/rateLimit.impl.js +0 -141
  41. package/src/platform/rateLimit.js +0 -1
  42. package/src/platform/requestContext.impl.js +0 -10
  43. package/src/platform/requestContext.js +0 -1
  44. package/src/platform/session.impl.js +0 -198
  45. package/src/platform/session.js +0 -1
  46. package/src/platform/stateKv.impl.js +0 -18
  47. package/src/platform/stateKv.js +0 -1
  48. package/src/platform/tasks.impl.js +0 -17
  49. package/src/platform/tasks.js +0 -1
  50. package/src/routes/requestParams.impl.js +0 -12
  51. package/src/routes/requestParams.js +0 -1
  52. package/src/search/engineRegistry.impl.js +0 -117
  53. package/src/search/engineRegistry.js +0 -1
  54. package/src/search/engineRequest.impl.js +0 -377
  55. package/src/search/engineRequest.js +0 -1
  56. package/src/search/engineUtils.impl.js +0 -227
  57. package/src/search/engineUtils.js +0 -1
  58. package/src/search/engines/baidu.impl.js +0 -145
  59. package/src/search/engines/baidu.js +0 -2
  60. package/src/search/engines/bing.impl.js +0 -509
  61. package/src/search/engines/bing.js +0 -2
  62. package/src/search/engines/brave.impl.js +0 -223
  63. package/src/search/engines/brave.js +0 -2
  64. package/src/search/engines/duckduckgo.impl.js +0 -164
  65. package/src/search/engines/duckduckgo.js +0 -2
  66. package/src/search/engines/mojeek.impl.js +0 -115
  67. package/src/search/engines/mojeek.js +0 -2
  68. package/src/search/engines/qwant.impl.js +0 -188
  69. package/src/search/engines/qwant.js +0 -2
  70. package/src/search/engines/startpage.impl.js +0 -237
  71. package/src/search/engines/startpage.js +0 -2
  72. package/src/search/engines/toutiao.impl.js +0 -265
  73. package/src/search/engines/toutiao.js +0 -2
  74. package/src/search/engines/yahoo.impl.js +0 -379
  75. package/src/search/engines/yahoo.js +0 -2
  76. package/src/search/gateway.impl.js +0 -423
  77. package/src/search/gateway.js +0 -1
  78. package/src/search/ranking.impl.js +0 -381
  79. package/src/search/ranking.js +0 -1
  80. package/src/search/requestPolicy.impl.js +0 -137
  81. package/src/search/requestPolicy.js +0 -1
  82. package/src/search/upstreamSession.impl.js +0 -148
  83. package/src/search/upstreamSession.js +0 -1
  84. /package/{index.d.ts → dist/index.d.ts} +0 -0
@@ -1,377 +0,0 @@
1
- import { ApiError } from "../core/errors.js";
2
- import {
3
- getBrowserProfileById,
4
- getBrowserProfilesForEngine,
5
- getRandomBrowserProfile,
6
- getAcceptLanguageHeader,
7
- } from "./engineUtils.js";
8
- import {
9
- buildEnginePolicy,
10
- enforceEngineThrottle,
11
- sleepBeforeRetry,
12
- shouldRetryUpstream,
13
- } from "./requestPolicy.js";
14
- import {
15
- getUpstreamSession,
16
- createDeferredUpstreamSessionWriter,
17
- } from "./upstreamSession.js";
18
- import { runDeferredTask } from "../platform/tasks.js";
19
- import { fetchWithOptionalCurlImpersonate } from "../platform/nodeHttpClient.js";
20
-
21
- function appendParam(target, key, value) {
22
- if (value === undefined || value === null || value === "") {
23
- return;
24
- }
25
-
26
- if (Array.isArray(value)) {
27
- value.forEach((item) => appendParam(target, key, item));
28
- return;
29
- }
30
-
31
- target.append(key, String(value));
32
- }
33
-
34
- function applyParams(target, params) {
35
- if (!params) {
36
- return;
37
- }
38
-
39
- if (params instanceof URLSearchParams) {
40
- for (const [key, value] of params.entries()) {
41
- appendParam(target, key, value);
42
- }
43
- return;
44
- }
45
-
46
- for (const [key, value] of Object.entries(params)) {
47
- appendParam(target, key, value);
48
- }
49
- }
50
-
51
- function normalizeHeaders(headers = {}) {
52
- const normalized = {};
53
- const entries =
54
- headers instanceof Headers ? headers.entries() : Object.entries(headers);
55
-
56
- for (const [key, value] of entries) {
57
- if (value === undefined || value === null || value === "") {
58
- continue;
59
- }
60
-
61
- normalized[String(key).toLowerCase()] = String(value);
62
- }
63
-
64
- return normalized;
65
- }
66
-
67
- function toFormBody(form) {
68
- const params = new URLSearchParams();
69
- applyParams(params, form);
70
- return params.toString();
71
- }
72
-
73
- export function buildCookieHeader(cookies) {
74
- if (!cookies) {
75
- return "";
76
- }
77
-
78
- if (typeof cookies === "string") {
79
- return cookies.trim();
80
- }
81
-
82
- const parts = [];
83
- const entries = Array.isArray(cookies) ? cookies : Object.entries(cookies);
84
-
85
- for (const entry of entries) {
86
- if (!Array.isArray(entry) || entry.length < 2) {
87
- continue;
88
- }
89
-
90
- const [key, value] = entry;
91
- if (!key || value === undefined || value === null || value === "") {
92
- continue;
93
- }
94
-
95
- parts.push(`${String(key).trim()}=${String(value).trim()}`);
96
- }
97
-
98
- return parts.join("; ");
99
- }
100
-
101
- function mergeCookieHeaders(baseCookies, extraCookies) {
102
- const merged = [buildCookieHeader(baseCookies), buildCookieHeader(extraCookies)]
103
- .filter(Boolean)
104
- .join("; ");
105
-
106
- return merged || "";
107
- }
108
-
109
- function parseResponseCookies(response) {
110
- const setCookie = response.headers.get("set-cookie");
111
- if (!setCookie) {
112
- return {};
113
- }
114
-
115
- const parsed = {};
116
- for (const item of setCookie.split(/,(?=[^;]+=[^;]+)/)) {
117
- const [pair] = item.split(";");
118
- const separatorIndex = pair.indexOf("=");
119
- if (separatorIndex <= 0) {
120
- continue;
121
- }
122
-
123
- const key = pair.slice(0, separatorIndex).trim();
124
- const value = pair.slice(separatorIndex + 1).trim();
125
- if (key) {
126
- parsed[key] = value;
127
- }
128
- }
129
-
130
- return parsed;
131
- }
132
-
133
- function buildNavigationContext({ referrer, origin, headers, requestMethod }) {
134
- const normalizedHeaders = normalizeHeaders(headers);
135
- const resolvedReferrer = referrer || normalizedHeaders.referer || normalizedHeaders.referrer;
136
- let derivedOrigin = "";
137
- if (resolvedReferrer) {
138
- try {
139
- derivedOrigin = new URL(resolvedReferrer).origin;
140
- } catch (_) {
141
- derivedOrigin = "";
142
- }
143
- }
144
- const resolvedOrigin =
145
- origin ||
146
- normalizedHeaders.origin ||
147
- derivedOrigin;
148
- const hasBody = requestMethod !== "GET";
149
- const sameOriginReferrer =
150
- resolvedReferrer && resolvedOrigin && resolvedReferrer.startsWith(resolvedOrigin);
151
-
152
- return {
153
- referrer: resolvedReferrer,
154
- origin: resolvedOrigin,
155
- secFetchSite:
156
- normalizedHeaders["sec-fetch-site"] ||
157
- (sameOriginReferrer ? "same-origin" : hasBody ? "same-site" : "none"),
158
- };
159
- }
160
-
161
- export function isChallengeResponse(source, patterns = []) {
162
- const text = String(source || "");
163
-
164
- return patterns.some((pattern) => {
165
- if (!pattern) {
166
- return false;
167
- }
168
-
169
- if (pattern instanceof RegExp) {
170
- pattern.lastIndex = 0;
171
- return pattern.test(text);
172
- }
173
-
174
- return text.includes(String(pattern));
175
- });
176
- }
177
-
178
- export function createBlockedUpstreamError({
179
- engine = "Upstream",
180
- surface = "response",
181
- message,
182
- details,
183
- } = {}) {
184
- return new ApiError({
185
- status: 502,
186
- code: "UPSTREAM_BLOCKED",
187
- category: "upstream",
188
- message: message || `${engine} returned a bot-detection challenge (${surface})`,
189
- details,
190
- });
191
- }
192
-
193
- export function throwBlockedUpstreamError(options = {}) {
194
- throw createBlockedUpstreamError(options);
195
- }
196
-
197
- export function buildEngineRequest(
198
- url,
199
- {
200
- engine,
201
- signal,
202
- language,
203
- method,
204
- searchParams,
205
- form,
206
- body,
207
- headers = {},
208
- cookies,
209
- referrer,
210
- accept,
211
- acceptLanguage,
212
- userAgent,
213
- origin,
214
- sessionHeaders = {},
215
- profile,
216
- extraCookies,
217
- } = {}
218
- ) {
219
- const requestUrl = new URL(url);
220
- const requestMethod = String(method || (form ? "POST" : "GET")).toUpperCase();
221
- const selectedProfile = profile || getRandomBrowserProfile(engine);
222
- const navigation = buildNavigationContext({
223
- referrer,
224
- origin,
225
- headers,
226
- requestMethod,
227
- });
228
- const requestHeaders = normalizeHeaders({
229
- accept:
230
- accept ||
231
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
232
- "accept-language": acceptLanguage || getAcceptLanguageHeader(language),
233
- "user-agent": userAgent || selectedProfile.ua,
234
- "sec-fetch-dest": "document",
235
- "sec-fetch-mode": "navigate",
236
- "sec-fetch-site": navigation.secFetchSite,
237
- "sec-fetch-user": "?1",
238
- "cache-control": requestMethod === "GET" ? "max-age=0" : "no-cache",
239
- pragma: "no-cache",
240
- "upgrade-insecure-requests": "1",
241
- ...normalizeHeaders(sessionHeaders),
242
- ...normalizeHeaders(headers),
243
- });
244
-
245
- // Chrome Client Hints; Firefox skips these
246
- if (selectedProfile.headers?.["sec-ch-ua"] && !requestHeaders["sec-ch-ua"]) {
247
- requestHeaders["sec-ch-ua"] = selectedProfile.headers["sec-ch-ua"];
248
- requestHeaders["sec-ch-ua-platform"] = selectedProfile.headers["sec-ch-ua-platform"];
249
- requestHeaders["sec-ch-ua-mobile"] = selectedProfile.headers["sec-ch-ua-mobile"];
250
- }
251
- const cookieHeader = mergeCookieHeaders(cookies, extraCookies);
252
-
253
- if (cookieHeader) {
254
- requestHeaders.cookie = cookieHeader;
255
- }
256
-
257
- if (navigation.origin) {
258
- requestHeaders.origin = navigation.origin;
259
- }
260
-
261
- applyParams(requestUrl.searchParams, searchParams);
262
-
263
- let requestBody = body;
264
- if (form && requestBody === undefined) {
265
- requestBody = toFormBody(form);
266
- if (!requestHeaders["content-type"]) {
267
- requestHeaders["content-type"] =
268
- "application/x-www-form-urlencoded; charset=UTF-8";
269
- }
270
- }
271
-
272
- const init = {
273
- method: requestMethod,
274
- signal,
275
- redirect: "follow",
276
- referrer: navigation.referrer,
277
- headers: requestHeaders,
278
- };
279
-
280
- if (requestBody !== undefined && requestMethod !== "GET") {
281
- init.body = requestBody;
282
- }
283
-
284
- return {
285
- url: requestUrl.toString(),
286
- init,
287
- };
288
- }
289
-
290
- export async function fetchSearchText(
291
- url,
292
- {
293
- engine,
294
- engineLabel,
295
- clientId,
296
- blockedStatuses = [],
297
- isBlocked,
298
- blockedSurface = "response",
299
- runtimeContext,
300
- ...requestOptions
301
- } = {}
302
- ) {
303
- const policy = buildEnginePolicy({ name: engine, requestPolicy: requestOptions.requestPolicy });
304
- const profiles = getBrowserProfilesForEngine(engine);
305
- const upstreamSession = await getUpstreamSession(clientId, engine, profiles);
306
- const selectedProfile =
307
- getBrowserProfileById(upstreamSession?.profileId) ||
308
- profiles[0] ||
309
- getRandomBrowserProfile(engine);
310
-
311
- for (let attempt = 0; ; attempt += 1) {
312
- await enforceEngineThrottle(engine || "upstream", policy);
313
- const { url: requestUrl, init } = buildEngineRequest(url, {
314
- ...requestOptions,
315
- engine,
316
- profile: selectedProfile,
317
- extraCookies: upstreamSession?.cookies,
318
- });
319
-
320
- try {
321
- const response = await fetchWithOptionalCurlImpersonate(requestUrl, init, {
322
- profile: selectedProfile,
323
- });
324
- const details = {
325
- engine,
326
- upstream_status: response.status,
327
- url: requestUrl,
328
- attempt,
329
- };
330
-
331
- if (blockedStatuses.includes(response.status)) {
332
- throw createBlockedUpstreamError({
333
- engine: engineLabel || engine || "Upstream",
334
- surface: `status ${response.status}`,
335
- details,
336
- });
337
- }
338
-
339
- if (!response.ok) {
340
- throw new ApiError({
341
- status: 502,
342
- code: "UPSTREAM_BAD_STATUS",
343
- category: "upstream",
344
- message: `Upstream request failed with status ${response.status}`,
345
- details,
346
- });
347
- }
348
-
349
- const text = await response.text();
350
-
351
- if (typeof isBlocked === "function" && isBlocked(text, response)) {
352
- throw createBlockedUpstreamError({
353
- engine: engineLabel || engine || "Upstream",
354
- surface: blockedSurface,
355
- details,
356
- });
357
- }
358
-
359
- const responseCookies = parseResponseCookies(response);
360
- if (clientId && (upstreamSession?.profileId || Object.keys(responseCookies).length > 0)) {
361
- const writeSession = createDeferredUpstreamSessionWriter(clientId, engine, {
362
- profileId: upstreamSession?.profileId || selectedProfile.id,
363
- cookies: responseCookies,
364
- });
365
- await runDeferredTask(runtimeContext, `upstream-session:${engine}`, writeSession);
366
- }
367
-
368
- return text;
369
- } catch (error) {
370
- if (!shouldRetryUpstream(error, attempt, policy)) {
371
- throw error;
372
- }
373
-
374
- await sleepBeforeRetry(policy);
375
- }
376
- }
377
- }
@@ -1 +0,0 @@
1
- export * from "./engineRequest.impl.js";
@@ -1,227 +0,0 @@
1
- import { ApiError } from "../core/errors.js";
2
- import { fetchWithOptionalCurlImpersonate } from "../platform/nodeHttpClient.js";
3
-
4
- const PROFILE_CATALOG = {
5
- default: [
6
- {
7
- id: "chrome-win",
8
- ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
9
- headers: {
10
- accept:
11
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
12
- "sec-ch-ua": '"Chromium";v="126", "Google Chrome";v="126", "Not-A.Brand";v="99"',
13
- "sec-ch-ua-platform": '"Windows"',
14
- "sec-ch-ua-mobile": "?0",
15
- "sec-fetch-dest": "document",
16
- "sec-fetch-mode": "navigate",
17
- "sec-fetch-user": "?1",
18
- "upgrade-insecure-requests": "1",
19
- },
20
- },
21
- {
22
- id: "chrome-mac",
23
- ua: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
24
- headers: {
25
- accept:
26
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
27
- "sec-ch-ua": '"Chromium";v="126", "Google Chrome";v="126", "Not-A.Brand";v="99"',
28
- "sec-ch-ua-platform": '"macOS"',
29
- "sec-ch-ua-mobile": "?0",
30
- "sec-fetch-dest": "document",
31
- "sec-fetch-mode": "navigate",
32
- "sec-fetch-user": "?1",
33
- "upgrade-insecure-requests": "1",
34
- },
35
- },
36
- {
37
- id: "firefox-win",
38
- ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
39
- headers: {
40
- accept:
41
- "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
42
- "sec-fetch-dest": "document",
43
- "sec-fetch-mode": "navigate",
44
- "sec-fetch-user": "?1",
45
- "upgrade-insecure-requests": "1",
46
- },
47
- },
48
- ],
49
- bing: ["chrome-win", "chrome-mac"],
50
- startpage: ["chrome-win", "firefox-win"],
51
- duckduckgo: ["chrome-mac", "firefox-win"],
52
- brave: ["chrome-win", "chrome-mac"],
53
- yahoo: ["chrome-win", "firefox-win"],
54
- qwant: ["chrome-mac", "firefox-win"],
55
- mojeek: ["chrome-win", "chrome-mac"],
56
- toutiao: ["chrome-mac"],
57
- };
58
-
59
- const PROFILE_LOOKUP = new Map(
60
- PROFILE_CATALOG.default.map((profile) => [profile.id, profile])
61
- );
62
-
63
- let profileIndex = 0;
64
-
65
- function getProfileIdsForEngine(engine) {
66
- const normalizedEngine = String(engine || "").toLowerCase();
67
- if (normalizedEngine === "default") {
68
- return PROFILE_CATALOG.default.map((profile) => profile.id);
69
- }
70
-
71
- const configured = PROFILE_CATALOG[normalizedEngine];
72
- if (!configured || configured.length === 0) {
73
- return PROFILE_CATALOG.default.map((profile) => profile.id);
74
- }
75
-
76
- return configured;
77
- }
78
-
79
- export function getBrowserProfilesForEngine(engine) {
80
- return getProfileIdsForEngine(engine)
81
- .map((id) => PROFILE_LOOKUP.get(id))
82
- .filter(Boolean);
83
- }
84
-
85
- export function getRandomUserAgent(engine = "default") {
86
- const profiles = getBrowserProfilesForEngine(engine);
87
- profileIndex = (profileIndex + 1) % profiles.length;
88
- return profiles[profileIndex].ua;
89
- }
90
-
91
- export function getRandomBrowserProfile(engine = "default") {
92
- const profiles = getBrowserProfilesForEngine(engine);
93
- profileIndex = (profileIndex + 1) % profiles.length;
94
- return profiles[profileIndex];
95
- }
96
-
97
- export function getBrowserProfileById(profileId) {
98
- return PROFILE_LOOKUP.get(profileId) || null;
99
- }
100
-
101
- export function getAcceptLanguageHeader(language) {
102
- const normalized = String(language || "").trim().toLowerCase();
103
-
104
- if (!normalized) {
105
- return "en-US,en;q=0.9";
106
- }
107
-
108
- if (normalized.startsWith("zh")) {
109
- return "zh-CN,zh;q=0.9,en;q=0.8";
110
- }
111
-
112
- if (normalized.startsWith("en-gb")) {
113
- return "en-GB,en;q=0.9";
114
- }
115
-
116
- if (normalized.startsWith("en")) {
117
- return "en-US,en;q=0.9";
118
- }
119
-
120
- return `${language},en;q=0.8`;
121
- }
122
-
123
- export function getDefaultFetchHeaders(language, extraHeaders = {}, engine = "default") {
124
- const profile = getRandomBrowserProfile(engine);
125
- return {
126
- ...profile.headers,
127
- "accept-language": getAcceptLanguageHeader(language),
128
- "user-agent": profile.ua,
129
- "sec-fetch-site": "none",
130
- ...extraHeaders,
131
- };
132
- }
133
-
134
- export async function fetchText(
135
- url,
136
- { signal, language, headers = {}, referrer, engine = "default" } = {}
137
- ) {
138
- const response = await fetchWithOptionalCurlImpersonate(url, {
139
- signal,
140
- redirect: "follow",
141
- referrer,
142
- headers: getDefaultFetchHeaders(language, headers, engine),
143
- });
144
-
145
- if (!response.ok) {
146
- throw new ApiError({
147
- status: 502,
148
- code: "UPSTREAM_BAD_STATUS",
149
- category: "upstream",
150
- message: `Upstream request failed with status ${response.status}`,
151
- details: {
152
- upstream_status: response.status,
153
- url,
154
- },
155
- });
156
- }
157
-
158
- return response.text();
159
- }
160
-
161
- export async function fetchJson(
162
- url,
163
- { signal, language, headers = {}, referrer, engine = "default" } = {}
164
- ) {
165
- const response = await fetchWithOptionalCurlImpersonate(url, {
166
- signal,
167
- redirect: "follow",
168
- referrer,
169
- headers: getDefaultFetchHeaders(
170
- language,
171
- {
172
- accept: "application/json,text/plain;q=0.9,*/*;q=0.8",
173
- ...headers,
174
- },
175
- engine
176
- ),
177
- });
178
-
179
- if (!response.ok) {
180
- throw new ApiError({
181
- status: 502,
182
- code: "UPSTREAM_BAD_STATUS",
183
- category: "upstream",
184
- message: `Upstream request failed with status ${response.status}`,
185
- details: {
186
- upstream_status: response.status,
187
- url,
188
- },
189
- });
190
- }
191
-
192
- return response.json();
193
- }
194
-
195
- export function ensureAbsoluteUrl(url, baseUrl) {
196
- try {
197
- return new URL(url, baseUrl).toString();
198
- } catch (_) {
199
- return String(url || "").trim();
200
- }
201
- }
202
-
203
- export function mapTimeRange(timeRange, mapping) {
204
- if (!timeRange) {
205
- return "";
206
- }
207
-
208
- return mapping[String(timeRange).toLowerCase()] || "";
209
- }
210
-
211
- export function mapLanguage(language, mapping, fallback = "") {
212
- if (!language) {
213
- return fallback;
214
- }
215
-
216
- const normalized = String(language).toLowerCase();
217
- const short = normalized.split("-")[0];
218
- return mapping[normalized] || mapping[short] || fallback;
219
- }
220
-
221
- export function resolvePageNumber(value) {
222
- const parsed = Number.parseInt(value ?? "0", 10);
223
- if (Number.isNaN(parsed) || parsed < 0) {
224
- return 0;
225
- }
226
- return parsed;
227
- }
@@ -1 +0,0 @@
1
- export * from "./engineUtils.impl.js";