tosijs-ui 1.7.4 → 1.9.0-beta.1

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 (40) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/dist/doc-browser.d.ts +3 -1
  3. package/dist/doc-browser.js +81 -24
  4. package/dist/doc-system/nav-tree.d.ts +24 -5
  5. package/dist/doc-system/nav-tree.js +0 -0
  6. package/dist/doc-system/site/audit-guard.d.ts +124 -0
  7. package/dist/doc-system/site/audit-guard.js +555 -0
  8. package/dist/doc-system/site/build-stamp.d.ts +23 -0
  9. package/dist/doc-system/site/build-stamp.js +65 -0
  10. package/dist/doc-system/site/dev-server.d.ts +18 -0
  11. package/dist/doc-system/site/dev-server.js +205 -12
  12. package/dist/doc-system/site/index.d.ts +4 -0
  13. package/dist/doc-system/site/index.js +2 -0
  14. package/dist/doc-system/site/open-browser.d.ts +53 -0
  15. package/dist/doc-system/site/open-browser.js +231 -0
  16. package/dist/doc-system/site/orchestrator.d.ts +3 -1
  17. package/dist/doc-system/site/orchestrator.js +19 -1
  18. package/dist/doc-system/site/site-config.d.ts +66 -0
  19. package/dist/hydrate/{ajs-language-qm5w38ae.js → ajs-language-xvpytqfm.js} +2 -2
  20. package/dist/hydrate/{code-editor-cm-95n31m4f.js → code-editor-cm-59je4d8g.js} +6 -6
  21. package/dist/hydrate/{code-editor-cm-95n31m4f.js.map → code-editor-cm-59je4d8g.js.map} +4 -4
  22. package/dist/hydrate/hydrate.js +56 -55
  23. package/dist/hydrate/hydrate.js.map +5 -5
  24. package/dist/hydrate/index-iife-2q1gndgz.js +27 -0
  25. package/dist/hydrate/{index-iife-kwhe0yt8.js.map → index-iife-2q1gndgz.js.map} +3 -3
  26. package/dist/hydrate/{index-iife-ype8sch3.js → index-iife-xv69gy4m.js} +6 -6
  27. package/dist/hydrate/index-iife-xv69gy4m.js.map +12 -0
  28. package/dist/hydrate/{index-0ps8jzdn.js → index-vk8ek4jm.js} +2 -2
  29. package/dist/iife.js +69 -68
  30. package/dist/iife.js.map +9 -9
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/llms.txt +1 -1
  35. package/package.json +20 -13
  36. package/tls/create-dev-certs.sh +1 -1
  37. package/dist/hydrate/index-iife-kwhe0yt8.js +0 -27
  38. package/dist/hydrate/index-iife-ype8sch3.js.map +0 -12
  39. /package/dist/hydrate/{ajs-language-qm5w38ae.js.map → ajs-language-xvpytqfm.js.map} +0 -0
  40. /package/dist/hydrate/{index-0ps8jzdn.js.map → index-vk8ek4jm.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,137 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.9.0-beta.1
4
+
5
+ > **Prerelease.** The preview-host system works end to end and is in daily use, but the
6
+ > plan it belongs to (`REMOTE-ACCESS-PLAN.md`) has two more phases. Published under the
7
+ > `beta` dist-tag — **install with `tosijs-ui@beta`**; `latest` stays on 1.8.0.
8
+ >
9
+ > It's a **minor**, not a patch, despite being unfinished: it adds public API
10
+ > (`SiteConfig.preview`), emits a new artifact into every adopter's output
11
+ > (`/version.json`), and changes what happens when a build fails. Semver tracks the
12
+ > shape of a change, not how done it feels.
13
+
14
+ - **New: preview hosting.** `bun run deploy` rsyncs the built site to a box you control,
15
+ so a phone, a client, or a reviewer can see it without your dev server — or your
16
+ laptop — being up. The whole artifact is a few MB, so this is a copy, not a pipeline.
17
+ - **`preview: { host, path?, url? }`** in the site config. Only `host` is required;
18
+ `path` defaults to `/srv/preview/<name>`. Flag > env > config, so a one-off push
19
+ elsewhere doesn't need a config edit.
20
+ - **Dry run by default.** It runs `rsync --delete` (the remote must mirror the build,
21
+ stale pages must not linger), so the destructive form has to be typed: `--go`. It
22
+ also refuses any target that isn't an absolute path at least two levels deep.
23
+ - **Projects self-register.** Each deploy writes its own Caddy fragment; the server
24
+ glob-imports them. Adding a project touches no shared config and needs no DNS
25
+ change. The deploy **validates before reloading** and refuses to reload on invalid
26
+ config, so one bad fragment can't take down every project on the box.
27
+ - `deploy/Caddyfile` and `deploy/build-index.sh` are committed — server config in git
28
+ beats server config in someone's shell history.
29
+ - **New: `/version.json`** in every build — `{ generator, site, commit, commitTime }`.
30
+ Nothing exposed which commit produced a given site before. It matters wherever a
31
+ deploy is a snapshot: a reviewer can otherwise report a bug you fixed this morning
32
+ with no way to tell from the page which of you is stale.
33
+ **Deliberately deterministic** — no build timestamp, no dirty flag — so a committed
34
+ output dir doesn't churn on every build. Git fields are omitted (never blank) when
35
+ git isn't available.
36
+ - **Fix: a failed rebuild no longer destroys the site.** `buildSite()` opens with
37
+ `rm -rf <outputDir>`, so a build failing after that left nothing to serve — save a
38
+ typo, and the next refresh was a dead site. The dev server now moves the working
39
+ build aside first and restores it on failure, **and says so on the page**: the
40
+ existing floating widget (which already means "the far end has something to tell
41
+ you") shows the failure until a good build clears it, with detail in the console.
42
+ Note it survives the "tests disabled" preference — otherwise the one person who
43
+ turned test indicators off is the one who never learns their build is broken.
44
+ - **New: `DEV_NO_WATCH=1`** serves the built site without watching, for automated
45
+ suites that want a server rather than a rebuilder.
46
+
47
+ ## 1.8.0
48
+
49
+ > **Read this first if you use `tosijs-ui/site`.** This release adds a **dependency-audit
50
+ > gate that is ON BY DEFAULT and can fail your build** — on advisories in _your_ dependency
51
+ > tree, not ours. That is deliberate, and there are three ways out (below). Minor, not patch,
52
+ > precisely because `bun run build` can now exit 1 where it previously didn't.
53
+
54
+ - **New: dependency-audit gate** (`src/doc-system/site/audit-guard.ts`, exported from
55
+ `tosijs-ui/site`). `bun audit` knows the registry advisory database; nothing in a normal
56
+ build ever asked it, so a high-severity advisory in a transitive dep stayed invisible until
57
+ someone ran it by hand. Now it's asked at the point a human is looking.
58
+ - **Runs synchronously in every mode** — inside `buildSite` for `bun run build` / `--test`,
59
+ and in `devServer` **just before it binds the port** (it throws, so the server never comes
60
+ up). Sub-second in practice; a gate you wait for can't be raced. **Watch rebuilds never
61
+ audit** — that would put a network call in your edit loop and break offline dev.
62
+ - **What blocks:** any advisory at or above `level` (default `high`) that isn't gated.
63
+ Consumer-facing and developer-facing advisories block **alike** — a dev-only dependency
64
+ still runs on your machine, and the escape hatch makes over-blocking cheap.
65
+ - **Three ways out:** `audit: { allow: [{ advisory, reason, expires }] }` to accept a risk
66
+ **on a deadline** (an expired or malformed gate stops suppressing, so it comes back for
67
+ review instead of living in an allowlist nobody re-reads); `audit: { mode: 'warn' }` to
68
+ report without failing; `audit: false` or `TOSIJS_AUDIT=off` to disable.
69
+ - **Fails open, never closed, when it can't check** — offline, registry down, `bun` too old,
70
+ or past its 20s timeout: it warns and proceeds. It fails **closed** only on a real finding.
71
+ - **Not downgraded in CI.** Unlike the machine-health preflight (a heuristic about someone's
72
+ local box), an advisory is deterministic and environment-independent.
73
+ - **Findings are grouped, sorted worst-first, and annotated** with the _nature_ of the risk
74
+ (`LEAK/ALTER` / `DoS-only` / `DoS?+ESCALATABLE` / `UNCLASSIFIED`), parsed from the CVSS
75
+ vector (3.x and 4.0) plus CWEs. Annotation only — it never changes what blocks, and it
76
+ fails **closed**: ~20% of real advisories carry no CVSS vector at all, and those skew
77
+ severe. Sub-threshold advisories are listed compactly (previously collected and never
78
+ shown), plus an **advisories-per-package tally** — a dependency that keeps producing them
79
+ is a code smell worth replacing rather than patching.
80
+ - **Fix (issue #24, reported by a consumer): nav `order` now sorts numerically.** Section nav
81
+ and the generated `<!-- toc -->` built a zero-padded *string* key and compared it lexically,
82
+ so a fractional order silently sorted to the **end** of its section — `order: 1.5` landed
83
+ after `2`, `3`, … because `"01.5" > "0002"` as text. "Insert a page at N.5" reads like a
84
+ supported idiom and did the opposite with no warning. The same padding capped `order` at 4
85
+ digits (10000+ overflowed the width) and mis-sorted negatives. `pinnedSort` is now a
86
+ multi-key comparator (pin bucket → numeric order → title → filename), which fixes all three.
87
+ **Removed `navSortKey`** (the string-key builder) — it was never in a public index, but was
88
+ reachable via the `./*` subpath, so: technically breaking. A string key cannot express this
89
+ ordering, so there is no corrected version of it.
90
+ - **`src/doc-system/nav-tree.ts` was a binary file.** The old sort key joined title and
91
+ filename with a raw NUL byte, which makes macOS `grep` silently match **nothing** in that
92
+ file (exit 1, no message) — so searching it for any symbol came back empty and the sort bug
93
+ above was effectively unfindable. The comparator needs no delimiter; the NUL is gone.
94
+ - **Adopting the gate — budget for one upgrade, not one pin.** From the first real
95
+ adoption (`tosijs`, which went from 12 blocking advisories to zero): if the gate flags
96
+ **`brace-expansion`**, note that `GHSA-mh99-v99m-4gvg` marks *every* version below
97
+ **5.0.8** affected, so the override is mandatory — and `brace-expansion@5` breaks
98
+ **eslint 8**'s bundled `minimatch@3` with `expand is not a function`. In practice that
99
+ means **an eslint 8 → 10 migration** (flat config + typescript-eslint 8), not a one-line
100
+ pin. The upside is that it clears the whole `minimatch`/`js-yaml`/`flatted` cluster at
101
+ once — which is exactly what the advisories-per-package tally is telling you when it
102
+ fingers a stale toolchain. Expect "replace, don't patch" to be the right answer more
103
+ often than a targeted override.
104
+ - **New: `openBrowser`** site-config option — opens the doc site on dev-server start, reusing
105
+ an existing tab on macOS rather than piling up new ones.
106
+ - **New: `DEV_NO_WATCH=1`** serves the built site without watching. The E2E lane used to run
107
+ `bun start`, so a file watcher stayed live for the whole suite and could `rm -rf docs/` and
108
+ regenerate underneath a test mid-navigation — the lane flaked against its own build system,
109
+ which presents as a product bug.
110
+ - **New public API on `tosijs-ui/site`:** `auditDependencies`, `reportAudit`,
111
+ `resolveAuditMode`, `classifyRisk`, `groupAdvisories`, `openBrowser`, plus their types.
112
+ `buildSite` takes an optional second argument (`{ skipAudit }`).
113
+ - **Fix: `bun run test-browser` no longer reports success on a failed build.** It computed the
114
+ build result and launched the test run regardless, so a failing build (or a blocking
115
+ advisory) still went green. One-shot modes now exit 1.
116
+ - **Fix: the E2E lane can register ServiceWorkers over the dev cert.** `ignoreHTTPSErrors` is a
117
+ browser-_context_ option and never covered a ServiceWorker's own script fetch, so the
118
+ import-resolver worker failed to register and `hydration.pw.ts` caught it as a console error.
119
+ Chromium now launches with `--ignore-certificate-errors`.
120
+ - **Dependency pins:** `overrides` for `flatted` (`>=3.4.2`) and `brace-expansion` (`>=5.0.8`)
121
+ clear live high-severity advisories. Added `.github/dependabot.yml` so advisories published
122
+ later against an unchanged lockfile still surface.
123
+ - Includes the unpublished 1.7.5 (header-logo sizing) — npm goes 1.7.4 → 1.8.0.
124
+
125
+ ## 1.7.5
126
+
127
+ - **Header logo size/spacing are now CSS-tunable** (follow-up to 1.7.4). The brand mark no
128
+ longer hard-wires a pixel height inline; it carries a `.logo-mark` class driven by two
129
+ variables — `--tosi-logo-mark-size` (default **32px**, was a fixed 40px) and
130
+ `--tosi-logo-mark-gap` (default 10px) — for icon, `<img>`, and inline-`<svg>` logos alike.
131
+ (Icon `<svg>`s take their height from `--tosi-icon-size`, set inline by `makeIcon()`, so the
132
+ class points that knob at the same variable rather than fighting the inline height.) Retune a
133
+ logo from CSS instead of editing the build.
134
+
3
135
  ## 1.7.4
4
136
 
5
137
  - **Configurable header logo.** A new `logo` option lets a doc site set the brand mark shown
@@ -67,7 +67,9 @@ export interface DocBrowserOptions {
67
67
  * - an image URL / path / data: URI (rendered as an `<img>`)
68
68
  * - a raw inline `<svg …>…</svg>` string
69
69
  * Omit to fall back to the tosijs-ui logo when `projectLinks.tosijs` is set,
70
- * or to no mark at all otherwise.
70
+ * or to no mark at all otherwise. Size and spacing are CSS-tunable via
71
+ * `--tosi-logo-mark-size` (default 32px) and `--tosi-logo-mark-gap` (default
72
+ * 10px) on the `.logo-mark` element.
71
73
  */
72
74
  logo?: string;
73
75
  navSize?: number;
@@ -191,6 +191,10 @@ const testIndicatorStyleSpec = {
191
191
  'body:not(.tests-enabled) .doc-link::after, body:not(.tests-enabled) .test-widget': {
192
192
  display: 'none !important',
193
193
  },
194
+ // ...EXCEPT a far-end status (a failed build). That is not a test result, so the
195
+ // "show test indicators" preference must not suppress it — otherwise the one
196
+ // person who turned tests off is the one who never learns their build is broken.
197
+ 'body .test-widget.-dev-status': { display: 'flex !important' },
194
198
  // Nav link dot indicators
195
199
  '.doc-link.-test-passed::after, .doc-link.-test-failed::after': {
196
200
  content: "''",
@@ -257,9 +261,9 @@ const testIndicatorStyleSpec = {
257
261
  // The brand mark shown left of the site title. `logo` (from config) may name a
258
262
  // known icon, be an inline `<svg>` string, or be an image URL; when absent we
259
263
  // keep the legacy behavior — the tosijs-ui logo iff this is a tosijs project,
260
- // else nothing. Sized to match the header height so hydration doesn't reflow.
261
- const LOGO_SIZE = 40;
262
- const LOGO_GAP = 10;
264
+ // else nothing. Size and spacing are class-driven (`.logo-mark`, see
265
+ // logoMarkStyleSpec) so a consumer retunes them with one CSS variable each,
266
+ // rather than a hard-wired pixel value.
263
267
  // `icons` is a Proxy that returns a factory for ANY string (unknown names get a
264
268
  // placeholder glyph), so we can't ask it whether a name is real — we route by the
265
269
  // shape of the string instead: an inline `<svg>` renders as-is, a URL/path/data:
@@ -274,33 +278,44 @@ function looksLikeImageUrl(s) {
274
278
  /\.(png|jpe?g|gif|webp|avif|svg|ico)(\?.*)?$/i.test(s));
275
279
  }
276
280
  function logoMark(logo, projectLinks) {
281
+ let mark = null;
277
282
  if (logo) {
278
283
  if (logo.trimStart().startsWith('<')) {
279
- const holder = span({
280
- style: {
281
- display: 'inline-flex',
282
- height: `${LOGO_SIZE}px`,
283
- marginRight: `${LOGO_GAP}px`,
284
- },
285
- });
286
- holder.innerHTML = logo;
287
- return holder;
284
+ // Inline <svg>: wrap in a flex holder so `.logo-mark`'s height sizes it.
285
+ mark = span({ style: { display: 'inline-flex', alignItems: 'center' } });
286
+ mark.innerHTML = logo;
288
287
  }
289
- if (looksLikeImageUrl(logo)) {
290
- return img({
291
- src: logo,
292
- alt: '',
293
- style: { height: `${LOGO_SIZE}px`, marginRight: `${LOGO_GAP}px` },
294
- });
288
+ else if (looksLikeImageUrl(logo)) {
289
+ mark = img({ src: logo, alt: '' });
295
290
  }
296
- return icons[logo]({
297
- style: { _xinIconSize: LOGO_SIZE, marginRight: LOGO_GAP },
298
- });
291
+ else {
292
+ mark = icons[logo]();
293
+ }
294
+ }
295
+ else if (projectLinks.tosijs) {
296
+ mark = icons.tosiUi();
299
297
  }
300
- return projectLinks.tosijs
301
- ? icons.tosiUi({ style: { _xinIconSize: LOGO_SIZE, marginRight: LOGO_GAP } })
302
- : span();
298
+ // No mark → an empty span with NO class, so it carries no margin (the title
299
+ // stays flush; a flex `gap` on the header link couldn't do that).
300
+ if (!mark)
301
+ return span();
302
+ mark.classList.add('logo-mark');
303
+ return mark;
303
304
  }
305
+ const logoMarkStyleSpec = {
306
+ '.logo-mark': {
307
+ height: 'var(--tosi-logo-mark-size, 32px)',
308
+ // An icon <svg> takes its height from --tosi-icon-size, set INLINE on the svg
309
+ // by makeIcon() — which beats a class `height` rule. Point that knob at the
310
+ // same variable so one --tosi-logo-mark-size drives an icon, an <img>, or an
311
+ // inline <svg> alike.
312
+ _tosiIconSize: 'var(--tosi-logo-mark-size, 32px)',
313
+ marginRight: 'var(--tosi-logo-mark-gap, 10px)',
314
+ flexShrink: '0',
315
+ },
316
+ // Inline-<svg> logos are wrapped in a flex holder; scale the embedded svg to it.
317
+ '.logo-mark > svg': { height: '100%', width: 'auto' },
318
+ };
304
319
  // Monotonic per-page counter so each createDocBrowser() call gets a distinct
305
320
  // tosi() registry key (see stateKey below) and two browsers can't share state.
306
321
  let docBrowserSeq = 0;
@@ -1190,6 +1205,8 @@ export function createDocBrowser(options) {
1190
1205
  showDoc(currentDoc);
1191
1206
  // Inject test indicator styles
1192
1207
  StyleSheet('test-indicators', testIndicatorStyleSpec);
1208
+ // Header brand-mark sizing (icon / <img> / inline-<svg>), one CSS var each.
1209
+ StyleSheet('doc-logo-mark', logoMarkStyleSpec);
1193
1210
  // Floating widget for test status
1194
1211
  const testWidget = button({
1195
1212
  class: 'test-widget',
@@ -1197,8 +1214,48 @@ export function createDocBrowser(options) {
1197
1214
  onClick: showTestMenu,
1198
1215
  }, span({ part: 'label' }, 'Tests'), span({ class: 'count', part: 'count' }, '0'));
1199
1216
  container.appendChild(testWidget);
1217
+ /*
1218
+ The far end has something to tell you.
1219
+
1220
+ The dev server injects `window.__tosiDevStatus` at serve time when a rebuild has
1221
+ FAILED — see statusSnippet() in site/dev-server.ts. That is the same kind of news
1222
+ as a failing browser test (something broke; here is what), so it uses the same
1223
+ floating widget rather than a second overlay competing for the same corner.
1224
+
1225
+ Two differences from a test run, both deliberate:
1226
+ - it does NOT auto-fade. A passing test run is over; a broken build is a standing
1227
+ condition you have to act on, and it stays until a good build clears it.
1228
+ - it wins ties. If tests also failed, the build failure is the more useful thing
1229
+ to read first — the tests probably failed BECAUSE the build did.
1230
+
1231
+ Nothing is injected when the server is healthy, so this is inert in production and
1232
+ in a normal session.
1233
+ */
1234
+ const devStatus = globalThis.__tosiDevStatus;
1235
+ let devStatusShown = false;
1236
+ if (devStatus && devStatus.ok === false) {
1237
+ devStatusShown = true;
1238
+ const labelEl = testWidget.querySelector('[part="label"]');
1239
+ const countEl = testWidget.querySelector('[part="count"]');
1240
+ if (labelEl)
1241
+ labelEl.textContent = devStatus.label || 'Build failed';
1242
+ if (countEl)
1243
+ countEl.hidden = true;
1244
+ testWidget.classList.add('-failed', '-dev-status');
1245
+ testWidget.hidden = false;
1246
+ testWidget.title = devStatus.detail || '';
1247
+ // The page you are reading is the LAST GOOD build — the dev server keeps serving
1248
+ // it rather than the wreckage of the failed one — so say so plainly in the
1249
+ // console, where the detail is readable and copyable.
1250
+ console.error(`[tosi] ${devStatus.label || 'Build failed'} — showing the last good build.\n` +
1251
+ (devStatus.detail || ''));
1252
+ }
1200
1253
  let testsRunning = false;
1201
1254
  function setTestWidgetRunning() {
1255
+ // A broken build outranks a test run: the tests very likely failed BECAUSE the
1256
+ // build did, so replacing "Build failed" with "Tests" would hide the cause.
1257
+ if (devStatusShown)
1258
+ return;
1202
1259
  testsRunning = true;
1203
1260
  testWidget.hidden = false;
1204
1261
  testWidget.classList.remove('-passed', '-failed');
@@ -14,12 +14,31 @@ export interface NavNode<T extends NavDoc = NavDoc> {
14
14
  depth: number;
15
15
  }
16
16
  /**
17
- * Total-order sort key: pin bucket (top<none<bottom), then `order` (default
18
- * 500), then title, then filename. The filename tiebreak guarantees a stable
19
- * total order even for equal titles — required so the generated TOC blocks are
20
- * idempotent (no build-to-build churn).
17
+ * Total order: pin bucket (top<none<bottom), then `order` (NUMERIC, default 500),
18
+ * then title, then filename. The filename tiebreak guarantees a stable total order
19
+ * even for equal titles — required so the generated TOC blocks are idempotent (no
20
+ * build-to-build churn).
21
+ *
22
+ * This is a multi-key COMPARATOR, not a concatenated string key, and that is the
23
+ * whole point (issue #24, reported by a consumer). The old version built
24
+ * `bucket + String(order).padStart(4,'0') + title + filename` and compared it
25
+ * lexically, so `order` sorted as TEXT:
26
+ *
27
+ * order 1 -> "0001"
28
+ * order 2 -> "0002"
29
+ * order 1.5 -> String(1.5) = "1.5" -> padStart(4,'0') = "01.5"
30
+ *
31
+ * and lexically "01.5" > "0002" (at index 1, '1' > '0'), so a doc at 1.5 sorted
32
+ * AFTER 2, 3, ... — last in its section. "Insert a page at N.5" reads like a
33
+ * supported idiom and silently did the opposite, with no warning. The same padding
34
+ * also capped `order` at 4 digits (10000+ overflowed the width) and mis-sorted
35
+ * negatives. Comparing numbers as numbers fixes all three.
36
+ *
37
+ * The old key also joined title and filename with a raw NUL byte, which made this
38
+ * file BINARY: macOS grep then silently matches nothing in it (exit 1, no message),
39
+ * so searching this file for any symbol came back empty and the padStart bug was
40
+ * effectively unfindable. A comparator needs no delimiter, so the NUL is gone.
21
41
  */
22
- export declare function navSortKey(doc: NavDoc): string;
23
42
  export declare function pinnedSort(a: NavDoc, b: NavDoc): number;
24
43
  /**
25
44
  * Build the forest of top-level nav nodes. Children are grouped under their
Binary file
@@ -0,0 +1,124 @@
1
+ export type AuditSeverity = 'info' | 'low' | 'moderate' | 'high' | 'critical';
2
+ export type AuditMode = 'fail' | 'warn' | 'off';
3
+ /** One advisory as `bun audit --json` reports it, flattened with its package. */
4
+ export interface AuditAdvisory {
5
+ package: string;
6
+ id: number;
7
+ url: string;
8
+ title: string;
9
+ severity: AuditSeverity;
10
+ vulnerableVersions?: string;
11
+ /** GHSA id parsed from `url`, e.g. 'GHSA-25h7-pfq9-p65f' (if present) */
12
+ ghsa?: string;
13
+ /** what KIND of harm this is — annotation only, never changes whether it blocks */
14
+ risk?: Classification;
15
+ }
16
+ export type RiskNature =
17
+ /** C or I impact — can leak or alter data / execute code */
18
+ 'compromise'
19
+ /** availability-only — resource exhaustion, hang, crash */
20
+ | 'dos'
21
+ /** no or unparseable vector, or an escalatable CWE — assume the worst */
22
+ | 'unknown';
23
+ export interface Classification {
24
+ nature: RiskNature;
25
+ /** short label for the report line */
26
+ label: string;
27
+ /** what drove the call, so the reader can second-guess it */
28
+ basis: string;
29
+ }
30
+ /** Classify one advisory's nature from its CVSS vector + CWEs. Fails CLOSED. */
31
+ export declare function classifyRisk(raw: {
32
+ cvss?: {
33
+ vectorString?: string;
34
+ } | null;
35
+ cwe?: string[] | null;
36
+ }): Classification;
37
+ /**
38
+ * A time-boxed exception. A gate suppresses a matching advisory ONLY while it is
39
+ * valid and unexpired — after `expires` it stops suppressing and the build fails
40
+ * again, which is the point: an accepted risk gets re-evaluated on a deadline
41
+ * rather than living forever in an allowlist nobody re-reads.
42
+ */
43
+ export interface AuditGate {
44
+ /** GHSA id, the numeric advisory id, or a package name (package match is broad) */
45
+ advisory: string;
46
+ /** why this is temporarily allowed — required, non-empty */
47
+ reason: string;
48
+ /** YYYY-MM-DD; on/after this date the gate no longer suppresses — required */
49
+ expires: string;
50
+ }
51
+ export interface AuditConfig {
52
+ /** 'fail' (default) blocks the build; 'warn' reports and proceeds; 'off' skips it */
53
+ mode?: AuditMode;
54
+ /** minimum severity that blocks, default 'high' */
55
+ level?: AuditSeverity;
56
+ /** time-boxed exceptions */
57
+ allow?: AuditGate[];
58
+ }
59
+ export interface GatedFinding {
60
+ advisory: AuditAdvisory;
61
+ reason: string;
62
+ /** whole days until the gate expires (active gate) */
63
+ daysLeft?: number;
64
+ /** whole days since the gate expired (expired gate) */
65
+ daysAgo?: number;
66
+ }
67
+ export interface AuditResult {
68
+ /** did the audit actually run and parse? false => fail-open, treated as pass */
69
+ ran: boolean;
70
+ /** no blocking findings (or couldn't run) */
71
+ ok: boolean;
72
+ mode: AuditMode;
73
+ level: AuditSeverity;
74
+ /** at/above threshold and NOT suppressed — these fail the build */
75
+ blocking: AuditAdvisory[];
76
+ /** suppressed by a valid, unexpired gate */
77
+ gated: GatedFinding[];
78
+ /** matched a gate whose expiry has passed — NOT suppressed (also in `blocking`) */
79
+ expired: GatedFinding[];
80
+ /** gates that are structurally invalid (missing reason/expires) — ignored */
81
+ invalid: Array<{
82
+ gate: AuditGate;
83
+ problem: string;
84
+ }>;
85
+ /** valid gates that matched no current advisory — safe to delete */
86
+ stale: AuditGate[];
87
+ /** findings below the blocking threshold (reported, never blocking) */
88
+ belowThreshold: AuditAdvisory[];
89
+ }
90
+ /** Injectable subprocess seam for tests. */
91
+ export type AuditRunner = () => Promise<string>;
92
+ export declare const AUDIT_TIMEOUT_MS = 20000;
93
+ /**
94
+ * Parse `bun audit --json` output — `{ "<pkg>": [advisory, …] }` — into a flat
95
+ * advisory list. Returns null when the text is not a JSON object (offline, an
96
+ * error dump, an incompatible bun): the caller reads null as "couldn't check".
97
+ */
98
+ export declare function parseAuditJson(text: string): AuditAdvisory[] | null;
99
+ /** Resolve the effective mode from config + the TOSIJS_AUDIT env override. */
100
+ export declare function resolveAuditMode(config: boolean | AuditConfig | undefined): AuditMode;
101
+ /** Test-only: forget the per-process audit memo. */
102
+ export declare function resetAuditMemo(): void;
103
+ /**
104
+ * Run `bun audit`, classify findings against the configured threshold and the
105
+ * time-boxed allowlist, and return a structured verdict. Never exits.
106
+ *
107
+ * Memoized per process (see above) unless `opts.runAudit` is injected.
108
+ */
109
+ export declare function auditDependencies(config: boolean | AuditConfig | undefined, opts?: {
110
+ now?: Date;
111
+ runAudit?: AuditRunner;
112
+ }): Promise<AuditResult>;
113
+ export interface GroupedAdvisory {
114
+ advisory: AuditAdvisory;
115
+ /** every vulnerable range this advisory matched for this package */
116
+ ranges: string[];
117
+ }
118
+ export declare function groupAdvisories(advisories: AuditAdvisory[]): GroupedAdvisory[];
119
+ /**
120
+ * Print the verdict. Blocking findings first (why the build is failing), then
121
+ * active gates, expired/invalid gates, stale gates, and — when blocking — the
122
+ * due-diligence footer. Returns nothing; the caller decides what to do with `ok`.
123
+ */
124
+ export declare function reportAudit(result: AuditResult, label?: string): void;