ssrwire 0.1.0 → 0.2.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/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ All notable changes to SSRWire are documented here. The project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.2.0] - 2026-08-24
9
+
10
+ ### Added
11
+
12
+ - Bounded `repeat` configuration and `--repeat` CLI sampling for 1–10 requests
13
+ per target-agent pair.
14
+ - Per-agent minimum, median, nearest-rank p95, maximum, and spread summaries for
15
+ response-header, first-byte, required-signal, and completion timings.
16
+ - Response and streamed-HTML stability findings across repeated samples.
17
+ - Sample attribution in terminal and JSON reports.
18
+
19
+ ### Changed
20
+
21
+ - Added an npm package badge to the README.
22
+ - Samples for one target-agent pair run sequentially while independent pairs
23
+ retain bounded concurrency.
24
+ - Repeated policy findings are coalesced with occurrence and sample evidence.
25
+ - Exact body-fingerprint variation alone is informational; timing variation
26
+ alone remains evidence rather than a policy failure.
27
+
8
28
  ## [0.1.0] - 2026-08-22
9
29
 
10
30
  ### Added
@@ -16,5 +36,6 @@ All notable changes to SSRWire are documented here. The project follows
16
36
  - Terminal, JSON, and SARIF reports with CI-safe exit codes.
17
37
  - YAML configuration, one-off URL checks, Docker support, and GitHub Actions examples.
18
38
 
19
- [Unreleased]: https://github.com/lame13/ssrwire/compare/v0.1.0...HEAD
39
+ [Unreleased]: https://github.com/lame13/ssrwire/compare/v0.2.0...HEAD
40
+ [0.2.0]: https://github.com/lame13/ssrwire/compare/v0.1.0...v0.2.0
20
41
  [0.1.0]: https://github.com/lame13/ssrwire/releases/tag/v0.1.0
package/CONTRIBUTING.md CHANGED
@@ -29,6 +29,7 @@ Keep each change narrow. Include tests for behavior changes, especially for:
29
29
  - redirects, timeouts, aborted bodies, and size limits;
30
30
  - malformed, duplicated, late, or body-located metadata;
31
31
  - user-agent differences and comparison findings;
32
+ - repeated-sample ordering, aggregation, and instability classification;
32
33
  - redaction of configured header values;
33
34
  - terminal, JSON, SARIF, and exit-code behavior.
34
35
 
@@ -39,11 +40,12 @@ positions instead.
39
40
 
40
41
  ## Scope
41
42
 
42
- SSRWire is an HTTP stream observer, not a browser, crawler, packet capture,
43
- performance benchmark, or framework plugin. New features should preserve that
44
- boundary. In particular, avoid conclusions that claim to reveal the server's
45
- original flush calls: proxies, compression, TLS, HTTP stacks, and client
46
- buffering can coalesce data before SSRWire sees it.
43
+ SSRWire is an HTTP stream observer and bounded consistency sampler, not a
44
+ browser, crawler, packet capture, load generator, performance benchmark, or
45
+ framework plugin. New features should preserve that boundary. In particular,
46
+ avoid conclusions that claim to reveal the server's original flush calls:
47
+ proxies, compression, TLS, HTTP stacks, and client buffering can coalesce data
48
+ before SSRWire sees it.
47
49
 
48
50
  By contributing, you agree that your contribution is licensed under the MIT
49
51
  License.
package/PUBLISHING.md CHANGED
@@ -1,109 +1,124 @@
1
- # Publish SSRWire from a local machine
1
+ # Publish SSRWire 0.2.0 from a local machine
2
2
 
3
- This repository intentionally includes no npm publishing workflow. Every npm
4
- release is published from a local interactive terminal after GitHub CI passes.
5
- Do not configure an `NPM_TOKEN`, trusted publisher, OIDC identity, or automated
6
- release workflow for npm publication.
3
+ This repository intentionally includes no npm publishing workflow. Publish from
4
+ a foreground local terminal only after the GitHub `CI` workflow passes. Do not
5
+ configure an `NPM_TOKEN`, trusted publisher, OIDC identity, or automated npm
6
+ release job.
7
7
 
8
- ## 1. Prepare and verify
8
+ The `main` branch is protected. Land every release change through a pull
9
+ request, and do not use an administrator bypass or a direct push to `main`.
9
10
 
