@xagents_in/unmask 0.1.1 → 0.1.3

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.
@@ -9,7 +9,6 @@ const MODELS = new Set([
9
9
  "gemini-3-flash-preview",
10
10
  ]);
11
11
  export const DEFAULT_MODEL_ROUTE = Object.freeze([
12
- Object.freeze({ modelId: "gemini-3.5-flash", thinkingLevel: "high" }),
13
12
  Object.freeze({
14
13
  modelId: "gemini-3-flash-preview",
15
14
  thinkingLevel: "minimal",
@@ -661,28 +660,46 @@ const delay = (ms, signal) =>
661
660
  function quotaScope(body, modelId) {
662
661
  const details = body?.error?.details;
663
662
  if (!Array.isArray(details) || details.length > 32) return "unknown";
663
+ const windows = [];
664
+ const normalize = (value) => value.toLowerCase().replace(/[^a-z0-9]/g, "");
665
+ function bindingWindow(binding, identity) {
666
+ if (/hour|week|month|year|second/.test(identity)) return null;
667
+ if (binding.includes("perminute") && !/day|daily/.test(identity))
668
+ return "minute";
669
+ if (binding.includes("perday") && !identity.includes("perminute"))
670
+ return "daily";
671
+ return null;
672
+ }
664
673
  for (const info of details.filter(
665
674
  (item) => item?.["@type"] === "type.googleapis.com/google.rpc.ErrorInfo",
666
675
  )) {
667
676
  const metadata = info.metadata;
668
- const identity =
669
- `${info.reason ?? ""} ${metadata?.quota_limit ?? ""} ${metadata?.quota_metric ?? ""}`
670
- .toLowerCase()
671
- .replace(/[^a-z0-9]/g, "");
672
- if (identity.includes("day")) return "daily";
677
+ const binding = normalize(
678
+ `${metadata?.quota_limit ?? ""} ${metadata?.quota_metric ?? ""}`,
679
+ );
680
+ const reason = normalize(String(info.reason ?? ""));
673
681
  if (
674
682
  ["global", "project"].includes(metadata?.scope) ||
675
- (identity.includes("perproject") && !identity.includes("permodel"))
683
+ reason.includes("global") ||
684
+ (reason.includes("project") && !reason.includes("model")) ||
685
+ (binding.includes("perproject") && !binding.includes("permodel"))
676
686
  )
677
687
  return "global";
688
+ const window = bindingWindow(binding, binding + reason);
678
689
  if (
679
690
  !metadata ||
691
+ !["", "ratelimitexceeded", "quotaexceeded", "resourceexhausted"].includes(
692
+ reason,
693
+ ) ||
680
694
  ![modelId, `models/${modelId}`].includes(metadata.model) ||
681
- !identity.includes("permodel") ||
682
- !identity.includes("perminute") ||
683
- /hour|week|month|year|second/.test(identity)
695
+ !binding.includes("permodel") ||
696
+ !window ||
697
+ (metadata.scope !== undefined &&
698
+ metadata.scope !== "model" &&
699
+ !(metadata.scope === "daily" && window === "daily"))
684
700
  )
685
701
  return "unknown";
702
+ windows.push(window);
686
703
  }
687
704
  const violations = [];
688
705
  for (const detail of details) {
@@ -697,40 +714,34 @@ function quotaScope(body, modelId) {
697
714
  violations.push(...detail.violations);
698
715
  }
699
716
  if (!violations.length) return "unknown";
700
- if (
701
- violations.some((item) =>
702
- `${item?.quotaId ?? ""} ${item?.quotaMetric ?? ""}`
703
- .toLowerCase()
704
- .replace(/[^a-z0-9]/g, "")
705
- .includes("day"),
706
- )
707
- )
708
- return "daily";
709
717
  for (const item of violations) {
710
718
  const dimensions = item?.quotaDimensions;
711
719
  if (
712
720
  !dimensions ||
713
721
  typeof dimensions !== "object" ||
714
722
  Array.isArray(dimensions) ||
715
- !Object.hasOwn(dimensions, "model")
723
+ !Object.hasOwn(dimensions, "model") ||
724
+ ["global", "project"].includes(dimensions.scope)
716
725
  )
717
726
  return "global";
718
- if (["global", "project", "daily"].includes(dimensions.scope))
719
- return dimensions.scope === "daily" ? "daily" : "global";
720
727
  if (![modelId, `models/${modelId}`].includes(dimensions.model))
721
728
  return "unknown";
722
- const binding = `${item.quotaId ?? ""} ${item.quotaMetric ?? ""}`
723
- .toLowerCase()
724
- .replace(/[^a-z0-9]/g, "");
729
+ const binding = normalize(
730
+ `${item.quotaId ?? ""} ${item.quotaMetric ?? ""}`,
731
+ );
725
732
  if (!binding.includes("permodel"))
726
733
  return binding.includes("perproject") ? "global" : "unknown";
734
+ const window = bindingWindow(binding, binding);
727
735
  if (
728
- !binding.includes("perminute") ||
729
- /hour|week|month|year|second/.test(binding)
736
+ !window ||
737
+ (dimensions.scope !== undefined &&
738
+ dimensions.scope !== "model" &&
739
+ !(dimensions.scope === "daily" && window === "daily"))
730
740
  )
731
741
  return "unknown";
742
+ windows.push(window);
732
743
  }
733
- return "model";
744
+ return windows.includes("daily") ? "model-daily" : "model";
734
745
  }
735
746
 
736
747
  export function createGemmaClient(options = {}) {
@@ -753,6 +764,11 @@ export function createGemmaClient(options = {}) {
753
764
  )
754
765
  )
755
766
  throw new TypeError("Invalid model route");
767
+ if (
768
+ new Set(requestedRoute.map((item) => item.modelId)).size !==
769
+ requestedRoute.length
770
+ )
771
+ throw new TypeError("Invalid model route: duplicate model");
756
772
  if (
757
773
  options.route !== undefined &&
758
774
  options.modelId !== undefined &&
@@ -795,10 +811,16 @@ export function createGemmaClient(options = {}) {
795
811
  }));
796
812
  const generationConfig = route[0].generationConfig;
797
813
  const routingPolicy = {
798
- version: "ordered-fallback-1",
814
+ version: "ordered-fallback-2",
799
815
  maxGenerationAttempts: Math.max(2, route.length),
800
- fallthrough: ["model-scoped-429", "transient-5xx", "timeout"],
816
+ fallthrough: [
817
+ "model-scoped-minute-429",
818
+ "model-scoped-daily-429",
819
+ "transient-5xx",
820
+ "timeout",
821
+ ],
801
822
  invalidOutputRetry: "same-tier",
823
+ modelDailyFallback: "next-tier-no-wait",
802
824
  };
803
825
  const fetchImpl = options.fetchImpl ?? globalThis.fetch;
804
826
  const now = options.now ?? (() => performance.now());
@@ -1108,12 +1130,17 @@ export function createGemmaClient(options = {}) {
1108
1130
  body = await jsonResponse(response, controller.signal);
1109
1131
  } catch {}
1110
1132
  failureScope = quotaScope(body, tier.modelId);
1111
- const retry = failureScope === "model";
1133
+ const retry =
1134
+ failureScope === "model" ||
1135
+ (failureScope === "model-daily" &&
1136
+ tierIndex + 1 < route.length);
1112
1137
  throw Object.assign(
1113
1138
  new Failure(
1114
1139
  "quota",
1115
1140
  retry,
1116
- retryDelay(response, wallNow()),
1141
+ failureScope === "model-daily"
1142
+ ? 0
1143
+ : retryDelay(response, wallNow()),
1117
1144
  retry,
1118
1145
  ),
1119
1146
  { scope: failureScope },
@@ -1,6 +1,10 @@
1
1
  import { createServer } from "node:http";
2
+ import { createRegistryFactsHandler } from "./registry-facts.mjs";
3
+ import { buildPackageDetails } from "../core/package-details.mjs";
4
+ import { attachVulnerabilityCheck } from "./enrichment.mjs";
2
5
  import { createQueue, resolveLimits, systemClock } from "./queue.mjs";
3
6
  import { createReportStore } from "./store.mjs";
7
+ import { handleReportExploration } from "./report-exploration.mjs";
4
8
  import { renderReport } from "../viewer/render.mjs";
5
9
  import {
6
10
  renderHome,
@@ -99,9 +103,11 @@ export function createCacheKeyPrefix(versionInfo, limits) {
99
103
 
100
104
  export function createApp({
101
105
  runAnalysis,
106
+ enrichJob,
102
107
  versionInfo,
103
108
  limits: overrides,
104
109
  clock = systemClock,
110
+ registryFacts = {},
105
111
  } = {}) {
106
112
  if (typeof runAnalysis !== "function")
107
113
  throw new TypeError("runAnalysis is required");
@@ -114,6 +120,9 @@ export function createApp({
114
120
  throw new TypeError(
115
121
  "versionInfo requires version, commit and tarballSha256 strings",
116
122
  );
123
+ if (enrichJob !== undefined && typeof enrichJob !== "function")
124
+ throw new TypeError("enrichJob must be a function");
125
+ const registryFactsHandler = createRegistryFactsHandler(registryFacts);
117
126
  const limits = resolveLimits(overrides);
118
127
  const version = Object.fromEntries(
119
128
  ["version", "commit", "tarballSha256"].map((key) => [
@@ -128,7 +137,14 @@ export function createApp({
128
137
  clock,
129
138
  });
130
139
  const keyPrefix = createCacheKeyPrefix(versionInfo, limits);
131
- const queue = createQueue({ runAnalysis, store, limits, clock, keyPrefix });
140
+ const queue = createQueue({
141
+ runAnalysis,
142
+ enrichJob,
143
+ store,
144
+ limits,
145
+ clock,
146
+ keyPrefix,
147
+ });
132
148
  const server = createServer(
133
149
  { maxHeaderSize: 8192, headersTimeout: 10000, requestTimeout: 15000 },
134
150
  (req, res) => {
@@ -162,6 +178,7 @@ export function createApp({
162
178
  }
163
179
  async function handle() {
164
180
  const path = req.url;
181
+ if (await registryFactsHandler(req, json)) return;
165
182
  if (path === "/check") {
166
183
  if (req.method !== "POST")
167
184
  return error(
@@ -245,10 +262,13 @@ export function createApp({
245
262
  "Use the release form to start a check.",
246
263
  { allow: "GET" },
247
264
  );
265
+ if (handleReportExploration({ path, getJob: queue.get, send })) return;
248
266
  if (path === "/") return send(200, renderHome());
249
267
  if (path === "/healthz") return json(200, { status: "ok" });
250
268
  if (path === "/version") return json(200, version);
251
- const match = /^\/r\/([a-f0-9]{32})(\.json)?$/.exec(path);
269
+ const match = /^\/r\/([a-f0-9]{32})(\.details\.json|\.json)?$/.exec(
270
+ path,
271
+ );
252
272
  if (!match)
253
273
  return error(
254
274
  404,
@@ -265,10 +285,21 @@ export function createApp({
265
285
  const pending = ["queued", "running"].includes(job.status);
266
286
  if (match[2]) {
267
287
  if (job.report)
268
- return json(200, job.report, {
269
- "content-disposition":
270
- 'attachment; filename="unmask-report.json"',
271
- });
288
+ return json(
289
+ 200,
290
+ match[2] === ".details.json"
291
+ ? attachVulnerabilityCheck(
292
+ buildPackageDetails(job.report),
293
+ job.vulnerabilityCheck,
294
+ )
295
+ : job.report,
296
+ {
297
+ "content-disposition":
298
+ match[2] === ".details.json"
299
+ ? 'attachment; filename="unmask-package-details.json"'
300
+ : 'attachment; filename="unmask-report.json"',
301
+ },
302
+ );
272
303
  const { key, ...publicJob } = job;
273
304
  return json(
274
305
  pending ? 202 : 200,
@@ -0,0 +1,116 @@
1
+ export const DETAILS_RESPONSE_LIMITS = Object.freeze({
2
+ packageDetailsBytes: 2097152,
3
+ vulnerabilityCheckBytes: 2096128,
4
+ combinedBytes: 4194304,
5
+ });
6
+
7
+ export function unavailableVulnerabilityCheck(spec, clock, reason) {
8
+ const separator = typeof spec === "string" ? spec.lastIndexOf("@") : -1;
9
+ return {
10
+ schemaVersion: "1",
11
+ source: "OSV",
12
+ package:
13
+ separator > 0
14
+ ? {
15
+ ecosystem: "npm",
16
+ name: spec.slice(0, separator),
17
+ version: spec.slice(separator + 1),
18
+ }
19
+ : null,
20
+ queriedAt: new Date(clock.now()).toISOString(),
21
+ status: "unavailable",
22
+ complete: false,
23
+ reasons: [reason],
24
+ advisories: [],
25
+ pagesFetched: 0,
26
+ limits: { resultBytes: DETAILS_RESPONSE_LIMITS.vulnerabilityCheckBytes },
27
+ };
28
+ }
29
+
30
+ export function boundedVulnerabilityCheck(value, spec, clock) {
31
+ try {
32
+ const json = JSON.stringify(value);
33
+ if (
34
+ Buffer.byteLength(json) > DETAILS_RESPONSE_LIMITS.vulnerabilityCheckBytes
35
+ )
36
+ return unavailableVulnerabilityCheck(spec, clock, "result-byte-cap");
37
+ const snapshot = JSON.parse(json);
38
+ if (
39
+ snapshot.schemaVersion !== "1" ||
40
+ snapshot.source !== "OSV" ||
41
+ !["found", "none-reported", "incomplete", "unavailable"].includes(
42
+ snapshot.status,
43
+ ) ||
44
+ snapshot.complete !==
45
+ ["found", "none-reported"].includes(snapshot.status) ||
46
+ !Array.isArray(snapshot.reasons) ||
47
+ !snapshot.reasons.every((reason) => typeof reason === "string") ||
48
+ !Array.isArray(snapshot.advisories) ||
49
+ snapshot.advisories.some(
50
+ (item) => !item || typeof item !== "object" || Array.isArray(item),
51
+ ) ||
52
+ (snapshot.status === "found" && snapshot.advisories.length === 0) ||
53
+ (snapshot.status === "none-reported" &&
54
+ snapshot.advisories.length !== 0) ||
55
+ !Number.isSafeInteger(snapshot.pagesFetched) ||
56
+ snapshot.pagesFetched < 0 ||
57
+ (snapshot.complete &&
58
+ (snapshot.pagesFetched === 0 || snapshot.reasons.length !== 0)) ||
59
+ typeof snapshot.queriedAt !== "string" ||
60
+ !Number.isFinite(Date.parse(snapshot.queriedAt)) ||
61
+ !snapshot.limits ||
62
+ typeof snapshot.limits !== "object"
63
+ )
64
+ throw new TypeError("Invalid vulnerability enrichment");
65
+ const expected = unavailableVulnerabilityCheck(
66
+ spec,
67
+ clock,
68
+ "unused",
69
+ ).package;
70
+ if (
71
+ snapshot.package !== null &&
72
+ (snapshot.package.ecosystem !== "npm" ||
73
+ snapshot.package.name !== expected?.name ||
74
+ snapshot.package.version !== expected?.version)
75
+ )
76
+ throw new TypeError("Vulnerability enrichment identity mismatch");
77
+ if (snapshot.complete && snapshot.package === null)
78
+ throw new TypeError("Vulnerability enrichment identity missing");
79
+ return snapshot;
80
+ } catch {
81
+ return unavailableVulnerabilityCheck(spec, clock, "invalid-result");
82
+ }
83
+ }
84
+
85
+ export function attachVulnerabilityCheck(details, value) {
86
+ const vulnerabilityCheck = value ?? {
87
+ source: "OSV",
88
+ status: "not-assessed",
89
+ complete: false,
90
+ reasons: ["hosted-enrichment-disabled"],
91
+ advisories: [],
92
+ };
93
+ const complete =
94
+ vulnerabilityCheck.complete === true &&
95
+ ["found", "none-reported"].includes(vulnerabilityCheck.status);
96
+ const result = {
97
+ ...details,
98
+ scope: {
99
+ ...details.scope,
100
+ vulnerabilityScan: complete
101
+ ? "OSV-exact-package-query-complete"
102
+ : "not-assessed",
103
+ knownVulnerabilities: complete
104
+ ? "queried-exact-package-only"
105
+ : "not-assessed",
106
+ },
107
+ vulnerabilityCheck,
108
+ responseLimits: DETAILS_RESPONSE_LIMITS,
109
+ };
110
+ if (
111
+ Buffer.byteLength(JSON.stringify(result)) >
112
+ DETAILS_RESPONSE_LIMITS.combinedBytes
113
+ )
114
+ throw new TypeError("Combined details response exceeds cap");
115
+ return result;
116
+ }
@@ -47,6 +47,7 @@ export async function startServer({
47
47
  runnerOptions = {},
48
48
  versionInfo,
49
49
  modelClient,
50
+ enrichJob,
50
51
  } = {}) {
51
52
  if (
52
53
  typeof host !== "string" ||
@@ -62,6 +63,14 @@ export async function startServer({
62
63
  : modelClient === undefined
63
64
  ? await modelClientFromEnv()
64
65
  : modelClient;
66
+ const enrichment =
67
+ enrichJob === null
68
+ ? undefined
69
+ : enrichJob === undefined
70
+ ? (await import("../core/advisories.mjs")).lookupNpmAdvisories
71
+ : enrichJob;
72
+ if (enrichment !== undefined && typeof enrichment !== "function")
73
+ throw new TypeError("enrichJob must be a function or null");
65
74
  const run = await createRunner({
66
75
  ...runnerOptions,
67
76
  tempRoot,
@@ -81,6 +90,13 @@ export async function startServer({
81
90
  };
82
91
  const server = createApp({
83
92
  runAnalysis,
93
+ enrichJob:
94
+ enrichment === undefined
95
+ ? undefined
96
+ : (spec, { signal }) =>
97
+ enrichment(spec, {
98
+ signal: AbortSignal.any([signal, shutdown.signal]),
99
+ }),
84
100
  versionInfo: {
85
101
  ...(versionInfo || (await loadVersionInfo())),
86
102
  modelConfigDigest:
@@ -1,4 +1,8 @@
1
1
  import { randomBytes } from "node:crypto";
2
+ import {
3
+ boundedVulnerabilityCheck,
4
+ unavailableVulnerabilityCheck,
5
+ } from "./enrichment.mjs";
2
6
 
3
7
  export const DEFAULT_LIMITS = Object.freeze({
4
8
  maxBodyBytes: 4096,
@@ -28,6 +32,7 @@ export function resolveLimits(overrides = {}) {
28
32
 
29
33
  export function createQueue({
30
34
  runAnalysis,
35
+ enrichJob,
31
36
  store,
32
37
  limits,
33
38
  clock = systemClock,
@@ -39,7 +44,20 @@ export function createQueue({
39
44
  let admissions = [];
40
45
  function persist(job) {
41
46
  if (!store.put(job)) {
42
- const { report, ...small } = job;
47
+ if (
48
+ job.report &&
49
+ job.vulnerabilityCheck &&
50
+ store.put({
51
+ ...job,
52
+ vulnerabilityCheck: unavailableVulnerabilityCheck(
53
+ job.nameAtExactVersion,
54
+ clock,
55
+ "storage-byte-cap",
56
+ ),
57
+ })
58
+ )
59
+ return;
60
+ const { report, vulnerabilityCheck, ...small } = job;
43
61
  store.put({ ...small, status: "incomplete", reason: "report-too-large" });
44
62
  }
45
63
  }
@@ -61,12 +79,26 @@ export function createQueue({
61
79
  job.status = "running";
62
80
  job.startedAt = clock.now();
63
81
  const controller = new AbortController();
82
+ let coreReport;
64
83
  const timer = clock.setTimeout(() => {
65
- job.status = "incomplete";
66
- job.reason = "execution-timeout";
67
84
  job.finishedAt = clock.now();
85
+ if (coreReport) {
86
+ job.status = coreReport.policy.exitCode === 2 ? "incomplete" : "done";
87
+ persist({
88
+ ...publicJob(job),
89
+ report: coreReport,
90
+ vulnerabilityCheck: unavailableVulnerabilityCheck(
91
+ job.nameAtExactVersion,
92
+ clock,
93
+ "aborted",
94
+ ),
95
+ });
96
+ } else {
97
+ job.status = "incomplete";
98
+ job.reason = "execution-timeout";
99
+ persist(job);
100
+ }
68
101
  controller.abort();
69
- persist(job);
70
102
  }, limits.executionMs);
71
103
  timer?.unref?.();
72
104
  job.cancel = () => {
@@ -77,7 +109,7 @@ export function createQueue({
77
109
  .then(() =>
78
110
  runAnalysis(job.nameAtExactVersion, { signal: controller.signal }),
79
111
  )
80
- .then((report) => {
112
+ .then(async (report) => {
81
113
  if (job.status !== "running" || closed) return;
82
114
  if (
83
115
  !report ||
@@ -86,9 +118,56 @@ export function createQueue({
86
118
  ![0, 2, 3].includes(report.policy?.exitCode)
87
119
  )
88
120
  throw new TypeError("Invalid report");
121
+ coreReport = report;
122
+ let vulnerabilityCheck;
123
+ if (enrichJob) {
124
+ let abort;
125
+ const interrupted = new Promise((resolve) => {
126
+ abort = () =>
127
+ resolve(
128
+ unavailableVulnerabilityCheck(
129
+ job.nameAtExactVersion,
130
+ clock,
131
+ "aborted",
132
+ ),
133
+ );
134
+ controller.signal.addEventListener("abort", abort, { once: true });
135
+ if (controller.signal.aborted) abort();
136
+ });
137
+ try {
138
+ const value = await Promise.race([
139
+ Promise.resolve().then(() =>
140
+ enrichJob(job.nameAtExactVersion, {
141
+ signal: controller.signal,
142
+ }),
143
+ ),
144
+ interrupted,
145
+ ]);
146
+ vulnerabilityCheck = boundedVulnerabilityCheck(
147
+ value,
148
+ job.nameAtExactVersion,
149
+ clock,
150
+ );
151
+ } catch {
152
+ vulnerabilityCheck = unavailableVulnerabilityCheck(
153
+ job.nameAtExactVersion,
154
+ clock,
155
+ "lookup-failed",
156
+ );
157
+ } finally {
158
+ controller.signal.removeEventListener("abort", abort);
159
+ }
160
+ }
161
+ if (job.status !== "running" || closed) return;
89
162
  const status = report.policy.exitCode === 2 ? "incomplete" : "done";
90
163
  const finishedAt = clock.now();
91
- persist({ ...publicJob(job), status, report, finishedAt });
164
+ persist({
165
+ ...publicJob(job),
166
+ status,
167
+ report,
168
+ finishedAt,
169
+ ...(vulnerabilityCheck ? { vulnerabilityCheck } : {}),
170
+ });
92
171
  job.status = status;
93
172
  job.finishedAt = finishedAt;
94
173
  })
@@ -0,0 +1,118 @@
1
+ import {
2
+ getRegistryFacts,
3
+ getRegistryHistory,
4
+ registryIdentity,
5
+ } from "../core/registry-facts.mjs";
6
+
7
+ export function createRegistryFactsHandler(options = {}) {
8
+ const maxConcurrent = options.maxConcurrent ?? 2;
9
+ const maxRequests = options.maxRequests ?? 30;
10
+ if (
11
+ !Number.isSafeInteger(maxConcurrent) ||
12
+ maxConcurrent < 1 ||
13
+ maxConcurrent > 2 ||
14
+ !Number.isSafeInteger(maxRequests) ||
15
+ maxRequests < 1 ||
16
+ maxRequests > 30
17
+ )
18
+ throw new TypeError("Invalid registry lookup admission limits");
19
+ let active = 0,
20
+ used = 0,
21
+ windowStart = Date.now();
22
+ return async function handleRegistryFacts(req, json) {
23
+ const url = new URL(req.url, "http://localhost");
24
+ const history = url.pathname === "/api/registry/history";
25
+ if (url.pathname !== "/api/registry/facts" && !history) return false;
26
+ if (req.method !== "GET") {
27
+ json(
28
+ 405,
29
+ { status: "rejected", reason: "method-not-allowed" },
30
+ { allow: "GET" },
31
+ );
32
+ return true;
33
+ }
34
+ const spec = url.searchParams.get("package");
35
+ let limit = 20;
36
+ try {
37
+ const allowed = history ? ["package", "limit"] : ["package"];
38
+ if (
39
+ !spec ||
40
+ [...url.searchParams.keys()].some((key) => !allowed.includes(key)) ||
41
+ allowed.some((key) => url.searchParams.getAll(key).length > 1)
42
+ )
43
+ throw new Error();
44
+ if (history) {
45
+ registryIdentity(`${spec}@0.0.0`);
46
+ if (url.searchParams.has("limit")) {
47
+ const raw = url.searchParams.get("limit");
48
+ if (!/^[1-9]\d{0,2}$/.test(raw)) throw new Error();
49
+ limit = Number(raw);
50
+ if (limit > 100) throw new Error();
51
+ }
52
+ } else registryIdentity(spec);
53
+ } catch {
54
+ json(400, {
55
+ status: "rejected",
56
+ reason: history
57
+ ? "package-name-and-bounded-limit-required"
58
+ : "exact-package-required",
59
+ });
60
+ return true;
61
+ }
62
+ const now = Date.now();
63
+ if (now - windowStart >= 60000) {
64
+ used = 0;
65
+ windowStart = now;
66
+ }
67
+ if (active >= maxConcurrent || used >= maxRequests) {
68
+ json(
69
+ 429,
70
+ { status: "unavailable", reason: "registry-lookup-limit" },
71
+ {
72
+ "retry-after": String(
73
+ Math.max(1, Math.ceil((60000 - (now - windowStart)) / 1000)),
74
+ ),
75
+ },
76
+ );
77
+ return true;
78
+ }
79
+ used++;
80
+ active++;
81
+ try {
82
+ json(
83
+ 200,
84
+ history
85
+ ? await getRegistryHistory(spec, { ...options, limit })
86
+ : await getRegistryFacts(spec, options),
87
+ );
88
+ } catch (error) {
89
+ const reason = [
90
+ "acquisition-timeout",
91
+ "response-cap",
92
+ "redirect-refused",
93
+ "invalid-registry-document",
94
+ "registry-identity-mismatch",
95
+ "http-status",
96
+ "network-failure",
97
+ "invalid-response",
98
+ "registry-history-cap",
99
+ ].includes(error.code)
100
+ ? error.code
101
+ : "registry-unavailable";
102
+ const status =
103
+ reason === "acquisition-timeout"
104
+ ? 504
105
+ : error.observation?.status === 404
106
+ ? 404
107
+ : 502;
108
+ json(status, {
109
+ status: "unavailable",
110
+ reason,
111
+ safetyVerdict: "not-provided",
112
+ });
113
+ } finally {
114
+ active--;
115
+ }
116
+ return true;
117
+ };
118
+ }