@prenta/core 2.3.2 → 2.4.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.
@@ -7,6 +7,7 @@
7
7
  * preceding backslash as part of the URL — every probe then hits a malformed
8
8
  * target and floods false positives.
9
9
  */
10
+ import { safeFetch, SsrfBlockedError } from '../security/safe-fetch.js';
10
11
  /** Browser-like headers so hosts that reject bare HEAD/bot UAs still respond. */
11
12
  export const LINK_PROBE_HEADERS = Object.freeze({
12
13
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
@@ -80,11 +81,28 @@ export function isCmsBlobMediaUrl(url) {
80
81
  return false;
81
82
  }
82
83
  }
84
+ function resolveHrefForProbe(href, siteUrl) {
85
+ if (/^https?:\/\//i.test(href))
86
+ return normalizeProbeUrl(href);
87
+ if (!siteUrl)
88
+ return null;
89
+ if (!href.startsWith('/') || href.startsWith('//'))
90
+ return null;
91
+ try {
92
+ return new URL(href, `${siteUrl.replace(/\/+$/, '')}/`).href;
93
+ }
94
+ catch {
95
+ return null;
96
+ }
97
+ }
83
98
  /**
84
99
  * Extract unique http(s) URLs from document `data` for outbound probing.
85
100
  * Prefers `href` / `src` attributes; also picks up bare URLs in plain text.
101
+ * When `siteUrl` is set, site-relative hrefs (`/privacy/`) are resolved and
102
+ * included so trailing-slash 308s are visible to the live scan.
86
103
  */
87
- export function extractOutboundUrlsFromDocumentData(data) {
104
+ export function extractOutboundUrlsFromDocumentData(data, options = {}) {
105
+ const siteUrl = options.siteUrl?.replace(/\/+$/, '') || null;
88
106
  const strings = collectDocumentStrings(data);
89
107
  const found = new Set();
90
108
  for (const text of strings) {
@@ -95,9 +113,7 @@ export function extractOutboundUrlsFromDocumentData(data) {
95
113
  if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) {
96
114
  continue;
97
115
  }
98
- if (!/^https?:\/\//i.test(href))
99
- continue;
100
- const clean = normalizeProbeUrl(href);
116
+ const clean = resolveHrefForProbe(href, siteUrl);
101
117
  if (clean && !isCmsBlobMediaUrl(clean))
102
118
  found.add(clean);
103
119
  }
@@ -110,6 +126,43 @@ export function extractOutboundUrlsFromDocumentData(data) {
110
126
  }
111
127
  return [...found];
112
128
  }
129
+ export function classifyLinkHealthIssue(status) {
130
+ if (status >= 300 && status < 400)
131
+ return 'redirected';
132
+ if (isLinkProbeIssueStatus(status))
133
+ return 'broken';
134
+ return null;
135
+ }
136
+ export async function probeOutboundLink(url, options = {}) {
137
+ const fetchFn = options.fetchFn ?? safeFetch;
138
+ const timeoutMs = options.timeoutMs ?? 4000;
139
+ const attempt = async () => {
140
+ try {
141
+ const resp = await fetchFn(url, {
142
+ method: 'GET',
143
+ headers: { ...LINK_PROBE_HEADERS },
144
+ timeoutMs,
145
+ });
146
+ try {
147
+ await resp.body?.cancel();
148
+ }
149
+ catch {
150
+ /* noop */
151
+ }
152
+ const redirectTarget = resp.status >= 300 && resp.status < 400
153
+ ? (resp.headers.get('location') ?? undefined)
154
+ : undefined;
155
+ return { status: resp.status, redirectTarget };
156
+ }
157
+ catch (err) {
158
+ return { status: err instanceof SsrfBlockedError ? -1 : 0 };
159
+ }
160
+ };
161
+ const first = await attempt();
162
+ if (first.status === 0 && options.retryUnreachable !== false)
163
+ return attempt();
164
+ return first;
165
+ }
113
166
  /**
114
167
  * Whether a probe HTTP status should appear in the Live link health **issues**
115
168
  * list. 2xx/3xx are healthy; 403/405/429/999 are treated as unverifiable
@@ -1 +1 @@
1
- {"version":3,"file":"link-probe.js","sourceRoot":"","sources":["../../src/seo/link-probe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"link-probe.js","sourceRoot":"","sources":["../../src/seo/link-probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Rewrite internal hrefs to the site's canonical URL form on document save.
3
+ *
4
+ * Policy matches {@link resolveDocumentUrl} / sitemap / canonical tags:
5
+ * no trailing slash on non-root paths, https when the site origin is https.
6
+ * Relative `/privacy/` and same-host `https://site/privacy/` both normalize.
7
+ * External hosts, hashes, mailto/tel, and protocol-relative URLs are left alone.
8
+ */
9
+ /** Strip trailing slashes from a path, keeping `/` as `/`. */
10
+ export declare function canonicalizePathname(pathname: string): string;
11
+ /**
12
+ * Return the canonical form of an internal href, or the original string when
13
+ * the href is external / not a path.
14
+ */
15
+ export declare function canonicalizeInternalHref(href: string, siteUrl: string | null): string;
16
+ /** Rewrite `href` attributes and bare path/URL strings in one text field. */
17
+ export declare function rewriteInternalHrefsInText(text: string, siteUrl: string | null): string;
18
+ /** Deep-walk document `data` and rewrite internal hrefs in place (new object if changed). */
19
+ export declare function normalizeInternalHrefsInData(data: Record<string, unknown>, siteUrl: string | null): Record<string, unknown>;
20
+ //# sourceMappingURL=normalize-internal-hrefs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-internal-hrefs.d.ts","sourceRoot":"","sources":["../../src/seo/normalize-internal-hrefs.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAeH,8DAA8D;AAC9D,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAG7D;AAeD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CA8BrF;AAED,6EAA6E;AAC7E,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAevF;AA2BD,6FAA6F;AAC7F,wBAAgB,4BAA4B,CAC1C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAKzB"}
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Rewrite internal hrefs to the site's canonical URL form on document save.
3
+ *
4
+ * Policy matches {@link resolveDocumentUrl} / sitemap / canonical tags:
5
+ * no trailing slash on non-root paths, https when the site origin is https.
6
+ * Relative `/privacy/` and same-host `https://site/privacy/` both normalize.
7
+ * External hosts, hashes, mailto/tel, and protocol-relative URLs are left alone.
8
+ */
9
+ const HREF_ATTR_RE = /(href\s*=\s*)(["'])([^"']*)\2/gi;
10
+ const SKIP_SCHEME_RE = /^(mailto|tel|javascript|data):/i;
11
+ const MAX_DEPTH = 10;
12
+ function siteOrigin(siteUrl) {
13
+ if (!siteUrl)
14
+ return null;
15
+ try {
16
+ return new URL(siteUrl);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ /** Strip trailing slashes from a path, keeping `/` as `/`. */
23
+ export function canonicalizePathname(pathname) {
24
+ if (!pathname || pathname === '/')
25
+ return pathname || '/';
26
+ return pathname.replace(/\/+$/, '') || '/';
27
+ }
28
+ function splitRef(ref) {
29
+ const hashIdx = ref.indexOf('#');
30
+ const queryIdx = ref.indexOf('?');
31
+ const cut = [hashIdx, queryIdx].filter((i) => i >= 0).sort((a, b) => a - b)[0];
32
+ if (cut === undefined)
33
+ return { path: ref, rest: '' };
34
+ return { path: ref.slice(0, cut), rest: ref.slice(cut) };
35
+ }
36
+ function canonicalizeRelativeRef(ref) {
37
+ const { path, rest } = splitRef(ref);
38
+ return canonicalizePathname(path) + rest;
39
+ }
40
+ /**
41
+ * Return the canonical form of an internal href, or the original string when
42
+ * the href is external / not a path.
43
+ */
44
+ export function canonicalizeInternalHref(href, siteUrl) {
45
+ const trimmed = href.trim();
46
+ if (!trimmed || trimmed.startsWith('#') || SKIP_SCHEME_RE.test(trimmed))
47
+ return href;
48
+ if (trimmed.startsWith('//'))
49
+ return href;
50
+ if (trimmed.startsWith('/')) {
51
+ const next = canonicalizeRelativeRef(trimmed);
52
+ return next === trimmed ? href : next;
53
+ }
54
+ if (!/^https?:\/\//i.test(trimmed))
55
+ return href;
56
+ const site = siteOrigin(siteUrl);
57
+ if (!site)
58
+ return href;
59
+ let parsed;
60
+ try {
61
+ parsed = new URL(trimmed);
62
+ }
63
+ catch {
64
+ return href;
65
+ }
66
+ if (parsed.hostname.toLowerCase() !== site.hostname.toLowerCase())
67
+ return href;
68
+ if (site.protocol === 'https:' && parsed.protocol === 'http:') {
69
+ parsed.protocol = 'https:';
70
+ }
71
+ parsed.pathname = canonicalizePathname(parsed.pathname);
72
+ if (parsed.pathname === '/' && !parsed.search && !parsed.hash) {
73
+ return parsed.origin;
74
+ }
75
+ return parsed.toString();
76
+ }
77
+ /** Rewrite `href` attributes and bare path/URL strings in one text field. */
78
+ export function rewriteInternalHrefsInText(text, siteUrl) {
79
+ let changed = false;
80
+ const withAttrs = text.replace(HREF_ATTR_RE, (full, prefix, quote, href) => {
81
+ const next = canonicalizeInternalHref(href, siteUrl);
82
+ if (next === href)
83
+ return full;
84
+ changed = true;
85
+ return `${prefix}${quote}${next}${quote}`;
86
+ });
87
+ if (changed)
88
+ return withAttrs;
89
+ const whole = canonicalizeInternalHref(text, siteUrl);
90
+ return whole === text ? text : whole;
91
+ }
92
+ function walk(value, siteUrl, depth) {
93
+ if (depth > MAX_DEPTH)
94
+ return value;
95
+ if (typeof value === 'string')
96
+ return rewriteInternalHrefsInText(value, siteUrl);
97
+ if (Array.isArray(value)) {
98
+ let changed = false;
99
+ const next = value.map((item) => {
100
+ const rewritten = walk(item, siteUrl, depth + 1);
101
+ if (rewritten !== item)
102
+ changed = true;
103
+ return rewritten;
104
+ });
105
+ return changed ? next : value;
106
+ }
107
+ if (value && typeof value === 'object') {
108
+ let changed = false;
109
+ const out = {};
110
+ for (const [key, child] of Object.entries(value)) {
111
+ const rewritten = walk(child, siteUrl, depth + 1);
112
+ if (rewritten !== child)
113
+ changed = true;
114
+ out[key] = rewritten;
115
+ }
116
+ return changed ? out : value;
117
+ }
118
+ return value;
119
+ }
120
+ /** Deep-walk document `data` and rewrite internal hrefs in place (new object if changed). */
121
+ export function normalizeInternalHrefsInData(data, siteUrl) {
122
+ const next = walk(data, siteUrl, 0);
123
+ return next && typeof next === 'object' && !Array.isArray(next)
124
+ ? next
125
+ : data;
126
+ }
127
+ //# sourceMappingURL=normalize-internal-hrefs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-internal-hrefs.js","sourceRoot":"","sources":["../../src/seo/normalize-internal-hrefs.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prenta/core",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",