10
- From the extracted archive:
11
+ ## 1. Update the existing repository
12
+
13
+ Work from a clean clone of the existing public repository. Copy the 0.2.0
14
+ source files into that clone while preserving its `.git` directory.
11
15
 
12
16
  ```bash
13
- unzip ssrwire.zip # skip when already inside a source checkout
14
17
  cd ssrwire
18
+ git switch main
19
+ git pull --ff-only origin main
20
+ git status --short
21
+ git switch -c release/0.2.0
22
+ ```
23
+
24
+ `git status --short` must be empty before creating the release branch and
25
+ applying the release files.
26
+
27
+ ## 2. Verify the release locally
28
+
29
+ ```bash
15
30
  nvm use 24
16
31
  node --version
17
32
  npm --version
18
33
  npm ci
19
34
  npm run check
20
35
  npm pack --dry-run
36
+ node dist/bin.js --version
21
37
  ```
22
38
 
23
- Install and authenticate GitHub CLI if needed:
24
-
25
- ```bash
26
- brew install gh
27
- gh auth login -h github.com --web
28
- gh auth status -h github.com
29
- ```
39
+ The final command must print `0.2.0`. Inspect the dry-run file list. It must not
40
+ contain `.env`, `.github`, `node_modules`, `test`, ZIP files, or tarballs.
30
41
 
31
- ## 2. Create the Git history
42
+ Review the release diff and version references:
32
43
 
33
44
  ```bash
34
- git init -b main
35
- git add -- \
36
- .dockerignore .editorconfig .env.example .github .gitignore \
37
- CHANGELOG.md CONTRIBUTING.md Dockerfile LICENSE PUBLISHING.md README.md SECURITY.md \
38
- biome.json examples package.json package-lock.json scripts src test \
39
- tsconfig.build.json tsconfig.json vitest.config.ts
40
- git commit -m "Initial SSRWire release"
45
+ git diff --check
46
+ git diff --stat
47
+ git diff -- package.json package-lock.json CHANGELOG.md README.md PUBLISHING.md
48
+ rg '0\.1\.0' README.md examples package.json package-lock.json src test
41
49
  ```
42
50
 
43
- ## 3. Create and push the public repository
51
+ The final search should return nothing. Historical entries in `CHANGELOG.md`
52
+ are excluded intentionally.
53
+
54
+ ## 3. Commit, push the release branch, and open a pull request
44
55
 
45
56
  ```bash
46
- gh repo create lame13/ssrwire \
47
- --public \
48
- --source=. \
49
- --remote=origin \
50
- --push \
51
- --description "Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from the command line." \
52
- --homepage "https://nikom.work"
57
+ gh auth status -h github.com || gh auth login -h github.com --web
58
+ git add --all
59
+ git diff --cached --check
60
+ git diff --cached --stat
61
+ git commit -m "feat: release SSRWire 0.2.0"
62
+ git push --set-upstream origin release/0.2.0
63
+ gh pr create --base main --head release/0.2.0 --fill
64
+ PR_NUMBER="$(gh pr view release/0.2.0 --json number --jq .number)"
65
+ test -n "$PR_NUMBER"
66
+ gh pr checks "$PR_NUMBER" --watch --fail-fast
53
67
  ```
54
68
 
55
- ## 4. Add repository topics
69
+ Do not merge while any Node, package-smoke, or Docker job is failing. Satisfy
70
+ all review and branch-protection requirements, then merge with GitHub's allowed
71
+ strategy. Running `gh pr merge "$PR_NUMBER"` without a strategy flag lets the
72
+ CLI prompt for one of the repository's permitted methods. Do not select an
73
+ administrator bypass.
56
74
 
57
75
  ```bash
58
- gh repo edit lame13/ssrwire \
59
- --add-topic technical-seo \
60
- --add-topic seo \
61
- --add-topic ssr \
62
- --add-topic streaming-html \
63
- --add-topic nextjs \
64
- --add-topic nuxt \
65
- --add-topic astro \
66
- --add-topic crawler \
67
- --add-topic typescript \
68
- --add-topic cli \
69
- --add-topic seo-tools \
70
- --add-topic github-actions \
71
- --add-topic sarif
76
+ gh pr merge "$PR_NUMBER"
72
77
  ```
