pagerts 0.4.1 → 1.0.2
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/bin/main.js +6 -27
- package/bin/main.js.map +4 -4
- package/package.json +6 -2
- package/.github/codeql/codeql-config.yml +0 -7
- package/.github/workflows/ci.yml +0 -146
- package/.github/workflows/dependency-update.yml +0 -52
- package/.prettierignore +0 -5
- package/.prettierrc.json +0 -10
- package/MAINTAINERS.md +0 -30
- package/POST-INSTALL.md +0 -205
- package/SECURITY.md +0 -160
- package/eslint.config.mjs +0 -83
- package/jest.config.cjs +0 -213
- package/src/__tests__/PageFetcher.test.ts +0 -48
- package/src/__tests__/security.test.ts +0 -153
- package/src/extractors/AbstractExtractor.ts +0 -4
- package/src/extractors/PageExtractor.ts +0 -21
- package/src/extractors/ResourceExtractor.ts +0 -31
- package/src/extractors/TagExtractor.ts +0 -13
- package/src/extractors/index.ts +0 -4
- package/src/main.ts +0 -71
- package/src/page/Page.ts +0 -24
- package/src/page/PageFetcher.ts +0 -81
- package/src/page/index.ts +0 -3
- package/src/printers/AbstractResourcePrinter.ts +0 -6
- package/src/printers/JSONStylePrinter.ts +0 -9
- package/src/printers/LogStylePrinter.ts +0 -30
- package/src/printers/index.ts +0 -3
- package/src/resource.ts +0 -88
- package/src/security.ts +0 -184
- package/tsconfig.eslint.json +0 -5
- package/tsconfig.json +0 -28
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PageMetadata } from '../page/index.js';
|
|
2
|
-
import { AbstractResourcePrinter } from './AbstractResourcePrinter.js';
|
|
3
|
-
|
|
4
|
-
export class JSONStylePrinter extends AbstractResourcePrinter {
|
|
5
|
-
print(...pages: PageMetadata[]): void | Promise<void> {
|
|
6
|
-
const json = JSON.stringify(pages);
|
|
7
|
-
process.stdout.write(json + '\n');
|
|
8
|
-
}
|
|
9
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { isPage, type PageMetadata } from '../page/index.js';
|
|
2
|
-
import { AbstractResourcePrinter } from './AbstractResourcePrinter.js';
|
|
3
|
-
|
|
4
|
-
export class LogStylePrinter extends AbstractResourcePrinter {
|
|
5
|
-
write(str: string): void {
|
|
6
|
-
process.stdout.write(str);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async print(...pages: PageMetadata[]): Promise<void> {
|
|
10
|
-
for (const page of pages) {
|
|
11
|
-
if (!isPage(page)) {
|
|
12
|
-
this.write(page.error);
|
|
13
|
-
continue;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const { resources, title, url } = page;
|
|
17
|
-
|
|
18
|
-
this.write(`Title: ${title}\n`);
|
|
19
|
-
this.write(`URL: ${url}\n\n`);
|
|
20
|
-
|
|
21
|
-
for (const resource of resources) {
|
|
22
|
-
const {
|
|
23
|
-
link: { url },
|
|
24
|
-
text: { value },
|
|
25
|
-
} = resource;
|
|
26
|
-
this.write(`${value}: ${url}\n`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
package/src/printers/index.ts
DELETED
package/src/resource.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* We are interested in visualising a page as a collection of tags.
|
|
4
|
-
*
|
|
5
|
-
* We wish to work with tags that can be compactly previewed on a webpage.
|
|
6
|
-
* Here we must declare all of the element types that can be used to represent
|
|
7
|
-
* a resource that can be hyperlinked off a webpage.
|
|
8
|
-
*/
|
|
9
|
-
type Tags = HTMLElementTagNameMap;
|
|
10
|
-
|
|
11
|
-
function findDefinedKey(element: Resource, keys: LinkKey[]): LinkKey | undefined {
|
|
12
|
-
for (const key of keys) {
|
|
13
|
-
if (isKeyDefined(key, element)) {
|
|
14
|
-
return key;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
return undefined;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export const RESOURCE_DISPLAYABLE_KEYS = [
|
|
22
|
-
'id',
|
|
23
|
-
'innerText',
|
|
24
|
-
'textContent',
|
|
25
|
-
'class',
|
|
26
|
-
'ariaLabel',
|
|
27
|
-
'ariaDescription',
|
|
28
|
-
'alt',
|
|
29
|
-
] as const;
|
|
30
|
-
|
|
31
|
-
export type DisplayableKey = (typeof RESOURCE_DISPLAYABLE_KEYS)[number];
|
|
32
|
-
|
|
33
|
-
export type ResourceKey = {
|
|
34
|
-
key: DisplayableKey;
|
|
35
|
-
value: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export const RESOURCE_LINK_KEYS = ['href', 'data-src', 'target', 'action', 'src', 'url'] as const;
|
|
39
|
-
|
|
40
|
-
export type LinkKey = (typeof RESOURCE_LINK_KEYS)[number];
|
|
41
|
-
|
|
42
|
-
export type ResourceLink = {
|
|
43
|
-
key: LinkKey;
|
|
44
|
-
url: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export function findResourceText(element: Resource): ResourceKey | undefined {
|
|
48
|
-
for (const key of RESOURCE_DISPLAYABLE_KEYS) {
|
|
49
|
-
const value = element[key];
|
|
50
|
-
if (value && typeof value === 'string' && value.trim() !== '') return { key, value };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function findResourceLink(element: Resource): ResourceLink | undefined {
|
|
57
|
-
const key = findDefinedKey(element, [...RESOURCE_LINK_KEYS]);
|
|
58
|
-
if (!key) {
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const url = element[key];
|
|
63
|
-
if (url && typeof url === 'string' && url.trim() !== '') return { key, url };
|
|
64
|
-
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export type ExternalResource = {
|
|
69
|
-
text: ResourceKey;
|
|
70
|
-
link: ResourceLink;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const isResourceKey = (key: string): key is LinkKey => key in RESOURCE_LINK_KEYS;
|
|
74
|
-
|
|
75
|
-
export const isKeyDefined = (key: DisplayableKey | LinkKey, element: Resource): boolean =>
|
|
76
|
-
key in element && element[key] !== undefined;
|
|
77
|
-
|
|
78
|
-
export type ResourceElement<T, U> = {
|
|
79
|
-
[K in keyof T]: U extends keyof T[K] ? T[K] : never;
|
|
80
|
-
}[keyof T];
|
|
81
|
-
|
|
82
|
-
export type Tag = keyof Tags;
|
|
83
|
-
|
|
84
|
-
export type Resource = HTMLElement & {
|
|
85
|
-
[K in DisplayableKey | LinkKey]?: string | null;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export type ResourceByName<T extends keyof Tags> = Tags[T];
|
package/src/security.ts
DELETED
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Security utilities for URL validation and sanitization
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
const ALLOWED_PROTOCOLS = ['http:', 'https:', 'file:'];
|
|
6
|
-
const MAX_URL_LENGTH = 2048;
|
|
7
|
-
const SUSPICIOUS_PATTERNS = [
|
|
8
|
-
/javascript:/i,
|
|
9
|
-
/data:/i,
|
|
10
|
-
/vbscript:/i,
|
|
11
|
-
/<script/i,
|
|
12
|
-
/on\w+=/i, // Event handlers like onclick=
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
export interface ValidationResult {
|
|
16
|
-
isValid: boolean;
|
|
17
|
-
error?: string;
|
|
18
|
-
sanitizedUrl?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Validates a URL for security concerns
|
|
23
|
-
* @param url - The URL to validate
|
|
24
|
-
* @returns ValidationResult object with validation status
|
|
25
|
-
*/
|
|
26
|
-
export function validateUrl(url: string): ValidationResult {
|
|
27
|
-
// Check if URL is empty or whitespace
|
|
28
|
-
if (!url || !url.trim()) {
|
|
29
|
-
return {
|
|
30
|
-
isValid: false,
|
|
31
|
-
error: 'URL cannot be empty',
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const trimmedUrl = url.trim();
|
|
36
|
-
|
|
37
|
-
// Check URL length to prevent DoS
|
|
38
|
-
if (trimmedUrl.length > MAX_URL_LENGTH) {
|
|
39
|
-
return {
|
|
40
|
-
isValid: false,
|
|
41
|
-
error: `URL exceeds maximum length of ${MAX_URL_LENGTH} characters`,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Check for suspicious patterns
|
|
46
|
-
for (const pattern of SUSPICIOUS_PATTERNS) {
|
|
47
|
-
if (pattern.test(trimmedUrl)) {
|
|
48
|
-
return {
|
|
49
|
-
isValid: false,
|
|
50
|
-
error: 'URL contains suspicious patterns',
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Parse the URL
|
|
56
|
-
let parsedUrl: URL;
|
|
57
|
-
try {
|
|
58
|
-
parsedUrl = new URL(trimmedUrl);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
// If URL parsing fails, it might be a file path
|
|
61
|
-
if (trimmedUrl.startsWith('file://')) {
|
|
62
|
-
return {
|
|
63
|
-
isValid: true,
|
|
64
|
-
sanitizedUrl: trimmedUrl,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
return {
|
|
68
|
-
isValid: false,
|
|
69
|
-
error: 'Invalid URL format',
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check protocol
|
|
74
|
-
if (!ALLOWED_PROTOCOLS.includes(parsedUrl.protocol)) {
|
|
75
|
-
return {
|
|
76
|
-
isValid: false,
|
|
77
|
-
error: `Protocol ${parsedUrl.protocol} is not allowed. Allowed protocols: ${ALLOWED_PROTOCOLS.join(', ')}`,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Check for localhost/internal IPs in production (security consideration)
|
|
82
|
-
const hostname = parsedUrl.hostname.toLowerCase();
|
|
83
|
-
const isLocalhost =
|
|
84
|
-
hostname === 'localhost' ||
|
|
85
|
-
hostname === '127.0.0.1' ||
|
|
86
|
-
hostname === '::1' ||
|
|
87
|
-
hostname.startsWith('192.168.') ||
|
|
88
|
-
hostname.startsWith('10.') ||
|
|
89
|
-
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname);
|
|
90
|
-
|
|
91
|
-
if (isLocalhost && parsedUrl.protocol !== 'file:') {
|
|
92
|
-
// Allow but warn about localhost URLs
|
|
93
|
-
console.warn(`Warning: Accessing local network resource: ${trimmedUrl}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return {
|
|
97
|
-
isValid: true,
|
|
98
|
-
sanitizedUrl: parsedUrl.toString(),
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Validates an array of URLs
|
|
104
|
-
* @param urls - Array of URLs to validate
|
|
105
|
-
* @returns Object with valid URLs and errors
|
|
106
|
-
*/
|
|
107
|
-
export function validateUrls(urls: string[]): {
|
|
108
|
-
validUrls: string[];
|
|
109
|
-
errors: Array<{ url: string; error: string }>;
|
|
110
|
-
} {
|
|
111
|
-
const validUrls: string[] = [];
|
|
112
|
-
const errors: Array<{ url: string; error: string }> = [];
|
|
113
|
-
|
|
114
|
-
for (const url of urls) {
|
|
115
|
-
const result = validateUrl(url);
|
|
116
|
-
if (result.isValid && result.sanitizedUrl) {
|
|
117
|
-
validUrls.push(result.sanitizedUrl);
|
|
118
|
-
} else {
|
|
119
|
-
errors.push({
|
|
120
|
-
url,
|
|
121
|
-
error: result.error || 'Unknown validation error',
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return { validUrls, errors };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Rate limiter to prevent abuse
|
|
131
|
-
*/
|
|
132
|
-
export class RateLimiter {
|
|
133
|
-
private requests: number[] = [];
|
|
134
|
-
private readonly maxRequests: number;
|
|
135
|
-
private readonly windowMs: number;
|
|
136
|
-
|
|
137
|
-
constructor(maxRequests = 10, windowMs = 60000) {
|
|
138
|
-
this.maxRequests = maxRequests;
|
|
139
|
-
this.windowMs = windowMs;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Check if a request is allowed under rate limiting
|
|
144
|
-
* @returns true if request is allowed, false otherwise
|
|
145
|
-
*/
|
|
146
|
-
public isAllowed(): boolean {
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
|
|
149
|
-
// Remove old requests outside the time window
|
|
150
|
-
this.requests = this.requests.filter((time) => now - time < this.windowMs);
|
|
151
|
-
|
|
152
|
-
if (this.requests.length >= this.maxRequests) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
this.requests.push(now);
|
|
157
|
-
return true;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Get remaining requests in current window
|
|
162
|
-
*/
|
|
163
|
-
public getRemainingRequests(): number {
|
|
164
|
-
const now = Date.now();
|
|
165
|
-
this.requests = this.requests.filter((time) => now - time < this.windowMs);
|
|
166
|
-
return Math.max(0, this.maxRequests - this.requests.length);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Sanitizes HTML content to prevent XSS attacks
|
|
172
|
-
* @param text - Text to sanitize
|
|
173
|
-
* @returns Sanitized text
|
|
174
|
-
*/
|
|
175
|
-
export function sanitizeText(text: string): string {
|
|
176
|
-
if (!text) return '';
|
|
177
|
-
|
|
178
|
-
return text
|
|
179
|
-
.replace(/</g, '<')
|
|
180
|
-
.replace(/>/g, '>')
|
|
181
|
-
.replace(/"/g, '"')
|
|
182
|
-
.replace(/'/g, ''')
|
|
183
|
-
.replace(/\//g, '/');
|
|
184
|
-
}
|
package/tsconfig.eslint.json
DELETED
package/tsconfig.json
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "NodeNext",
|
|
4
|
-
"target": "ES2022",
|
|
5
|
-
"lib": ["ES2022"],
|
|
6
|
-
"moduleResolution": "NodeNext",
|
|
7
|
-
"resolveJsonModule": true,
|
|
8
|
-
"outDir": "bin",
|
|
9
|
-
"sourceMap": true,
|
|
10
|
-
"strict": true,
|
|
11
|
-
"noImplicitAny": true,
|
|
12
|
-
"strictNullChecks": true,
|
|
13
|
-
"strictFunctionTypes": true,
|
|
14
|
-
"strictBindCallApply": true,
|
|
15
|
-
"strictPropertyInitialization": true,
|
|
16
|
-
"noImplicitThis": true,
|
|
17
|
-
"alwaysStrict": true,
|
|
18
|
-
"noUnusedLocals": true,
|
|
19
|
-
"noUnusedParameters": true,
|
|
20
|
-
"noImplicitReturns": true,
|
|
21
|
-
"noFallthroughCasesInSwitch": true,
|
|
22
|
-
"esModuleInterop": true,
|
|
23
|
-
"skipLibCheck": true,
|
|
24
|
-
"forceConsistentCasingInFileNames": true
|
|
25
|
-
},
|
|
26
|
-
"include": ["src/**/*"],
|
|
27
|
-
"exclude": ["node_modules", "bin", "coverage", "**/*.test.ts", "**/*.spec.ts"]
|
|
28
|
-
}
|