@xagents_in/unmask 0.1.2 → 0.1.4

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 (56) hide show
  1. package/README.md +301 -85
  2. package/dist/cli/annotations.mjs +145 -0
  3. package/dist/cli/audit.mjs +223 -0
  4. package/dist/cli/batch.mjs +218 -0
  5. package/dist/cli/cache.mjs +488 -0
  6. package/dist/cli/compare.mjs +261 -0
  7. package/dist/cli/config.mjs +141 -0
  8. package/dist/cli/discovery.mjs +372 -0
  9. package/dist/cli/doctor.mjs +354 -0
  10. package/dist/cli/exceptions.mjs +170 -0
  11. package/dist/cli/file.mjs +271 -0
  12. package/dist/cli/files.mjs +141 -0
  13. package/dist/cli/history.mjs +232 -0
  14. package/dist/cli/hosted.mjs +251 -43
  15. package/dist/cli/info.mjs +125 -0
  16. package/dist/cli/init.mjs +57 -0
  17. package/dist/cli/install-guard.mjs +896 -0
  18. package/dist/cli/lockfile.mjs +330 -0
  19. package/dist/cli/main.mjs +101 -14
  20. package/dist/cli/preflight.mjs +388 -0
  21. package/dist/cli/sarif.mjs +100 -0
  22. package/dist/cli/scan.mjs +128 -0
  23. package/dist/cli/shell-integration.mjs +122 -0
  24. package/dist/cli/terminal.mjs +396 -0
  25. package/dist/cli/watch.mjs +233 -0
  26. package/dist/cli/why.mjs +256 -0
  27. package/dist/core/advisories.mjs +101 -24
  28. package/dist/core/advisory-cache.mjs +295 -0
  29. package/dist/core/behavior-signals.mjs +1088 -0
  30. package/dist/core/dependency-graph.mjs +332 -0
  31. package/dist/core/evidence-exports.mjs +469 -0
  32. package/dist/core/guard-config.mjs +463 -0
  33. package/dist/core/guard-policy.mjs +308 -0
  34. package/dist/core/package-details.mjs +42 -6
  35. package/dist/core/registry-facts.mjs +342 -0
  36. package/dist/core/report-exploration.mjs +328 -0
  37. package/dist/core/report.mjs +7 -0
  38. package/dist/core/sarif.mjs +556 -0
  39. package/dist/core/syntax.mjs +296 -15
  40. package/dist/model/client.mjs +204 -56
  41. package/dist/model/diff-evidence.mjs +177 -0
  42. package/dist/model/summary.mjs +183 -0
  43. package/dist/server/advisory-result.mjs +137 -0
  44. package/dist/server/app.mjs +66 -2
  45. package/dist/server/dependency-screening.mjs +386 -0
  46. package/dist/server/exports.mjs +63 -0
  47. package/dist/server/guard-decision.mjs +37 -0
  48. package/dist/server/main.mjs +25 -7
  49. package/dist/server/queue.mjs +95 -53
  50. package/dist/server/registry-facts.mjs +118 -0
  51. package/dist/server/report-exploration.mjs +87 -0
  52. package/dist/server/store.mjs +21 -1
  53. package/dist/version.json +1 -1
  54. package/dist/viewer/render.mjs +10 -2
  55. package/dist/viewer/shell.mjs +1 -1
  56. package/package.json +1 -1
