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.
@@ -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"]}