@juspay/neurolink 9.94.4 → 9.94.5
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/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +369 -369
- package/dist/lib/utils/fileDetector.js +113 -24
- package/dist/lib/utils/imageCache.js +11 -8
- package/dist/lib/utils/imageProcessor.d.ts +10 -0
- package/dist/lib/utils/imageProcessor.js +135 -10
- package/dist/lib/utils/logSanitize.d.ts +100 -1
- package/dist/lib/utils/logSanitize.js +152 -2
- package/dist/utils/fileDetector.js +113 -24
- package/dist/utils/imageCache.js +11 -8
- package/dist/utils/imageProcessor.d.ts +10 -0
- package/dist/utils/imageProcessor.js +135 -10
- package/dist/utils/logSanitize.d.ts +100 -1
- package/dist/utils/logSanitize.js +152 -2
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* jina_ (Jina), fish- (Fish Audio)
|
|
16
16
|
* - Generic key=value pairs: api_key=…, access_token: …, secret_key=…
|
|
17
17
|
*/
|
|
18
|
+
import { basename, resolve as resolvePath } from "path";
|
|
18
19
|
const TOKEN_PREFIXES = [
|
|
19
20
|
"sk",
|
|
20
21
|
"pk",
|
|
@@ -156,12 +157,161 @@ export function safeDebugSerialize(value, maxLen = 10_000) {
|
|
|
156
157
|
* for diagnostics. Use this for any `baseURL`/endpoint a caller may have
|
|
157
158
|
* supplied with inline credentials. The match is global, so every `//…@`
|
|
158
159
|
* authority in the string is redacted (e.g. a proxy chain or a URL embedded
|
|
159
|
-
* in a query parameter), not just the first.
|
|
160
|
+
* in a query parameter), not just the first. IPv6 literal hosts (`//u:p@[::1]/x`)
|
|
161
|
+
* and percent-encoded userinfo (`//u:p%40x@host/x`) are handled naturally —
|
|
162
|
+
* neither `[`/`]` nor `%` stop either pass below, so the bracketed host or
|
|
163
|
+
* encoded byte just rides along as part of the (non-redacted) remainder.
|
|
164
|
+
*
|
|
165
|
+
* This is best-effort defense-in-depth for free-form logging, deliberately
|
|
166
|
+
* regex-based so it can scrub *multiple* embedded authorities out of
|
|
167
|
+
* arbitrary text (a real URL parser only ever parses one URL at a time).
|
|
168
|
+
* It is NOT the primary protection for user-facing errors — that's
|
|
169
|
+
* {@link redactUrlForError}, which parses the single input via `new URL()`
|
|
170
|
+
* and reconstructs from protocol+host+pathname, never touching userinfo at
|
|
171
|
+
* all. Prefer that function whenever the input is known to be exactly one
|
|
172
|
+
* URL.
|
|
173
|
+
*
|
|
174
|
+
* Two passes, deliberately:
|
|
175
|
+
* 1. Well-formed authorities — credentials with no raw `/` — bounded at the
|
|
176
|
+
* authority's first `/` (or string end). Greedy backtracking naturally
|
|
177
|
+
* absorbs multiple `@` segments within one authority (e.g. `//a@b@host`
|
|
178
|
+
* → `//***@host`) while leaving a second, separate `//...@` authority
|
|
179
|
+
* elsewhere in the string (e.g. one embedded in a query parameter)
|
|
180
|
+
* untouched, because pass 1's own exclusion of `/` stops it at the first
|
|
181
|
+
* `/`, before it can ever reach a nested authority.
|
|
182
|
+
* 2. Malformed authorities whose credentials contain an unescaped `/`
|
|
183
|
+
* (e.g. `//user:sec/ret@host/path`) — pass 1 can't find an `@` before
|
|
184
|
+
* hitting that `/`, so this pass allows `/` and redacts up to the last
|
|
185
|
+
* remaining `@`. It stops before any *nested* `//` (via a negative
|
|
186
|
+
* lookahead) rather than excluding a specific character like `*` —
|
|
187
|
+
* excluding a literal character is what let a password containing both
|
|
188
|
+
* `/` and `*` (e.g. `//user:pa*ss/word@host/path`, legal under RFC 3986)
|
|
189
|
+
* slip through both passes entirely in an earlier version of this
|
|
190
|
+
* function.
|
|
160
191
|
*
|
|
161
192
|
* @param url - The URL (or URL-shaped string) to redact.
|
|
162
193
|
*/
|
|
163
194
|
export function redactUrlCredentials(url) {
|
|
164
|
-
|
|
195
|
+
const wellFormed = url.replace(/\/\/[^/\s"'<>]*@/g, "//***@");
|
|
196
|
+
return wellFormed.replace(/\/\/(?:(?!\/\/)[^\s"'<>])*@/g, "//***@");
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Reduce a URL to its origin + pathname for safe inclusion in error messages
|
|
200
|
+
* and logs, dropping the query string and fragment — where presigned-URL
|
|
201
|
+
* signatures, SAS tokens, and other secrets commonly live — while keeping
|
|
202
|
+
* enough of the URL (scheme, host, path) for the error to stay diagnostic.
|
|
203
|
+
*
|
|
204
|
+
* Falls back to a best-effort redacted string (query/fragment and
|
|
205
|
+
* `user:pass@` stripped) when the input isn't a parseable absolute URL.
|
|
206
|
+
* Either way the result is capped at 200 chars so a very long host/path
|
|
207
|
+
* can't blow up the error message.
|
|
208
|
+
*
|
|
209
|
+
* @param url - The URL (or URL-shaped string) to redact.
|
|
210
|
+
*/
|
|
211
|
+
export function redactUrlForError(url) {
|
|
212
|
+
const truncate = (s) => s.length > 200 ? `${s.slice(0, 200)}…` : s;
|
|
213
|
+
try {
|
|
214
|
+
const parsed = new URL(url);
|
|
215
|
+
// Reconstruct explicitly from protocol + host + pathname — never
|
|
216
|
+
// parsed.username/parsed.password, and never parsed.href (which
|
|
217
|
+
// re-serializes any embedded userinfo) — so a `user:pass@host` in the
|
|
218
|
+
// input can't leak into a diagnostic error message.
|
|
219
|
+
return truncate(`${parsed.protocol}//${parsed.host}${parsed.pathname}`);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
// Unparseable input: `new URL()` gave us nothing to work with, so
|
|
223
|
+
// best-effort drop the query/fragment (where secrets like tokens live)
|
|
224
|
+
// and strip any `//user:pass@` segment before falling back to the raw
|
|
225
|
+
// string — never echo the input completely unredacted.
|
|
226
|
+
const withoutQueryOrFragment = url.split(/[?#]/)[0];
|
|
227
|
+
return truncate(redactUrlCredentials(withoutQueryOrFragment));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Scrub any embedded absolute URLs out of free-form text — typically the
|
|
232
|
+
* `.message` of an underlying network/DNS/TLS error thrown by `fetch`,
|
|
233
|
+
* undici's `request()`, or Node's resolver, all of which frequently embed
|
|
234
|
+
* the full request URL (query string, credentials, and all) verbatim.
|
|
235
|
+
* Interpolating that message raw into a thrown or logged error would leak a
|
|
236
|
+
* presigned URL's token even after the explicit URL argument was already
|
|
237
|
+
* redacted via {@link redactUrlForError}. Finds each `scheme://…` run in the
|
|
238
|
+
* text and replaces it with its redacted form.
|
|
239
|
+
*
|
|
240
|
+
* @param text - Free-form text (typically `Error.message`) to scrub.
|
|
241
|
+
*/
|
|
242
|
+
export function redactUrlsInText(text) {
|
|
243
|
+
return text.replace(/[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^\s"'<>)]+/g, (match) => redactUrlForError(match));
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Redact every occurrence of `filePath` from an error message, checking both
|
|
247
|
+
* the path exactly as given and its `path.resolve()`'d form.
|
|
248
|
+
*
|
|
249
|
+
* Node's own fs errors (ENOENT/EACCES) embed the exact string passed to the
|
|
250
|
+
* fs call, so a plain substring match usually suffices — but wrapped or
|
|
251
|
+
* lower-level errors sometimes normalize the path first (relative→absolute,
|
|
252
|
+
* trailing-slash variations), which an exact-match-only redaction would miss.
|
|
253
|
+
* This is defense-in-depth, not a guarantee: a `realpath`-resolved (symlink-
|
|
254
|
+
* following) variant can still differ from both forms and would slip through.
|
|
255
|
+
*
|
|
256
|
+
* @param message - The error message to scrub.
|
|
257
|
+
* @param filePath - The path to redact (both as given and resolved).
|
|
258
|
+
*/
|
|
259
|
+
export function redactPathFromMessage(message, filePath) {
|
|
260
|
+
const safeName = basename(filePath);
|
|
261
|
+
// Longest-first: the resolved (absolute) form always contains the relative
|
|
262
|
+
// form as a substring, so redacting the relative form first would only
|
|
263
|
+
// strip the trailing segment of an absolute path and leave its parent
|
|
264
|
+
// directory behind (e.g. "/Users/me/project/foo/bar.png" redacting
|
|
265
|
+
// "foo/bar.png" first leaves "/Users/me/project/bar.png"). Redacting the
|
|
266
|
+
// longer resolved variant first removes the whole absolute path in one
|
|
267
|
+
// pass, so the shorter relative pass below has nothing left to match.
|
|
268
|
+
return message
|
|
269
|
+
.split(resolvePath(filePath))
|
|
270
|
+
.join(safeName)
|
|
271
|
+
.split(filePath)
|
|
272
|
+
.join(safeName);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Build a sanitized copy of a network/URL/file error, safe to attach as
|
|
276
|
+
* `Error.cause` on a rethrow.
|
|
277
|
+
*
|
|
278
|
+
* Redacting only the OUTER thrown error's message isn't enough: if the RAW
|
|
279
|
+
* underlying error is attached as `cause`, anything that walks the cause
|
|
280
|
+
* chain (cause-aware logging, telemetry spans) can still recover an
|
|
281
|
+
* unredacted URL/path/secret via `error.cause.message`. This returns a NEW
|
|
282
|
+
* `Error` whose message has been through {@link redactUrlsInText} — and,
|
|
283
|
+
* when `options.filePath` is supplied, {@link redactPathFromMessage} too, so
|
|
284
|
+
* a filesystem error's ENOENT/EACCES text doesn't leak the host's directory
|
|
285
|
+
* layout via the cause chain the way the outer message already avoids.
|
|
286
|
+
* `.name` and `.code` (`NodeJS.ErrnoException`) are copied over so retry
|
|
287
|
+
* classification (e.g. `isRetryableNetworkError`) still works when it
|
|
288
|
+
* inspects the cause. The raw original is never referenced by the result.
|
|
289
|
+
*
|
|
290
|
+
* Handles non-`Error` thrown values too (a raw string/object can just as
|
|
291
|
+
* easily carry an unredacted URL or path) by running their `String()` form
|
|
292
|
+
* through the same redaction before wrapping.
|
|
293
|
+
*
|
|
294
|
+
* @param error - The raw underlying error (or thrown value) to sanitize.
|
|
295
|
+
* @param options - `filePath`: a known filesystem path to redact to its
|
|
296
|
+
* basename, in addition to URL redaction.
|
|
297
|
+
*/
|
|
298
|
+
export function sanitizeErrorCause(error, options) {
|
|
299
|
+
const redact = (text) => {
|
|
300
|
+
const urlSafe = redactUrlsInText(text);
|
|
301
|
+
return options?.filePath
|
|
302
|
+
? redactPathFromMessage(urlSafe, options.filePath)
|
|
303
|
+
: urlSafe;
|
|
304
|
+
};
|
|
305
|
+
if (!(error instanceof Error)) {
|
|
306
|
+
return new Error(redact(String(error)));
|
|
307
|
+
}
|
|
308
|
+
const sanitized = new Error(redact(error.message));
|
|
309
|
+
sanitized.name = error.name;
|
|
310
|
+
const code = error.code;
|
|
311
|
+
if (code !== undefined) {
|
|
312
|
+
sanitized.code = code;
|
|
313
|
+
}
|
|
314
|
+
return sanitized;
|
|
165
315
|
}
|
|
166
316
|
/**
|
|
167
317
|
* Recursively sanitize a record/array, returning a structurally identical
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses multi-strategy approach for reliable type identification
|
|
5
5
|
*/
|
|
6
6
|
import { open, readFile, realpath } from "fs/promises";
|
|
7
|
-
import { isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
|
|
7
|
+
import { basename, isAbsolute as isAbsolutePath, relative as relativePath, resolve as resolvePath, sep, } from "path";
|
|
8
8
|
import { getGlobalDispatcher, interceptors, request } from "undici";
|
|
9
9
|
// Lazy-loaded processor singletons — avoids loading heavy media deps
|
|
10
10
|
// (mediabunny, fluent-ffmpeg, music-metadata, adm-zip) on every generate() call.
|
|
@@ -24,6 +24,7 @@ import { tracers, ATTR, withSpan } from "../telemetry/index.js";
|
|
|
24
24
|
import { CSVProcessor } from "./csvProcessor.js";
|
|
25
25
|
import { ImageProcessor } from "./imageProcessor.js";
|
|
26
26
|
import { logger } from "./logger.js";
|
|
27
|
+
import { redactUrlForError, sanitizeErrorCause } from "./logSanitize.js";
|
|
27
28
|
import { mimeHintToExtension, mimeHintToFileType, normalizeMimeHint, } from "./mimeTypeHints.js";
|
|
28
29
|
import { PDFProcessor } from "./pdfProcessor.js";
|
|
29
30
|
/**
|
|
@@ -1338,25 +1339,55 @@ export class FileDetector {
|
|
|
1338
1339
|
const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1339
1340
|
const retryDelay = options?.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
1340
1341
|
return withRetry(async () => {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
for await (const chunk of response.body) {
|
|
1353
|
-
totalSize += chunk.length;
|
|
1354
|
-
if (totalSize > maxSize) {
|
|
1355
|
-
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
1342
|
+
try {
|
|
1343
|
+
const response = await request(url, {
|
|
1344
|
+
dispatcher: getGlobalDispatcher().compose(interceptors.redirect({ maxRedirections: 5 })),
|
|
1345
|
+
method: "GET",
|
|
1346
|
+
headersTimeout: timeout,
|
|
1347
|
+
bodyTimeout: timeout,
|
|
1348
|
+
});
|
|
1349
|
+
if (response.statusCode !== 200) {
|
|
1350
|
+
// Query string / fragment stripped — a presigned URL's token must
|
|
1351
|
+
// not be echoed into a thrown error.
|
|
1352
|
+
throw new Error(`HTTP ${response.statusCode} fetching ${redactUrlForError(url)}`);
|
|
1356
1353
|
}
|
|
1357
|
-
chunks
|
|
1354
|
+
const chunks = [];
|
|
1355
|
+
let totalSize = 0;
|
|
1356
|
+
for await (const chunk of response.body) {
|
|
1357
|
+
totalSize += chunk.length;
|
|
1358
|
+
if (totalSize > maxSize) {
|
|
1359
|
+
throw new Error(`File too large: ${formatFileSize(totalSize)} (max: ${formatFileSize(maxSize)})`);
|
|
1360
|
+
}
|
|
1361
|
+
chunks.push(chunk);
|
|
1362
|
+
}
|
|
1363
|
+
return Buffer.concat(chunks);
|
|
1364
|
+
}
|
|
1365
|
+
catch (error) {
|
|
1366
|
+
// Node/undici DNS, TLS, and connect-timeout errors embed the full
|
|
1367
|
+
// request URL (including a presigned query token) in
|
|
1368
|
+
// `error.message`. Redact into a NEW error instead of mutating the
|
|
1369
|
+
// original in place, so anything that still holds a reference to
|
|
1370
|
+
// the original — debug logs, telemetry spans, upstream callers —
|
|
1371
|
+
// keeps seeing the real message. `.code` is copied onto the new
|
|
1372
|
+
// error so `isRetryableNetworkError`'s retry check in the outer
|
|
1373
|
+
// `withRetry` catch still classifies it correctly. The raw
|
|
1374
|
+
// original error is NEVER attached as `cause` — that would leave
|
|
1375
|
+
// the unredacted URL reachable via `error.cause.message` for
|
|
1376
|
+
// anything that walks the cause chain (cause-aware logging,
|
|
1377
|
+
// telemetry). `cause` instead gets its own sanitized copy.
|
|
1378
|
+
// `sanitizeErrorCause` handles non-`Error` thrown values too (a raw
|
|
1379
|
+
// string/object can just as easily carry the full URL), so there is
|
|
1380
|
+
// no unconditional `throw error` fallback that would bypass
|
|
1381
|
+
// redaction for that case.
|
|
1382
|
+
const cause = sanitizeErrorCause(error);
|
|
1383
|
+
const redacted = new Error(cause.message, { cause });
|
|
1384
|
+
redacted.name = cause.name;
|
|
1385
|
+
const code = cause.code;
|
|
1386
|
+
if (code !== undefined) {
|
|
1387
|
+
redacted.code = code;
|
|
1388
|
+
}
|
|
1389
|
+
throw redacted;
|
|
1358
1390
|
}
|
|
1359
|
-
return Buffer.concat(chunks);
|
|
1360
1391
|
}, { maxRetries, retryDelay });
|
|
1361
1392
|
}
|
|
1362
1393
|
/**
|
|
@@ -1374,6 +1405,13 @@ export class FileDetector {
|
|
|
1374
1405
|
// the base dir pointing outside cannot bypass containment. The
|
|
1375
1406
|
// path.relative check (not a string prefix) correctly handles the root dir
|
|
1376
1407
|
// ("/") and sibling-prefix ("/app" vs "/app-evil") edge cases.
|
|
1408
|
+
//
|
|
1409
|
+
// The actual open() below MUST target this validated `real` path, not the
|
|
1410
|
+
// original `filePath` — otherwise a symlink swapped between the realpath()
|
|
1411
|
+
// check and the open() call routes the read outside the sandbox even
|
|
1412
|
+
// though validation passed (TOCTOU). With no sandbox configured there's no
|
|
1413
|
+
// boundary to defend, so the original path is used as given.
|
|
1414
|
+
let pathToOpen = filePath;
|
|
1377
1415
|
if (options?.allowedBaseDir) {
|
|
1378
1416
|
let base;
|
|
1379
1417
|
let real;
|
|
@@ -1381,24 +1419,66 @@ export class FileDetector {
|
|
|
1381
1419
|
base = await realpath(resolvePath(options.allowedBaseDir));
|
|
1382
1420
|
real = await realpath(filePath);
|
|
1383
1421
|
}
|
|
1384
|
-
catch {
|
|
1385
|
-
|
|
1422
|
+
catch (error) {
|
|
1423
|
+
// Full path stays in the debug log; the thrown (potentially
|
|
1424
|
+
// client-facing) error only gets the basename to avoid leaking the
|
|
1425
|
+
// host's directory layout to an untrusted caller. The cause is
|
|
1426
|
+
// sanitized too — Node's realpath ENOENT/EACCES messages embed the
|
|
1427
|
+
// full path verbatim, which would otherwise survive on the cause
|
|
1428
|
+
// chain (cause-aware logging, telemetry) even though the outer
|
|
1429
|
+
// message is already redacted.
|
|
1430
|
+
logger.debug("loadFromPath: realpath resolution failed", {
|
|
1431
|
+
filePath,
|
|
1432
|
+
error,
|
|
1433
|
+
});
|
|
1434
|
+
// Assigned to a variable before the throw (rather than an inline
|
|
1435
|
+
// `{ cause: sanitizeErrorCause(...) }`) so the sanitized, path-redacted
|
|
1436
|
+
// copy is unambiguously the attached cause — the raw `error`, whose
|
|
1437
|
+
// message still embeds the full path, is never reachable from the
|
|
1438
|
+
// thrown result.
|
|
1439
|
+
const cause = sanitizeErrorCause(error, { filePath });
|
|
1440
|
+
const denied = new Error(`Access denied: "${basename(filePath)}" could not be resolved within the allowed base directory`, { cause });
|
|
1441
|
+
throw denied;
|
|
1386
1442
|
}
|
|
1387
1443
|
const rel = relativePath(base, real);
|
|
1388
1444
|
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolutePath(rel)) {
|
|
1389
|
-
|
|
1445
|
+
logger.debug("loadFromPath: path resolves outside allowed base dir", {
|
|
1446
|
+
filePath,
|
|
1447
|
+
real,
|
|
1448
|
+
});
|
|
1449
|
+
throw new Error(`Access denied: "${basename(filePath)}" resolves outside the allowed base directory`);
|
|
1390
1450
|
}
|
|
1451
|
+
pathToOpen = real;
|
|
1391
1452
|
}
|
|
1392
1453
|
// Open a handle and stat/read through the SAME descriptor so a symlink
|
|
1393
1454
|
// swap between the size check and the read cannot occur (TOCTOU).
|
|
1394
|
-
|
|
1455
|
+
let handle;
|
|
1456
|
+
try {
|
|
1457
|
+
handle = await open(pathToOpen, "r");
|
|
1458
|
+
}
|
|
1459
|
+
catch (error) {
|
|
1460
|
+
// A failed open (ENOENT/EACCES/…) embeds the opened path verbatim in
|
|
1461
|
+
// its message. When a sandbox is configured `pathToOpen` is the
|
|
1462
|
+
// realpath-resolved target (`real`), which differs from both `filePath`
|
|
1463
|
+
// and its resolved form — so redact `pathToOpen` specifically, or the
|
|
1464
|
+
// full host path would survive on both the thrown message and the cause
|
|
1465
|
+
// chain despite this PR's path-redaction hardening.
|
|
1466
|
+
const cause = sanitizeErrorCause(error, { filePath: pathToOpen });
|
|
1467
|
+
const failed = new Error(cause.message, { cause });
|
|
1468
|
+
failed.name = cause.name;
|
|
1469
|
+
const code = cause.code;
|
|
1470
|
+
if (code !== undefined) {
|
|
1471
|
+
failed.code = code;
|
|
1472
|
+
}
|
|
1473
|
+
throw failed;
|
|
1474
|
+
}
|
|
1395
1475
|
try {
|
|
1396
1476
|
const statInfo = await handle.stat();
|
|
1397
1477
|
if (!statInfo.isFile()) {
|
|
1398
|
-
throw new Error(`Not a file: ${filePath}`);
|
|
1478
|
+
throw new Error(`Not a file: ${basename(filePath)}`);
|
|
1399
1479
|
}
|
|
1400
1480
|
if (statInfo.size > maxSize) {
|
|
1401
|
-
throw new Error(`File too large: ${filePath} is ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
|
|
1481
|
+
throw new Error(`File too large: ${basename(filePath)} is ${formatFileSize(statInfo.size)} (max: ${formatFileSize(maxSize)})`);
|
|
1402
1482
|
}
|
|
1403
1483
|
return await handle.readFile();
|
|
1404
1484
|
}
|
|
@@ -1450,6 +1530,15 @@ class MagicBytesStrategy {
|
|
|
1450
1530
|
input[6] === 0x79 &&
|
|
1451
1531
|
input[7] === 0x70) {
|
|
1452
1532
|
const brand = input.length >= 12 ? input.toString("latin1", 8, 12) : "";
|
|
1533
|
+
// AVIF images share the ISO-BMFF ftyp box with MP4/MOV; the major brand
|
|
1534
|
+
// ('avif' still, 'avis' sequence, 'avio' intra-only AV1 image/sequence
|
|
1535
|
+
// — spec-listed under compatible_brands but also emitted as
|
|
1536
|
+
// major_brand by real encoders) distinguishes them. Detect before the
|
|
1537
|
+
// audio/video branches so an AVIF buffer isn't misrouted to the video
|
|
1538
|
+
// pipeline (#286).
|
|
1539
|
+
if (/^(avif|avis|avio)/.test(brand)) {
|
|
1540
|
+
return this.result("image", "image/avif", 95);
|
|
1541
|
+
}
|
|
1453
1542
|
if (/^(M4A|M4B|M4P|F4A|F4B)/.test(brand)) {
|
|
1454
1543
|
return this.result("audio", "audio/mp4", 95);
|
|
1455
1544
|
}
|
package/dist/utils/imageCache.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from "crypto";
|
|
16
16
|
import { logger } from "./logger.js";
|
|
17
|
+
import { redactUrlForError } from "./logSanitize.js";
|
|
17
18
|
/**
|
|
18
19
|
* LRU Cache for downloaded images
|
|
19
20
|
*
|
|
@@ -142,7 +143,9 @@ export class ImageCache {
|
|
|
142
143
|
const entry = this.cache.get(normalizedUrl);
|
|
143
144
|
if (!entry) {
|
|
144
145
|
this.stats.misses++;
|
|
145
|
-
logger.debug("Image cache miss", {
|
|
146
|
+
logger.debug("Image cache miss", {
|
|
147
|
+
url: redactUrlForError(normalizedUrl),
|
|
148
|
+
});
|
|
146
149
|
return null;
|
|
147
150
|
}
|
|
148
151
|
// Check TTL expiration
|
|
@@ -150,7 +153,7 @@ export class ImageCache {
|
|
|
150
153
|
this.stats.expirations++;
|
|
151
154
|
this.delete(normalizedUrl);
|
|
152
155
|
logger.debug("Image cache entry expired", {
|
|
153
|
-
url: normalizedUrl
|
|
156
|
+
url: redactUrlForError(normalizedUrl),
|
|
154
157
|
});
|
|
155
158
|
return null;
|
|
156
159
|
}
|
|
@@ -162,7 +165,7 @@ export class ImageCache {
|
|
|
162
165
|
this.cache.set(normalizedUrl, entry);
|
|
163
166
|
this.stats.hits++;
|
|
164
167
|
logger.debug("Image cache hit", {
|
|
165
|
-
url: normalizedUrl
|
|
168
|
+
url: redactUrlForError(normalizedUrl),
|
|
166
169
|
accessCount: entry.accessCount,
|
|
167
170
|
});
|
|
168
171
|
return entry;
|
|
@@ -192,7 +195,7 @@ export class ImageCache {
|
|
|
192
195
|
// Skip caching if image exceeds max size
|
|
193
196
|
if (size > this.maxImageSize) {
|
|
194
197
|
logger.debug("Image too large to cache", {
|
|
195
|
-
url: normalizedUrl
|
|
198
|
+
url: redactUrlForError(normalizedUrl),
|
|
196
199
|
size,
|
|
197
200
|
maxSize: this.maxImageSize,
|
|
198
201
|
});
|
|
@@ -211,8 +214,8 @@ export class ImageCache {
|
|
|
211
214
|
// Update content hash index to point to the new URL as well
|
|
212
215
|
this.contentHashIndex.set(contentHash, normalizedUrl);
|
|
213
216
|
logger.debug("Image cache dedup hit", {
|
|
214
|
-
newUrl: normalizedUrl
|
|
215
|
-
existingUrl: existingUrl
|
|
217
|
+
newUrl: redactUrlForError(normalizedUrl),
|
|
218
|
+
existingUrl: redactUrlForError(existingUrl),
|
|
216
219
|
});
|
|
217
220
|
return;
|
|
218
221
|
}
|
|
@@ -234,7 +237,7 @@ export class ImageCache {
|
|
|
234
237
|
this.cache.set(normalizedUrl, entry);
|
|
235
238
|
this.contentHashIndex.set(contentHash, normalizedUrl);
|
|
236
239
|
logger.debug("Image cached", {
|
|
237
|
-
url: normalizedUrl
|
|
240
|
+
url: redactUrlForError(normalizedUrl),
|
|
238
241
|
size,
|
|
239
242
|
contentHash: contentHash.substring(0, 8),
|
|
240
243
|
cacheSize: this.cache.size,
|
|
@@ -272,7 +275,7 @@ export class ImageCache {
|
|
|
272
275
|
this.cache.delete(oldestKey);
|
|
273
276
|
this.stats.evictions++;
|
|
274
277
|
logger.debug("Image cache eviction", {
|
|
275
|
-
url: String(oldestKey)
|
|
278
|
+
url: redactUrlForError(String(oldestKey)),
|
|
276
279
|
});
|
|
277
280
|
}
|
|
278
281
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Image processing utilities for multimodal support
|
|
3
3
|
* Handles format conversion for different AI providers
|
|
4
4
|
*/
|
|
5
|
+
import { redactPathFromMessage } from "./logSanitize.js";
|
|
5
6
|
import type { ProcessedImage, FileProcessingResult } from "../types/index.js";
|
|
6
7
|
/**
|
|
7
8
|
* Image processor class for handling provider-specific image formatting
|
|
@@ -171,6 +172,15 @@ export declare const imageUtils: {
|
|
|
171
172
|
* Convert base64 string to Buffer
|
|
172
173
|
*/
|
|
173
174
|
base64ToBuffer: (base64: string) => Buffer;
|
|
175
|
+
/**
|
|
176
|
+
* Redact `filePath` (both as given and its `path.resolve()`'d form) from
|
|
177
|
+
* an error message. Exposed directly (not just used internally by
|
|
178
|
+
* {@link fileToBase64DataUri}) so the resolved-path branch can be verified
|
|
179
|
+
* by a deterministic unit test — real `fs` errors only ever embed the
|
|
180
|
+
* literal path as passed, never a resolved variant, so that branch isn't
|
|
181
|
+
* otherwise observable through the async file-reading API.
|
|
182
|
+
*/
|
|
183
|
+
redactPathFromMessage: typeof redactPathFromMessage;
|
|
174
184
|
/**
|
|
175
185
|
* Convert file path to base64 data URI
|
|
176
186
|
*/
|