ssrwire 0.1.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 +20 -0
- package/CONTRIBUTING.md +49 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +134 -0
- package/README.md +403 -0
- package/SECURITY.md +26 -0
- package/dist/agents.d.ts +14 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +100 -0
- package/dist/agents.js.map +1 -0
- package/dist/analyze.d.ts +4 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +494 -0
- package/dist/analyze.js.map +1 -0
- package/dist/audit.d.ts +3 -0
- package/dist/audit.d.ts.map +1 -0
- package/dist/audit.js +69 -0
- package/dist/audit.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +4 -0
- package/dist/bin.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +173 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +262 -0
- package/dist/config.js.map +1 -0
- package/dist/http-probe.d.ts +7 -0
- package/dist/http-probe.d.ts.map +1 -0
- package/dist/http-probe.js +406 -0
- package/dist/http-probe.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/redact.d.ts +10 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +154 -0
- package/dist/redact.js.map +1 -0
- package/dist/reporters.d.ts +9 -0
- package/dist/reporters.d.ts.map +1 -0
- package/dist/reporters.js +220 -0
- package/dist/reporters.js.map +1 -0
- package/dist/stream-parser.d.ts +9 -0
- package/dist/stream-parser.d.ts.map +1 -0
- package/dist/stream-parser.js +366 -0
- package/dist/stream-parser.js.map +1 -0
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +14 -0
- package/dist/version.js.map +1 -0
- package/examples/github-actions.yml +73 -0
- package/examples/ssrwire.config.yml +37 -0
- package/package.json +91 -0
- package/src/agents.ts +129 -0
- package/src/analyze.ts +628 -0
- package/src/audit.ts +89 -0
- package/src/bin.ts +5 -0
- package/src/cli.ts +207 -0
- package/src/config.ts +313 -0
- package/src/http-probe.ts +461 -0
- package/src/index.ts +34 -0
- package/src/redact.ts +173 -0
- package/src/reporters.ts +274 -0
- package/src/stream-parser.ts +424 -0
- package/src/types.ts +160 -0
- package/src/version.ts +19 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to SSRWire are documented here. The project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [Unreleased]
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Stream-observation probes for browser and crawler user-agent profiles.
|
|
13
|
+
- Timing and byte-position evidence for SEO metadata, H1, main text, and JSON-LD.
|
|
14
|
+
- Redirect, status, response-header, truncation, timeout, and network-failure evidence.
|
|
15
|
+
- Cross-agent delivery comparisons with configurable expectations.
|
|
16
|
+
- Terminal, JSON, and SARIF reports with CI-safe exit codes.
|
|
17
|
+
- YAML configuration, one-off URL checks, Docker support, and GitHub Actions examples.
|
|
18
|
+
|
|
19
|
+
[Unreleased]: https://github.com/lame13/ssrwire/compare/v0.1.0...HEAD
|
|
20
|
+
[0.1.0]: https://github.com/lame13/ssrwire/releases/tag/v0.1.0
|
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributing to SSRWire
|
|
2
|
+
|
|
3
|
+
Bug reports and focused pull requests are welcome. SSRWire stays useful by
|
|
4
|
+
remaining small, deterministic, and honest about what an HTTP client can
|
|
5
|
+
observe.
|
|
6
|
+
|
|
7
|
+
## Development setup
|
|
8
|
+
|
|
9
|
+
Requirements:
|
|
10
|
+
|
|
11
|
+
- Node.js 22.12.0 or newer
|
|
12
|
+
- npm 10 or newer
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone https://github.com/lame13/ssrwire.git
|
|
16
|
+
cd ssrwire
|
|
17
|
+
npm ci
|
|
18
|
+
npm run check
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Use `npm run dev -- --help` while developing the CLI. Run `npm run format`
|
|
22
|
+
before opening a pull request, then run `npm run check` again.
|
|
23
|
+
|
|
24
|
+
## Pull requests
|
|
25
|
+
|
|
26
|
+
Keep each change narrow. Include tests for behavior changes, especially for:
|
|
27
|
+
|
|
28
|
+
- response chunk boundaries and elements split across chunks;
|
|
29
|
+
- redirects, timeouts, aborted bodies, and size limits;
|
|
30
|
+
- malformed, duplicated, late, or body-located metadata;
|
|
31
|
+
- user-agent differences and comparison findings;
|
|
32
|
+
- redaction of configured header values;
|
|
33
|
+
- terminal, JSON, SARIF, and exit-code behavior.
|
|
34
|
+
|
|
35
|
+
Do not make timing tests depend on exact millisecond values. Shared runners and
|
|
36
|
+
local machines have unavoidable scheduling variance. Test ordering,
|
|
37
|
+
presence/absence, bounds with generous margins, and deterministic byte
|
|
38
|
+
positions instead.
|
|
39
|
+
|
|
40
|
+
## Scope
|
|
41
|
+
|
|
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.
|
|
47
|
+
|
|
48
|
+
By contributing, you agree that your contribution is licensed under the MIT
|
|
49
|
+
License.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Niko M.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Publish SSRWire from a local machine
|
|
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.
|
|
7
|
+
|
|
8
|
+
## 1. Prepare and verify
|
|
9
|
+
|
|
10
|
+
From the extracted archive:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
unzip ssrwire.zip # skip when already inside a source checkout
|
|
14
|
+
cd ssrwire
|
|
15
|
+
nvm use 24
|
|
16
|
+
node --version
|
|
17
|
+
npm --version
|
|
18
|
+
npm ci
|
|
19
|
+
npm run check
|
|
20
|
+
npm pack --dry-run
|
|
21
|
+
```
|
|
22
|
+
|
|
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
|
+
```
|
|
30
|
+
|
|
31
|
+
## 2. Create the Git history
|
|
32
|
+
|
|
33
|
+
```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"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 3. Create and push the public repository
|
|
44
|
+
|
|
45
|
+
```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"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 4. Add repository topics
|
|
56
|
+
|
|
57
|
+
```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
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Enable the private vulnerability-reporting channel referenced by
|
|
75
|
+
`SECURITY.md` (this requires repository admin permission):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
gh api --method PUT repos/lame13/ssrwire/private-vulnerability-reporting
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Alternatively, enable **Private vulnerability reporting** in the repository's
|
|
82
|
+
Settings → Security settings before publishing the first release.
|
|
83
|
+
|
|
84
|
+
Recommended repository description:
|
|
85
|
+
|
|
86
|
+
> Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from the command line.
|
|
87
|
+
|
|
88
|
+
Wait for the repository's `CI` workflow to pass before publishing.
|
|
89
|
+
|
|
90
|
+
## 5. Publish version 0.1.0 to npm locally
|
|
91
|
+
|
|
92
|
+
Verify the release from a clean checkout before starting npm's short-lived
|
|
93
|
+
authenticated session:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
npm ci
|
|
97
|
+
npm run check
|
|
98
|
+
npm pack --dry-run
|
|
99
|
+
npm view ssrwire
|
|
100
|
+
```
|
|
101
|
+
|
|
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:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm login --auth-type=web --registry=https://registry.npmjs.org
|
|
108
|
+
npm whoami --registry=https://registry.npmjs.org
|
|
109
|
+
npm publish --access public --registry=https://registry.npmjs.org
|
|
110
|
+
npm view ssrwire version dist-tags homepage keywords repository.url --json
|
|
111
|
+
npm logout --registry=https://registry.npmjs.org
|
|
112
|
+
```
|
|
113
|
+
|
|
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.
|
|
118
|
+
|
|
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.
|
|
122
|
+
|
|
123
|
+
After npm confirms `0.1.0`, create the matching source release:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
node scripts/clean.mjs
|
|
127
|
+
rm -rf node_modules/.vite
|
|
128
|
+
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"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`git status --short` must print nothing before tagging the release.
|
package/README.md
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# SSRWire
|
|
2
|
+
|
|
3
|
+
[](https://github.com/lame13/ssrwire/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from
|
|
7
|
+
the command line.
|
|
8
|
+
|
|
9
|
+
SSRWire makes a real HTTP request for each selected user-agent profile, reads
|
|
10
|
+
the response incrementally, and records when important SEO signals become
|
|
11
|
+
observable to its parser. It reports their elapsed time, observed byte
|
|
12
|
+
position, and document location without launching a browser or executing
|
|
13
|
+
JavaScript.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npx ssrwire https://example.com/product
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Why this exists
|
|
20
|
+
|
|
21
|
+
Modern SSR output is not always one complete HTML document delivered at once:
|
|
22
|
+
|
|
23
|
+
- metadata may arrive later than the first meaningful content;
|
|
24
|
+
- a framework may intentionally stream metadata into `<body>` for a capable
|
|
25
|
+
crawler while blocking for an HTML-limited bot;
|
|
26
|
+
- browser, search-crawler, and social-crawler user agents may receive different
|
|
27
|
+
titles, canonicals, robots directives, redirects, or statuses;
|
|
28
|
+
- an interrupted or oversized stream may never deliver the expected elements;
|
|
29
|
+
- a working hydrated page can hide thin or incomplete source HTML.
|
|
30
|
+
|
|
31
|
+
SSRWire turns those behaviors into small, repeatable HTTP-level checks. It is
|
|
32
|
+
not a browser, a JavaScript renderer, a packet capture, or a replacement for a
|
|
33
|
+
site crawler.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Node.js 22.12.0 or newer
|
|
38
|
+
- No browser installation
|
|
39
|
+
|
|
40
|
+
Run it without installing:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npx ssrwire https://example.com/
|
|
44
|
+
npx ssrwire https://example.com/ https://example.com/pricing/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or add it to a project:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install --save-dev ssrwire
|
|
51
|
+
npx ssrwire init
|
|
52
|
+
npx ssrwire check
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Create `ssrwire.config.yml`:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx ssrwire init
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Then run all configured targets:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
npx ssrwire check
|
|
67
|
+
npx ssrwire check --format json --output reports/ssrwire.json
|
|
68
|
+
npx ssrwire check --format sarif --output reports/ssrwire.sarif
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
One-off checks need no config:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx ssrwire check \
|
|
75
|
+
https://example.com/ \
|
|
76
|
+
https://example.com/pricing/ \
|
|
77
|
+
--agent browser \
|
|
78
|
+
--agent googlebot \
|
|
79
|
+
--fail-on warning
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The root command and `check` are equivalent, so `npx ssrwire URL` is the short
|
|
83
|
+
form of `npx ssrwire check URL`.
|
|
84
|
+
|
|
85
|
+
## What it observes
|
|
86
|
+
|
|
87
|
+
For each target and agent, SSRWire captures:
|
|
88
|
+
|
|
89
|
+
- response status, final URL, redirect chain, and an allowlisted response-header snapshot;
|
|
90
|
+
- time to response headers, first response-body bytes, and completed body;
|
|
91
|
+
- total bytes delivered to the stream parser and a body fingerprint;
|
|
92
|
+
- title, meta description, canonical, meta robots, H1, first main-content
|
|
93
|
+
text, and JSON-LD blocks;
|
|
94
|
+
- elapsed arrival time, observed byte position, and `head`/`body` location for
|
|
95
|
+
each signal;
|
|
96
|
+
- clean completion, timeout, network failure, invalid response, or configured
|
|
97
|
+
byte-limit termination.
|
|
98
|
+
|
|
99
|
+
It then checks:
|
|
100
|
+
|
|
101
|
+
| Contract | Default finding |
|
|
102
|
+
|---|---|
|
|
103
|
+
| Unexpected status or configured final URL | Error |
|
|
104
|
+
| Missing or explicit non-HTML `Content-Type` | Error / incomplete run |
|
|
105
|
+
| Missing title | Error |
|
|
106
|
+
| Missing description, canonical, H1, or main text | Warning |
|
|
107
|
+
| Duplicate or conflicting title, description, canonical, or robots values | Warning |
|
|
108
|
+
| Invalid JSON-LD | Warning |
|
|
109
|
+
| JSON-LD block/count exceeds the bounded analysis budget | Warning |
|
|
110
|
+
| Critical metadata in `<body>` for a profile that requires head metadata | Error |
|
|
111
|
+
| Status, final URL, title, canonical, or robots drift between profiles | Warning |
|
|
112
|
+
| First byte or required-signal arrival over a configured limit | Warning |
|
|
113
|
+
| Timeout, truncation, network error, or another incomplete probe | Error / incomplete run |
|
|
114
|
+
|
|
115
|
+
Body-located metadata is not inherently an error. SSRWire only fails it for an
|
|
116
|
+
agent whose profile declares `requiresHeadMetadata: true`. This matters for
|
|
117
|
+
frameworks such as Next.js that can deliberately stream metadata differently
|
|
118
|
+
for JavaScript-capable and HTML-limited bots.
|
|
119
|
+
|
|
120
|
+
## Agent profiles
|
|
121
|
+
|
|
122
|
+
The default run uses:
|
|
123
|
+
|
|
124
|
+
| Key | Intended view | Requires metadata in `<head>` |
|
|
125
|
+
|---|---|---:|
|
|
126
|
+
| `browser` | Normal browser user agent | No |
|
|
127
|
+
| `googlebot` | Googlebot user agent | No |
|
|
128
|
+
| `bingbot` | Bingbot user agent | Yes |
|
|
129
|
+
| `twitterbot` | X/Twitter link-preview user agent | Yes |
|
|
130
|
+
|
|
131
|
+
`facebook` is also built in and can be selected explicitly. A repeated CLI
|
|
132
|
+
`--agent` list replaces the configured/default list for that run:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx ssrwire https://example.com/ --agent googlebot --agent facebook
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
These profiles send user-agent strings; they do not prove how a real crawler
|
|
139
|
+
will fetch, render, index, or cache a page. SSRWire does not perform crawler IP
|
|
140
|
+
or reverse-DNS verification. `requiresHeadMetadata` is SSRWire's default audit
|
|
141
|
+
policy for a profile, not a guarantee about that crawler's current parser or
|
|
142
|
+
rendering capabilities; use a custom profile when your contract differs.
|
|
143
|
+
|
|
144
|
+
Generic `robots` directives apply to every profile. Matching `googlebot` and
|
|
145
|
+
`bingbot` directives are combined with the generic directives, with restrictive
|
|
146
|
+
rules winning. SSRWire keeps audiences separate for duplicate checks and warns
|
|
147
|
+
when a crawler-specific permissive rule cannot relax a generic restriction.
|
|
148
|
+
|
|
149
|
+
Custom profiles are supported in configuration:
|
|
150
|
+
|
|
151
|
+
```yaml
|
|
152
|
+
agents:
|
|
153
|
+
- browser
|
|
154
|
+
- key: internal-preview-bot
|
|
155
|
+
label: Internal preview bot
|
|
156
|
+
userAgent: ExamplePreviewBot/1.0
|
|
157
|
+
requiresHeadMetadata: true
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Configuration
|
|
161
|
+
|
|
162
|
+
SSRWire automatically looks for `ssrwire.config.yml`,
|
|
163
|
+
`ssrwire.config.yaml`, or `ssrwire.config.json`. An explicit `--config` path
|
|
164
|
+
takes precedence.
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
targets:
|
|
168
|
+
- url: https://example.com/
|
|
169
|
+
expectedStatus: 200
|
|
170
|
+
expectedFinalUrl: https://example.com/
|
|
171
|
+
require:
|
|
172
|
+
title: true
|
|
173
|
+
description: true
|
|
174
|
+
canonical: true
|
|
175
|
+
h1: true
|
|
176
|
+
mainText: true
|
|
177
|
+
maxFirstByteMs: 1200
|
|
178
|
+
maxCriticalMs: 2500
|
|
179
|
+
|
|
180
|
+
- url: https://example.com/not-found/
|
|
181
|
+
expectedStatus: [404]
|
|
182
|
+
require:
|
|
183
|
+
title: true
|
|
184
|
+
description: false
|
|
185
|
+
canonical: false
|
|
186
|
+
h1: true
|
|
187
|
+
mainText: true
|
|
188
|
+
|
|
189
|
+
agents:
|
|
190
|
+
- browser
|
|
191
|
+
- googlebot
|
|
192
|
+
- bingbot
|
|
193
|
+
- twitterbot
|
|
194
|
+
|
|
195
|
+
headers:
|
|
196
|
+
x-preview-token: "${PREVIEW_TOKEN}"
|
|
197
|
+
|
|
198
|
+
timeoutMs: 15000
|
|
199
|
+
maxBytes: 10485760
|
|
200
|
+
maxRedirects: 10
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A target can also be a plain URL string when defaults are sufficient:
|
|
204
|
+
|
|
205
|
+
```yaml
|
|
206
|
+
targets:
|
|
207
|
+
- https://example.com/
|
|
208
|
+
- https://example.com/pricing/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Defaults:
|
|
212
|
+
|
|
213
|
+
- expected status: `200`;
|
|
214
|
+
- title, description, canonical, H1, and main text: required;
|
|
215
|
+
- agents: `browser`, `googlebot`, `bingbot`, and `twitterbot`;
|
|
216
|
+
- timeout: 15 seconds per probe;
|
|
217
|
+
- response limit: 10 MiB;
|
|
218
|
+
- redirect limit: 10.
|
|
219
|
+
|
|
220
|
+
Unknown configuration keys are rejected. URLs must be absolute HTTP or HTTPS
|
|
221
|
+
URLs and cannot contain embedded credentials.
|
|
222
|
+
|
|
223
|
+
### Protected previews
|
|
224
|
+
|
|
225
|
+
Header values can interpolate environment variables. Export them in the
|
|
226
|
+
current shell or provide them through the CI secret store; SSRWire does not
|
|
227
|
+
load `.env` files itself.
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
export PREVIEW_TOKEN="..."
|
|
231
|
+
npx ssrwire check
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
For an ephemeral override:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
npx ssrwire https://preview.example.com/ \
|
|
238
|
+
--header "x-preview-token: $PREVIEW_TOKEN"
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
CLI headers override a configured header with the same case-insensitive name.
|
|
242
|
+
SSRWire rejects `Accept-Encoding`, `Host`, `Content-Length`, `Connection`,
|
|
243
|
+
`Transfer-Encoding`, and `User-Agent` overrides. Custom headers are sent to the
|
|
244
|
+
initial origin and same-origin redirects only; the first cross-origin redirect removes them for
|
|
245
|
+
the rest of that probe. Request-header configuration is not serialized. The
|
|
246
|
+
CLI and `runAudit()` redact known values and common URL/base64 encodings from
|
|
247
|
+
the complete probe, including parsed HTML signals, response-header snapshots,
|
|
248
|
+
redirects, and errors. A target can apply an unknown transformation before
|
|
249
|
+
reflecting a secret, so review reports from untrusted targets before sharing
|
|
250
|
+
them. Direct low-level `probeUrl()` callers should apply `redactProbe()` before
|
|
251
|
+
persisting results.
|
|
252
|
+
|
|
253
|
+
The final response must declare `text/html` or `application/xhtml+xml` as its
|
|
254
|
+
`Content-Type`; parameters such as `charset=utf-8` are allowed. SSRWire does not
|
|
255
|
+
sniff headerless, JSON, text, or binary responses for HTML-looking fragments.
|
|
256
|
+
|
|
257
|
+
To keep hostile or accidentally huge pages bounded, SSRWire retains at most
|
|
258
|
+
256 signals of each repeated metadata kind, analyzes at most 64 JSON-LD blocks,
|
|
259
|
+
and captures at most 1,048,576 characters from one JSON-LD block. Exceeding a
|
|
260
|
+
JSON-LD analysis budget produces a dedicated warning rather than being
|
|
261
|
+
mislabeled as invalid JSON. The configured response-byte limit remains the
|
|
262
|
+
outer bound.
|
|
263
|
+
|
|
264
|
+
## Timing interpretation
|
|
265
|
+
|
|
266
|
+
SSRWire reports when its own process observed bytes and parsed elements. That
|
|
267
|
+
is useful for regression testing, but it is not a record of the application's
|
|
268
|
+
original `flush()` calls or network packets.
|
|
269
|
+
|
|
270
|
+
CDNs, reverse proxies, compression, TLS, HTTP implementations, and local
|
|
271
|
+
buffering can split or coalesce data before the process receives it. Agent
|
|
272
|
+
profiles are requested separately, and network/cache variance can affect their
|
|
273
|
+
times. Use timing thresholds with margin and compare repeated CI runs from a
|
|
274
|
+
stable location. Treat byte positions as parser-observation offsets, not
|
|
275
|
+
transfer-size or packet-boundary evidence. They count bytes delivered by the
|
|
276
|
+
Fetch implementation to SSRWire; those bytes are post-content-decoding when a
|
|
277
|
+
server ignores SSRWire's `Accept-Encoding: identity` request.
|
|
278
|
+
|
|
279
|
+
## CLI reference
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
ssrwire [urls...] [options]
|
|
283
|
+
ssrwire check [urls...] [options]
|
|
284
|
+
ssrwire init [path] [--force]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Check options:
|
|
288
|
+
|
|
289
|
+
| Option | Purpose |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `-c, --config <path>` | Use a specific YAML or JSON config |
|
|
292
|
+
| `-a, --agent <name>` | Select a built-in agent; repeatable |
|
|
293
|
+
| `-H, --header "Name: value"` | Add/override a request header; repeatable |
|
|
294
|
+
| `--timeout <ms>` | Override request timeout |
|
|
295
|
+
| `--max-bytes <bytes>` | Override response-body limit |
|
|
296
|
+
| `--max-redirects <count>` | Override redirect limit |
|
|
297
|
+
| `-f, --format <format>` | `terminal`, `json`, or `sarif` |
|
|
298
|
+
| `-o, --output <path>` | Write the report to a file |
|
|
299
|
+
| `--fail-on <level>` | `error`, `warning`, or `never` |
|
|
300
|
+
| `--no-color` | Disable terminal color |
|
|
301
|
+
|
|
302
|
+
Config-file targets and CLI URLs are combined, with exact duplicate URLs
|
|
303
|
+
removed.
|
|
304
|
+
|
|
305
|
+
## Reports and exit codes
|
|
306
|
+
|
|
307
|
+
- `terminal`: compact tables and findings for local use.
|
|
308
|
+
- `json`: structured machine-readable evidence, including every probe and timing
|
|
309
|
+
signal.
|
|
310
|
+
- `sarif`: findings suitable for GitHub Code Scanning and other SARIF 2.1.0
|
|
311
|
+
consumers.
|
|
312
|
+
|
|
313
|
+
Exit codes are stable:
|
|
314
|
+
|
|
315
|
+
- `0`: the run completed and passed the selected policy;
|
|
316
|
+
- `1`: the run completed but crossed the `--fail-on` threshold;
|
|
317
|
+
- `2`: configuration/setup failure or incomplete probe evidence.
|
|
318
|
+
|
|
319
|
+
`--fail-on never` suppresses policy failures, but it never converts an
|
|
320
|
+
incomplete probe into a pass.
|
|
321
|
+
|
|
322
|
+
## GitHub Actions
|
|
323
|
+
|
|
324
|
+
Copy [examples/github-actions.yml](examples/github-actions.yml) into the site
|
|
325
|
+
repository and commit [examples/ssrwire.config.yml](examples/ssrwire.config.yml)
|
|
326
|
+
as `ssrwire.config.yml`. Store preview credentials as repository or environment
|
|
327
|
+
secrets. The example deliberately withholds those credentials from pull-request
|
|
328
|
+
runs because the checked-out configuration is controlled by that pull request;
|
|
329
|
+
credentialed checks run only after trusted code reaches the protected branch or
|
|
330
|
+
through a manual dispatch. Keep PR targets public and credential-free. If the
|
|
331
|
+
main audit requires `${PREVIEW_TOKEN}`, point the PR step at a separate
|
|
332
|
+
credential-free configuration or remove the PR trigger.
|
|
333
|
+
|
|
334
|
+
The workflow always keeps the SARIF file as a downloadable artifact. It also
|
|
335
|
+
uploads findings to Code Scanning for public repositories. Private/internal
|
|
336
|
+
repositories can remove the public-only condition after GitHub Code Security is
|
|
337
|
+
enabled for that repository.
|
|
338
|
+
|
|
339
|
+
This repository's own CI tests Node.js 22.12.0 and 24, runs the packaged CLI smoke
|
|
340
|
+
test on macOS and Windows, validates the npm tarball, and builds and executes
|
|
341
|
+
the Docker image. It contains no automatic npm publishing job; npm releases
|
|
342
|
+
are made manually from a local interactive terminal.
|
|
343
|
+
|
|
344
|
+
## Docker
|
|
345
|
+
|
|
346
|
+
The image is a small, browser-free Node.js runtime and runs as the non-root
|
|
347
|
+
`node` user:
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
docker build -t ssrwire .
|
|
351
|
+
docker run --rm ssrwire https://example.com/
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Run a mounted configuration:
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
docker run --rm \
|
|
358
|
+
--env PREVIEW_TOKEN \
|
|
359
|
+
--volume "$PWD/ssrwire.config.yml:/work/ssrwire.config.yml:ro" \
|
|
360
|
+
--workdir /work \
|
|
361
|
+
ssrwire check
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Programmatic API
|
|
365
|
+
|
|
366
|
+
The package exports the probe, parser, analysis, and reporter primitives used
|
|
367
|
+
by the CLI:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
import { loadConfig, renderJson, runAudit } from "ssrwire";
|
|
371
|
+
|
|
372
|
+
const config = await loadConfig({ urls: ["https://example.com/"] });
|
|
373
|
+
const audit = await runAudit(config);
|
|
374
|
+
process.stdout.write(renderJson(audit));
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
Treat the JSON report's top-level `version` as the SSRWire software version,
|
|
378
|
+
not a promise that every nested field will remain unchanged across major
|
|
379
|
+
versions.
|
|
380
|
+
|
|
381
|
+
## Scope
|
|
382
|
+
|
|
383
|
+
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.
|
|
388
|
+
|
|
389
|
+
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.
|
|
392
|
+
|
|
393
|
+
## Development
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
npm ci
|
|
397
|
+
npm run check
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md), [SECURITY.md](SECURITY.md),
|
|
401
|
+
[CHANGELOG.md](CHANGELOG.md), and [PUBLISHING.md](PUBLISHING.md).
|
|
402
|
+
|
|
403
|
+
MIT licensed. Built by [Niko M.](https://nikom.work).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the latest released version.
|
|
6
|
+
|
|
7
|
+
## Reporting a vulnerability
|
|
8
|
+
|
|
9
|
+
Please do not open a public issue for a vulnerability that could expose
|
|
10
|
+
credentials, bypass SSRWire's header protections, or cause unsafe file or
|
|
11
|
+
network behavior. Use GitHub's private vulnerability reporting for
|
|
12
|
+
`lame13/ssrwire` instead:
|
|
13
|
+
|
|
14
|
+
<https://github.com/lame13/ssrwire/security/advisories/new>
|
|
15
|
+
|
|
16
|
+
Include the affected version, reproduction steps, expected impact, and any
|
|
17
|
+
suggested mitigation. Remove real tokens, cookies, and private URLs from the
|
|
18
|
+
report. You should receive an initial response within seven days.
|
|
19
|
+
|
|
20
|
+
## Operational safety
|
|
21
|
+
|
|
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.
|
package/dist/agents.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AgentProfile } from "./types.js";
|
|
2
|
+
export declare const BUILTIN_AGENTS: Readonly<Record<string, AgentProfile>>;
|
|
3
|
+
export type BuiltinAgentKey = "browser" | "googlebot" | "bingbot" | "twitterbot" | "facebook";
|
|
4
|
+
export type AgentInput = string | {
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly label?: string;
|
|
7
|
+
readonly userAgent: string;
|
|
8
|
+
readonly requiresHeadMetadata?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Resolve a built-in key/alias or validate a complete custom agent profile. */
|
|
11
|
+
export declare function resolveAgent(input: AgentInput): AgentProfile;
|
|
12
|
+
/** Resolve an ordered list and reject duplicate keys, which would make reports ambiguous. */
|
|
13
|
+
export declare function resolveAgents(inputs: readonly AgentInput[]): AgentProfile[];
|
|
14
|
+
//# sourceMappingURL=agents.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAgChE,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,GAAG,YAAY,GAAG,UAAU,CAAC;AAC9F,MAAM,MAAM,UAAU,GAClB,MAAM,GACN;IACE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC,CAAC;AAqCN,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,YAAY,CAgC5D;AAED,6FAA6F;AAC7F,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,GAAG,YAAY,EAAE,CAW3E"}
|