@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 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 intended npm name is `@xagents_in/unmask`;
18
- version `0.1.0` is configured for public npm publication. The default service is
19
- `https://unmask-386156642190.asia-south1.run.app`. Publication, public access and final
20
- model/runtime acceptance must still be verified; this configuration is not a
21
- released-artifact claim.
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.0` candidate, subject to CI and runtime
110
- release gates. The flow builds an immutable
111
- versioned artifact in CI, then a deployer selects that release separately.
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
+ }