@reddoorla/maintenance 0.50.0 → 0.51.0

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/dist/cli/bin.js CHANGED
@@ -87,6 +87,38 @@ var init_url = __esm({
87
87
  }
88
88
  });
89
89
 
90
+ // src/reports/airtable/throttle.ts
91
+ function createMinIntervalThrottle(opts) {
92
+ const { minIntervalMs, now, delay } = opts;
93
+ return function wrap(fn) {
94
+ let chain = Promise.resolve();
95
+ let last = Number.NEGATIVE_INFINITY;
96
+ return (...args) => {
97
+ chain = chain.then(async () => {
98
+ const wait = minIntervalMs - (now() - last);
99
+ if (wait > 0) await delay(wait);
100
+ last = now();
101
+ fn(...args);
102
+ }).catch(() => {
103
+ });
104
+ };
105
+ };
106
+ }
107
+ function applyThrottle(base, opts) {
108
+ const real = base._base?.runAction;
109
+ if (typeof real !== "function") return base;
110
+ const wrap = createMinIntervalThrottle(opts);
111
+ const throttled = wrap(real.bind(base._base));
112
+ base._base.runAction = throttled;
113
+ base.runAction = throttled;
114
+ return base;
115
+ }
116
+ var init_throttle = __esm({
117
+ "src/reports/airtable/throttle.ts"() {
118
+ "use strict";
119
+ }
120
+ });
121
+
90
122
  // src/reports/airtable/client.ts
91
123
  var client_exports = {};
