docorbit 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/LICENSE +21 -0
- package/README.md +660 -0
- package/apps/cli/bin/docorbit.js +8 -0
- package/apps/cli/src/commands/add.ts +44 -0
- package/apps/cli/src/commands/api.ts +38 -0
- package/apps/cli/src/commands/context.ts +47 -0
- package/apps/cli/src/commands/dashboard.ts +55 -0
- package/apps/cli/src/commands/diff.ts +30 -0
- package/apps/cli/src/commands/evaluate.ts +133 -0
- package/apps/cli/src/commands/examples.ts +39 -0
- package/apps/cli/src/commands/export.ts +89 -0
- package/apps/cli/src/commands/impact.ts +31 -0
- package/apps/cli/src/commands/init.ts +69 -0
- package/apps/cli/src/commands/inspect.ts +30 -0
- package/apps/cli/src/commands/mcp.ts +72 -0
- package/apps/cli/src/commands/pitfalls.ts +38 -0
- package/apps/cli/src/commands/recipes.ts +35 -0
- package/apps/cli/src/commands/search.ts +48 -0
- package/apps/cli/src/commands/update.ts +73 -0
- package/apps/cli/src/commands/verify.ts +48 -0
- package/apps/cli/src/formatters/colors.ts +23 -0
- package/apps/cli/src/formatters/inspection.ts +102 -0
- package/apps/cli/src/formatters/knowledge.ts +272 -0
- package/apps/cli/src/formatters/retrieval.ts +74 -0
- package/apps/cli/src/formatters/terminal.ts +6 -0
- package/apps/cli/src/formatters/verification.ts +126 -0
- package/apps/cli/src/index.ts +409 -0
- package/bin/docorbit.js +8 -0
- package/package.json +46 -0
- package/packages/core/src/dashboard/server.ts +314 -0
- package/packages/core/src/dashboard/ui.ts +586 -0
- package/packages/core/src/implementation-service.ts +451 -0
- package/packages/core/src/index.ts +7 -0
- package/packages/core/src/inspector.ts +71 -0
- package/packages/core/src/pipeline.ts +331 -0
- package/packages/crawler/src/config.ts +12 -0
- package/packages/crawler/src/fetcher.ts +185 -0
- package/packages/crawler/src/index.ts +2 -0
- package/packages/discovery/src/index.ts +31 -0
- package/packages/discovery/src/provider.ts +47 -0
- package/packages/discovery/src/providers/generic.ts +98 -0
- package/packages/discovery/src/providers/github.ts +61 -0
- package/packages/discovery/src/providers/llms-txt.ts +73 -0
- package/packages/discovery/src/providers/markdown.ts +48 -0
- package/packages/discovery/src/providers/openapi.ts +91 -0
- package/packages/discovery/src/providers/sitemap.ts +62 -0
- package/packages/discovery/src/providers/skill.ts +54 -0
- package/packages/discovery/src/ranker.ts +123 -0
- package/packages/evaluation/src/dataset.ts +963 -0
- package/packages/evaluation/src/index.ts +8 -0
- package/packages/evaluation/src/runner.ts +241 -0
- package/packages/evaluation/src/strategies/context7-runner.ts +269 -0
- package/packages/evaluation/src/strategies/docorbit-runner.ts +228 -0
- package/packages/evaluation/src/strategies/firecrawl-runner.ts +172 -0
- package/packages/evaluation/src/strategies/web-search-runner.ts +194 -0
- package/packages/evaluation/src/types.ts +34 -0
- package/packages/evaluation/src/version-matcher.ts +73 -0
- package/packages/export/src/agents-md.ts +200 -0
- package/packages/export/src/claude-md.ts +141 -0
- package/packages/export/src/docs-map.ts +150 -0
- package/packages/export/src/index.ts +6 -0
- package/packages/export/src/llms-txt.ts +96 -0
- package/packages/export/src/service.ts +250 -0
- package/packages/export/src/skill-md.ts +128 -0
- package/packages/mcp/src/index.ts +46 -0
- package/packages/mcp/src/resources/index.ts +189 -0
- package/packages/mcp/src/server.ts +278 -0
- package/packages/mcp/src/tools/analyze-impact.ts +74 -0
- package/packages/mcp/src/tools/check-api.ts +86 -0
- package/packages/mcp/src/tools/diff-docs.ts +68 -0
- package/packages/mcp/src/tools/export-context.ts +73 -0
- package/packages/mcp/src/tools/find-api.ts +99 -0
- package/packages/mcp/src/tools/find-example.ts +100 -0
- package/packages/mcp/src/tools/find-pitfall.ts +94 -0
- package/packages/mcp/src/tools/find-recipe.ts +98 -0
- package/packages/mcp/src/tools/get-doc.ts +130 -0
- package/packages/mcp/src/tools/get-docs-map.ts +64 -0
- package/packages/mcp/src/tools/get-version.ts +118 -0
- package/packages/mcp/src/tools/implementation-context.ts +88 -0
- package/packages/mcp/src/tools/index.ts +59 -0
- package/packages/mcp/src/tools/list-sources.ts +85 -0
- package/packages/mcp/src/tools/search-docs.ts +123 -0
- package/packages/mcp/src/tools/types.ts +28 -0
- package/packages/mcp/src/transports/http.ts +256 -0
- package/packages/mcp/src/transports/stdio.ts +105 -0
- package/packages/mcp/src/transports/types.ts +6 -0
- package/packages/mcp/src/types.ts +102 -0
- package/packages/normalizer/src/example-indexer.ts +240 -0
- package/packages/normalizer/src/html.ts +253 -0
- package/packages/normalizer/src/index.ts +8 -0
- package/packages/normalizer/src/llms.ts +83 -0
- package/packages/normalizer/src/openapi/endpoint-parser.ts +406 -0
- package/packages/normalizer/src/openapi/schema-resolver.ts +111 -0
- package/packages/normalizer/src/openapi.ts +2 -0
- package/packages/normalizer/src/page.ts +184 -0
- package/packages/normalizer/src/pitfall-extractor.ts +190 -0
- package/packages/normalizer/src/slicer.ts +455 -0
- package/packages/retrieval/src/engine.ts +120 -0
- package/packages/retrieval/src/index.ts +7 -0
- package/packages/retrieval/src/intent.ts +43 -0
- package/packages/retrieval/src/packer.ts +145 -0
- package/packages/retrieval/src/recipe-engine.ts +313 -0
- package/packages/retrieval/src/scorer.ts +139 -0
- package/packages/retrieval/src/weights.ts +31 -0
- package/packages/security/src/annotations.ts +112 -0
- package/packages/security/src/index.ts +2 -0
- package/packages/security/src/ssrf.ts +153 -0
- package/packages/shared/src/errors.ts +53 -0
- package/packages/shared/src/hashing.ts +23 -0
- package/packages/shared/src/index.ts +3 -0
- package/packages/shared/src/types.ts +881 -0
- package/packages/storage/src/db.ts +72 -0
- package/packages/storage/src/index.ts +11 -0
- package/packages/storage/src/interfaces.ts +115 -0
- package/packages/storage/src/repositories/api-repository.ts +219 -0
- package/packages/storage/src/repositories/chunk-repository.ts +316 -0
- package/packages/storage/src/repositories/example-repository.ts +206 -0
- package/packages/storage/src/repositories/page-repository.ts +205 -0
- package/packages/storage/src/repositories/pitfall-repository.ts +188 -0
- package/packages/storage/src/repositories/source-repository.ts +205 -0
- package/packages/storage/src/repository.ts +256 -0
- package/packages/storage/src/schema.ts +269 -0
- package/packages/storage/src/search-tokens.ts +28 -0
- package/packages/verification/src/diff-engine.ts +258 -0
- package/packages/verification/src/extractor.ts +339 -0
- package/packages/verification/src/impact-scanner.ts +203 -0
- package/packages/verification/src/index.ts +5 -0
- package/packages/verification/src/services.ts +238 -0
- package/packages/verification/src/verifier.ts +375 -0
- package/packages/workspace/src/detector.ts +143 -0
- package/packages/workspace/src/ecosystems/cargo.ts +84 -0
- package/packages/workspace/src/ecosystems/composer.ts +42 -0
- package/packages/workspace/src/ecosystems/go.ts +54 -0
- package/packages/workspace/src/ecosystems/index.ts +34 -0
- package/packages/workspace/src/ecosystems/maven.ts +34 -0
- package/packages/workspace/src/ecosystems/npm.ts +83 -0
- package/packages/workspace/src/ecosystems/pub.ts +40 -0
- package/packages/workspace/src/ecosystems/pypi.ts +100 -0
- package/packages/workspace/src/ecosystems/rubygems.ts +30 -0
- package/packages/workspace/src/ecosystems/types.ts +18 -0
- package/packages/workspace/src/index.ts +5 -0
- package/packages/workspace/src/lockfile.ts +194 -0
- package/packages/workspace/src/resolver.ts +234 -0
- package/packages/workspace/src/semver.ts +259 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SecurityAnnotation } from '../../shared/src/index.ts';
|
|
2
|
+
|
|
3
|
+
const MULTILINE_INJECTION_PATTERNS = [
|
|
4
|
+
{ pattern: /<system\b[^>]*>([\s\S]*?)<\/system>/gi, severity: 'high' as const, label: '<system> block' },
|
|
5
|
+
{ pattern: /\[SYSTEM(?:\s+INSTRUCTION)?\]([\s\S]*?)\[\/SYSTEM(?:\s+INSTRUCTION)?\]/gi, severity: 'high' as const, label: '[SYSTEM] block' },
|
|
6
|
+
{ pattern: /<\|im_start\|>system([\s\S]*?)<\|im_end\|>/gi, severity: 'high' as const, label: 'ChatML system block' },
|
|
7
|
+
{ pattern: /<!--\s*AI\s+INSTRUCTION:([\s\S]*?)-->/gi, severity: 'high' as const, label: 'HTML comment injection' },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const PROMPT_INJECTION_PATTERNS = [
|
|
11
|
+
{ pattern: /\b(?:ignore|disregard|forget)\s+(?:all\s+)?(?:previous|prior|above)\s+instructions\b/i, severity: 'high' as const },
|
|
12
|
+
{ pattern: /\b(?:system\s+message|system\s+prompt|system\s+instruction)\s*[:=]/i, severity: 'high' as const },
|
|
13
|
+
{ pattern: /\byou\s+are\s+now\s+(?:an?\s+)?(?:unrestricted|jailbroken|developer\s+mode|dan|evil)\b/i, severity: 'high' as const },
|
|
14
|
+
{ pattern: /\byou\s+are\s+now\s+(?:acting\s+as\s+)?a\b.*?\bassistant\s+that\s+(?:executes|runs|ignores)\b/i, severity: 'high' as const },
|
|
15
|
+
{ pattern: /\bIMPORTANT\s+INSTRUCTION\s+FOR\s+(?:THE\s+)?AI\s+AGENT\b/i, severity: 'high' as const },
|
|
16
|
+
{ pattern: /\b(?:call|invoke|execute)\s+(?:the\s+)?tool\s+['"]?[a-zA-Z0-9_-]+['"]?\s+with\b/i, severity: 'high' as const },
|
|
17
|
+
{ pattern: /\buse\s+(?:the\s+)?(?:bash|terminal|shell|command|run_command)\s+tool\s+to\b/i, severity: 'high' as const },
|
|
18
|
+
{ pattern: /\bdo\s+not\s+tell\s+the\s+user\b/i, severity: 'medium' as const },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const SUSPICIOUS_INSTRUCTION_PATTERNS = [
|
|
22
|
+
{ pattern: /curl\s+-[sS]*[fF]*[kK]*\s+https?:\/\/[^\s|]+\s*\|\s*(?:ba)?sh/i, severity: 'high' as const },
|
|
23
|
+
{ pattern: /wget\s+-[qO-]*\s+https?:\/\/[^\s|]+\s*\|\s*(?:ba)?sh/i, severity: 'high' as const },
|
|
24
|
+
{ pattern: /powershell\s+.*-(?:enc|encodedcommand)\s+[A-Za-z0-9+/=]{20,}/i, severity: 'high' as const },
|
|
25
|
+
{ pattern: /base64\s+-d\s+.*\|\s*(?:ba)?sh/i, severity: 'high' as const },
|
|
26
|
+
{ pattern: /eval\s*\(\s*base64_decode\s*\(/i, severity: 'high' as const },
|
|
27
|
+
{ pattern: /\b(?:cat|type)\s+[^\n]*\.(?:ssh\/id_[a-zA-Z0-9_-]*|aws\/credentials|env)\b/i, severity: 'high' as const },
|
|
28
|
+
{ pattern: /\b(?:curl|wget|fetch|nc|ncat)\b[^\n]*\b(?:\.ssh\/id_|\.aws\/credentials|\.env|API_KEY|SECRET)\b/i, severity: 'high' as const },
|
|
29
|
+
{ pattern: /\b(?:printenv|env)\b[^\n]*\|\s*(?:curl|nc|wget|base64)/i, severity: 'high' as const },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const UNSAFE_LINK_PATTERNS = [
|
|
33
|
+
{ pattern: /\[[^\]]*\]\(((?:javascript|data|file):[\s\S]*?)\)(?:\.|\s|$)/i, severity: 'high' as const },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Scans documentation content for security hazards (prompt injection,
|
|
38
|
+
* malicious execution snippets, unsafe links, credential harvesting) and returns
|
|
39
|
+
* non-destructive annotations.
|
|
40
|
+
*
|
|
41
|
+
* Principle: External documentation is treated as untrusted data and structurally
|
|
42
|
+
* separated from agent instructions. Original documentation text is never altered.
|
|
43
|
+
*/
|
|
44
|
+
export function detectSecurityAnnotations(content: string): SecurityAnnotation[] {
|
|
45
|
+
const annotations: SecurityAnnotation[] = [];
|
|
46
|
+
if (!content) return annotations;
|
|
47
|
+
|
|
48
|
+
// 1. Multiline patterns across whole content
|
|
49
|
+
for (const rule of MULTILINE_INJECTION_PATTERNS) {
|
|
50
|
+
let match: RegExpExecArray | null;
|
|
51
|
+
const regex = new RegExp(rule.pattern.source, rule.pattern.flags);
|
|
52
|
+
while ((match = regex.exec(content)) !== null) {
|
|
53
|
+
const matchStart = match.index;
|
|
54
|
+
const lineNumber = content.slice(0, matchStart).split('\n').length;
|
|
55
|
+
annotations.push({
|
|
56
|
+
type: 'prompt_injection_suspected',
|
|
57
|
+
location: `Line ${lineNumber} (${rule.label})`,
|
|
58
|
+
evidence: match[0].slice(0, 150).trim(),
|
|
59
|
+
severity: rule.severity,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 2. Line-by-line checks
|
|
65
|
+
const lines = content.split('\n');
|
|
66
|
+
|
|
67
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
68
|
+
const line = lines[lineIdx];
|
|
69
|
+
const loc = `Line ${lineIdx + 1}`;
|
|
70
|
+
|
|
71
|
+
// Prompt injection checks
|
|
72
|
+
for (const rule of PROMPT_INJECTION_PATTERNS) {
|
|
73
|
+
const match = line.match(rule.pattern);
|
|
74
|
+
if (match) {
|
|
75
|
+
annotations.push({
|
|
76
|
+
type: 'prompt_injection_suspected',
|
|
77
|
+
location: loc,
|
|
78
|
+
evidence: match[0].trim(),
|
|
79
|
+
severity: rule.severity,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Suspicious shell execution / credential theft checks
|
|
85
|
+
for (const rule of SUSPICIOUS_INSTRUCTION_PATTERNS) {
|
|
86
|
+
const match = line.match(rule.pattern);
|
|
87
|
+
if (match) {
|
|
88
|
+
annotations.push({
|
|
89
|
+
type: 'suspicious_instruction',
|
|
90
|
+
location: loc,
|
|
91
|
+
evidence: match[0].trim(),
|
|
92
|
+
severity: rule.severity,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Unsafe link protocols check
|
|
98
|
+
for (const rule of UNSAFE_LINK_PATTERNS) {
|
|
99
|
+
const match = line.match(rule.pattern);
|
|
100
|
+
if (match) {
|
|
101
|
+
annotations.push({
|
|
102
|
+
type: 'unsafe_link',
|
|
103
|
+
location: loc,
|
|
104
|
+
evidence: match[1] || match[0],
|
|
105
|
+
severity: rule.severity,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return annotations;
|
|
112
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { lookup } from 'node:dns/promises';
|
|
2
|
+
import { isIP } from 'node:net';
|
|
3
|
+
import { SsrfError } from '../../shared/src/index.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether an IPv4 or IPv6 address is in a private, link-local, loopback,
|
|
7
|
+
* or cloud provider metadata range.
|
|
8
|
+
*/
|
|
9
|
+
export function isPrivateOrBlockedIp(ip: string): boolean {
|
|
10
|
+
// IPv4 check
|
|
11
|
+
if (isIP(ip) === 4) {
|
|
12
|
+
const parts = ip.split('.').map(Number);
|
|
13
|
+
if (parts.length !== 4 || parts.some(isNaN)) return true;
|
|
14
|
+
|
|
15
|
+
const [b0, b1, b2, b3] = parts;
|
|
16
|
+
|
|
17
|
+
// 0.0.0.0/8 (Current network)
|
|
18
|
+
if (b0 === 0) return true;
|
|
19
|
+
|
|
20
|
+
// 127.0.0.0/8 (Loopback)
|
|
21
|
+
if (b0 === 127) return true;
|
|
22
|
+
|
|
23
|
+
// 10.0.0.0/8 (RFC 1918 Private)
|
|
24
|
+
if (b0 === 10) return true;
|
|
25
|
+
|
|
26
|
+
// 172.16.0.0/12 (RFC 1918 Private: 172.16.0.0 - 172.31.255.255)
|
|
27
|
+
if (b0 === 172 && b1 >= 16 && b1 <= 31) return true;
|
|
28
|
+
|
|
29
|
+
// 192.168.0.0/16 (RFC 1918 Private)
|
|
30
|
+
if (b0 === 192 && b1 === 168) return true;
|
|
31
|
+
|
|
32
|
+
// 169.254.0.0/16 (Link-local & AWS/GCP/Azure instance metadata 169.254.169.254)
|
|
33
|
+
if (b0 === 169 && b1 === 254) return true;
|
|
34
|
+
|
|
35
|
+
// 100.64.0.0/10 (Carrier-grade NAT)
|
|
36
|
+
if (b0 === 100 && b1 >= 64 && b1 <= 127) return true;
|
|
37
|
+
|
|
38
|
+
// 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 (TEST-NET-1, 2, 3)
|
|
39
|
+
if (b0 === 192 && b1 === 0 && b2 === 2) return true;
|
|
40
|
+
if (b0 === 198 && b1 === 51 && b2 === 100) return true;
|
|
41
|
+
if (b0 === 203 && b1 === 0 && b2 === 113) return true;
|
|
42
|
+
|
|
43
|
+
// 224.0.0.0/4 (Multicast) & 240.0.0.0/4 (Reserved)
|
|
44
|
+
if (b0 >= 224) return true;
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// IPv6 check
|
|
50
|
+
if (isIP(ip) === 6) {
|
|
51
|
+
const normalized = ip.toLowerCase();
|
|
52
|
+
|
|
53
|
+
// ::1 (Loopback)
|
|
54
|
+
if (normalized === '::1' || normalized === '0:0:0:0:0:0:0:1') return true;
|
|
55
|
+
|
|
56
|
+
// :: (Unspecified)
|
|
57
|
+
if (normalized === '::' || normalized === '0:0:0:0:0:0:0:0') return true;
|
|
58
|
+
|
|
59
|
+
// fc00::/7 (Unique local addresses / private)
|
|
60
|
+
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
|
|
61
|
+
|
|
62
|
+
// fe80::/10 (Link-local unicast)
|
|
63
|
+
if (normalized.startsWith('fe8') || normalized.startsWith('fe9') || normalized.startsWith('fea') || normalized.startsWith('feb')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// IPv4-mapped IPv6 addresses (::ffff:x.x.x.x)
|
|
68
|
+
if (normalized.startsWith('::ffff:')) {
|
|
69
|
+
const ipv4Part = normalized.slice(7);
|
|
70
|
+
return isPrivateOrBlockedIp(ipv4Part);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return true; // Not a recognized IP, block by default
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SsrfValidationOptions {
|
|
80
|
+
allowLocalhostForTesting?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validates a target URL against SSRF threats:
|
|
85
|
+
* - Ensures valid HTTP/HTTPS protocol
|
|
86
|
+
* - Validates hostname does not resolve to private or loopback IP
|
|
87
|
+
*/
|
|
88
|
+
export async function validateTargetUrl(
|
|
89
|
+
urlStr: string,
|
|
90
|
+
options: SsrfValidationOptions = {}
|
|
91
|
+
): Promise<{ url: URL; resolvedIp: string }> {
|
|
92
|
+
let url: URL;
|
|
93
|
+
try {
|
|
94
|
+
url = new URL(urlStr);
|
|
95
|
+
} catch {
|
|
96
|
+
throw new SsrfError(`Malformed target URL: "${urlStr}"`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Protocol check
|
|
100
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
|
101
|
+
throw new SsrfError(`Disallowed protocol "${url.protocol}". Only http: and https: are allowed.`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const hostname = url.hostname.toLowerCase();
|
|
105
|
+
const cleanHost = (hostname.startsWith('[') && hostname.endsWith(']'))
|
|
106
|
+
? hostname.slice(1, -1)
|
|
107
|
+
: hostname;
|
|
108
|
+
|
|
109
|
+
// Handle explicit localhost / loopback strings
|
|
110
|
+
if (cleanHost === 'localhost' || cleanHost === '127.0.0.1' || cleanHost === '::1') {
|
|
111
|
+
if (options.allowLocalhostForTesting) {
|
|
112
|
+
return { url, resolvedIp: cleanHost === 'localhost' ? '127.0.0.1' : cleanHost };
|
|
113
|
+
}
|
|
114
|
+
throw new SsrfError(`Access to localhost is prohibited for security.`, '127.0.0.1', cleanHost);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// If the hostname itself is a literal IP address (IPv4 or IPv6)
|
|
118
|
+
if (isIP(cleanHost)) {
|
|
119
|
+
if (isPrivateOrBlockedIp(cleanHost)) {
|
|
120
|
+
if (options.allowLocalhostForTesting && (cleanHost === '127.0.0.1' || cleanHost === '::1')) {
|
|
121
|
+
return { url, resolvedIp: cleanHost };
|
|
122
|
+
}
|
|
123
|
+
throw new SsrfError(`Access to private/blocked IP address "${cleanHost}" is prohibited.`, cleanHost, cleanHost);
|
|
124
|
+
}
|
|
125
|
+
return { url, resolvedIp: cleanHost };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Resolve hostname via DNS
|
|
129
|
+
try {
|
|
130
|
+
const res = await lookup(cleanHost, { all: true });
|
|
131
|
+
if (!res || res.length === 0) {
|
|
132
|
+
throw new SsrfError(`Hostname "${cleanHost}" could not be resolved via DNS.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const record of res) {
|
|
136
|
+
if (isPrivateOrBlockedIp(record.address)) {
|
|
137
|
+
if (options.allowLocalhostForTesting && (record.address === '127.0.0.1' || record.address === '::1')) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
throw new SsrfError(
|
|
141
|
+
`Hostname "${cleanHost}" resolved to blocked/private IP address "${record.address}".`,
|
|
142
|
+
record.address,
|
|
143
|
+
cleanHost
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { url, resolvedIp: res[0].address };
|
|
149
|
+
} catch (err: unknown) {
|
|
150
|
+
if (err instanceof SsrfError) throw err;
|
|
151
|
+
throw new SsrfError(`DNS resolution failure for host "${cleanHost}": ${err instanceof Error ? err.message : String(err)}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class DocOrbitError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, code: string = 'DOCORBIT_ERROR') {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'DocOrbitError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SsrfError extends DocOrbitError {
|
|
12
|
+
ip?: string;
|
|
13
|
+
host?: string;
|
|
14
|
+
|
|
15
|
+
constructor(message: string, ip?: string, host?: string) {
|
|
16
|
+
super(message, 'SSRF_BLOCKED');
|
|
17
|
+
this.name = 'SsrfError';
|
|
18
|
+
this.ip = ip;
|
|
19
|
+
this.host = host;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FetchTimeoutError extends DocOrbitError {
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
|
|
26
|
+
constructor(message: string, timeoutMs?: number) {
|
|
27
|
+
super(message, 'FETCH_TIMEOUT');
|
|
28
|
+
this.name = 'FetchTimeoutError';
|
|
29
|
+
this.timeoutMs = timeoutMs;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PayloadTooLargeError extends DocOrbitError {
|
|
34
|
+
byteCount?: number;
|
|
35
|
+
maxBytes?: number;
|
|
36
|
+
|
|
37
|
+
constructor(message: string, byteCount?: number, maxBytes?: number) {
|
|
38
|
+
super(message, 'PAYLOAD_TOO_LARGE');
|
|
39
|
+
this.name = 'PayloadTooLargeError';
|
|
40
|
+
this.byteCount = byteCount;
|
|
41
|
+
this.maxBytes = maxBytes;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class ValidationError extends DocOrbitError {
|
|
46
|
+
validationDetails?: unknown;
|
|
47
|
+
|
|
48
|
+
constructor(message: string, validationDetails?: unknown) {
|
|
49
|
+
super(message, 'VALIDATION_FAILED');
|
|
50
|
+
this.name = 'ValidationError';
|
|
51
|
+
this.validationDetails = validationDetails;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export function computeSha256(content: string | Buffer): string {
|
|
4
|
+
return createHash('sha256').update(content).digest('hex');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function computeContentHash(content: string): string {
|
|
8
|
+
// Normalize line endings and line-by-line trailing whitespace for canonical stability
|
|
9
|
+
const normalized = content
|
|
10
|
+
.replace(/\r\n/g, '\n')
|
|
11
|
+
.replace(/\r/g, '\n')
|
|
12
|
+
.split('\n')
|
|
13
|
+
.map(line => line.trimEnd())
|
|
14
|
+
.join('\n')
|
|
15
|
+
.trim();
|
|
16
|
+
return computeSha256(normalized);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function estimateTokenCount(text: string): number {
|
|
20
|
+
// Heuristic: roughly ~4 characters per token for English and Markdown/code
|
|
21
|
+
if (!text) return 0;
|
|
22
|
+
return Math.ceil(text.length / 4);
|
|
23
|
+
}
|