73
78
 
74
- Enable the private vulnerability-reporting channel referenced by
75
- `SECURITY.md` (this requires repository admin permission):
79
+ After GitHub reports the pull request as merged, update local `main` and wait
80
+ for the CI run on the exact merged commit:
76
81
 
77
82
  ```bash
78
- gh api --method PUT repos/lame13/ssrwire/private-vulnerability-reporting
83
+ git switch main
84
+ git pull --ff-only origin main
85
+ git status --short
86
+ gh pr view "$PR_NUMBER" --json state,mergedAt,mergeCommit
87
+ COMMIT_SHA="$(git rev-parse HEAD)"
88
+ RUN_ID="$(gh run list --workflow CI --branch main --commit "$COMMIT_SHA" \
89
+ --limit 1 --json databaseId --jq '.[0].databaseId')"
90
+ test -n "$RUN_ID"
91
+ gh run watch "$RUN_ID" --exit-status
79
92
  ```
80
93
 
81
- Alternatively, enable **Private vulnerability reporting** in the repository's
82
- Settings → Security settings before publishing the first release.
94
+ `git status --short` must be empty. Do not publish until the merged-commit CI
95
+ run succeeds.
83
96
 
84
- Recommended repository description:
97
+ ## 4. Verify npm state
85
98
 
86
- > Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from the command line.
99
+ ```bash
100
+ npm view ssrwire version dist-tags homepage keywords repository.url --json
101
+ npm config get registry
102
+ npm config get provenance
103
+ ```
87
104
 
88
- Wait for the repository's `CI` workflow to pass before publishing.
105
+ The published version and `latest` tag must still be `0.1.0`. If npm already
106
+ reports `0.2.0`, stop: never reuse a version that npm accepted.
89
107
 
90
- ## 5. Publish version 0.1.0 to npm locally
108
+ In the npm package settings, select **Require two-factor authentication and
109
+ disallow tokens**. npm documents this as the strongest package publishing
110
+ setting:
91
111
 
92
- Verify the release from a clean checkout before starting npm's short-lived
93
- authenticated session:
112
+ - <https://docs.npmjs.com/requiring-2fa-for-package-publishing-and-settings-modification/>
113
+ - <https://docs.npmjs.com/about-two-factor-authentication/>
94
114
 
95
- ```bash
96
- npm ci
97
- npm run check
98
- npm pack --dry-run
99
- npm view ssrwire
100
- ```
115
+ ## 5. Publish interactively
101
116
 
102
- For the first release, `npm view` returning a 404 means the name is not
103
- currently published. Start authentication only after verification so the
104
- session remains fresh for publication:
117
+ Remove inherited automation credentials before starting the interactive
118
+ session:
105
119
 
106
120
  ```bash
121
+ unset NODE_AUTH_TOKEN NPM_TOKEN NPM_CONFIG_OTP npm_config_otp
107
122
  npm login --auth-type=web --registry=https://registry.npmjs.org
108
123
  npm whoami --registry=https://registry.npmjs.org
109
124
  npm publish --access public --registry=https://registry.npmjs.org
@@ -111,24 +126,23 @@ npm view ssrwire version dist-tags homepage keywords repository.url --json
111
126
  npm logout --registry=https://registry.npmjs.org
112
127
  ```
113
128
 
114
- Run login and publish in a foreground interactive terminal. Complete npm's
115
- browser, passkey, or two-factor-authentication challenge when prompted. Never
116
- put an OTP in a command argument, and do not add an `NPM_TOKEN` to this
117
- repository.
129
+ Complete npm's browser, passkey, or two-factor-authentication challenge when
130
+ prompted. Do not use a token with bypass 2FA, put an OTP in a command argument,
131
+ or add an npm credential to the repository or GitHub Actions. The package-level
132
+ "disallow tokens" setting ensures that publication remains interactive.
118
133
 
119
- If the package is not immediately visible after `npm publish` succeeds, wait
120
- for npm's publish-time scanning to finish instead of publishing the same
121
- version again.
134
+ If npm's publish-time scanning delays package visibility, wait. Do not publish
135
+ `0.2.0` again or change the tag to work around propagation.
122
136
 
