@xagents_in/unmask 0.1.0 → 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/README.md +13 -8
- package/dist/core/advisories.mjs +457 -0
- package/dist/core/package-details.mjs +638 -0
- package/dist/model/client.mjs +60 -33
- package/dist/server/app.mjs +31 -6
- package/dist/server/enrichment.mjs +116 -0
- package/dist/server/main.mjs +16 -0
- package/dist/server/queue.mjs +85 -6
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,11 +14,12 @@ Hacktoberfest 2026, Hack Day Sonipat. See [HACKATHON.md](HACKATHON.md).
|
|
|
14
14
|
|
|
15
15
|
The local candidate includes an npm CLI, a shared analyzer, a website with a
|
|
16
16
|
bounded job queue, and an optional hosted Gemma adapter. It uses plain ESM
|
|
17
|
-
JavaScript on Node.js 24.22.2. The
|
|
18
|
-
|
|
19
|
-
`
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
JavaScript on Node.js 24.22.2. The npm package is `@xagents_in/unmask`.
|
|
18
|
+
Release `0.1.0` was published and verified; the current source candidate is
|
|
19
|
+
`0.1.1`, whose release and deployment acceptance are pending. The default service is
|
|
20
|
+
`https://unmask-386156642190.asia-south1.run.app`. See the dated
|
|
21
|
+
[deployment evidence](docs/README.md) for observed runtime versions and commits;
|
|
22
|
+
source configuration does not establish acceptance of a new release.
|
|
22
23
|
|
|
23
24
|
The analyzer reads package contents as data. It does not install the inspected
|
|
24
25
|
package, run its scripts, follow archive links, or reproduce its build.
|
|
@@ -77,6 +78,9 @@ queues analysis, and serves the same report format as the CLI. It accepts no
|
|
|
77
78
|
uploads. Its development worker limits and temporary-file cleanup do not
|
|
78
79
|
constitute an operating-system security sandbox.
|
|
79
80
|
|
|
81
|
+
The Next.js frontend in [`web/`](web/README.md) can sit in front of that server.
|
|
82
|
+
Start it with `UNMASK_API_URL=http://127.0.0.1:3000`; see its README.
|
|
83
|
+
|
|
80
84
|
## Inspection limits
|
|
81
85
|
|
|
82
86
|
- File correspondence uses hashes and bounded byte comparisons. Transpilation,
|
|
@@ -106,9 +110,10 @@ installed into a temporary directory and invoked through its real CLI binary.
|
|
|
106
110
|
`npm run check` checks formatting; it is not static type checking. Product tests
|
|
107
111
|
and local HTTP checks do not establish deployment readiness.
|
|
108
112
|
|
|
109
|
-
Release publication is enabled for the `0.1.
|
|
110
|
-
release gates.
|
|
111
|
-
|
|
113
|
+
Release publication is enabled for the `0.1.1` source candidate, subject to CI
|
|
114
|
+
and runtime release gates. A completed immutable release with verified npm
|
|
115
|
+
publication triggers API deployment in the same workflow. Successful API
|
|
116
|
+
verification then triggers website deployment for that exact release commit.
|
|
112
117
|
See [release prerequisites](docs/releases.md). npm access is not required for
|
|
113
118
|
local builds or testing.
|
|
114
119
|
|
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { parsePackageSpec } from "./acquire.mjs";
|
|
2
|
+
|
|
3
|
+
const ENDPOINT = "https://api.osv.dev/v1/query";
|
|
4
|
+
export const ADVISORY_LIMITS = Object.freeze({
|
|
5
|
+
timeoutMs: 12000,
|
|
6
|
+
responseBytes: 1024 * 1024,
|
|
7
|
+
pages: 3,
|
|
8
|
+
advisories: 100,
|
|
9
|
+
});
|
|
10
|
+
const systemClock = { now: Date.now, setTimeout, clearTimeout };
|
|
11
|
+
class LookupFailure extends Error {
|
|
12
|
+
constructor(reason) {
|
|
13
|
+
super(reason);
|
|
14
|
+
this.reason = reason;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const fail = (reason) => {
|
|
18
|
+
throw new LookupFailure(reason);
|
|
19
|
+
};
|
|
20
|
+
const object = (value) =>
|
|
21
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
22
|
+
function advisory(value, mark) {
|
|
23
|
+
if (
|
|
24
|
+
!object(value) ||
|
|
25
|
+
typeof value.id !== "string" ||
|
|
26
|
+
!/^[a-zA-Z0-9:_.-]{1,256}$/.test(value.id)
|
|
27
|
+
) {
|
|
28
|
+
mark("invalid-record");
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
let truncated = false;
|
|
32
|
+
const recordMark = mark;
|
|
33
|
+
mark = (reason) => {
|
|
34
|
+
if (reason === "record-limit") truncated = true;
|
|
35
|
+
recordMark(reason);
|
|
36
|
+
};
|
|
37
|
+
function string(value, max, clip = false) {
|
|
38
|
+
if (value === undefined) return undefined;
|
|
39
|
+
if (typeof value !== "string") {
|
|
40
|
+
mark("invalid-record");
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const bytes = Buffer.from(value);
|
|
44
|
+
if (bytes.length <= max) return value;
|
|
45
|
+
mark("record-limit");
|
|
46
|
+
if (!clip) return undefined;
|
|
47
|
+
for (let end = max; end >= Math.max(0, max - 3); end--) {
|
|
48
|
+
try {
|
|
49
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(
|
|
50
|
+
bytes.subarray(0, end),
|
|
51
|
+
);
|
|
52
|
+
} catch {}
|
|
53
|
+
}
|
|
54
|
+
return "";
|
|
55
|
+
}
|
|
56
|
+
function list(value, max, convert) {
|
|
57
|
+
if (value === undefined) return [];
|
|
58
|
+
if (!Array.isArray(value)) {
|
|
59
|
+
mark("invalid-record");
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
if (value.length > max) mark("record-limit");
|
|
63
|
+
return value
|
|
64
|
+
.slice(0, max)
|
|
65
|
+
.map(convert)
|
|
66
|
+
.filter((item) => item !== null && item !== undefined);
|
|
67
|
+
}
|
|
68
|
+
function https(value) {
|
|
69
|
+
if (
|
|
70
|
+
typeof value !== "string" ||
|
|
71
|
+
value.length > 2048 ||
|
|
72
|
+
!value.startsWith("https://") ||
|
|
73
|
+
/[\s\x00-\x1f\x7f]/.test(value)
|
|
74
|
+
) {
|
|
75
|
+
mark("invalid-record");
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const url = new URL(value);
|
|
80
|
+
if (url.protocol === "https:" && !url.username && !url.password)
|
|
81
|
+
return value;
|
|
82
|
+
} catch {}
|
|
83
|
+
mark("invalid-record");
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
function timestamp(value) {
|
|
87
|
+
if (value === undefined) return undefined;
|
|
88
|
+
if (
|
|
89
|
+
typeof value === "string" &&
|
|
90
|
+
value.length <= 64 &&
|
|
91
|
+
/^\d{4}-\d{2}-\d{2}T.+Z$/.test(value) &&
|
|
92
|
+
Number.isFinite(Date.parse(value))
|
|
93
|
+
)
|
|
94
|
+
return value;
|
|
95
|
+
mark("invalid-record");
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
function severity(value) {
|
|
99
|
+
return list(value, 8, (entry) => {
|
|
100
|
+
if (
|
|
101
|
+
!object(entry) ||
|
|
102
|
+
typeof entry.type !== "string" ||
|
|
103
|
+
typeof entry.score !== "string"
|
|
104
|
+
) {
|
|
105
|
+
mark("invalid-record");
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const type = string(entry.type, 32),
|
|
109
|
+
score = string(entry.score, 512);
|
|
110
|
+
if (type === undefined || score === undefined) return null;
|
|
111
|
+
const out = { type, score };
|
|
112
|
+
if (entry.source !== undefined) {
|
|
113
|
+
const source = https(entry.source);
|
|
114
|
+
if (source) out.source = source;
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function range(value) {
|
|
120
|
+
if (!object(value) || typeof value.type !== "string") {
|
|
121
|
+
mark("invalid-record");
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const type = string(value.type, 32);
|
|
125
|
+
if (type === undefined) return null;
|
|
126
|
+
const out = {
|
|
127
|
+
type,
|
|
128
|
+
events: list(value.events, 32, (event) => {
|
|
129
|
+
if (!object(event)) {
|
|
130
|
+
mark("invalid-record");
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
const keys = Object.keys(event);
|
|
134
|
+
if (
|
|
135
|
+
keys.length !== 1 ||
|
|
136
|
+
!["introduced", "fixed", "last_affected", "limit"].includes(
|
|
137
|
+
keys[0],
|
|
138
|
+
) ||
|
|
139
|
+
typeof event[keys[0]] !== "string"
|
|
140
|
+
) {
|
|
141
|
+
mark("invalid-record");
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const version = string(event[keys[0]], 256);
|
|
145
|
+
return version === undefined ? null : { [keys[0]]: version };
|
|
146
|
+
}),
|
|
147
|
+
};
|
|
148
|
+
if (value.repo !== undefined) {
|
|
149
|
+
const repo = https(value.repo);
|
|
150
|
+
if (repo) out.repo = repo;
|
|
151
|
+
}
|
|
152
|
+
return out;
|
|
153
|
+
}
|
|
154
|
+
const out = {
|
|
155
|
+
id: value.id,
|
|
156
|
+
aliases: [
|
|
157
|
+
...new Set(
|
|
158
|
+
list(value.aliases, 16, (alias) => {
|
|
159
|
+
if (
|
|
160
|
+
typeof alias !== "string" ||
|
|
161
|
+
!/^[a-zA-Z0-9:_.-]{1,256}$/.test(alias)
|
|
162
|
+
) {
|
|
163
|
+
mark("invalid-record");
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
return alias;
|
|
167
|
+
}),
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
severitySource: "OSV-declaration",
|
|
171
|
+
severity: severity(value.severity),
|
|
172
|
+
references: list(value.references, 16, (reference) => {
|
|
173
|
+
if (!object(reference)) {
|
|
174
|
+
mark("invalid-record");
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
const url = https(reference.url);
|
|
178
|
+
if (!url) return null;
|
|
179
|
+
const type = string(reference.type, 32);
|
|
180
|
+
return { ...(type !== undefined ? { type } : {}), url };
|
|
181
|
+
}),
|
|
182
|
+
affected: list(value.affected, 8, (affected) => {
|
|
183
|
+
if (
|
|
184
|
+
!object(affected) ||
|
|
185
|
+
!object(affected.package) ||
|
|
186
|
+
typeof affected.package.name !== "string" ||
|
|
187
|
+
typeof affected.package.ecosystem !== "string"
|
|
188
|
+
) {
|
|
189
|
+
mark("invalid-record");
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const name = string(affected.package.name, 214),
|
|
193
|
+
ecosystem = string(affected.package.ecosystem, 64);
|
|
194
|
+
if (name === undefined || ecosystem === undefined) return null;
|
|
195
|
+
const pkg = { name, ecosystem };
|
|
196
|
+
if (affected.package.purl !== undefined) {
|
|
197
|
+
const purl = string(affected.package.purl, 1024);
|
|
198
|
+
if (purl !== undefined) pkg.purl = purl;
|
|
199
|
+
}
|
|
200
|
+
const entry = {
|
|
201
|
+
package: pkg,
|
|
202
|
+
ranges: list(affected.ranges, 8, range),
|
|
203
|
+
versions: list(affected.versions, 64, (version) =>
|
|
204
|
+
string(version, 256),
|
|
205
|
+
),
|
|
206
|
+
severity: severity(affected.severity),
|
|
207
|
+
};
|
|
208
|
+
const declared = string(affected.ecosystem_specific?.severity, 64);
|
|
209
|
+
if (declared !== undefined) entry.ecosystemSeverity = declared;
|
|
210
|
+
return entry;
|
|
211
|
+
}),
|
|
212
|
+
};
|
|
213
|
+
for (const field of ["summary", "modified", "published", "withdrawn"]) {
|
|
214
|
+
const item =
|
|
215
|
+
field === "summary"
|
|
216
|
+
? string(value[field], 2048, true)
|
|
217
|
+
: timestamp(value[field]);
|
|
218
|
+
if (item !== undefined) out[field] = item;
|
|
219
|
+
}
|
|
220
|
+
const declared = string(value.database_specific?.severity, 64);
|
|
221
|
+
if (declared !== undefined) out.databaseSeverity = declared;
|
|
222
|
+
if (truncated) out.truncated = true;
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function lookupNpmAdvisories(
|
|
227
|
+
spec,
|
|
228
|
+
{
|
|
229
|
+
fetchImpl = globalThis.fetch,
|
|
230
|
+
clock = systemClock,
|
|
231
|
+
signal,
|
|
232
|
+
limits: overrides = {},
|
|
233
|
+
} = {},
|
|
234
|
+
) {
|
|
235
|
+
const started = clock.now();
|
|
236
|
+
const result = {
|
|
237
|
+
schemaVersion: "1",
|
|
238
|
+
source: "OSV",
|
|
239
|
+
package: null,
|
|
240
|
+
queriedAt: new Date(started).toISOString(),
|
|
241
|
+
status: "unavailable",
|
|
242
|
+
complete: false,
|
|
243
|
+
reasons: [],
|
|
244
|
+
advisories: [],
|
|
245
|
+
pagesFetched: 0,
|
|
246
|
+
limits: { ...ADVISORY_LIMITS },
|
|
247
|
+
};
|
|
248
|
+
const reasons = new Set();
|
|
249
|
+
const records = new Map();
|
|
250
|
+
const mark = (reason) => reasons.add(reason);
|
|
251
|
+
function finish() {
|
|
252
|
+
result.advisories = [...records.values()].sort((a, b) =>
|
|
253
|
+
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
254
|
+
);
|
|
255
|
+
result.reasons = [...reasons].sort();
|
|
256
|
+
result.complete = !reasons.size;
|
|
257
|
+
result.status = reasons.size
|
|
258
|
+
? result.pagesFetched || records.size
|
|
259
|
+
? "incomplete"
|
|
260
|
+
: "unavailable"
|
|
261
|
+
: records.size
|
|
262
|
+
? "found"
|
|
263
|
+
: "none-reported";
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
let claim;
|
|
267
|
+
try {
|
|
268
|
+
claim = parsePackageSpec(spec);
|
|
269
|
+
} catch {
|
|
270
|
+
mark("invalid-package-spec");
|
|
271
|
+
return finish();
|
|
272
|
+
}
|
|
273
|
+
result.package = { ...claim, ecosystem: "npm" };
|
|
274
|
+
const limits = { ...ADVISORY_LIMITS, ...overrides };
|
|
275
|
+
if (
|
|
276
|
+
Object.keys(limits).some(
|
|
277
|
+
(key) =>
|
|
278
|
+
!Object.hasOwn(ADVISORY_LIMITS, key) ||
|
|
279
|
+
!Number.isSafeInteger(limits[key]) ||
|
|
280
|
+
limits[key] < 1 ||
|
|
281
|
+
limits[key] > ADVISORY_LIMITS[key],
|
|
282
|
+
)
|
|
283
|
+
) {
|
|
284
|
+
mark("invalid-limits");
|
|
285
|
+
return finish();
|
|
286
|
+
}
|
|
287
|
+
result.limits = limits;
|
|
288
|
+
if (signal?.aborted) {
|
|
289
|
+
mark("aborted");
|
|
290
|
+
return finish();
|
|
291
|
+
}
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
const combined = signal
|
|
294
|
+
? AbortSignal.any([signal, controller.signal])
|
|
295
|
+
: controller.signal;
|
|
296
|
+
let timedOut = false,
|
|
297
|
+
active = true;
|
|
298
|
+
const readers = new Set();
|
|
299
|
+
const timer = clock.setTimeout(() => {
|
|
300
|
+
timedOut = true;
|
|
301
|
+
controller.abort();
|
|
302
|
+
}, limits.timeoutMs);
|
|
303
|
+
let interrupt;
|
|
304
|
+
const interrupted = new Promise((_, reject) => {
|
|
305
|
+
interrupt = () => {
|
|
306
|
+
for (const reader of readers) reader.cancel().catch(() => {});
|
|
307
|
+
reject(new LookupFailure(timedOut ? "timeout" : "aborted"));
|
|
308
|
+
};
|
|
309
|
+
combined.addEventListener("abort", interrupt, { once: true });
|
|
310
|
+
});
|
|
311
|
+
function alive() {
|
|
312
|
+
if (!active || combined.aborted) fail(timedOut ? "timeout" : "aborted");
|
|
313
|
+
if (clock.now() - started >= limits.timeoutMs) {
|
|
314
|
+
timedOut = true;
|
|
315
|
+
controller.abort();
|
|
316
|
+
fail("timeout");
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function page(token) {
|
|
320
|
+
alive();
|
|
321
|
+
const response = await fetchImpl(ENDPOINT, {
|
|
322
|
+
method: "POST",
|
|
323
|
+
redirect: "manual",
|
|
324
|
+
credentials: "omit",
|
|
325
|
+
referrerPolicy: "no-referrer",
|
|
326
|
+
signal: combined,
|
|
327
|
+
headers: {
|
|
328
|
+
"content-type": "application/json",
|
|
329
|
+
accept: "application/json",
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({
|
|
332
|
+
package: { name: claim.name, ecosystem: "npm" },
|
|
333
|
+
version: claim.version,
|
|
334
|
+
...(token ? { page_token: token } : {}),
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
if (combined.aborted || !active) response.body?.cancel().catch(() => {});
|
|
338
|
+
alive();
|
|
339
|
+
if (
|
|
340
|
+
response.redirected ||
|
|
341
|
+
(response.url && response.url !== ENDPOINT) ||
|
|
342
|
+
(response.status >= 300 && response.status < 400)
|
|
343
|
+
) {
|
|
344
|
+
response.body?.cancel().catch(() => {});
|
|
345
|
+
fail("redirect-rejected");
|
|
346
|
+
}
|
|
347
|
+
if (response.status !== 200) {
|
|
348
|
+
response.body?.cancel().catch(() => {});
|
|
349
|
+
fail("http-error");
|
|
350
|
+
}
|
|
351
|
+
if (
|
|
352
|
+
!/^application\/json(?:\s*;\s*charset=utf-8)?$/i.test(
|
|
353
|
+
response.headers.get("content-type") ?? "",
|
|
354
|
+
)
|
|
355
|
+
) {
|
|
356
|
+
response.body?.cancel().catch(() => {});
|
|
357
|
+
fail("invalid-response");
|
|
358
|
+
}
|
|
359
|
+
const declared = response.headers.get("content-length");
|
|
360
|
+
if (
|
|
361
|
+
declared !== null &&
|
|
362
|
+
(!/^\d+$/.test(declared) || Number(declared) > limits.responseBytes)
|
|
363
|
+
) {
|
|
364
|
+
response.body?.cancel().catch(() => {});
|
|
365
|
+
fail("response-limit");
|
|
366
|
+
}
|
|
367
|
+
const reader = response.body?.getReader();
|
|
368
|
+
if (!reader) fail("invalid-response");
|
|
369
|
+
readers.add(reader);
|
|
370
|
+
const chunks = [];
|
|
371
|
+
let size = 0;
|
|
372
|
+
try {
|
|
373
|
+
for (;;) {
|
|
374
|
+
alive();
|
|
375
|
+
const { done, value } = await reader.read();
|
|
376
|
+
alive();
|
|
377
|
+
if (done) break;
|
|
378
|
+
size += value.byteLength;
|
|
379
|
+
if (size > limits.responseBytes) fail("response-limit");
|
|
380
|
+
chunks.push(value);
|
|
381
|
+
}
|
|
382
|
+
} finally {
|
|
383
|
+
readers.delete(reader);
|
|
384
|
+
reader.cancel().catch(() => {});
|
|
385
|
+
reader.releaseLock();
|
|
386
|
+
}
|
|
387
|
+
let parsed;
|
|
388
|
+
try {
|
|
389
|
+
parsed = JSON.parse(
|
|
390
|
+
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
391
|
+
Buffer.concat(chunks, size),
|
|
392
|
+
),
|
|
393
|
+
);
|
|
394
|
+
} catch {
|
|
395
|
+
fail("invalid-response");
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
!object(parsed) ||
|
|
399
|
+
Object.hasOwn(parsed, "error") ||
|
|
400
|
+
(parsed.vulns !== undefined && !Array.isArray(parsed.vulns)) ||
|
|
401
|
+
(parsed.vulns === undefined &&
|
|
402
|
+
parsed.next_page_token === undefined &&
|
|
403
|
+
Object.keys(parsed).length > 0)
|
|
404
|
+
)
|
|
405
|
+
fail("invalid-response");
|
|
406
|
+
if (
|
|
407
|
+
parsed.next_page_token !== undefined &&
|
|
408
|
+
(typeof parsed.next_page_token !== "string" ||
|
|
409
|
+
parsed.next_page_token.length > 4096 ||
|
|
410
|
+
/[\x00-\x1f\x7f]/.test(parsed.next_page_token))
|
|
411
|
+
)
|
|
412
|
+
fail("invalid-response");
|
|
413
|
+
return parsed;
|
|
414
|
+
}
|
|
415
|
+
async function query() {
|
|
416
|
+
const tokens = new Set();
|
|
417
|
+
let token;
|
|
418
|
+
for (let i = 0; i < limits.pages; i++) {
|
|
419
|
+
const value = await page(token);
|
|
420
|
+
alive();
|
|
421
|
+
result.pagesFetched++;
|
|
422
|
+
for (const raw of value.vulns ?? []) {
|
|
423
|
+
alive();
|
|
424
|
+
const item = advisory(raw, mark);
|
|
425
|
+
if (!item || records.has(item.id)) continue;
|
|
426
|
+
if (records.size >= limits.advisories) {
|
|
427
|
+
mark("advisory-limit");
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
records.set(item.id, item);
|
|
431
|
+
}
|
|
432
|
+
token = value.next_page_token;
|
|
433
|
+
if (!token) return;
|
|
434
|
+
if (tokens.has(token)) {
|
|
435
|
+
mark("pagination-loop");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
tokens.add(token);
|
|
439
|
+
if (records.size >= limits.advisories) {
|
|
440
|
+
mark("advisory-limit");
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
mark("page-limit");
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
await Promise.race([query(), interrupted]);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
mark(error instanceof LookupFailure ? error.reason : "request-failed");
|
|
450
|
+
} finally {
|
|
451
|
+
active = false;
|
|
452
|
+
clock.clearTimeout(timer);
|
|
453
|
+
combined.removeEventListener("abort", interrupt);
|
|
454
|
+
for (const reader of readers) reader.cancel().catch(() => {});
|
|
455
|
+
}
|
|
456
|
+
return finish();
|
|
457
|
+
}
|