@@ -0,0 +1,295 @@
1
+ import { parsePackageSpec } from "./acquire.mjs";
2
+ import { ADVISORY_LIMITS, lookupNpmAdvisories } from "./advisories.mjs";
3
+
4
+ export const ADVISORY_CACHE_LIMITS = Object.freeze({
5
+ ttlMs: 15 * 60 * 1000,
6
+ staleMs: 6 * 60 * 60 * 1000,
7
+ entries: 256,
8
+ entryBytes: 2 * 1024 * 1024,
9
+ totalBytes: 32 * 1024 * 1024,
10
+ });
11
+ const systemClock = { now: Date.now };
12
+ const COMPLETE = ["found", "none-reported"];
13
+
14
+ function identity(spec) {
15
+ try {
16
+ const { name, version } = parsePackageSpec(spec);
17
+ return { name, version, key: `${name}@${version}` };
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ function unavailable(claim, clock, reasons) {
24
+ return {
25
+ schemaVersion: "1",
26
+ source: "OSV",
27
+ package: claim
28
+ ? { name: claim.name, version: claim.version, ecosystem: "npm" }
29
+ : null,
30
+ queriedAt: new Date(clock.now()).toISOString(),
31
+ status: "unavailable",
32
+ complete: false,
33
+ reasons: [...new Set(reasons)].sort(),
34
+ advisories: [],
35
+ pagesFetched: 0,
36
+ limits: { ...ADVISORY_LIMITS },
37
+ };
38
+ }
39
+
40
+ const NOT_CONTACTED = Object.freeze({
41
+ name: "OSV",
42
+ availability: "not-contacted",
43
+ requests: 0,
44
+ retries: 0,
45
+ timeouts: 0,
46
+ failures: [],
47
+ durationMs: 0,
48
+ });
49
+ const FRESHNESS = {
50
+ hit: "cached",
51
+ miss: "live",
52
+ coalesced: "live",
53
+ stale: "stale",
54
+ bypass: "none",
55
+ };
56
+
57
+ export function createAdvisoryCache({
58
+ lookup = lookupNpmAdvisories,
59
+ lookupOptions = {},
60
+ exposeStats = false,
61
+ clock = systemClock,
62
+ limits: overrides = {},
63
+ } = {}) {
64
+ if (typeof lookup !== "function")
65
+ throw new TypeError("lookup must be a function");
66
+ const limits = { ...ADVISORY_CACHE_LIMITS, ...overrides };
67
+ if (
68
+ Object.keys(limits).some(
69
+ (key) =>
70
+ !Object.hasOwn(ADVISORY_CACHE_LIMITS, key) ||
71
+ !Number.isSafeInteger(limits[key]) ||
72
+ limits[key] < 1 ||
73
+ limits[key] > ADVISORY_CACHE_LIMITS[key],
74
+ ) ||
75
+ limits.staleMs < limits.ttlMs ||
76
+ limits.totalBytes < limits.entryBytes
77
+ )
78
+ throw new TypeError("Invalid advisory cache limits");
79
+ const entries = new Map();
80
+ const flights = new Map();
81
+ let bytes = 0;
82
+ const counts = {
83
+ hit: 0,
84
+ miss: 0,
85
+ coalesced: 0,
86
+ stale: 0,
87
+ uncached: 0,
88
+ bypass: 0,
89
+ evicted: 0,
90
+ expired: 0,
91
+ };
92
+
93
+ function drop(key) {
94
+ const entry = entries.get(key);
95
+ if (!entry) return;
96
+ entries.delete(key);
97
+ bytes -= entry.bytes;
98
+ }
99
+ function prune(now) {
100
+ for (const [key, entry] of entries)
101
+ if (now < entry.storedAt || now - entry.storedAt >= limits.staleMs) {
102
+ drop(key);
103
+ counts.expired++;
104
+ }
105
+ }
106
+ function cacheable(result, claim) {
107
+ return (
108
+ result !== null &&
109
+ typeof result === "object" &&
110
+ result.complete === true &&
111
+ COMPLETE.includes(result.status) &&
112
+ Array.isArray(result.reasons) &&
113
+ result.reasons.length === 0 &&
114
+ result.package?.name === claim.name &&
115
+ result.package?.version === claim.version
116
+ );
117
+ }
118
+ function store(claim, result, now) {
119
+ const json = JSON.stringify(result);
120
+ const size = Buffer.byteLength(json);
121
+ if (size > limits.entryBytes) return false;
122
+ drop(claim.key);
123
+ entries.set(claim.key, { json, bytes: size, storedAt: now });
124
+ bytes += size;
125
+ for (const key of entries.keys()) {
126
+ if (entries.size <= limits.entries && bytes <= limits.totalBytes) break;
127
+ drop(key);
128
+ counts.evicted++;
129
+ }
130
+ return entries.has(claim.key);
131
+ }
132
+ function provenance(status, entry, now) {
133
+ return {
134
+ status,
135
+ ttlMs: limits.ttlMs,
136
+ ...(entry
137
+ ? {
138
+ storedAt: new Date(entry.storedAt).toISOString(),
139
+ ageMs: now - entry.storedAt,
140
+ }
141
+ : {}),
142
+ };
143
+ }
144
+ function fly(claim, spec) {
145
+ const controller = new AbortController();
146
+ const flight = { controller, waiters: 0 };
147
+ flight.promise = Promise.resolve()
148
+ .then(() => lookup(spec, { ...lookupOptions, signal: controller.signal }))
149
+ .catch(() => null)
150
+ .then((result) => {
151
+ if (flights.get(claim.key) === flight) flights.delete(claim.key);
152
+ const now = clock.now();
153
+ if (cacheable(result, claim) && store(claim, result, now))
154
+ return { stored: entries.get(claim.key), now };
155
+ return {
156
+ result: result ?? unavailable(claim, clock, ["lookup-failed"]),
157
+ now,
158
+ };
159
+ });
160
+ flights.set(claim.key, flight);
161
+ return flight;
162
+ }
163
+ function settle(claim, outcome, coalesced) {
164
+ const { now } = outcome;
165
+ if (outcome.stored)
166
+ return {
167
+ ...JSON.parse(outcome.stored.json),
168
+ cache: provenance(
169
+ coalesced ? "coalesced" : "miss",
170
+ outcome.stored,
171
+ now,
172
+ ),
173
+ };
174
+ const result = structuredClone(outcome.result);
175
+ const prior = entries.get(claim.key);
176
+ // Only a refresh that produced no advisory data may fall back to an older complete snapshot.
177
+ if (
178
+ result.status === "unavailable" &&
179
+ prior &&
180
+ now >= prior.storedAt &&
181
+ now - prior.storedAt < limits.staleMs
182
+ ) {
183
+ const snapshot = JSON.parse(prior.json);
184
+ return {
185
+ ...snapshot,
186
+ status: "incomplete",
187
+ complete: false,
188
+ reasons: [...new Set(["stale-cache", ...result.reasons])].sort(),
189
+ diagnostics: result.diagnostics,
190
+ cache: {
191
+ ...provenance("stale", prior, now),
192
+ snapshotStatus: snapshot.status,
193
+ refreshStatus: result.status,
194
+ },
195
+ };
196
+ }
197
+ return { ...result, cache: provenance("uncached", null, now) };
198
+ }
199
+
200
+ // Aggregate counters only: never package identities, keys or timestamps of individual entries.
201
+ function stats() {
202
+ return {
203
+ entries: entries.size,
204
+ bytes,
205
+ inflight: flights.size,
206
+ hits: counts.hit,
207
+ misses: counts.miss,
208
+ coalesced: counts.coalesced,
209
+ staleServed: counts.stale,
210
+ uncached: counts.uncached,
211
+ bypassed: counts.bypass,
212
+ evicted: counts.evicted,
213
+ expired: counts.expired,
214
+ limits: { ...limits },
215
+ };
216
+ }
217
+ async function cachedLookup(spec, options) {
218
+ const result = await resolve(spec, options);
219
+ const now = clock.now();
220
+ const { status, storedAt } = result.cache;
221
+ counts[status]++;
222
+ const state =
223
+ FRESHNESS[status] ?? (result.status === "unavailable" ? "none" : "live");
224
+ const queried = Date.parse(result.queriedAt);
225
+ const provider =
226
+ status === "hit" ? NOT_CONTACTED : result.diagnostics?.provider;
227
+ return {
228
+ ...result,
229
+ diagnostics: {
230
+ ...(provider ? { provider: structuredClone(provider) } : {}),
231
+ freshness: {
232
+ state,
233
+ ...(state !== "none" && Number.isFinite(queried)
234
+ ? { queriedAt: result.queriedAt, ageMs: Math.max(0, now - queried) }
235
+ : {}),
236
+ ...(storedAt && state !== "stale"
237
+ ? {
238
+ freshUntil: new Date(
239
+ Date.parse(storedAt) + limits.ttlMs,
240
+ ).toISOString(),
241
+ }
242
+ : {}),
243
+ },
244
+ ...(exposeStats ? { cacheStats: stats() } : {}),
245
+ },
246
+ };
247
+ }
248
+ async function resolve(spec, { signal } = {}) {
249
+ const claim = identity(spec);
250
+ if (!claim) {
251
+ const result = await lookup(spec, { ...lookupOptions, signal });
252
+ return { ...result, cache: provenance("bypass", null, clock.now()) };
253
+ }
254
+ const now = clock.now();
255
+ prune(now);
256
+ const entry = entries.get(claim.key);
257
+ if (entry && now - entry.storedAt < limits.ttlMs) {
258
+ entries.delete(claim.key);
259
+ entries.set(claim.key, entry);
260
+ return {
261
+ ...JSON.parse(entry.json),
262
+ cache: provenance("hit", entry, now),
263
+ };
264
+ }
265
+ if (signal?.aborted)
266
+ return {
267
+ ...unavailable(claim, clock, ["aborted"]),
268
+ cache: provenance("uncached", null, now),
269
+ };
270
+ const existing = flights.get(claim.key);
271
+ const flight = existing ?? fly(claim, spec);
272
+ flight.waiters++;
273
+ let release;
274
+ const aborted = new Promise((resolve) => {
275
+ release = () => resolve(null);
276
+ signal?.addEventListener("abort", release, { once: true });
277
+ });
278
+ try {
279
+ const outcome = await Promise.race([flight.promise, aborted]);
280
+ if (outcome) return settle(claim, outcome, Boolean(existing));
281
+ return {
282
+ ...unavailable(claim, clock, ["aborted"]),
283
+ cache: provenance("uncached", null, clock.now()),
284
+ };
285
+ } finally {
286
+ signal?.removeEventListener("abort", release);
287
+ // The shared upstream request is cancelled only once every waiter has gone.
288
+ if (--flight.waiters === 0 && flights.get(claim.key) === flight) {
289
+ flights.delete(claim.key);
290
+ flight.controller.abort();
291
+ }
292
+ }
293
+ }
294
+ return { lookup: cachedLookup, stats };
295
+ }