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.
- package/CHANGELOG.md +132 -0
- package/dist/doc-browser.d.ts +3 -1
- package/dist/doc-browser.js +81 -24
- package/dist/doc-system/nav-tree.d.ts +24 -5
- package/dist/doc-system/nav-tree.js +0 -0
- package/dist/doc-system/site/audit-guard.d.ts +124 -0
- package/dist/doc-system/site/audit-guard.js +555 -0
- package/dist/doc-system/site/build-stamp.d.ts +23 -0
- package/dist/doc-system/site/build-stamp.js +65 -0
- package/dist/doc-system/site/dev-server.d.ts +18 -0
- package/dist/doc-system/site/dev-server.js +205 -12
- package/dist/doc-system/site/index.d.ts +4 -0
- package/dist/doc-system/site/index.js +2 -0
- package/dist/doc-system/site/open-browser.d.ts +53 -0
- package/dist/doc-system/site/open-browser.js +231 -0
- package/dist/doc-system/site/orchestrator.d.ts +3 -1
- package/dist/doc-system/site/orchestrator.js +19 -1
- package/dist/doc-system/site/site-config.d.ts +66 -0
- package/dist/hydrate/{ajs-language-qm5w38ae.js → ajs-language-xvpytqfm.js} +2 -2
- package/dist/hydrate/{code-editor-cm-95n31m4f.js → code-editor-cm-59je4d8g.js} +6 -6
- package/dist/hydrate/{code-editor-cm-95n31m4f.js.map → code-editor-cm-59je4d8g.js.map} +4 -4
- package/dist/hydrate/hydrate.js +56 -55
- package/dist/hydrate/hydrate.js.map +5 -5
- package/dist/hydrate/index-iife-2q1gndgz.js +27 -0
- package/dist/hydrate/{index-iife-kwhe0yt8.js.map → index-iife-2q1gndgz.js.map} +3 -3
- package/dist/hydrate/{index-iife-ype8sch3.js → index-iife-xv69gy4m.js} +6 -6
- package/dist/hydrate/index-iife-xv69gy4m.js.map +12 -0
- package/dist/hydrate/{index-0ps8jzdn.js → index-vk8ek4jm.js} +2 -2
- package/dist/iife.js +69 -68
- package/dist/iife.js.map +9 -9
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +20 -13
- package/tls/create-dev-certs.sh +1 -1
- package/dist/hydrate/index-iife-kwhe0yt8.js +0 -27
- package/dist/hydrate/index-iife-ype8sch3.js.map +0 -12
- /package/dist/hydrate/{ajs-language-qm5w38ae.js.map → ajs-language-xvpytqfm.js.map} +0 -0
- /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
|
package/dist/doc-browser.d.ts
CHANGED
|
@@ -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;
|
package/dist/doc-browser.js
CHANGED
|
@@ -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.
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
291
|
+
else {
|
|
292
|
+
mark = icons[logo]();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (projectLinks.tosijs) {
|
|
296
|
+
mark = icons.tosiUi();
|
|
299
297
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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;
|