92
124
  __export(client_exports, {
@@ -110,12 +142,20 @@ function readAirtableConfig() {
110
142
  return { apiKey, baseId };
111
143
  }
112
144
  function openBase(cfg) {
113
- return new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
145
+ const base = new Airtable({ apiKey: cfg.apiKey }).base(cfg.baseId);
146
+ return applyThrottle(base, {
147
+ minIntervalMs: MIN_REQUEST_INTERVAL_MS,
148
+ now: () => Date.now(),
149
+ delay: (ms) => new Promise((resolve12) => setTimeout(resolve12, ms))
150
+ });
114
151
  }
152
+ var MIN_REQUEST_INTERVAL_MS;
115
153
  var init_client = __esm({
116
154
  "src/reports/airtable/client.ts"() {
117
155
  "use strict";
118
156
  init_credentials();
157
+ init_throttle();
158
+ MIN_REQUEST_INTERVAL_MS = 220;
119
159
  }
120
160
  });
121
161
 
@@ -123,12 +163,15 @@ var init_client = __esm({
123
163
  var websites_exports = {};
124
164
  __export(websites_exports, {
125
165
  ACTIVE_STATUSES: () => ACTIVE_STATUSES,
166
+ SEVERITY_RANK: () => SEVERITY_RANK,
126
167
  WEBSITES_TABLE: () => WEBSITES_TABLE,
127
168
  getWebsiteBySlug: () => getWebsiteBySlug,
128
169
  isDashboardVisible: () => isDashboardVisible,
129
170
  listWebsites: () => listWebsites,
130
171
  mapRow: () => mapRow,
172
+ normalizeSecurityAdvisory: () => normalizeSecurityAdvisory,
131
173
  parseNotifyRouting: () => parseNotifyRouting,
174
+ parseSecurityAdvisories: () => parseSecurityAdvisories,
132
175
  siteSlug: () => siteSlug,
133
176
  updateA11yCounts: () => updateA11yCounts,
134
177
  updateAuditFields: () => updateAuditFields,
@@ -209,6 +252,7 @@ function mapRow(rec) {
209
252
  securityVulnsModerate: f["Security Vulns Moderate"] ?? null,
210
253
  securityVulnsLow: f["Security Vulns Low"] ?? null,
211
254
  lastSecurityAuditAt: f["Last security audit at"] ?? null,
255
+ securityAdvisories: parseSecurityAdvisories(f["Security advisories"]),
212
256
  certDaysRemaining: f["Cert days remaining"] ?? null,
213
257
  domainCheckedAt: f["Domain checked at"] ?? null,
214
258
  crossbrowserOk: typeof f["Crossbrowser OK"] === "boolean" ? f["Crossbrowser OK"] : null,
@@ -272,6 +316,38 @@ function depsFields(counts) {
272
316
  }
273
317
  return fields;
274
318
  }
319
+ function normalizeSecurityAdvisory(raw) {
320
+ if (!raw || typeof raw !== "object") return null;
321
+ const e = raw;
322
+ const module = typeof e["module"] === "string" ? e["module"] : null;
323
+ const severity = e["severity"];
324
+ if (module === null) return null;
325
+ if (severity !== "low" && severity !== "moderate" && severity !== "high" && severity !== "critical")
326
+ return null;
327
+ const cves = Array.isArray(e["cves"]) ? e["cves"].filter((c) => typeof c === "string") : [];
328
+ return {
329
+ module,
330
+ severity,
331
+ title: typeof e["title"] === "string" ? e["title"] : "",
332
+ cves,
333
+ url: typeof e["url"] === "string" ? e["url"] : null
334
+ };
335
+ }
336
+ function parseSecurityAdvisories(raw) {
337
+ if (typeof raw !== "string" || raw.trim() === "") return null;
338
+ let parsed;
339
+ try {
340
+ parsed = JSON.parse(raw);
341
+ } catch {
342
+ return null;
343
+ }
344
+ if (!Array.isArray(parsed)) return null;
345
+ return parsed.map(normalizeSecurityAdvisory).filter((a) => a !== null);
346
+ }
347
+ function securityAdvisoryFields(advisories) {
348
+ const capped = [...advisories].sort((a, b) => SEVERITY_RANK[a.severity] - SEVERITY_RANK[b.severity]).slice(0, MAX_PERSISTED_ADVISORIES);
349
+ return { "Security advisories": JSON.stringify(capped) };
350
+ }
275
351
  function securityFields(counts) {
276
352
  return {
277
353
  "Security Vulns Critical": counts.critical,
@@ -315,6 +391,8 @@ async function updateAuditFields(base, recordId, audits) {
315
391
  if (audits.a11y) Object.assign(fields, a11yFields(audits.a11y));
316
392
  if (audits.deps) Object.assign(fields, depsFields(audits.deps));
317
393
  if (audits.security) Object.assign(fields, securityFields(audits.security));
394
+ if (audits.securityAdvisories)
395
+ Object.assign(fields, securityAdvisoryFields(audits.securityAdvisories));
318
396
  if (audits.domain) Object.assign(fields, domainFields(audits.domain));
319
397
  if (audits.browser) Object.assign(fields, browserFields(audits.browser));
320
398
  await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
@@ -335,7 +413,7 @@ async function updateLaunched(base, recordId, at) {
335
413
  const fields = { Status: "maintenance", "Launched at": at };
336
414
  await base(WEBSITES_TABLE).update([{ id: recordId, fields }]);
337
415
  }
338
- var WEBSITES_TABLE, ACTIVE_STATUSES, FREQUENCIES;
416
+ var WEBSITES_TABLE, ACTIVE_STATUSES, FREQUENCIES, SEVERITY_RANK, MAX_PERSISTED_ADVISORIES;
339
417
  var init_websites = __esm({
340
418
  "src/reports/airtable/websites.ts"() {
341
419
  "use strict";
@@ -345,6 +423,13 @@ var init_websites = __esm({
345
423
  "launch period"
346
424
  ]);
347
425
  FREQUENCIES = ["None", "Monthly", "Quarterly", "Yearly"];
426
+ SEVERITY_RANK = {
427
+ critical: 0,
428
+ high: 1,
429
+ moderate: 2,
430
+ low: 3
431
+ };
432
+ MAX_PERSISTED_ADVISORIES = 25;
348
433
  }
349
434
  });
350
435
 
@@ -505,9 +590,19 @@ function securityCountsFromResult(result) {
505
590
  const c = details?.counts ?? { low: 0, moderate: 0, high: 0, critical: 0 };
506
591
  return { critical: c.critical, high: c.high, moderate: c.moderate, low: c.low };
507
592
  }
593
+ function advisoriesFromResult(result) {
594
+ if (result.audit !== "security") {
595
+ throw new Error(`Expected a 'security' AuditResult, got '${result.audit}'`);
596
+ }
597
+ const details = result.details;
598
+ const raw = details?.advisories;
599
+ if (!Array.isArray(raw)) return [];
600
+ return raw.map(normalizeSecurityAdvisory).filter((a) => a !== null);
601
+ }
508
602
  var init_security_airtable = __esm({
509
603
  "src/audits/security-airtable.ts"() {
510
604
  "use strict";
605
+ init_websites();
511
606
  }
512
607
  });
513
608
 
@@ -596,6 +691,7 @@ async function writeAuditsToAirtable(args) {
596
691
  if (sec && hasSecurityCounts(sec)) {
597
692
  const counts = securityCountsFromResult(sec);
598
693
  audits.security = counts;
694
+ audits.securityAdvisories = advisoriesFromResult(sec);
599
695
  writes.push({ audit: "security", counts });
600
696
  }
601
697
  const dom = results.find((r) => r.audit === "domain");