@nebulacomponents/citable 1.18.1 → 1.19.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 +76 -0
- package/dist/universal/.agents/skills/citable/SKILL.md +1 -1
- package/dist/universal/.agents/skills/citable/VERSION +1 -1
- package/dist/universal/.agents/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.agents/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.agents/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.agents/skills/citable/manifest.json +15 -10
- package/dist/universal/.agents/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.agents/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.agents/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.agents/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.agents/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.agents/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.claude/agents/citable/manifest.json +2 -2
- package/dist/universal/.claude/skills/citable/SKILL.md +1 -1
- package/dist/universal/.claude/skills/citable/VERSION +1 -1
- package/dist/universal/.claude/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.claude/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.claude/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.claude/skills/citable/manifest.json +15 -10
- package/dist/universal/.claude/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.claude/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.claude/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.claude/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.claude/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.claude/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.cursor/skills/citable/SKILL.md +1 -1
- package/dist/universal/.cursor/skills/citable/VERSION +1 -1
- package/dist/universal/.cursor/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.cursor/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.cursor/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.cursor/skills/citable/manifest.json +15 -10
- package/dist/universal/.cursor/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.cursor/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.cursor/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.cursor/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.cursor/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.cursor/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.gemini/skills/citable/SKILL.md +1 -1
- package/dist/universal/.gemini/skills/citable/VERSION +1 -1
- package/dist/universal/.gemini/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.gemini/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.gemini/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.gemini/skills/citable/manifest.json +15 -10
- package/dist/universal/.gemini/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.gemini/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.gemini/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.gemini/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.gemini/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.gemini/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.github/skills/citable/SKILL.md +1 -1
- package/dist/universal/.github/skills/citable/VERSION +1 -1
- package/dist/universal/.github/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.github/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.github/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.github/skills/citable/manifest.json +15 -10
- package/dist/universal/.github/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.github/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.github/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.github/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.github/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.github/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.github/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.kiro/skills/citable/SKILL.md +1 -1
- package/dist/universal/.kiro/skills/citable/VERSION +1 -1
- package/dist/universal/.kiro/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.kiro/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.kiro/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.kiro/skills/citable/manifest.json +15 -10
- package/dist/universal/.kiro/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.kiro/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.kiro/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.kiro/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.kiro/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.kiro/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.opencode/skills/citable/SKILL.md +1 -1
- package/dist/universal/.opencode/skills/citable/VERSION +1 -1
- package/dist/universal/.opencode/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.opencode/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.opencode/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.opencode/skills/citable/manifest.json +15 -10
- package/dist/universal/.opencode/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.opencode/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.opencode/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.opencode/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.opencode/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.opencode/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.pi/agent/skills/citable/SKILL.md +1 -1
- package/dist/universal/.pi/agent/skills/citable/VERSION +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.pi/agent/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.pi/agent/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.pi/agent/skills/citable/manifest.json +15 -10
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.pi/agent/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.qoder/skills/citable/SKILL.md +1 -1
- package/dist/universal/.qoder/skills/citable/VERSION +1 -1
- package/dist/universal/.qoder/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.qoder/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.qoder/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.qoder/skills/citable/manifest.json +15 -10
- package/dist/universal/.qoder/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.qoder/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.qoder/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.qoder/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.qoder/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.qoder/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.rovodev/skills/citable/SKILL.md +1 -1
- package/dist/universal/.rovodev/skills/citable/VERSION +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.rovodev/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.rovodev/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.rovodev/skills/citable/manifest.json +15 -10
- package/dist/universal/.rovodev/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.rovodev/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.rovodev/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.rovodev/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.rovodev/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.rovodev/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.trae/skills/citable/SKILL.md +1 -1
- package/dist/universal/.trae/skills/citable/VERSION +1 -1
- package/dist/universal/.trae/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.trae/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.trae/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.trae/skills/citable/manifest.json +15 -10
- package/dist/universal/.trae/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.trae/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.trae/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.trae/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/.trae-cn/skills/citable/SKILL.md +1 -1
- package/dist/universal/.trae-cn/skills/citable/VERSION +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/artifact-interchange.md +5 -0
- package/dist/universal/.trae-cn/skills/citable/commands/remediation.md +1 -1
- package/dist/universal/.trae-cn/skills/citable/commands/sow.md +2 -2
- package/dist/universal/.trae-cn/skills/citable/manifest.json +15 -10
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-evidence-plan.schema.json +186 -3
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-network-events.schema.json +142 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-runtime-events.schema.json +112 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/browser-state-observations.schema.json +97 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/cro-report.schema.json +112 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/search-report.schema.json +99 -0
- package/dist/universal/.trae-cn/skills/citable/schemas/sow.schema.json +3 -2
- package/dist/universal/manifest.json +184 -124
- package/package.json +2 -1
- package/schemas/browser-evidence-plan.schema.json +186 -3
- package/schemas/browser-network-events.schema.json +142 -0
- package/schemas/browser-runtime-events.schema.json +112 -0
- package/schemas/browser-state-observations.schema.json +97 -0
- package/schemas/cro-report.schema.json +112 -0
- package/schemas/search-report.schema.json +99 -0
- package/schemas/sow.schema.json +3 -2
- package/skill/SKILL.md +1 -1
- package/skill/commands/artifact-interchange.md +5 -0
- package/skill/commands/remediation.md +1 -1
- package/skill/commands/sow.md +2 -2
- package/src/analysis/behavioral.js +5 -4
- package/src/analysis/eeat.js +10 -9
- package/src/analysis/offpage.js +19 -36
- package/src/analysis/readiness.js +8 -2
- package/src/artifacts/verifyCustomerArtifact.js +283 -0
- package/src/cli/index.js +8 -1
- package/src/observations/browser/correlation.js +83 -0
- package/src/observations/browser/networkCollector.js +413 -0
- package/src/observations/browser/runtimeEventCollector.js +337 -0
- package/src/observations/browser/sanitizer.js +322 -0
- package/src/observations/browser/stateCollector.js +386 -0
- package/src/observations/browserJourney.js +131 -3
- package/src/reporting/executiveCroReport.js +393 -229
- package/src/reporting/executiveExport.js +6 -2
- package/src/reporting/executiveSearchReport.js +519 -271
- package/src/shared/domainUtils.js +140 -0
- package/src/shared/epistemicStatus.js +111 -0
- package/src/shared/evidenceSourceResolver.js +286 -0
- package/src/shared/htmlEscape.js +65 -0
- package/src/shared/runPackageVerifier.js +153 -0
- package/src/sow/admissibilityGate.js +20 -2
- package/src/sow/generateSow.js +125 -159
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Domain & Public Suffix Extraction Utility
|
|
3
|
+
*
|
|
4
|
+
* Implements bounded registrable domain extraction and authority domain validation
|
|
5
|
+
* without naive substring matching.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Common multi-part public suffixes across gTLDs and ccTLDs
|
|
9
|
+
const MULTI_PART_SUFFIXES = new Set([
|
|
10
|
+
// UK
|
|
11
|
+
'co.uk', 'org.uk', 'gov.uk', 'ac.uk', 'ltd.uk', 'plc.uk', 'me.uk', 'net.uk', 'sch.uk',
|
|
12
|
+
// Australia
|
|
13
|
+
'com.au', 'net.au', 'org.au', 'edu.au', 'gov.au', 'asn.au', 'id.au',
|
|
14
|
+
// New Zealand
|
|
15
|
+
'co.nz', 'org.nz', 'net.nz', 'govt.nz', 'ac.nz', 'school.nz', 'geek.nz',
|
|
16
|
+
// Japan
|
|
17
|
+
'co.jp', 'ne.jp', 'or.jp', 'ac.jp', 'go.jp', 'ed.jp', 'gr.jp',
|
|
18
|
+
// Brazil
|
|
19
|
+
'com.br', 'org.br', 'gov.br', 'edu.br', 'net.br',
|
|
20
|
+
// Mexico
|
|
21
|
+
'com.mx', 'org.mx', 'gob.mx', 'edu.mx', 'net.mx',
|
|
22
|
+
// India
|
|
23
|
+
'co.in', 'net.in', 'org.in', 'gen.in', 'firm.in', 'ind.in', 'ac.in', 'edu.in', 'res.in', 'gov.in',
|
|
24
|
+
// Singapore
|
|
25
|
+
'com.sg', 'org.sg', 'edu.sg', 'gov.sg', 'net.sg',
|
|
26
|
+
// Hong Kong
|
|
27
|
+
'com.hk', 'org.hk', 'edu.hk', 'gov.hk', 'net.hk',
|
|
28
|
+
// South Africa
|
|
29
|
+
'co.za', 'org.za', 'gov.za', 'ac.za', 'net.za',
|
|
30
|
+
// Turkey
|
|
31
|
+
'com.tr', 'org.tr', 'gov.tr', 'edu.tr', 'net.tr',
|
|
32
|
+
// South Korea
|
|
33
|
+
'co.kr', 'ne.kr', 're.kr', 'or.kr', 'go.kr', 'ac.kr',
|
|
34
|
+
// Canada
|
|
35
|
+
'gc.ca', 'qc.ca', 'on.ca', 'bc.ca', 'ab.ca',
|
|
36
|
+
// France / Germany / Israel / etc.
|
|
37
|
+
'tm.fr', 'asso.fr', 'co.il', 'org.il', 'gov.il', 'muni.il', 'ac.il',
|
|
38
|
+
// Commercial second-level
|
|
39
|
+
'us.com', 'eu.com', 'uk.com', 'de.com',
|
|
40
|
+
// Developer platforms (treated as public suffixes for site identification)
|
|
41
|
+
'github.io', 'gitlab.io', 'pages.dev', 'vercel.app', 'webflow.io',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const AUTHORITATIVE_EXACT_DOMAINS = new Set([
|
|
45
|
+
'w3.org',
|
|
46
|
+
'rfc-editor.org',
|
|
47
|
+
'iana.org',
|
|
48
|
+
'ietf.org',
|
|
49
|
+
'iso.org',
|
|
50
|
+
'nist.gov',
|
|
51
|
+
'arxiv.org',
|
|
52
|
+
'developer.mozilla.org',
|
|
53
|
+
'web.dev',
|
|
54
|
+
'github.com',
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
export function extractHostname(urlString) {
|
|
58
|
+
if (!urlString || typeof urlString !== 'string') return null;
|
|
59
|
+
const trimmed = urlString.trim();
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(trimmed.includes('://') ? trimmed : `https://${trimmed}`);
|
|
62
|
+
return url.hostname.toLowerCase();
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function extractRegistrableDomain(hostnameOrUrl) {
|
|
69
|
+
const host = extractHostname(hostnameOrUrl);
|
|
70
|
+
if (!host) return null;
|
|
71
|
+
|
|
72
|
+
// Split into dot-separated parts
|
|
73
|
+
const parts = host.split('.');
|
|
74
|
+
if (parts.length <= 1) return host;
|
|
75
|
+
|
|
76
|
+
// Check 2-part suffix (e.g. co.uk, com.au)
|
|
77
|
+
if (parts.length >= 3) {
|
|
78
|
+
const twoPartSuffix = parts.slice(-2).join('.');
|
|
79
|
+
if (MULTI_PART_SUFFIXES.has(twoPartSuffix)) {
|
|
80
|
+
return parts.slice(-3).join('.');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check 3-part suffix if any exist
|
|
85
|
+
if (parts.length >= 4) {
|
|
86
|
+
const threePartSuffix = parts.slice(-3).join('.');
|
|
87
|
+
if (MULTI_PART_SUFFIXES.has(threePartSuffix)) {
|
|
88
|
+
return parts.slice(-4).join('.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Default: last 2 parts (e.g. example.com)
|
|
93
|
+
return parts.slice(-2).join('.');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function extractPublicSuffix(hostnameOrUrl) {
|
|
97
|
+
const host = extractHostname(hostnameOrUrl);
|
|
98
|
+
if (!host) return null;
|
|
99
|
+
|
|
100
|
+
const parts = host.split('.');
|
|
101
|
+
if (parts.length <= 1) return null;
|
|
102
|
+
|
|
103
|
+
if (parts.length >= 3) {
|
|
104
|
+
const twoPart = parts.slice(-2).join('.');
|
|
105
|
+
if (MULTI_PART_SUFFIXES.has(twoPart)) return twoPart;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return parts.at(-1) || null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isAuthoritativeDomain(urlOrHost) {
|
|
112
|
+
const host = extractHostname(urlOrHost);
|
|
113
|
+
if (!host) return false;
|
|
114
|
+
|
|
115
|
+
const regDomain = extractRegistrableDomain(host);
|
|
116
|
+
|
|
117
|
+
// Exact match to known authority domain or its subdomains
|
|
118
|
+
if (AUTHORITATIVE_EXACT_DOMAINS.has(host) || (regDomain && AUTHORITATIVE_EXACT_DOMAINS.has(regDomain))) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check government TLD / suffix: .gov, .gov.uk, .gov.au, etc.
|
|
123
|
+
if (host.endsWith('.gov') || host.endsWith('.mil') || (regDomain && (regDomain.endsWith('.gov') || regDomain.endsWith('.mil')))) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
const suffix = extractPublicSuffix(host);
|
|
127
|
+
if (suffix && (suffix.startsWith('gov.') || suffix.endsWith('.gov') || suffix.startsWith('gob.'))) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check academic / educational TLD: .edu, .edu.au, .ac.uk, etc.
|
|
132
|
+
if (host.endsWith('.edu') || (regDomain && regDomain.endsWith('.edu'))) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
if (suffix && (suffix.startsWith('edu.') || suffix.startsWith('ac.') || suffix.endsWith('.edu'))) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Epistemic Status & Provenance Envelope
|
|
3
|
+
*
|
|
4
|
+
* Core Operating Doctrine:
|
|
5
|
+
* OBSERVED stays OBSERVED
|
|
6
|
+
* DERIVED stays DERIVED
|
|
7
|
+
* MODELED stays MODELED
|
|
8
|
+
* HUMAN_REVIEWED stays HUMAN_REVIEWED
|
|
9
|
+
* NOT_OBSERVED stays NOT_OBSERVED
|
|
10
|
+
* NOT_APPLICABLE stays NOT_APPLICABLE
|
|
11
|
+
* UNRESOLVED stays UNRESOLVED
|
|
12
|
+
* SYNTHETIC stays SAMPLE-ONLY
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const EPISTEMIC_STATUS = Object.freeze({
|
|
16
|
+
OBSERVED: 'OBSERVED',
|
|
17
|
+
DERIVED: 'DERIVED',
|
|
18
|
+
MODELED: 'MODELED',
|
|
19
|
+
HUMAN_REVIEWED: 'HUMAN_REVIEWED',
|
|
20
|
+
NOT_OBSERVED: 'NOT_OBSERVED',
|
|
21
|
+
NOT_APPLICABLE: 'NOT_APPLICABLE',
|
|
22
|
+
UNRESOLVED: 'UNRESOLVED',
|
|
23
|
+
SYNTHETIC_SAMPLE: 'SYNTHETIC_SAMPLE',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export const DECISION_CATEGORY = Object.freeze({
|
|
27
|
+
CONFIRMED_OBSERVATION: 'CONFIRMED_OBSERVATION',
|
|
28
|
+
DERIVED_FINDING: 'DERIVED_FINDING',
|
|
29
|
+
MODELED_HYPOTHESIS: 'MODELED_HYPOTHESIS',
|
|
30
|
+
CONTROLLED_CAUSAL_EVIDENCE: 'CONTROLLED_CAUSAL_EVIDENCE',
|
|
31
|
+
UNRESOLVED: 'UNRESOLVED',
|
|
32
|
+
REQUIRES_HUMAN_REVIEW: 'REQUIRES_HUMAN_REVIEW',
|
|
33
|
+
REQUIRES_EXTERNAL_DATA: 'REQUIRES_EXTERNAL_DATA',
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export function wrapValue(value, status, options = {}) {
|
|
37
|
+
if (!Object.values(EPISTEMIC_STATUS).includes(status)) {
|
|
38
|
+
throw new Error(`Invalid epistemic status: ${status}`);
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
value,
|
|
42
|
+
status,
|
|
43
|
+
evidence_refs: options.evidence_refs ? [].concat(options.evidence_refs) : [],
|
|
44
|
+
method: options.method || null,
|
|
45
|
+
confidence: options.confidence || null,
|
|
46
|
+
derivation_id: options.derivation_id || null,
|
|
47
|
+
model_id: options.model_id || null,
|
|
48
|
+
required_input: options.required_input || null,
|
|
49
|
+
note: options.note || null,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function observed(value, evidenceRefs = [], method = null, confidence = 'empirical_observation') {
|
|
54
|
+
return wrapValue(value, EPISTEMIC_STATUS.OBSERVED, {
|
|
55
|
+
evidence_refs: evidenceRefs,
|
|
56
|
+
method,
|
|
57
|
+
confidence,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function derived(value, evidenceRefs = [], method = null, derivationId = null) {
|
|
62
|
+
return wrapValue(value, EPISTEMIC_STATUS.DERIVED, {
|
|
63
|
+
evidence_refs: evidenceRefs,
|
|
64
|
+
method,
|
|
65
|
+
derivation_id: derivationId,
|
|
66
|
+
confidence: 'reproducible_derivation',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function modeled(value, method, modelId = null, confidence = 'modeled_heuristic') {
|
|
71
|
+
return wrapValue(value, EPISTEMIC_STATUS.MODELED, {
|
|
72
|
+
method,
|
|
73
|
+
model_id: modelId,
|
|
74
|
+
confidence,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function humanReviewed(value, reviewerId, reviewTimestamp, note = null) {
|
|
79
|
+
return wrapValue(value, EPISTEMIC_STATUS.HUMAN_REVIEWED, {
|
|
80
|
+
method: `human_review:${reviewerId}`,
|
|
81
|
+
note,
|
|
82
|
+
confidence: 'attested_human_review',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function notObserved(requiredInput, note = null) {
|
|
87
|
+
return wrapValue(null, EPISTEMIC_STATUS.NOT_OBSERVED, {
|
|
88
|
+
required_input: requiredInput,
|
|
89
|
+
note: note || `Metric unavailable: required input "${requiredInput}" was not provided or not observed.`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function notApplicable(reason) {
|
|
94
|
+
return wrapValue(null, EPISTEMIC_STATUS.NOT_APPLICABLE, {
|
|
95
|
+
note: reason,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function unresolved(reason, requiredInput = null) {
|
|
100
|
+
return wrapValue(null, EPISTEMIC_STATUS.UNRESOLVED, {
|
|
101
|
+
required_input: requiredInput,
|
|
102
|
+
note: reason,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function syntheticSample(value, rationale = 'Non-contractual synthetic demonstration sample') {
|
|
107
|
+
return wrapValue(value, EPISTEMIC_STATUS.SYNTHETIC_SAMPLE, {
|
|
108
|
+
note: rationale,
|
|
109
|
+
confidence: 'synthetic_sample_only',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readJson, sha256, sha256File } from './io.js';
|
|
4
|
+
import { verifyRunPackage, RunVerificationError } from './runPackageVerifier.js';
|
|
5
|
+
import { buildContext } from '../commands/context.js';
|
|
6
|
+
import { selectDetectors } from '../detectors/index.js';
|
|
7
|
+
import { runDetectors, indexTargets } from '../detectors/framework.js';
|
|
8
|
+
|
|
9
|
+
export class SourceResolutionError extends Error {
|
|
10
|
+
constructor(message, code = 'SOURCE_RESOLUTION_ERROR', details = {}) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'SourceResolutionError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.details = details;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class RunNotFoundError extends SourceResolutionError {
|
|
19
|
+
constructor(runId) {
|
|
20
|
+
super(`Specified run not found: ${runId}`, 'RUN_NOT_FOUND', { runId });
|
|
21
|
+
this.runId = runId;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class FindingsMissingError extends SourceResolutionError {
|
|
26
|
+
constructor(runId, filePath) {
|
|
27
|
+
super(`Run ${runId} is missing findings.json at ${filePath}`, 'FINDINGS_MISSING', { runId, filePath });
|
|
28
|
+
this.runId = runId;
|
|
29
|
+
this.filePath = filePath;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class FindingsInvalidError extends SourceResolutionError {
|
|
34
|
+
constructor(runId, message) {
|
|
35
|
+
super(`Run ${runId} contains malformed or invalid findings: ${message}`, 'FINDINGS_INVALID', { runId, message });
|
|
36
|
+
this.runId = runId;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class LiveInspectionFailedError extends SourceResolutionError {
|
|
41
|
+
constructor(target, reason) {
|
|
42
|
+
super(`Live target inspection failed for ${target}: ${reason}`, 'LIVE_INSPECTION_FAILED', { target, reason });
|
|
43
|
+
this.target = target;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class NoFindingsAvailableError extends SourceResolutionError {
|
|
48
|
+
constructor(source, mode = 'CONTRACTUAL') {
|
|
49
|
+
super(
|
|
50
|
+
`No verified audit findings available from ${source} for ${mode} generation. Production generation requires authoritative findings. (Use --sample or --demo to generate non-contractual sample)`,
|
|
51
|
+
'NO_FINDINGS',
|
|
52
|
+
{ source, mode }
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extract canonical timestamp (epoch ms) from run package
|
|
59
|
+
* Hierarchy:
|
|
60
|
+
* 1. manifest.json `timestamp` or `created_at` ISO string
|
|
61
|
+
* 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
|
|
62
|
+
* 3. findings.json mtime
|
|
63
|
+
* 4. run directory mtime
|
|
64
|
+
*/
|
|
65
|
+
export function getCanonicalRunTimestamp(runDir, runId = '') {
|
|
66
|
+
// 1. manifest.json
|
|
67
|
+
const manifestPath = path.join(runDir, 'manifest.json');
|
|
68
|
+
if (fs.existsSync(manifestPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
71
|
+
if (manifest.timestamp) {
|
|
72
|
+
const ms = Date.parse(manifest.timestamp);
|
|
73
|
+
if (!Number.isNaN(ms)) return ms;
|
|
74
|
+
}
|
|
75
|
+
if (manifest.created_at) {
|
|
76
|
+
const ms = Date.parse(manifest.created_at);
|
|
77
|
+
if (!Number.isNaN(ms)) return ms;
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 2. Run ID timestamp prefix (YYYYMMDDTHHmmss)
|
|
83
|
+
const m = String(runId).match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})/);
|
|
84
|
+
if (m) {
|
|
85
|
+
const iso = `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`;
|
|
86
|
+
const ms = Date.parse(iso);
|
|
87
|
+
if (!Number.isNaN(ms)) return ms;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 3. findings.json mtime
|
|
91
|
+
const findPath = path.join(runDir, 'findings.json');
|
|
92
|
+
if (fs.existsSync(findPath)) {
|
|
93
|
+
try {
|
|
94
|
+
return fs.statSync(findPath).mtimeMs;
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 4. Directory mtime
|
|
99
|
+
try {
|
|
100
|
+
return fs.statSync(runDir).mtimeMs;
|
|
101
|
+
} catch {
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Sort run candidate directory names by chronological timestamp descending,
|
|
108
|
+
* with deterministic lexicographical tie-breaking.
|
|
109
|
+
* Invariant:
|
|
110
|
+
* latest run = max(canonical run timestamp)
|
|
111
|
+
* tie = deterministic secondary key
|
|
112
|
+
*/
|
|
113
|
+
export function sortRunCandidatesChronologically(runsDir, candidateRunIds = []) {
|
|
114
|
+
return [...candidateRunIds].sort((a, b) => {
|
|
115
|
+
const timeA = getCanonicalRunTimestamp(path.join(runsDir, a), a);
|
|
116
|
+
const timeB = getCanonicalRunTimestamp(path.join(runsDir, b), b);
|
|
117
|
+
if (timeB !== timeA) {
|
|
118
|
+
return timeB - timeA; // Descending: latest timestamp first
|
|
119
|
+
}
|
|
120
|
+
return b.localeCompare(a); // Deterministic tie-breaker
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Unified Canonical Evidence Source Resolver
|
|
126
|
+
*
|
|
127
|
+
* Enforces strict precedence and fail-closed integrity across SOW and Executive Reports:
|
|
128
|
+
* 1. DIRECT_FINDINGS (in-memory findings array)
|
|
129
|
+
* 2. EXPLICIT_RUN (--run <id>): verified via runPackageVerifier
|
|
130
|
+
* 3. LIVE_INSPECTION (--live or target): executed on demand
|
|
131
|
+
* 4. LATEST_RECORDED_RUN: chronologically latest run in .citable/runs verified via runPackageVerifier
|
|
132
|
+
* 5. SAMPLE: synthetic demonstration sample
|
|
133
|
+
*/
|
|
134
|
+
export async function resolveEvidenceSource(root, options = {}) {
|
|
135
|
+
const {
|
|
136
|
+
findings: inputFindings = null,
|
|
137
|
+
runId = null,
|
|
138
|
+
target = null,
|
|
139
|
+
live = false,
|
|
140
|
+
baseUrl = null,
|
|
141
|
+
refDate = null,
|
|
142
|
+
sample = false,
|
|
143
|
+
demo = false,
|
|
144
|
+
draft = false,
|
|
145
|
+
contractual = false,
|
|
146
|
+
scopes = ['technical', 'seo', 'aeo', 'geo', 'schema', 'entity', 'cro'],
|
|
147
|
+
} = options;
|
|
148
|
+
|
|
149
|
+
const isSample = Boolean(sample || demo);
|
|
150
|
+
const isDraft = Boolean(draft);
|
|
151
|
+
const generationMode = isSample ? 'NON_CONTRACTUAL_SAMPLE' : (isDraft ? 'DRAFT' : 'CONTRACTUAL');
|
|
152
|
+
|
|
153
|
+
// 1. Direct findings in memory
|
|
154
|
+
if (Array.isArray(inputFindings)) {
|
|
155
|
+
const findingsJson = JSON.stringify(inputFindings);
|
|
156
|
+
return {
|
|
157
|
+
source_type: 'DIRECT_FINDINGS',
|
|
158
|
+
source_identifier: 'in_memory',
|
|
159
|
+
findings: inputFindings,
|
|
160
|
+
findings_count: inputFindings.length,
|
|
161
|
+
integrity_hash: sha256(findingsJson),
|
|
162
|
+
generation_mode: generationMode,
|
|
163
|
+
run_metadata: null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 2. Explicit run requested via runId
|
|
168
|
+
if (runId) {
|
|
169
|
+
const runsDir = path.join(root, '.citable', 'runs');
|
|
170
|
+
const runPath = path.join(runsDir, runId);
|
|
171
|
+
if (!fs.existsSync(runPath)) {
|
|
172
|
+
throw new RunNotFoundError(runId);
|
|
173
|
+
}
|
|
174
|
+
const findPath = path.join(runPath, 'findings.json');
|
|
175
|
+
if (!fs.existsSync(findPath)) {
|
|
176
|
+
throw new FindingsMissingError(runId, findPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const verification = verifyRunPackage(runPath, {
|
|
181
|
+
requireFindings: true,
|
|
182
|
+
requireCompleted: generationMode === 'CONTRACTUAL',
|
|
183
|
+
requireChecksums: true,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
source_type: 'HISTORICAL_RUN',
|
|
188
|
+
source_identifier: runId,
|
|
189
|
+
findings: verification.findings,
|
|
190
|
+
findings_count: verification.findingsCount,
|
|
191
|
+
integrity_hash: sha256File(findPath),
|
|
192
|
+
generation_mode: generationMode,
|
|
193
|
+
run_metadata: verification.manifest,
|
|
194
|
+
checksums_verified: verification.checksumsVerified,
|
|
195
|
+
};
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof RunVerificationError) {
|
|
198
|
+
throw new FindingsInvalidError(runId, err.message);
|
|
199
|
+
}
|
|
200
|
+
throw err;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 3. Live target inspection
|
|
205
|
+
if (live || target) {
|
|
206
|
+
if (!target) {
|
|
207
|
+
throw new SourceResolutionError('Target is required for live inspection (e.g. --target <url|dir>)', 'TARGET_REQUIRED');
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const ctx = await buildContext(root, { target, baseUrl, refDate });
|
|
211
|
+
if (!ctx?.site) {
|
|
212
|
+
throw new Error(`Target ${target} did not produce a valid site context`);
|
|
213
|
+
}
|
|
214
|
+
indexTargets(ctx);
|
|
215
|
+
const detectors = selectDetectors({ scopes });
|
|
216
|
+
const res = runDetectors(detectors, ctx);
|
|
217
|
+
const liveFindings = res.findings || [];
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
source_type: 'LIVE_INSPECTION',
|
|
221
|
+
source_identifier: target,
|
|
222
|
+
findings: liveFindings,
|
|
223
|
+
findings_count: liveFindings.length,
|
|
224
|
+
integrity_hash: sha256(JSON.stringify(liveFindings)),
|
|
225
|
+
generation_mode: generationMode,
|
|
226
|
+
run_metadata: null,
|
|
227
|
+
context: ctx,
|
|
228
|
+
};
|
|
229
|
+
} catch (err) {
|
|
230
|
+
throw new LiveInspectionFailedError(target, err.message);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 4. Latest recorded run in .citable/runs
|
|
235
|
+
const runsDir = path.join(root, '.citable', 'runs');
|
|
236
|
+
if (fs.existsSync(runsDir)) {
|
|
237
|
+
const candidateDirs = fs.readdirSync(runsDir).filter((name) => {
|
|
238
|
+
const p = path.join(runsDir, name);
|
|
239
|
+
return fs.statSync(p).isDirectory() && fs.existsSync(path.join(p, 'findings.json'));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
if (candidateDirs.length > 0) {
|
|
243
|
+
const sorted = sortRunCandidatesChronologically(runsDir, candidateDirs);
|
|
244
|
+
for (const candidateId of sorted) {
|
|
245
|
+
const runPath = path.join(runsDir, candidateId);
|
|
246
|
+
const findPath = path.join(runPath, 'findings.json');
|
|
247
|
+
try {
|
|
248
|
+
const verification = verifyRunPackage(runPath, {
|
|
249
|
+
requireFindings: true,
|
|
250
|
+
requireCompleted: false, // allow non-completed in discovery if valid
|
|
251
|
+
requireChecksums: false,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
source_type: 'LATEST_RECORDED_RUN',
|
|
256
|
+
source_identifier: candidateId,
|
|
257
|
+
findings: verification.findings,
|
|
258
|
+
findings_count: verification.findingsCount,
|
|
259
|
+
integrity_hash: sha256File(findPath),
|
|
260
|
+
generation_mode: generationMode,
|
|
261
|
+
run_metadata: verification.manifest,
|
|
262
|
+
};
|
|
263
|
+
} catch {
|
|
264
|
+
// Try next candidate
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 5. Sample / Demo mode
|
|
272
|
+
if (isSample) {
|
|
273
|
+
return {
|
|
274
|
+
source_type: 'SAMPLE',
|
|
275
|
+
source_identifier: 'synthetic_sample_template',
|
|
276
|
+
findings: [],
|
|
277
|
+
findings_count: 0,
|
|
278
|
+
integrity_hash: null,
|
|
279
|
+
generation_mode: 'NON_CONTRACTUAL_SAMPLE',
|
|
280
|
+
run_metadata: null,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If we reach here in contractual mode, fail closed
|
|
285
|
+
throw new NoFindingsAvailableError('.citable/runs or input target', generationMode);
|
|
286
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical HTML and Markdown Escaping Utility
|
|
3
|
+
*
|
|
4
|
+
* Prevents HTML/XSS injection vulnerabilities in generated reports, SOWs,
|
|
5
|
+
* dashboards, and markdown/HTML interchange representations.
|
|
6
|
+
* Treats all finding summaries, client names, URLs, titles, and detector
|
|
7
|
+
* outputs as potentially hostile user input.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const HTML_CHARS = {
|
|
11
|
+
'&': '&',
|
|
12
|
+
'<': '<',
|
|
13
|
+
'>': '>',
|
|
14
|
+
'"': '"',
|
|
15
|
+
"'": ''',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const HTML_RX = /[&<>"']/g;
|
|
19
|
+
|
|
20
|
+
export function escapeHtml(str) {
|
|
21
|
+
if (str === null || str === undefined) return '';
|
|
22
|
+
return String(str).replace(HTML_RX, (char) => HTML_CHARS[char]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function escapeHtmlAttr(str) {
|
|
26
|
+
return escapeHtml(str);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']);
|
|
30
|
+
|
|
31
|
+
export function sanitizeUrl(urlStr, fallback = '#') {
|
|
32
|
+
if (!urlStr || typeof urlStr !== 'string') return fallback;
|
|
33
|
+
const trimmed = urlStr.trim();
|
|
34
|
+
if (trimmed.startsWith('/') || trimmed.startsWith('#')) {
|
|
35
|
+
// Relative path or anchor is safe
|
|
36
|
+
return escapeHtmlAttr(trimmed);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const parsed = new URL(trimmed);
|
|
40
|
+
if (SAFE_PROTOCOLS.has(parsed.protocol)) {
|
|
41
|
+
return escapeHtmlAttr(trimmed);
|
|
42
|
+
}
|
|
43
|
+
return fallback;
|
|
44
|
+
} catch {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function escapeMarkdownTableCell(str) {
|
|
50
|
+
if (str === null || str === undefined) return '';
|
|
51
|
+
return String(str)
|
|
52
|
+
.replace(/\r?\n/g, ' ')
|
|
53
|
+
.replace(/\\/g, '\\\\')
|
|
54
|
+
.replace(/\|/g, '\\|')
|
|
55
|
+
.replace(/</g, '<')
|
|
56
|
+
.replace(/>/g, '>');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function sanitizeForMarkdown(str) {
|
|
60
|
+
if (str === null || str === undefined) return '';
|
|
61
|
+
// Prevent raw script or malicious HTML tag injections in markdown by encoding brackets
|
|
62
|
+
return String(str)
|
|
63
|
+
.replace(/</g, '<')
|
|
64
|
+
.replace(/>/g, '>');
|
|
65
|
+
}
|