safe-link-checker 1.0.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 +25 -0
- package/CONTRIBUTING.md +39 -0
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/SECURITY.md +20 -0
- package/dist/chunk-CS7EDB5I.js +1806 -0
- package/dist/chunk-CS7EDB5I.js.map +1 -0
- package/dist/cli.cjs +1899 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +1986 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +497 -0
- package/dist/index.d.ts +497 -0
- package/dist/index.js +156 -0
- package/dist/index.js.map +1 -0
- package/package.json +80 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SafeLinkChecker
|
|
3
|
+
* Copyright (c) 2026
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
type RiskLevel = 'SAFE' | 'SUSPICIOUS' | 'DANGEROUS';
|
|
9
|
+
type HttpsStatus = 'HTTPS' | 'HTTP_ONLY' | 'CERT_ERROR' | 'TIMEOUT' | 'UNREACHABLE' | 'SKIPPED';
|
|
10
|
+
type RedirectAnomalyKind = 'LOOP' | 'PROTOCOL_DOWNGRADE' | 'MAX_REDIRECTS_EXCEEDED';
|
|
11
|
+
type RiskCategory = 'domain' | 'certificate' | 'redirect' | 'content' | 'network' | 'provider' | 'browser' | 'email' | 'qr' | 'download' | 'behavior' | 'ai' | 'other';
|
|
12
|
+
type RiskSeverity = 'critical' | 'high' | 'medium' | 'low' | 'info';
|
|
13
|
+
type DecisionAction = 'allow' | 'warn' | 'review' | 'block';
|
|
14
|
+
interface VerifyOptions {
|
|
15
|
+
maxRedirects?: number;
|
|
16
|
+
timeout?: number;
|
|
17
|
+
customShorteners?: string[];
|
|
18
|
+
bypassCache?: boolean;
|
|
19
|
+
removeTrackingParams?: boolean;
|
|
20
|
+
checkHttps?: boolean;
|
|
21
|
+
signal?: AbortSignal;
|
|
22
|
+
policy?: string;
|
|
23
|
+
}
|
|
24
|
+
interface CheckResult {
|
|
25
|
+
name: string;
|
|
26
|
+
safe: boolean;
|
|
27
|
+
scoreImpact: number;
|
|
28
|
+
message: string;
|
|
29
|
+
weight?: number;
|
|
30
|
+
fatal?: boolean;
|
|
31
|
+
detector?: string;
|
|
32
|
+
category?: RiskCategory;
|
|
33
|
+
severity?: RiskSeverity;
|
|
34
|
+
confidence?: number;
|
|
35
|
+
scoreContribution?: number;
|
|
36
|
+
title?: string;
|
|
37
|
+
description?: string;
|
|
38
|
+
recommendation?: string;
|
|
39
|
+
references?: string[];
|
|
40
|
+
executionTime?: number;
|
|
41
|
+
timestamp?: number;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
interface Provider {
|
|
45
|
+
name: string;
|
|
46
|
+
check(url: string, options?: VerifyOptions): Promise<CheckResult | null>;
|
|
47
|
+
}
|
|
48
|
+
interface RedirectHop {
|
|
49
|
+
url: string;
|
|
50
|
+
statusCode: number;
|
|
51
|
+
}
|
|
52
|
+
interface RedirectTrace$1 {
|
|
53
|
+
chain: string[];
|
|
54
|
+
finalUrl: string;
|
|
55
|
+
redirectCount: number;
|
|
56
|
+
anomalies: RedirectAnomalyKind[];
|
|
57
|
+
}
|
|
58
|
+
interface ExecutionTimeline {
|
|
59
|
+
phase: string;
|
|
60
|
+
startTime: number;
|
|
61
|
+
durationMs: number;
|
|
62
|
+
status: 'success' | 'error' | 'skipped';
|
|
63
|
+
}
|
|
64
|
+
interface RichMetadata {
|
|
65
|
+
favicon?: string;
|
|
66
|
+
title?: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
openGraph?: Record<string, string>;
|
|
69
|
+
twitterCards?: Record<string, string>;
|
|
70
|
+
canonicalUrl?: string;
|
|
71
|
+
detectedBrand?: string;
|
|
72
|
+
detectedLanguage?: string;
|
|
73
|
+
contentType?: string;
|
|
74
|
+
server?: string;
|
|
75
|
+
country?: string;
|
|
76
|
+
hostingProvider?: string;
|
|
77
|
+
asn?: string;
|
|
78
|
+
}
|
|
79
|
+
interface ExecutionStats {
|
|
80
|
+
totalTimeMs: number;
|
|
81
|
+
startTime: number;
|
|
82
|
+
endTime: number;
|
|
83
|
+
}
|
|
84
|
+
interface VerificationResult {
|
|
85
|
+
url: string;
|
|
86
|
+
normalizedUrl: string;
|
|
87
|
+
safe: boolean;
|
|
88
|
+
score: number;
|
|
89
|
+
confidence: number;
|
|
90
|
+
riskLevel: RiskLevel;
|
|
91
|
+
reasons: string[];
|
|
92
|
+
recommendations: string[];
|
|
93
|
+
redirectChain: string[];
|
|
94
|
+
redirectTrace: RedirectTrace$1;
|
|
95
|
+
checks: CheckResult[];
|
|
96
|
+
fromCache: boolean;
|
|
97
|
+
decision?: DecisionAction;
|
|
98
|
+
trustScore?: number;
|
|
99
|
+
summary?: string;
|
|
100
|
+
action?: string;
|
|
101
|
+
policy?: string;
|
|
102
|
+
timeline?: ExecutionTimeline[];
|
|
103
|
+
evidence?: CheckResult[];
|
|
104
|
+
providerResults?: CheckResult[];
|
|
105
|
+
categories?: Record<string, number>;
|
|
106
|
+
execution?: ExecutionStats;
|
|
107
|
+
metadata?: RichMetadata | Record<string, unknown>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* SafeLinkChecker
|
|
112
|
+
* Copyright (c) 2026
|
|
113
|
+
*
|
|
114
|
+
* This source code is licensed under the MIT license found in the
|
|
115
|
+
* LICENSE file in the root directory of this source tree.
|
|
116
|
+
*/
|
|
117
|
+
|
|
118
|
+
declare function verifyLink(url: string, options?: VerifyOptions): Promise<VerificationResult>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* SafeLinkChecker
|
|
122
|
+
* Copyright (c) 2026
|
|
123
|
+
*
|
|
124
|
+
* This source code is licensed under the MIT license found in the
|
|
125
|
+
* LICENSE file in the root directory of this source tree.
|
|
126
|
+
*/
|
|
127
|
+
interface MetadataResult {
|
|
128
|
+
title?: string;
|
|
129
|
+
description?: string;
|
|
130
|
+
image?: string;
|
|
131
|
+
favicon?: string;
|
|
132
|
+
url?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* SafeLinkChecker
|
|
137
|
+
* Copyright (c) 2026
|
|
138
|
+
*
|
|
139
|
+
* This source code is licensed under the MIT license found in the
|
|
140
|
+
* LICENSE file in the root directory of this source tree.
|
|
141
|
+
*/
|
|
142
|
+
type EventCallback = (...args: unknown[]) => void;
|
|
143
|
+
declare class EventEmitter {
|
|
144
|
+
private listeners;
|
|
145
|
+
on(event: string, callback: EventCallback): void;
|
|
146
|
+
off(event: string, callback: EventCallback): void;
|
|
147
|
+
emit(event: string, arg1?: unknown, arg2?: unknown): void;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* SafeLinkChecker
|
|
152
|
+
* Copyright (c) 2026
|
|
153
|
+
*
|
|
154
|
+
* This source code is licensed under the MIT license found in the
|
|
155
|
+
* LICENSE file in the root directory of this source tree.
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
type PluginType = 'network' | 'content' | 'heuristic' | 'provider';
|
|
159
|
+
interface RedirectTrace {
|
|
160
|
+
finalUrl: string;
|
|
161
|
+
redirectCount: number;
|
|
162
|
+
chain: string[];
|
|
163
|
+
anomalies: RedirectAnomalyKind[];
|
|
164
|
+
}
|
|
165
|
+
interface PluginState {
|
|
166
|
+
finalUrl?: string;
|
|
167
|
+
isShortener?: boolean;
|
|
168
|
+
redirectTrace?: RedirectTrace;
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
}
|
|
171
|
+
interface PluginContext {
|
|
172
|
+
url: string;
|
|
173
|
+
normalizedUrl: string;
|
|
174
|
+
options: VerifyOptions;
|
|
175
|
+
state: PluginState;
|
|
176
|
+
}
|
|
177
|
+
interface VerificationPlugin {
|
|
178
|
+
id: string;
|
|
179
|
+
name: string;
|
|
180
|
+
version: string;
|
|
181
|
+
description: string;
|
|
182
|
+
author: string;
|
|
183
|
+
type: PluginType;
|
|
184
|
+
capabilities: string[];
|
|
185
|
+
priority: number;
|
|
186
|
+
weight?: number;
|
|
187
|
+
/**
|
|
188
|
+
* Initialization hook (e.g. connecting to DB, setting up local models)
|
|
189
|
+
*/
|
|
190
|
+
initialize?(): Promise<void>;
|
|
191
|
+
/**
|
|
192
|
+
* Main execution hook.
|
|
193
|
+
* Returns a CheckResult or null if the plugin decides to skip.
|
|
194
|
+
*/
|
|
195
|
+
execute(ctx: PluginContext): Promise<CheckResult | null>;
|
|
196
|
+
/**
|
|
197
|
+
* Optional teardown hook
|
|
198
|
+
*/
|
|
199
|
+
dispose?(): Promise<void>;
|
|
200
|
+
/**
|
|
201
|
+
* Optional health check hook
|
|
202
|
+
*/
|
|
203
|
+
health?(): Promise<boolean>;
|
|
204
|
+
}
|
|
205
|
+
declare class PluginManager {
|
|
206
|
+
private plugins;
|
|
207
|
+
register(plugin: VerificationPlugin): void;
|
|
208
|
+
initializeAll(): Promise<void>;
|
|
209
|
+
disposeAll(): Promise<void>;
|
|
210
|
+
getPluginsByType(type: PluginType): VerificationPlugin[];
|
|
211
|
+
getAll(): VerificationPlugin[];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* SafeLinkChecker
|
|
216
|
+
* Copyright (c) 2026
|
|
217
|
+
*
|
|
218
|
+
* This source code is licensed under the MIT license found in the
|
|
219
|
+
* LICENSE file in the root directory of this source tree.
|
|
220
|
+
*/
|
|
221
|
+
|
|
222
|
+
interface ConsensusConfig {
|
|
223
|
+
baseScore: number;
|
|
224
|
+
riskThresholds: {
|
|
225
|
+
suspicious: number;
|
|
226
|
+
dangerous: number;
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
interface ConsensusResult {
|
|
230
|
+
score: number;
|
|
231
|
+
trustScore: number;
|
|
232
|
+
confidence: number;
|
|
233
|
+
riskLevel: RiskLevel;
|
|
234
|
+
reasons: string[];
|
|
235
|
+
safe: boolean;
|
|
236
|
+
summary: string;
|
|
237
|
+
}
|
|
238
|
+
declare class ConsensusEngine {
|
|
239
|
+
private config;
|
|
240
|
+
constructor(config?: Partial<ConsensusConfig>);
|
|
241
|
+
evaluate(results: (CheckResult | null)[]): ConsensusResult;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* SafeLinkChecker
|
|
246
|
+
* Copyright (c) 2026
|
|
247
|
+
*
|
|
248
|
+
* This source code is licensed under the MIT license found in the
|
|
249
|
+
* LICENSE file in the root directory of this source tree.
|
|
250
|
+
*/
|
|
251
|
+
|
|
252
|
+
interface PolicyContext {
|
|
253
|
+
riskLevel: RiskLevel;
|
|
254
|
+
score: number;
|
|
255
|
+
confidence: number;
|
|
256
|
+
}
|
|
257
|
+
interface PolicyResult {
|
|
258
|
+
decision: DecisionAction;
|
|
259
|
+
action: string;
|
|
260
|
+
}
|
|
261
|
+
type PolicyDefinition = (ctx: PolicyContext) => PolicyResult;
|
|
262
|
+
declare class PolicyEngine {
|
|
263
|
+
private policies;
|
|
264
|
+
constructor();
|
|
265
|
+
private registerBuiltIns;
|
|
266
|
+
register(name: string, policy: PolicyDefinition): void;
|
|
267
|
+
evaluate(policyName: string | undefined, ctx: PolicyContext): PolicyResult;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* SafeLinkChecker
|
|
272
|
+
* Copyright (c) 2026
|
|
273
|
+
*
|
|
274
|
+
* This source code is licensed under the MIT license found in the
|
|
275
|
+
* LICENSE file in the root directory of this source tree.
|
|
276
|
+
*/
|
|
277
|
+
|
|
278
|
+
interface CheckerOptions extends VerifyOptions {
|
|
279
|
+
mode?: 'local' | 'cloud';
|
|
280
|
+
apiKey?: string;
|
|
281
|
+
endpoint?: string;
|
|
282
|
+
cache?: boolean | {
|
|
283
|
+
get(url: string): VerificationResult | null;
|
|
284
|
+
set(url: string, result: VerificationResult): void;
|
|
285
|
+
};
|
|
286
|
+
providers?: (Provider | 'openphish' | 'urlhaus')[];
|
|
287
|
+
onStart?: (url: string) => void;
|
|
288
|
+
onComplete?: (result: VerificationResult) => void;
|
|
289
|
+
onError?: (error: Error, url: string) => void;
|
|
290
|
+
}
|
|
291
|
+
declare class SafeLinkError extends Error {
|
|
292
|
+
constructor(message: string);
|
|
293
|
+
}
|
|
294
|
+
declare class TimeoutError extends SafeLinkError {
|
|
295
|
+
constructor(message: string);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The main orchestrator for validating URLs.
|
|
299
|
+
* Allows configuration of caching, timeouts, concurrent provider plugins, and batch processing.
|
|
300
|
+
*/
|
|
301
|
+
declare class SafeLinkChecker extends EventEmitter {
|
|
302
|
+
private cache;
|
|
303
|
+
private metadataCache;
|
|
304
|
+
private options;
|
|
305
|
+
pluginManager: PluginManager;
|
|
306
|
+
consensusEngine: ConsensusEngine;
|
|
307
|
+
policyEngine: PolicyEngine;
|
|
308
|
+
constructor(options?: CheckerOptions);
|
|
309
|
+
/**
|
|
310
|
+
* Adds a legacy Provider or a new VerificationPlugin to the checker.
|
|
311
|
+
*/
|
|
312
|
+
use(plugin: Provider | VerificationPlugin): this;
|
|
313
|
+
getMetadata(url: string): Promise<MetadataResult | null>;
|
|
314
|
+
/**
|
|
315
|
+
* Verifies a single URL through the core engine and any registered providers.
|
|
316
|
+
* Caches results if configured.
|
|
317
|
+
*
|
|
318
|
+
* @param url The URL to check.
|
|
319
|
+
* @param runtimeOptions Options overriding the global checker options for this specific call.
|
|
320
|
+
* @returns A detailed VerificationResult including the final risk score.
|
|
321
|
+
*/
|
|
322
|
+
verify(url: string, runtimeOptions?: VerifyOptions): Promise<VerificationResult>;
|
|
323
|
+
private verifyCloud;
|
|
324
|
+
private verifyLocal;
|
|
325
|
+
/**
|
|
326
|
+
* Concurrently verifies multiple URLs with a bounded concurrency limit.
|
|
327
|
+
* Results are returned in the exact same order as the input array.
|
|
328
|
+
*
|
|
329
|
+
* @param urls Array of URLs to verify.
|
|
330
|
+
* @param runtimeOptions Options overriding the global checker options.
|
|
331
|
+
* @param concurrency The maximum number of concurrent verifications (defaults to 5).
|
|
332
|
+
* @returns Array of VerificationResult corresponding to the input URLs.
|
|
333
|
+
*/
|
|
334
|
+
verifyLinks(urls: string[], runtimeOptions?: VerifyOptions, concurrency?: number): Promise<VerificationResult[]>;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* SafeLinkChecker
|
|
339
|
+
* Copyright (c) 2026
|
|
340
|
+
*
|
|
341
|
+
* This source code is licensed under the MIT license found in the
|
|
342
|
+
* LICENSE file in the root directory of this source tree.
|
|
343
|
+
*/
|
|
344
|
+
declare function normalizeLink(url: string, options?: {
|
|
345
|
+
removeTrackingParams?: boolean;
|
|
346
|
+
}): string;
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* SafeLinkChecker
|
|
350
|
+
* Copyright (c) 2026
|
|
351
|
+
*
|
|
352
|
+
* This source code is licensed under the MIT license found in the
|
|
353
|
+
* LICENSE file in the root directory of this source tree.
|
|
354
|
+
*/
|
|
355
|
+
interface CacheOptions {
|
|
356
|
+
maxSize?: number;
|
|
357
|
+
ttlMs?: number;
|
|
358
|
+
}
|
|
359
|
+
declare class LRUCache<T> {
|
|
360
|
+
private cache;
|
|
361
|
+
private maxSize;
|
|
362
|
+
private ttlMs;
|
|
363
|
+
constructor(options?: CacheOptions);
|
|
364
|
+
get(url: string): T | null;
|
|
365
|
+
set(url: string, result: T): void;
|
|
366
|
+
clear(): void;
|
|
367
|
+
get size(): number;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* SafeLinkChecker
|
|
372
|
+
* Copyright (c) 2026
|
|
373
|
+
*
|
|
374
|
+
* This source code is licensed under the MIT license found in the
|
|
375
|
+
* LICENSE file in the root directory of this source tree.
|
|
376
|
+
*/
|
|
377
|
+
|
|
378
|
+
declare const defaultCache: LRUCache<VerificationResult>;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* SafeLinkChecker
|
|
382
|
+
* Copyright (c) 2026
|
|
383
|
+
*
|
|
384
|
+
* This source code is licensed under the MIT license found in the
|
|
385
|
+
* LICENSE file in the root directory of this source tree.
|
|
386
|
+
*/
|
|
387
|
+
|
|
388
|
+
declare function validateUrl(urlStr: string): CheckResult;
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* SafeLinkChecker
|
|
392
|
+
* Copyright (c) 2026
|
|
393
|
+
*
|
|
394
|
+
* This source code is licensed under the MIT license found in the
|
|
395
|
+
* LICENSE file in the root directory of this source tree.
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Probes the URL for HTTPS availability and certificate validity.
|
|
400
|
+
* Returns a CheckResult that is always fulfilled — never rejects.
|
|
401
|
+
*
|
|
402
|
+
* Score impact guide:
|
|
403
|
+
* HTTPS → 0 (good)
|
|
404
|
+
* HTTP_ONLY → 20 (SUSPICIOUS — no encryption)
|
|
405
|
+
* CERT_ERROR → 40 (DANGEROUS — cert problem, MITM risk)
|
|
406
|
+
* TIMEOUT → 0 (ambiguous; don't penalise for slow servers)
|
|
407
|
+
* UNREACHABLE → 0 (ambiguous; server may be fine — just unreachable from CI)
|
|
408
|
+
* SKIPPED → 0 (opt-out)
|
|
409
|
+
*/
|
|
410
|
+
declare function validateHttps(urlStr: string, timeoutMs?: number, signal?: AbortSignal): Promise<CheckResult>;
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* SafeLinkChecker
|
|
414
|
+
* Copyright (c) 2026
|
|
415
|
+
*
|
|
416
|
+
* This source code is licensed under the MIT license found in the
|
|
417
|
+
* LICENSE file in the root directory of this source tree.
|
|
418
|
+
*/
|
|
419
|
+
|
|
420
|
+
declare function validateIp(urlStr: string): CheckResult;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* SafeLinkChecker
|
|
424
|
+
* Copyright (c) 2026
|
|
425
|
+
*
|
|
426
|
+
* This source code is licensed under the MIT license found in the
|
|
427
|
+
* LICENSE file in the root directory of this source tree.
|
|
428
|
+
*/
|
|
429
|
+
|
|
430
|
+
declare function validatePunycode(urlStr: string): CheckResult;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* SafeLinkChecker
|
|
434
|
+
* Copyright (c) 2026
|
|
435
|
+
*
|
|
436
|
+
* This source code is licensed under the MIT license found in the
|
|
437
|
+
* LICENSE file in the root directory of this source tree.
|
|
438
|
+
*/
|
|
439
|
+
|
|
440
|
+
declare function validateShortener(urlStr: string, customShorteners?: string[]): CheckResult;
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* SafeLinkChecker
|
|
444
|
+
* Copyright (c) 2026
|
|
445
|
+
*
|
|
446
|
+
* This source code is licensed under the MIT license found in the
|
|
447
|
+
* LICENSE file in the root directory of this source tree.
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
declare function validateHeuristics(url: string): CheckResult;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* SafeLinkChecker
|
|
454
|
+
* Copyright (c) 2026
|
|
455
|
+
*
|
|
456
|
+
* This source code is licensed under the MIT license found in the
|
|
457
|
+
* LICENSE file in the root directory of this source tree.
|
|
458
|
+
*/
|
|
459
|
+
|
|
460
|
+
declare function traceRedirects(urlStr: string, options?: VerifyOptions): Promise<RedirectTrace$1>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* SafeLinkChecker
|
|
464
|
+
* Copyright (c) 2026
|
|
465
|
+
*
|
|
466
|
+
* This source code is licensed under the MIT license found in the
|
|
467
|
+
* LICENSE file in the root directory of this source tree.
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
declare class URLHausProvider implements Provider {
|
|
471
|
+
name: string;
|
|
472
|
+
private cache;
|
|
473
|
+
private offlineDataset;
|
|
474
|
+
private datasetLoaded;
|
|
475
|
+
private updateInterval;
|
|
476
|
+
init(): Promise<void>;
|
|
477
|
+
private updateDataset;
|
|
478
|
+
check(url: string, options?: VerifyOptions): Promise<CheckResult | null>;
|
|
479
|
+
private doCheckOnline;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* SafeLinkChecker
|
|
484
|
+
* Copyright (c) 2026
|
|
485
|
+
*
|
|
486
|
+
* This source code is licensed under the MIT license found in the
|
|
487
|
+
* LICENSE file in the root directory of this source tree.
|
|
488
|
+
*/
|
|
489
|
+
|
|
490
|
+
declare class OpenPhishProvider implements Provider {
|
|
491
|
+
name: string;
|
|
492
|
+
private cache;
|
|
493
|
+
check(url: string, options?: VerifyOptions): Promise<CheckResult | null>;
|
|
494
|
+
private doCheck;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export { type CheckResult, type CheckerOptions, type DecisionAction, type ExecutionStats, type ExecutionTimeline, type HttpsStatus, LRUCache, LRUCache as MemoryCache, type MetadataResult, OpenPhishProvider, type Provider, type RedirectAnomalyKind, type RedirectHop, type RedirectTrace$1 as RedirectTrace, type RichMetadata, type RiskCategory, type RiskLevel, type RiskSeverity, SafeLinkChecker, SafeLinkError, TimeoutError, URLHausProvider, type VerificationResult, type VerifyOptions, defaultCache, normalizeLink, traceRedirects, validateHeuristics, validateHttps, validateIp, validatePunycode, validateShortener, validateUrl, verifyLink };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { normalizeLink, validateUrl, validateShortener, validateIp, validateHeuristics, traceRedirects, validatePunycode, validateHttps } from './chunk-CS7EDB5I.js';
|
|
2
|
+
export { LRUCache, LRUCache as MemoryCache, OpenPhishProvider, SafeLinkChecker, SafeLinkError, TimeoutError, URLHausProvider, defaultCache, normalizeLink, traceRedirects, validateHeuristics, validateHttps, validateIp, validatePunycode, validateShortener, validateUrl } from './chunk-CS7EDB5I.js';
|
|
3
|
+
|
|
4
|
+
// src/utils/score.ts
|
|
5
|
+
function calculateScore(validators) {
|
|
6
|
+
let totalPenalty = 0;
|
|
7
|
+
let safe = true;
|
|
8
|
+
const reasons = [];
|
|
9
|
+
const recommendations = [];
|
|
10
|
+
for (const v of validators) {
|
|
11
|
+
if (!v.safe) {
|
|
12
|
+
safe = false;
|
|
13
|
+
if (v.message) {
|
|
14
|
+
reasons.push(`[${v.name}] ${v.message}`);
|
|
15
|
+
}
|
|
16
|
+
switch (v.name) {
|
|
17
|
+
case "IP Validator":
|
|
18
|
+
recommendations.push("Avoid interacting with URLs that resolve to private or local network addresses.");
|
|
19
|
+
break;
|
|
20
|
+
case "Punycode Validator":
|
|
21
|
+
recommendations.push("Be cautious of potential homograph attacks; visually inspect the domain name.");
|
|
22
|
+
break;
|
|
23
|
+
case "HTTPS Validator":
|
|
24
|
+
recommendations.push("Prefer links that use secure HTTPS connections to protect your data.");
|
|
25
|
+
break;
|
|
26
|
+
case "Shortener Validator":
|
|
27
|
+
recommendations.push("URL shorteners can mask malicious destinations; verify the expanded URL before trusting it.");
|
|
28
|
+
break;
|
|
29
|
+
case "Heuristics Validator":
|
|
30
|
+
recommendations.push("The URL contains suspicious patterns, excessive subdomains, or lookalike domain traits; verify the source carefully.");
|
|
31
|
+
break;
|
|
32
|
+
case "URL Validator":
|
|
33
|
+
recommendations.push("Ensure the URL is correctly formatted and uses a supported protocol (http/https).");
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const weight = v.weight ?? 1;
|
|
38
|
+
totalPenalty += v.scoreImpact * weight;
|
|
39
|
+
}
|
|
40
|
+
let score = 100 - totalPenalty;
|
|
41
|
+
if (score < 0) {
|
|
42
|
+
score = 0;
|
|
43
|
+
} else if (score > 100) {
|
|
44
|
+
score = 100;
|
|
45
|
+
}
|
|
46
|
+
let riskLevel = "SAFE";
|
|
47
|
+
if (score <= 49) {
|
|
48
|
+
riskLevel = "DANGEROUS";
|
|
49
|
+
} else if (score <= 89) {
|
|
50
|
+
riskLevel = "SUSPICIOUS";
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
score,
|
|
54
|
+
riskLevel,
|
|
55
|
+
safe,
|
|
56
|
+
reasons,
|
|
57
|
+
recommendations: [...new Set(recommendations)]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/verify.ts
|
|
62
|
+
async function verifyLink(url, options = {}) {
|
|
63
|
+
const normalizedUrl = normalizeLink(url, options);
|
|
64
|
+
const urlVal = validateUrl(normalizedUrl);
|
|
65
|
+
if (!urlVal.safe) {
|
|
66
|
+
const scoreInfo2 = calculateScore([urlVal]);
|
|
67
|
+
return {
|
|
68
|
+
url,
|
|
69
|
+
normalizedUrl,
|
|
70
|
+
safe: scoreInfo2.safe,
|
|
71
|
+
score: scoreInfo2.score,
|
|
72
|
+
confidence: 100,
|
|
73
|
+
riskLevel: scoreInfo2.riskLevel,
|
|
74
|
+
reasons: scoreInfo2.reasons,
|
|
75
|
+
recommendations: scoreInfo2.recommendations,
|
|
76
|
+
redirectChain: [],
|
|
77
|
+
redirectTrace: { chain: [], finalUrl: normalizedUrl, redirectCount: 0, anomalies: [] },
|
|
78
|
+
checks: [urlVal],
|
|
79
|
+
fromCache: false
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const shortVal = validateShortener(normalizedUrl, options.customShorteners);
|
|
83
|
+
const isShortener = shortVal.metadata?.isShortener === true;
|
|
84
|
+
const initialIpVal = validateIp(normalizedUrl);
|
|
85
|
+
if (!initialIpVal.safe) {
|
|
86
|
+
const heurVal2 = validateHeuristics(normalizedUrl);
|
|
87
|
+
const checks2 = [urlVal, shortVal, initialIpVal, heurVal2];
|
|
88
|
+
const scoreInfo2 = calculateScore(checks2);
|
|
89
|
+
return {
|
|
90
|
+
url,
|
|
91
|
+
normalizedUrl,
|
|
92
|
+
safe: scoreInfo2.safe,
|
|
93
|
+
score: scoreInfo2.score,
|
|
94
|
+
confidence: 100,
|
|
95
|
+
riskLevel: scoreInfo2.riskLevel,
|
|
96
|
+
reasons: scoreInfo2.reasons,
|
|
97
|
+
recommendations: scoreInfo2.recommendations,
|
|
98
|
+
redirectChain: [],
|
|
99
|
+
redirectTrace: { chain: [], finalUrl: normalizedUrl, redirectCount: 0, anomalies: [] },
|
|
100
|
+
checks: checks2,
|
|
101
|
+
fromCache: false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const redirectTrace = await traceRedirects(normalizedUrl, options);
|
|
105
|
+
const targetUrl = isShortener ? redirectTrace.finalUrl : normalizedUrl;
|
|
106
|
+
let ipVal = initialIpVal;
|
|
107
|
+
if (isShortener && targetUrl !== normalizedUrl) {
|
|
108
|
+
ipVal = validateIp(targetUrl);
|
|
109
|
+
if (!ipVal.safe) {
|
|
110
|
+
const heurVal2 = validateHeuristics(targetUrl);
|
|
111
|
+
const checks2 = [urlVal, shortVal, ipVal, heurVal2];
|
|
112
|
+
const scoreInfo2 = calculateScore(checks2);
|
|
113
|
+
return {
|
|
114
|
+
url,
|
|
115
|
+
normalizedUrl,
|
|
116
|
+
safe: scoreInfo2.safe,
|
|
117
|
+
score: scoreInfo2.score,
|
|
118
|
+
confidence: 100,
|
|
119
|
+
riskLevel: scoreInfo2.riskLevel,
|
|
120
|
+
reasons: scoreInfo2.reasons,
|
|
121
|
+
recommendations: scoreInfo2.recommendations,
|
|
122
|
+
redirectChain: redirectTrace.chain,
|
|
123
|
+
redirectTrace,
|
|
124
|
+
checks: checks2,
|
|
125
|
+
fromCache: false
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const punyVal = validatePunycode(targetUrl);
|
|
130
|
+
const heurVal = validateHeuristics(targetUrl);
|
|
131
|
+
let checks = [urlVal, shortVal, ipVal, punyVal, heurVal];
|
|
132
|
+
if (options.checkHttps !== false) {
|
|
133
|
+
const timeout = options.timeout ?? 5e3;
|
|
134
|
+
const httpsVal = await validateHttps(targetUrl, timeout, options.signal);
|
|
135
|
+
checks.push(httpsVal);
|
|
136
|
+
}
|
|
137
|
+
const scoreInfo = calculateScore(checks);
|
|
138
|
+
return {
|
|
139
|
+
url,
|
|
140
|
+
normalizedUrl,
|
|
141
|
+
safe: scoreInfo.safe,
|
|
142
|
+
score: scoreInfo.score,
|
|
143
|
+
confidence: 100,
|
|
144
|
+
riskLevel: scoreInfo.riskLevel,
|
|
145
|
+
reasons: scoreInfo.reasons,
|
|
146
|
+
recommendations: scoreInfo.recommendations,
|
|
147
|
+
redirectChain: redirectTrace.chain,
|
|
148
|
+
redirectTrace,
|
|
149
|
+
checks,
|
|
150
|
+
fromCache: false
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export { verifyLink };
|
|
155
|
+
//# sourceMappingURL=index.js.map
|
|
156
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/score.ts","../src/verify.ts"],"names":["scoreInfo","heurVal","checks"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * SafeLinkChecker\n * Copyright (c) 2026\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { RiskLevel, CheckResult } from '../types/index.js';\n\nexport function calculateScore(validators: CheckResult[]): {\n score: number;\n riskLevel: RiskLevel;\n safe: boolean;\n reasons: string[];\n recommendations: string[];\n} {\n let totalPenalty = 0;\n let safe = true;\n const reasons: string[] = [];\n const recommendations: string[] = [];\n\n for (const v of validators) {\n if (!v.safe) {\n safe = false;\n if (v.message) {\n reasons.push(`[${v.name}] ${v.message}`);\n }\n\n // Generate standard recommendations based on the validator that flagged the URL\n switch (v.name) {\n case 'IP Validator':\n recommendations.push('Avoid interacting with URLs that resolve to private or local network addresses.');\n break;\n case 'Punycode Validator':\n recommendations.push('Be cautious of potential homograph attacks; visually inspect the domain name.');\n break;\n case 'HTTPS Validator':\n recommendations.push('Prefer links that use secure HTTPS connections to protect your data.');\n break;\n case 'Shortener Validator':\n recommendations.push('URL shorteners can mask malicious destinations; verify the expanded URL before trusting it.');\n break;\n case 'Heuristics Validator':\n recommendations.push('The URL contains suspicious patterns, excessive subdomains, or lookalike domain traits; verify the source carefully.');\n break;\n case 'URL Validator':\n recommendations.push('Ensure the URL is correctly formatted and uses a supported protocol (http/https).');\n break;\n }\n }\n\n // Apply weighted scoring. Default weight is 1 if not specified by the validator.\n const weight = v.weight ?? 1;\n totalPenalty += v.scoreImpact * weight;\n }\n\n // Calculate final score based on a starting score of 100 minus the weighted penalty sum\n let score = 100 - totalPenalty;\n if (score < 0) {\n score = 0;\n } else if (score > 100) {\n score = 100;\n }\n\n let riskLevel: RiskLevel = 'SAFE';\n if (score <= 49) {\n riskLevel = 'DANGEROUS';\n } else if (score <= 89) {\n riskLevel = 'SUSPICIOUS';\n }\n\n return {\n score,\n riskLevel,\n safe,\n reasons,\n recommendations: [...new Set(recommendations)],\n };\n}\n","/**\n * SafeLinkChecker\n * Copyright (c) 2026\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n\nimport type { VerifyOptions, VerificationResult } from './types/index.js';\nimport { normalizeLink } from './utils/normalize.js';\nimport { calculateScore } from './utils/score.js';\nimport { validateUrl } from './validators/url.js';\nimport { validateIp } from './validators/ip.js';\nimport { validateHttps } from './validators/https.js';\nimport { traceRedirects } from './validators/redirect.js';\nimport { validatePunycode } from './validators/punycode.js';\nimport { validateShortener } from './validators/shortener.js';\nimport { validateHeuristics } from './validators/heuristic.js';\n\nexport async function verifyLink(\n url: string,\n options: VerifyOptions = {}\n): Promise<VerificationResult> {\n const normalizedUrl = normalizeLink(url, options);\n const urlVal = validateUrl(normalizedUrl);\n\n // Short-circuit: malformed / unsupported-protocol URLs skip all further checks\n if (!urlVal.safe) {\n const scoreInfo = calculateScore([urlVal]);\n return {\n url,\n normalizedUrl,\n safe: scoreInfo.safe,\n score: scoreInfo.score, confidence: 100,\n riskLevel: scoreInfo.riskLevel,\n reasons: scoreInfo.reasons,\n recommendations: scoreInfo.recommendations,\n redirectChain: [],\n redirectTrace: { chain: [], finalUrl: normalizedUrl, redirectCount: 0, anomalies: [] },\n checks: [urlVal],\n fromCache: false,\n };\n }\n\n const shortVal = validateShortener(normalizedUrl, options.customShorteners);\n const isShortener = shortVal.metadata?.isShortener === true;\n\n const initialIpVal = validateIp(normalizedUrl);\n\n // If the host is private/local, skip the outbound redirect trace\n if (!initialIpVal.safe) {\n const heurVal = validateHeuristics(normalizedUrl);\n const checks = [urlVal, shortVal, initialIpVal, heurVal];\n const scoreInfo = calculateScore(checks);\n return {\n url,\n normalizedUrl,\n safe: scoreInfo.safe,\n score: scoreInfo.score, confidence: 100,\n riskLevel: scoreInfo.riskLevel,\n reasons: scoreInfo.reasons,\n recommendations: scoreInfo.recommendations,\n redirectChain: [],\n redirectTrace: { chain: [], finalUrl: normalizedUrl, redirectCount: 0, anomalies: [] },\n checks,\n fromCache: false,\n };\n }\n\n const redirectTrace = await traceRedirects(normalizedUrl, options);\n \n // If it's a shortener, we score the final expanded URL. Otherwise we score the initial URL.\n const targetUrl = isShortener ? redirectTrace.finalUrl : normalizedUrl;\n \n let ipVal = initialIpVal;\n if (isShortener && targetUrl !== normalizedUrl) {\n ipVal = validateIp(targetUrl);\n if (!ipVal.safe) {\n // The shortener resolved to a local IP\n const heurVal = validateHeuristics(targetUrl);\n const checks = [urlVal, shortVal, ipVal, heurVal];\n const scoreInfo = calculateScore(checks);\n return {\n url,\n normalizedUrl,\n safe: scoreInfo.safe,\n score: scoreInfo.score, confidence: 100,\n riskLevel: scoreInfo.riskLevel,\n reasons: scoreInfo.reasons,\n recommendations: scoreInfo.recommendations,\n redirectChain: redirectTrace.chain,\n redirectTrace,\n checks,\n fromCache: false,\n };\n }\n }\n\n const punyVal = validatePunycode(targetUrl);\n const heurVal = validateHeuristics(targetUrl);\n let checks = [urlVal, shortVal, ipVal, punyVal, heurVal];\n\n // HTTPS check — opt-out via checkHttps:false; defaults to enabled\n if (options.checkHttps !== false) {\n const timeout = options.timeout ?? 5000;\n const httpsVal = await validateHttps(targetUrl, timeout, options.signal);\n checks.push(httpsVal);\n }\n\n const scoreInfo = calculateScore(checks);\n\n return {\n url,\n normalizedUrl,\n safe: scoreInfo.safe,\n score: scoreInfo.score, confidence: 100,\n riskLevel: scoreInfo.riskLevel,\n reasons: scoreInfo.reasons,\n recommendations: scoreInfo.recommendations,\n redirectChain: redirectTrace.chain,\n redirectTrace,\n checks,\n fromCache: false,\n };\n}\n\n\n"]}
|