123
- After npm confirms `0.1.0`, create the matching source release:
137
+ ## 6. Tag the exact published commit
124
138
 
125
139
  ```bash
126
140
  node scripts/clean.mjs
127
141
  rm -rf node_modules/.vite
128
142
  git status --short
129
- git tag -a v0.1.0 -m "SSRWire v0.1.0"
130
- git push origin v0.1.0
131
- gh release create v0.1.0 --generate-notes --title "SSRWire v0.1.0"
143
+ git tag -a v0.2.0 -m "SSRWire v0.2.0"
144
+ git push origin v0.2.0
145
+ gh release create v0.2.0 --generate-notes --title "SSRWire v0.2.0"
132
146
  ```
133
147
 
134
- `git status --short` must print nothing before tagging the release.
148
+ `git status --short` must print nothing before tagging.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # SSRWire
2
2
 
3
3
  [![CI](https://github.com/lame13/ssrwire/actions/workflows/ci.yml/badge.svg)](https://github.com/lame13/ssrwire/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/ssrwire.svg)](https://www.npmjs.com/package/ssrwire)
4
5
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
6
 
6
7
  Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from
@@ -25,6 +26,7 @@ Modern SSR output is not always one complete HTML document delivered at once:
25
26
  crawler while blocking for an HTML-limited bot;
26
27
  - browser, search-crawler, and social-crawler user agents may receive different
27
28
  titles, canonicals, robots directives, redirects, or statuses;
29
+ - the same URL and user agent may receive inconsistent SSR output between requests;
28
30
  - an interrupted or oversized stream may never deliver the expected elements;
29
31
  - a working hydrated page can hide thin or incomplete source HTML.
30
32
 
@@ -84,7 +86,7 @@ form of `npx ssrwire check URL`.
84
86
 
85
87
  ## What it observes
86
88
 
87
- For each target and agent, SSRWire captures:
89
+ For each target, agent, and configured sample, SSRWire captures:
88
90
 
89
91
  - response status, final URL, redirect chain, and an allowlisted response-header snapshot;
90
92
  - time to response headers, first response-body bytes, and completed body;
@@ -109,6 +111,9 @@ It then checks:
109
111
  | JSON-LD block/count exceeds the bounded analysis budget | Warning |
110
112
  | Critical metadata in `<body>` for a profile that requires head metadata | Error |
111
113
  | Status, final URL, title, canonical, or robots drift between profiles | Warning |
114
+ | Completion, status, final URL, or redirect-chain drift between samples | Warning |
115
+ | Metadata value or document-location drift between complete samples | Warning |
116
+ | Exact body fingerprint drift without metadata drift | Information |
112
117
  | First byte or required-signal arrival over a configured limit | Warning |
113
118
  | Timeout, truncation, network error, or another incomplete probe | Error / incomplete run |
114
119
 
@@ -198,6 +203,7 @@ headers:
198
203
  timeoutMs: 15000
199
204
  maxBytes: 10485760
200
205
  maxRedirects: 10
206
+ repeat: 1
201
207
  ```
202
208
 
203
209
  A target can also be a plain URL string when defaults are sufficient:
@@ -215,7 +221,8 @@ Defaults:
215
221
  - agents: `browser`, `googlebot`, `bingbot`, and `twitterbot`;
216
222
  - timeout: 15 seconds per probe;
217
223
  - response limit: 10 MiB;
218
- - redirect limit: 10.
224
+ - redirect limit: 10;
225
+ - samples per target and agent: 1, with an allowed range of 1–10.
219
226
 
220
227
  Unknown configuration keys are rejected. URLs must be absolute HTTP or HTTPS
221
228
  URLs and cannot contain embedded credentials.
@@ -276,6 +283,38 @@ transfer-size or packet-boundary evidence. They count bytes delivered by the
276
283
  Fetch implementation to SSRWire; those bytes are post-content-decoding when a
277
284
  server ignores SSRWire's `Accept-Encoding: identity` request.
278
285
 
286
+ ## Repeated sampling
287
+
288
+ Use repeated sampling when one successful request does not prove that SSR output
289
+ is stable:
290
+
291
+ ```bash
292
+ npx ssrwire check --repeat 3
293
+ ```
294
+
295
+ `repeat` is the total number of samples, not a retry count. SSRWire retains
296
+ failures instead of replacing them with a later success. Samples for one
297
+ target-agent pair run sequentially; different target-agent pairs may still run
298
+ concurrently. SSRWire does not add delays, cache-busting parameters, or special
299
+ cache headers.
300
+
301
+ The request count is `targets × agents × repeat`, plus redirect hops. Configured
302
+ same-origin headers are sent for every sample and retain the existing
303
+ cross-origin stripping and report-redaction behavior.
304
+
305
+ For repeated runs, terminal reports include individual sample numbers and a
306
+ per-agent timing table. JSON retains every probe and adds per-agent stability
307
+ summaries. The summaries report sample count, minimum, median, nearest-rank p95,
308
+ maximum, and spread for available header, first-byte, required-signal, and
309
+ completion timings. With small sample counts, nearest-rank p95 will often equal
310
+ the maximum.
311
+
312
+ Timing spread alone is evidence, not a failure. Network and cache variation can
313
+ change timings without changing the response contract. SSRWire warns when HTTP
314
+ response evidence or streamed metadata changes across samples. Exact body-hash
315
+ variation by itself is informational because timestamps, nonces, and other
316
+ legitimate dynamic values commonly change source HTML.
317
+
279
318
  ## CLI reference
280
319
 
281
320
  ```text
@@ -294,6 +333,7 @@ Check options:
294
333
  | `--timeout <ms>` | Override request timeout |
295
334
  | `--max-bytes <bytes>` | Override response-body limit |
296
335
  | `--max-redirects <count>` | Override redirect limit |
336
+ | `--repeat <count>` | Run 1–10 sequential samples per URL and agent |
297
337
  | `-f, --format <format>` | `terminal`, `json`, or `sarif` |
298
338
  | `-o, --output <path>` | Write the report to a file |
299
339
  | `--fail-on <level>` | `error`, `warning`, or `never` |
@@ -304,9 +344,9 @@ removed.
304
344
 
305
345
  ## Reports and exit codes
306
346
 
307
- - `terminal`: compact tables and findings for local use.
347
+ - `terminal`: compact sample, aggregate-timing, and finding tables for local use.
308
348
  - `json`: structured machine-readable evidence, including every probe and timing
309
- signal.
349
+ signal plus repeated-run stability summaries.
310
350
  - `sarif`: findings suitable for GitHub Code Scanning and other SARIF 2.1.0
311
351
  consumers.
312
352
 
@@ -369,7 +409,7 @@ by the CLI:
369
409
  ```ts
370
410
  import { loadConfig, renderJson, runAudit } from "ssrwire";
371
411
 
372
- const config = await loadConfig({ urls: ["https://example.com/"] });
412
+ const config = await loadConfig({ urls: ["https://example.com/"], repeat: 3 });
373
413
  const audit = await runAudit(config);
374
414
  process.stdout.write(renderJson(audit));
375
415
  ```
@@ -381,14 +421,16 @@ versions.
381
421
  ## Scope
382
422
 
383
423
  SSRWire does not execute JavaScript, inspect a hydrated DOM, measure Core Web
384
- Vitals, discover URLs, validate indexing, bypass access controls, or emulate a
385
- crawler's rendering pipeline. Use [RoutePlay](https://github.com/lame13/routeplay)
386
- when the question is server HTML versus a cold browser versus real client-side
387
- navigation.
424
+ Vitals, discover URLs, validate indexing, bypass access controls, perform load
425
+ testing, or emulate a crawler's rendering pipeline. Use
426
+ [RoutePlay](https://github.com/lame13/routeplay) for server HTML versus a cold
427
+ browser versus real client-side navigation. Use
428
+ [RouteLint](https://github.com/lame13/routelint) for route discovery,
429
+ indexability, and technical SEO policy.
388
430
 
389
431
  Run SSRWire only against targets you are authorized to inspect. Keep target
390
- lists intentionally small; one run starts one probe per target/profile pair,
391
- with additional requests for redirect hops.
432
+ lists and repeat counts intentionally small. It is a consistency sampler, not a
433
+ load generator.
392
434
 
393
435
  ## Development
394
436
 
package/SECURITY.md CHANGED
@@ -20,7 +20,11 @@ report. You should receive an initial response within seven days.
20
20
  ## Operational safety
21
21
 
22
22
  SSRWire sends real HTTP requests to every configured target for every selected
23
- agent profile. Run it only against systems you are authorized to test. Treat
24
- custom headers as secrets, prefer environment-variable interpolation, and do
25
- not commit populated `.env` files or generated reports containing private
26
- URLs.
23
+ agent profile and sample. The base request count is
24
+ `targets × agents × repeat`, plus redirect hops. Samples for one target-agent
25
+ pair are sequential, but different pairs may run concurrently. Keep `repeat`
26
+ bounded and use SSRWire only against systems you are authorized to test.
27
+
28
+ Treat custom headers as secrets and remember that they are sent on every
29
+ same-origin sample. Prefer environment-variable interpolation, and do not
30
+ commit populated `.env` files or generated reports containing private URLs.
@@ -1 +1 @@
1
- {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAgB,aAAa,EAAqB,MAAM,YAAY,CAAC;AAoC9F,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAiD1E"}
1
+ {"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,WAAW,EAIX,aAAa,EAEd,MAAM,YAAY,CAAC;AAgHpB,wBAAsB,QAAQ,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAyE1E"}
package/dist/audit.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { analyzeTarget, summarizeAudit } from "./analyze.js";
2
2
  import { probeUrl } from "./http-probe.js";
3
3
  import { redactAudit } from "./redact.js";
4
+ import { analyzeStability } from "./stability.js";
4
5
  import { VERSION } from "./version.js";
5
6
  const DEFAULT_CONCURRENCY = 4;
6
7
  async function runPool(inputs, limit, worker) {
@@ -21,8 +22,63 @@ async function runPool(inputs, limit, worker) {
21
22
  await Promise.all(workers);
22
23
  return results;
23
24
  }
25
+ function repeatCount(value) {
26
+ const repeat = value ?? 1;
27
+ if (!Number.isInteger(repeat) || repeat < 1 || repeat > 10) {
28
+ throw new RangeError("repeat must be an integer between 1 and 10.");
29
+ }
30
+ return repeat;
31
+ }
32
+ function mergeEvidence(sampled, repeat) {
33
+ const merged = {};
34
+ const keys = [
35
+ ...new Set(sampled.flatMap(({ finding }) => Object.keys(finding.evidence ?? {}))),
36
+ ].sort();
37
+ for (const key of keys) {
38
+ const values = sampled.flatMap(({ finding }) => {
39
+ const value = finding.evidence?.[key];
40
+ return value === undefined ? [] : [value];
41
+ });
42
+ const unique = [
43
+ ...new Map(values.map((value) => [`${typeof value}:${String(value)}`, value])).values(),
44
+ ];
45
+ const first = unique[0];
46
+ if (first !== undefined) {
47
+ merged[key] = unique.length === 1 ? first : unique.map(String).join(" | ");
48
+ }
49
+ }
50
+ merged.sampleNumbers = [...new Set(sampled.map((item) => item.sample))]
51
+ .sort((a, b) => a - b)
52
+ .join(", ");
53
+ merged.occurrences = sampled.length;
54
+ merged.totalSamples = repeat;
55
+ return merged;
56
+ }
57
+ function coalesceFindings(sampled, repeat) {
58
+ const groups = new Map();
59
+ for (const item of sampled) {
60
+ const { finding } = item;
61
+ const key = JSON.stringify([
62
+ finding.code,
63
+ finding.severity,
64
+ finding.message,
65
+ finding.url,
66
+ finding.agent ?? "",
67
+ ]);
68
+ const group = groups.get(key) ?? [];
69
+ group.push(item);
70
+ groups.set(key, group);
71
+ }
72
+ return [...groups.values()].map((group) => {
73
+ const first = group[0]?.finding;
74
+ if (first === undefined)
75
+ throw new Error("Cannot coalesce an empty finding group.");
76
+ return { ...first, evidence: mergeEvidence(group, repeat) };
77
+ });
78
+ }
24
79
  export async function runAudit(config) {
25
80
  const started = performance.now();
81
+ const repeat = repeatCount(config.repeat);
26
82
  const tasks = [];
27
83
  for (const [targetIndex, target] of config.targets.entries()) {
28
84
  for (const [agentIndex, agent] of config.agents.entries()) {
@@ -42,25 +98,44 @@ export async function runAudit(config) {
42
98
  }
43
99
  }
44
100
  const secrets = Object.values(config.headers);
45
- const probes = await runPool(tasks, DEFAULT_CONCURRENCY, async (task) => {
46
- const probe = await probeUrl(task.options);
47
- return { task, probe };
101
+ const lanes = await runPool(tasks, DEFAULT_CONCURRENCY, async (task) => {
102
+ const probes = [];
103
+ for (let sample = 1; sample <= repeat; sample += 1) {
104
+ const probe = await probeUrl(task.options);
105
+ probes.push(repeat === 1 ? probe : { ...probe, sample });
106
+ }
107
+ return { task, probes };
48
108
  });
49
109
  const results = config.targets.map((target, targetIndex) => {
50
- const targetProbes = probes
51
- .filter((item) => item.task.targetIndex === targetIndex)
110
+ const targetProbes = lanes
111
+ .filter((lane) => lane.task.targetIndex === targetIndex)
52
112
  .sort((a, b) => a.task.agentIndex - b.task.agentIndex)
53
- .map((item) => item.probe);
113
+ .flatMap((lane) => lane.probes);
114
+ if (repeat === 1) {
115
+ return {
116
+ target,
117
+ probes: targetProbes,
118
+ findings: analyzeTarget(target, targetProbes),
119
+ };
120
+ }
121
+ const sampledFindings = [];
122
+ for (let sample = 1; sample <= repeat; sample += 1) {
123
+ const sampleProbes = targetProbes.filter((probe) => probe.sample === sample);
124
+ sampledFindings.push(...analyzeTarget(target, sampleProbes).map((finding) => ({ sample, finding })));
125
+ }
126
+ const stability = analyzeStability(target, targetProbes);
54
127
  return {
55
128
  target,
56
129
  probes: targetProbes,
57
- findings: analyzeTarget(target, targetProbes),
130
+ findings: [...coalesceFindings(sampledFindings, repeat), ...stability.findings],
131
+ stability: stability.stability,
58
132
  };
59
133
  });
60
134
  const audit = {
61
135
  version: VERSION,
62
136
  generatedAt: new Date().toISOString(),
63
137
  durationMs: Math.round(performance.now() - started),
138
+ ...(repeat === 1 ? {} : { repeat }),
64
139
  results,
65
140
  summary: summarizeAudit(results),
66
141
  };
package/dist/audit.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../src/audit.ts"],"names":[],"mappings":"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"}
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAkKA,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ChF"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAsKA,wBAAsB,IAAI,CAAC,IAAI,GAAE,SAAS,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA4ChF"}
package/dist/cli.js CHANGED
@@ -25,6 +25,7 @@ agents:
25
25
  timeoutMs: 15000
26
26
  maxBytes: 10485760
27
27
  maxRedirects: 10
28
+ repeat: 1
28
29
 
29
30
  # Keep preview credentials in environment variables. SSRWire redacts configured values from reports.
30
31
  # headers:
@@ -60,6 +61,7 @@ function addCheckOptions(command) {
60
61
  .option("--timeout <ms>", "request timeout in milliseconds", parseInteger)
61
62
  .option("--max-bytes <bytes>", "maximum response bytes", parseInteger)
62
63
  .option("--max-redirects <count>", "maximum redirects", parseInteger)
64
+ .option("--repeat <count>", "sequential samples per URL and agent", parseInteger)
63
65
  .option("-f, --format <format>", "terminal, json, or sarif", parseFormat, "terminal")
64
66
  .option("-o, --output <path>", "write the report to a file")
65
67
  .option("--fail-on <level>", "error, warning, or never", parseFailOn, "error")
@@ -97,6 +99,7 @@ async function check(urls, options) {
97
99
  ...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
98
100
  ...(options.maxBytes === undefined ? {} : { maxBytes: options.maxBytes }),
99
101
  ...(options.maxRedirects === undefined ? {} : { maxRedirects: options.maxRedirects }),
102
+ ...(options.repeat === undefined ? {} : { repeat: options.repeat }),
100
103
  });
101
104
  const audit = await runAudit(config);
102
105
  const color = options.color &&