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/dist/types.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export type Severity = "info" | "warning" | "error";
|
|
2
|
+
export type ElementLocation = "head" | "body" | "document";
|
|
3
|
+
export interface TimingMark {
|
|
4
|
+
readonly atMs: number;
|
|
5
|
+
readonly observedByByte: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ElementSignal extends TimingMark {
|
|
8
|
+
readonly value: string;
|
|
9
|
+
readonly location: ElementLocation;
|
|
10
|
+
}
|
|
11
|
+
export type RobotsAudience = "robots" | "googlebot" | "bingbot";
|
|
12
|
+
export interface RobotsSignal extends ElementSignal {
|
|
13
|
+
readonly audience: RobotsAudience;
|
|
14
|
+
}
|
|
15
|
+
export interface JsonLdSignal extends TimingMark {
|
|
16
|
+
readonly location: ElementLocation;
|
|
17
|
+
readonly valid?: boolean;
|
|
18
|
+
readonly types: readonly string[];
|
|
19
|
+
readonly bytes: number;
|
|
20
|
+
readonly analysisLimit?: string;
|
|
21
|
+
readonly error?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DocumentSignals {
|
|
24
|
+
readonly title?: ElementSignal;
|
|
25
|
+
readonly titles?: readonly ElementSignal[];
|
|
26
|
+
readonly descriptions: readonly ElementSignal[];
|
|
27
|
+
readonly canonicals: readonly ElementSignal[];
|
|
28
|
+
readonly robots: readonly RobotsSignal[];
|
|
29
|
+
readonly h1s: readonly ElementSignal[];
|
|
30
|
+
readonly firstMainText?: ElementSignal;
|
|
31
|
+
readonly jsonLd: readonly JsonLdSignal[];
|
|
32
|
+
readonly headClosed?: TimingMark;
|
|
33
|
+
readonly bodyStarted?: TimingMark;
|
|
34
|
+
readonly documentClosed?: TimingMark;
|
|
35
|
+
}
|
|
36
|
+
export interface AgentProfile {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly label: string;
|
|
39
|
+
readonly userAgent: string;
|
|
40
|
+
readonly requiresHeadMetadata: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface RedirectHop {
|
|
43
|
+
readonly url: string;
|
|
44
|
+
readonly status: number;
|
|
45
|
+
readonly location: string;
|
|
46
|
+
readonly durationMs: number;
|
|
47
|
+
}
|
|
48
|
+
export interface HeaderSnapshot {
|
|
49
|
+
readonly values: Readonly<Record<string, string>>;
|
|
50
|
+
readonly setCookiePresent: boolean;
|
|
51
|
+
}
|
|
52
|
+
export interface ProbeTimings {
|
|
53
|
+
readonly headersMs: number;
|
|
54
|
+
readonly firstByteMs?: number;
|
|
55
|
+
readonly completeMs?: number;
|
|
56
|
+
}
|
|
57
|
+
export type ProbeCompletion = "complete" | "max-bytes-exceeded" | "timeout" | "network-error" | "invalid-response";
|
|
58
|
+
export interface ProbeOptions {
|
|
59
|
+
readonly url: string;
|
|
60
|
+
readonly agent: AgentProfile;
|
|
61
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
62
|
+
readonly timeoutMs: number;
|
|
63
|
+
readonly maxBytes: number;
|
|
64
|
+
readonly maxRedirects: number;
|
|
65
|
+
/** Keep reflected header values internal so callers can analyze raw evidence before redaction. */
|
|
66
|
+
readonly redactHeaderValues?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface ProbeResult {
|
|
69
|
+
readonly requestedUrl: string;
|
|
70
|
+
readonly finalUrl: string;
|
|
71
|
+
readonly agent: AgentProfile;
|
|
72
|
+
readonly status?: number;
|
|
73
|
+
readonly redirects: readonly RedirectHop[];
|
|
74
|
+
readonly headers: HeaderSnapshot;
|
|
75
|
+
readonly timings: ProbeTimings;
|
|
76
|
+
readonly bytesRead: number;
|
|
77
|
+
readonly bodySha256?: string;
|
|
78
|
+
readonly signals: DocumentSignals;
|
|
79
|
+
readonly completion: ProbeCompletion;
|
|
80
|
+
readonly error?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface TargetExpectations {
|
|
83
|
+
readonly statuses: readonly number[];
|
|
84
|
+
readonly finalUrl?: string;
|
|
85
|
+
readonly requireTitle: boolean;
|
|
86
|
+
readonly requireDescription: boolean;
|
|
87
|
+
readonly requireCanonical: boolean;
|
|
88
|
+
readonly requireH1: boolean;
|
|
89
|
+
readonly requireMainText: boolean;
|
|
90
|
+
readonly maxFirstByteMs?: number;
|
|
91
|
+
readonly maxCriticalMs?: number;
|
|
92
|
+
}
|
|
93
|
+
export interface AuditTarget {
|
|
94
|
+
readonly url: string;
|
|
95
|
+
readonly expectations: TargetExpectations;
|
|
96
|
+
}
|
|
97
|
+
export interface SsrWireConfig {
|
|
98
|
+
readonly targets: readonly AuditTarget[];
|
|
99
|
+
readonly agents: readonly AgentProfile[];
|
|
100
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
101
|
+
readonly timeoutMs: number;
|
|
102
|
+
readonly maxBytes: number;
|
|
103
|
+
readonly maxRedirects: number;
|
|
104
|
+
}
|
|
105
|
+
export interface Finding {
|
|
106
|
+
readonly code: string;
|
|
107
|
+
readonly severity: Severity;
|
|
108
|
+
readonly message: string;
|
|
109
|
+
readonly url: string;
|
|
110
|
+
readonly agent?: string;
|
|
111
|
+
readonly evidence?: Readonly<Record<string, string | number | boolean>>;
|
|
112
|
+
}
|
|
113
|
+
export interface TargetAuditResult {
|
|
114
|
+
readonly target: AuditTarget;
|
|
115
|
+
readonly probes: readonly ProbeResult[];
|
|
116
|
+
readonly findings: readonly Finding[];
|
|
117
|
+
}
|
|
118
|
+
export interface AuditSummary {
|
|
119
|
+
readonly targets: number;
|
|
120
|
+
readonly probes: number;
|
|
121
|
+
readonly errors: number;
|
|
122
|
+
readonly warnings: number;
|
|
123
|
+
readonly info: number;
|
|
124
|
+
readonly incomplete: number;
|
|
125
|
+
}
|
|
126
|
+
export interface AuditResult {
|
|
127
|
+
readonly version: string;
|
|
128
|
+
readonly generatedAt: string;
|
|
129
|
+
readonly durationMs: number;
|
|
130
|
+
readonly results: readonly TargetAuditResult[];
|
|
131
|
+
readonly summary: AuditSummary;
|
|
132
|
+
}
|
|
133
|
+
export type ReportFormat = "terminal" | "json" | "sarif";
|
|
134
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAkBA,eAAO,MAAM,OAAO,QAA2B,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const packageJson = require("../package.json");
|
|
4
|
+
function readVersion(value) {
|
|
5
|
+
if (typeof value === "object" &&
|
|
6
|
+
value !== null &&
|
|
7
|
+
"version" in value &&
|
|
8
|
+
typeof value.version === "string") {
|
|
9
|
+
return value.version;
|
|
10
|
+
}
|
|
11
|
+
throw new Error("SSRWire could not read its package version.");
|
|
12
|
+
}
|
|
13
|
+
export const VERSION = readVersion(packageJson);
|
|
14
|
+
//# sourceMappingURL=version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,WAAW,GAAY,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAExD,SAAS,WAAW,CAAC,KAAc;IACjC,IACE,OAAO,KAAK,KAAK,QAAQ;QACzB,KAAK,KAAK,IAAI;QACd,SAAS,IAAI,KAAK;QAClB,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EACjC,CAAC;QACD,OAAO,KAAK,CAAC,OAAO,CAAC;IACvB,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,6CAA6C,CAAC,CAAC;AACjE,CAAC;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
name: SSRWire
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
security-events: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
audit:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
timeout-minutes: 10
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v7
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-node@v7
|
|
21
|
+
with:
|
|
22
|
+
node-version: 24
|
|
23
|
+
package-manager-cache: false
|
|
24
|
+
|
|
25
|
+
- name: Run SSRWire (pull request, no credentials)
|
|
26
|
+
id: ssrwire_pr
|
|
27
|
+
if: github.event_name == 'pull_request'
|
|
28
|
+
continue-on-error: true
|
|
29
|
+
run: >-
|
|
30
|
+
npx --yes ssrwire@0.1.0 check
|
|
31
|
+
--config ssrwire.config.yml
|
|
32
|
+
--format sarif
|
|
33
|
+
--output reports/ssrwire.sarif
|
|
34
|
+
--fail-on error
|
|
35
|
+
|
|
36
|
+
- name: Run SSRWire (trusted branch)
|
|
37
|
+
id: ssrwire_trusted
|
|
38
|
+
if: github.event_name != 'pull_request'
|
|
39
|
+
continue-on-error: true
|
|
40
|
+
run: >-
|
|
41
|
+
npx --yes ssrwire@0.1.0 check
|
|
42
|
+
--config ssrwire.config.yml
|
|
43
|
+
--format sarif
|
|
44
|
+
--output reports/ssrwire.sarif
|
|
45
|
+
--fail-on error
|
|
46
|
+
env:
|
|
47
|
+
PREVIEW_TOKEN: ${{ secrets.PREVIEW_TOKEN }}
|
|
48
|
+
|
|
49
|
+
- name: Upload SARIF
|
|
50
|
+
if: >-
|
|
51
|
+
always() &&
|
|
52
|
+
hashFiles('reports/ssrwire.sarif') != '' &&
|
|
53
|
+
github.event.repository.visibility == 'public' &&
|
|
54
|
+
(github.event_name != 'pull_request' ||
|
|
55
|
+
github.event.pull_request.head.repo.full_name == github.repository)
|
|
56
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
57
|
+
with:
|
|
58
|
+
sarif_file: reports/ssrwire.sarif
|
|
59
|
+
|
|
60
|
+
- name: Keep the report as a workflow artifact
|
|
61
|
+
if: always() && hashFiles('reports/ssrwire.sarif') != ''
|
|
62
|
+
uses: actions/upload-artifact@v7
|
|
63
|
+
with:
|
|
64
|
+
name: ssrwire-sarif
|
|
65
|
+
path: reports/ssrwire.sarif
|
|
66
|
+
retention-days: 14
|
|
67
|
+
|
|
68
|
+
- name: Enforce SSRWire result
|
|
69
|
+
if: >-
|
|
70
|
+
always() &&
|
|
71
|
+
(steps.ssrwire_pr.outcome == 'failure' ||
|
|
72
|
+
steps.ssrwire_trusted.outcome == 'failure')
|
|
73
|
+
run: exit 1
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Replace these URLs with a small set of high-value SSR routes.
|
|
2
|
+
targets:
|
|
3
|
+
- url: https://example.com/
|
|
4
|
+
expectedStatus: 200
|
|
5
|
+
expectedFinalUrl: https://example.com/
|
|
6
|
+
require:
|
|
7
|
+
title: true
|
|
8
|
+
description: true
|
|
9
|
+
canonical: true
|
|
10
|
+
h1: true
|
|
11
|
+
mainText: true
|
|
12
|
+
|
|
13
|
+
- url: https://example.com/pricing/
|
|
14
|
+
expectedStatus: 200
|
|
15
|
+
require:
|
|
16
|
+
title: true
|
|
17
|
+
description: true
|
|
18
|
+
canonical: true
|
|
19
|
+
h1: true
|
|
20
|
+
mainText: true
|
|
21
|
+
|
|
22
|
+
# Add maxFirstByteMs or maxCriticalMs per target only when the runner location
|
|
23
|
+
# and cache state are stable enough for a meaningful threshold.
|
|
24
|
+
|
|
25
|
+
agents:
|
|
26
|
+
- browser
|
|
27
|
+
- googlebot
|
|
28
|
+
- bingbot
|
|
29
|
+
- twitterbot
|
|
30
|
+
|
|
31
|
+
timeoutMs: 15000
|
|
32
|
+
maxBytes: 10485760
|
|
33
|
+
maxRedirects: 10
|
|
34
|
+
|
|
35
|
+
# Keep preview credentials in a CI secret or exported environment variable.
|
|
36
|
+
# headers:
|
|
37
|
+
# x-preview-token: "${PREVIEW_TOKEN}"
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ssrwire",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inspect streamed SSR HTML, metadata timing, and crawler-specific delivery from the command line.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ssrwire": "./dist/bin.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"examples",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"SECURITY.md",
|
|
24
|
+
"CHANGELOG.md",
|
|
25
|
+
"CONTRIBUTING.md",
|
|
26
|
+
"PUBLISHING.md"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "node scripts/clean.mjs && tsc -p tsconfig.build.json",
|
|
30
|
+
"check": "npm run lint && npm run typecheck && npm run test && npm run build && npm run package:check",
|
|
31
|
+
"dev": "tsx src/bin.ts",
|
|
32
|
+
"format": "biome check --write .",
|
|
33
|
+
"lint": "biome check .",
|
|
34
|
+
"package:check": "node scripts/package-check.mjs",
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22.12.0"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"ssr",
|
|
45
|
+
"seo",
|
|
46
|
+
"technical-seo",
|
|
47
|
+
"streaming-html",
|
|
48
|
+
"nextjs",
|
|
49
|
+
"nuxt",
|
|
50
|
+
"astro",
|
|
51
|
+
"crawler",
|
|
52
|
+
"cli",
|
|
53
|
+
"typescript",
|
|
54
|
+
"seo-tools",
|
|
55
|
+
"github-actions",
|
|
56
|
+
"sarif"
|
|
57
|
+
],
|
|
58
|
+
"author": {
|
|
59
|
+
"name": "Niko M.",
|
|
60
|
+
"url": "https://nikom.work"
|
|
61
|
+
},
|
|
62
|
+
"homepage": "https://nikom.work",
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/lame13/ssrwire.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/lame13/ssrwire/issues"
|
|
69
|
+
},
|
|
70
|
+
"license": "MIT",
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"access": "public"
|
|
73
|
+
},
|
|
74
|
+
"allowScripts": {
|
|
75
|
+
"esbuild@0.28.2": true,
|
|
76
|
+
"fsevents": false
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"commander": "14.0.0",
|
|
80
|
+
"htmlparser2": "10.0.0",
|
|
81
|
+
"yaml": "2.9.0",
|
|
82
|
+
"zod": "4.4.3"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"@biomejs/biome": "2.5.10",
|
|
86
|
+
"@types/node": "22.20.1",
|
|
87
|
+
"tsx": "4.23.12",
|
|
88
|
+
"typescript": "5.9.3",
|
|
89
|
+
"vitest": "3.2.7"
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/agents.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { AgentProfile } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const BUILTIN_AGENTS: Readonly<Record<string, AgentProfile>> = Object.freeze({
|
|
4
|
+
browser: Object.freeze({
|
|
5
|
+
key: "browser",
|
|
6
|
+
label: "Browser",
|
|
7
|
+
userAgent:
|
|
8
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
|
9
|
+
requiresHeadMetadata: false,
|
|
10
|
+
}),
|
|
11
|
+
googlebot: Object.freeze({
|
|
12
|
+
key: "googlebot",
|
|
13
|
+
label: "Googlebot",
|
|
14
|
+
userAgent: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
|
|
15
|
+
requiresHeadMetadata: false,
|
|
16
|
+
}),
|
|
17
|
+
bingbot: Object.freeze({
|
|
18
|
+
key: "bingbot",
|
|
19
|
+
label: "Bingbot",
|
|
20
|
+
userAgent: "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
|
|
21
|
+
requiresHeadMetadata: true,
|
|
22
|
+
}),
|
|
23
|
+
twitterbot: Object.freeze({
|
|
24
|
+
key: "twitterbot",
|
|
25
|
+
label: "Twitterbot",
|
|
26
|
+
userAgent: "Twitterbot/1.0",
|
|
27
|
+
requiresHeadMetadata: true,
|
|
28
|
+
}),
|
|
29
|
+
facebook: Object.freeze({
|
|
30
|
+
key: "facebook",
|
|
31
|
+
label: "Facebook crawler",
|
|
32
|
+
userAgent: "facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)",
|
|
33
|
+
requiresHeadMetadata: true,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export type BuiltinAgentKey = "browser" | "googlebot" | "bingbot" | "twitterbot" | "facebook";
|
|
38
|
+
export type AgentInput =
|
|
39
|
+
| string
|
|
40
|
+
| {
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly label?: string;
|
|
43
|
+
readonly userAgent: string;
|
|
44
|
+
readonly requiresHeadMetadata?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const ALIASES: Readonly<Record<string, BuiltinAgentKey>> = Object.freeze({
|
|
48
|
+
browser: "browser",
|
|
49
|
+
chrome: "browser",
|
|
50
|
+
google: "googlebot",
|
|
51
|
+
googlebot: "googlebot",
|
|
52
|
+
bing: "bingbot",
|
|
53
|
+
bingbot: "bingbot",
|
|
54
|
+
twitter: "twitterbot",
|
|
55
|
+
twitterbot: "twitterbot",
|
|
56
|
+
x: "twitterbot",
|
|
57
|
+
facebook: "facebook",
|
|
58
|
+
facebookbot: "facebook",
|
|
59
|
+
facebookexternalhit: "facebook",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function isCustomAgent(value: unknown): value is Exclude<AgentInput, string> {
|
|
63
|
+
if (typeof value !== "object" || value === null) return false;
|
|
64
|
+
const candidate = value as {
|
|
65
|
+
readonly key?: unknown;
|
|
66
|
+
readonly label?: unknown;
|
|
67
|
+
readonly userAgent?: unknown;
|
|
68
|
+
readonly requiresHeadMetadata?: unknown;
|
|
69
|
+
};
|
|
70
|
+
return (
|
|
71
|
+
typeof candidate.key === "string" &&
|
|
72
|
+
candidate.key.trim().length > 0 &&
|
|
73
|
+
typeof candidate.userAgent === "string" &&
|
|
74
|
+
candidate.userAgent.trim().length > 0 &&
|
|
75
|
+
(candidate.label === undefined ||
|
|
76
|
+
(typeof candidate.label === "string" && candidate.label.trim().length > 0)) &&
|
|
77
|
+
(candidate.requiresHeadMetadata === undefined ||
|
|
78
|
+
typeof candidate.requiresHeadMetadata === "boolean")
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Resolve a built-in key/alias or validate a complete custom agent profile. */
|
|
83
|
+
export function resolveAgent(input: AgentInput): AgentProfile {
|
|
84
|
+
if (typeof input === "string") {
|
|
85
|
+
const normalized = input.trim().toLowerCase();
|
|
86
|
+
const key = ALIASES[normalized];
|
|
87
|
+
if (key === undefined) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Unknown agent "${input}". Use browser, googlebot, bingbot, twitterbot, facebook, or a custom agent object.`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const agent = BUILTIN_AGENTS[key];
|
|
93
|
+
if (agent === undefined) {
|
|
94
|
+
throw new Error(`Built-in agent "${key}" is unavailable.`);
|
|
95
|
+
}
|
|
96
|
+
return agent;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!isCustomAgent(input)) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
"Custom agents require non-empty key and userAgent strings; label and requiresHeadMetadata are optional.",
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (/[\r\n]/u.test(input.userAgent)) {
|
|
106
|
+
throw new TypeError("Custom agent userAgent cannot contain a line break.");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Object.freeze({
|
|
110
|
+
key: input.key.trim(),
|
|
111
|
+
label: input.label?.trim() ?? input.key.trim(),
|
|
112
|
+
userAgent: input.userAgent.trim(),
|
|
113
|
+
requiresHeadMetadata: input.requiresHeadMetadata ?? false,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolve an ordered list and reject duplicate keys, which would make reports ambiguous. */
|
|
118
|
+
export function resolveAgents(inputs: readonly AgentInput[]): AgentProfile[] {
|
|
119
|
+
const resolved = inputs.map(resolveAgent);
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
for (const agent of resolved) {
|
|
122
|
+
const normalized = agent.key.toLowerCase();
|
|
123
|
+
if (seen.has(normalized)) {
|
|
124
|
+
throw new Error(`Duplicate agent key "${agent.key}".`);
|
|
125
|
+
}
|
|
126
|
+
seen.add(normalized);
|
|
127
|
+
}
|
|
128
|
+
return resolved;
|
|
129
|
+
}
|