@crawlith/core 0.1.0 → 0.1.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.
Files changed (238) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +70 -0
  3. package/dist/analysis/analysis_list.html +35 -0
  4. package/dist/analysis/analysis_page.html +123 -0
  5. package/dist/analysis/analyze.d.ts +40 -5
  6. package/dist/analysis/analyze.js +395 -347
  7. package/dist/analysis/clustering.d.ts +23 -0
  8. package/dist/analysis/clustering.js +206 -0
  9. package/dist/analysis/content.d.ts +1 -1
  10. package/dist/analysis/content.js +11 -5
  11. package/dist/analysis/duplicate.d.ts +34 -0
  12. package/dist/analysis/duplicate.js +305 -0
  13. package/dist/analysis/heading.d.ts +116 -0
  14. package/dist/analysis/heading.js +356 -0
  15. package/dist/analysis/images.d.ts +1 -1
  16. package/dist/analysis/images.js +6 -5
  17. package/dist/analysis/links.d.ts +1 -1
  18. package/dist/analysis/links.js +8 -8
  19. package/dist/{scoring/orphanSeverity.d.ts → analysis/orphan.d.ts} +12 -23
  20. package/dist/{scoring/orphanSeverity.js → analysis/orphan.js} +9 -3
  21. package/dist/analysis/scoring.js +11 -2
  22. package/dist/analysis/seo.d.ts +8 -4
  23. package/dist/analysis/seo.js +41 -30
  24. package/dist/analysis/soft404.d.ts +17 -0
  25. package/dist/analysis/soft404.js +62 -0
  26. package/dist/analysis/structuredData.d.ts +1 -1
  27. package/dist/analysis/structuredData.js +5 -4
  28. package/dist/analysis/templates.d.ts +2 -0
  29. package/dist/analysis/templates.js +7 -0
  30. package/dist/application/index.d.ts +2 -0
  31. package/dist/application/index.js +2 -0
  32. package/dist/application/usecase.d.ts +3 -0
  33. package/dist/application/usecase.js +1 -0
  34. package/dist/application/usecases.d.ts +114 -0
  35. package/dist/application/usecases.js +201 -0
  36. package/dist/audit/index.js +1 -1
  37. package/dist/audit/transport.d.ts +1 -1
  38. package/dist/audit/transport.js +5 -4
  39. package/dist/audit/types.d.ts +1 -0
  40. package/dist/constants.d.ts +17 -0
  41. package/dist/constants.js +23 -0
  42. package/dist/core/scope/scopeManager.js +3 -0
  43. package/dist/core/security/ipGuard.d.ts +11 -0
  44. package/dist/core/security/ipGuard.js +71 -3
  45. package/dist/crawler/crawl.d.ts +4 -22
  46. package/dist/crawler/crawl.js +4 -335
  47. package/dist/crawler/crawler.d.ts +87 -0
  48. package/dist/crawler/crawler.js +683 -0
  49. package/dist/crawler/extract.d.ts +4 -1
  50. package/dist/crawler/extract.js +7 -2
  51. package/dist/crawler/fetcher.d.ts +2 -1
  52. package/dist/crawler/fetcher.js +26 -11
  53. package/dist/crawler/metricsRunner.d.ts +23 -1
  54. package/dist/crawler/metricsRunner.js +202 -72
  55. package/dist/crawler/normalize.d.ts +41 -0
  56. package/dist/crawler/normalize.js +119 -3
  57. package/dist/crawler/parser.d.ts +1 -3
  58. package/dist/crawler/parser.js +2 -49
  59. package/dist/crawler/resolver.d.ts +11 -0
  60. package/dist/crawler/resolver.js +67 -0
  61. package/dist/crawler/sitemap.d.ts +6 -0
  62. package/dist/crawler/sitemap.js +27 -17
  63. package/dist/crawler/trap.d.ts +5 -1
  64. package/dist/crawler/trap.js +23 -2
  65. package/dist/db/CrawlithDB.d.ts +110 -0
  66. package/dist/db/CrawlithDB.js +500 -0
  67. package/dist/db/graphLoader.js +42 -30
  68. package/dist/db/index.d.ts +11 -0
  69. package/dist/db/index.js +41 -29
  70. package/dist/db/migrations.d.ts +2 -0
  71. package/dist/db/{schema.js → migrations.js} +90 -43
  72. package/dist/db/pluginRegistry.d.ts +9 -0
  73. package/dist/db/pluginRegistry.js +19 -0
  74. package/dist/db/repositories/EdgeRepository.d.ts +13 -0
  75. package/dist/db/repositories/EdgeRepository.js +20 -0
  76. package/dist/db/repositories/MetricsRepository.d.ts +16 -8
  77. package/dist/db/repositories/MetricsRepository.js +28 -7
  78. package/dist/db/repositories/PageRepository.d.ts +15 -2
  79. package/dist/db/repositories/PageRepository.js +169 -25
  80. package/dist/db/repositories/SiteRepository.d.ts +9 -0
  81. package/dist/db/repositories/SiteRepository.js +13 -0
  82. package/dist/db/repositories/SnapshotRepository.d.ts +14 -5
  83. package/dist/db/repositories/SnapshotRepository.js +64 -5
  84. package/dist/db/reset.d.ts +9 -0
  85. package/dist/db/reset.js +32 -0
  86. package/dist/db/statements.d.ts +12 -0
  87. package/dist/db/statements.js +40 -0
  88. package/dist/diff/compare.d.ts +0 -5
  89. package/dist/diff/compare.js +0 -12
  90. package/dist/diff/service.d.ts +16 -0
  91. package/dist/diff/service.js +41 -0
  92. package/dist/domain/index.d.ts +4 -0
  93. package/dist/domain/index.js +4 -0
  94. package/dist/events.d.ts +56 -0
  95. package/dist/events.js +1 -0
  96. package/dist/graph/graph.d.ts +36 -42
  97. package/dist/graph/graph.js +26 -17
  98. package/dist/graph/hits.d.ts +23 -0
  99. package/dist/graph/hits.js +111 -0
  100. package/dist/graph/metrics.d.ts +0 -4
  101. package/dist/graph/metrics.js +25 -9
  102. package/dist/graph/pagerank.d.ts +17 -4
  103. package/dist/graph/pagerank.js +126 -91
  104. package/dist/graph/simhash.d.ts +6 -0
  105. package/dist/graph/simhash.js +14 -0
  106. package/dist/index.d.ts +29 -8
  107. package/dist/index.js +29 -8
  108. package/dist/lock/hashKey.js +1 -1
  109. package/dist/lock/lockManager.d.ts +5 -1
  110. package/dist/lock/lockManager.js +38 -13
  111. package/dist/plugin-system/plugin-cli.d.ts +10 -0
  112. package/dist/plugin-system/plugin-cli.js +31 -0
  113. package/dist/plugin-system/plugin-config.d.ts +16 -0
  114. package/dist/plugin-system/plugin-config.js +36 -0
  115. package/dist/plugin-system/plugin-loader.d.ts +17 -0
  116. package/dist/plugin-system/plugin-loader.js +122 -0
  117. package/dist/plugin-system/plugin-registry.d.ts +25 -0
  118. package/dist/plugin-system/plugin-registry.js +167 -0
  119. package/dist/plugin-system/plugin-types.d.ts +205 -0
  120. package/dist/plugin-system/plugin-types.js +1 -0
  121. package/dist/ports/index.d.ts +9 -0
  122. package/dist/ports/index.js +1 -0
  123. package/{src/report/sitegraph_template.ts → dist/report/crawl.html} +330 -81
  124. package/dist/report/crawlExport.d.ts +3 -0
  125. package/dist/report/{sitegraphExport.js → crawlExport.js} +3 -3
  126. package/dist/report/crawl_template.d.ts +1 -0
  127. package/dist/report/crawl_template.js +7 -0
  128. package/dist/report/export.d.ts +3 -0
  129. package/dist/report/export.js +81 -0
  130. package/dist/report/html.js +15 -216
  131. package/dist/report/insight.d.ts +27 -0
  132. package/dist/report/insight.js +103 -0
  133. package/dist/scoring/health.d.ts +56 -0
  134. package/dist/scoring/health.js +213 -0
  135. package/dist/utils/chalk.d.ts +6 -0
  136. package/dist/utils/chalk.js +41 -0
  137. package/dist/utils/secureConfig.d.ts +23 -0
  138. package/dist/utils/secureConfig.js +128 -0
  139. package/package.json +12 -6
  140. package/CHANGELOG.md +0 -7
  141. package/dist/db/schema.d.ts +0 -2
  142. package/dist/graph/cluster.d.ts +0 -6
  143. package/dist/graph/cluster.js +0 -173
  144. package/dist/graph/duplicate.d.ts +0 -10
  145. package/dist/graph/duplicate.js +0 -251
  146. package/dist/report/sitegraphExport.d.ts +0 -3
  147. package/dist/report/sitegraph_template.d.ts +0 -1
  148. package/dist/report/sitegraph_template.js +0 -630
  149. package/dist/scoring/hits.d.ts +0 -9
  150. package/dist/scoring/hits.js +0 -111
  151. package/src/analysis/analyze.ts +0 -548
  152. package/src/analysis/content.ts +0 -62
  153. package/src/analysis/images.ts +0 -28
  154. package/src/analysis/links.ts +0 -41
  155. package/src/analysis/scoring.ts +0 -59
  156. package/src/analysis/seo.ts +0 -82
  157. package/src/analysis/structuredData.ts +0 -62
  158. package/src/audit/dns.ts +0 -49
  159. package/src/audit/headers.ts +0 -98
  160. package/src/audit/index.ts +0 -66
  161. package/src/audit/scoring.ts +0 -232
  162. package/src/audit/transport.ts +0 -258
  163. package/src/audit/types.ts +0 -102
  164. package/src/core/network/proxyAdapter.ts +0 -21
  165. package/src/core/network/rateLimiter.ts +0 -39
  166. package/src/core/network/redirectController.ts +0 -47
  167. package/src/core/network/responseLimiter.ts +0 -34
  168. package/src/core/network/retryPolicy.ts +0 -57
  169. package/src/core/scope/domainFilter.ts +0 -45
  170. package/src/core/scope/scopeManager.ts +0 -52
  171. package/src/core/scope/subdomainPolicy.ts +0 -39
  172. package/src/core/security/ipGuard.ts +0 -92
  173. package/src/crawler/crawl.ts +0 -382
  174. package/src/crawler/extract.ts +0 -34
  175. package/src/crawler/fetcher.ts +0 -233
  176. package/src/crawler/metricsRunner.ts +0 -124
  177. package/src/crawler/normalize.ts +0 -108
  178. package/src/crawler/parser.ts +0 -190
  179. package/src/crawler/sitemap.ts +0 -73
  180. package/src/crawler/trap.ts +0 -96
  181. package/src/db/graphLoader.ts +0 -105
  182. package/src/db/index.ts +0 -70
  183. package/src/db/repositories/EdgeRepository.ts +0 -29
  184. package/src/db/repositories/MetricsRepository.ts +0 -49
  185. package/src/db/repositories/PageRepository.ts +0 -128
  186. package/src/db/repositories/SiteRepository.ts +0 -32
  187. package/src/db/repositories/SnapshotRepository.ts +0 -74
  188. package/src/db/schema.ts +0 -177
  189. package/src/diff/compare.ts +0 -84
  190. package/src/graph/cluster.ts +0 -192
  191. package/src/graph/duplicate.ts +0 -286
  192. package/src/graph/graph.ts +0 -172
  193. package/src/graph/metrics.ts +0 -110
  194. package/src/graph/pagerank.ts +0 -125
  195. package/src/graph/simhash.ts +0 -61
  196. package/src/index.ts +0 -30
  197. package/src/lock/hashKey.ts +0 -51
  198. package/src/lock/lockManager.ts +0 -124
  199. package/src/lock/pidCheck.ts +0 -13
  200. package/src/report/html.ts +0 -227
  201. package/src/report/sitegraphExport.ts +0 -58
  202. package/src/scoring/hits.ts +0 -131
  203. package/src/scoring/orphanSeverity.ts +0 -176
  204. package/src/utils/version.ts +0 -18
  205. package/tests/__snapshots__/orphanSeverity.test.ts.snap +0 -49
  206. package/tests/analysis.unit.test.ts +0 -98
  207. package/tests/analyze.integration.test.ts +0 -98
  208. package/tests/audit/dns.test.ts +0 -31
  209. package/tests/audit/headers.test.ts +0 -45
  210. package/tests/audit/scoring.test.ts +0 -133
  211. package/tests/audit/security.test.ts +0 -12
  212. package/tests/audit/transport.test.ts +0 -112
  213. package/tests/clustering.test.ts +0 -118
  214. package/tests/crawler.test.ts +0 -358
  215. package/tests/db.test.ts +0 -159
  216. package/tests/diff.test.ts +0 -67
  217. package/tests/duplicate.test.ts +0 -110
  218. package/tests/fetcher.test.ts +0 -106
  219. package/tests/fetcher_safety.test.ts +0 -85
  220. package/tests/fixtures/analyze-crawl.json +0 -26
  221. package/tests/hits.test.ts +0 -134
  222. package/tests/html_report.test.ts +0 -58
  223. package/tests/lock/lockManager.test.ts +0 -138
  224. package/tests/metrics.test.ts +0 -196
  225. package/tests/normalize.test.ts +0 -101
  226. package/tests/orphanSeverity.test.ts +0 -160
  227. package/tests/pagerank.test.ts +0 -98
  228. package/tests/parser.test.ts +0 -117
  229. package/tests/proxy_safety.test.ts +0 -57
  230. package/tests/redirect_safety.test.ts +0 -73
  231. package/tests/safety.test.ts +0 -114
  232. package/tests/scope.test.ts +0 -66
  233. package/tests/scoring.test.ts +0 -59
  234. package/tests/sitemap.test.ts +0 -88
  235. package/tests/soft404.test.ts +0 -41
  236. package/tests/trap.test.ts +0 -39
  237. package/tests/visualization_data.test.ts +0 -46
  238. package/tsconfig.json +0 -11
@@ -1,34 +0,0 @@
1
- import { Readable } from 'stream';
2
-
3
- export class ResponseLimiter {
4
- static async streamToString(
5
- stream: Readable,
6
- maxBytes: number,
7
- onOversized?: (bytes: number) => void
8
- ): Promise<string> {
9
- return new Promise((resolve, reject) => {
10
- let accumulated = 0;
11
- const chunks: Buffer[] = [];
12
-
13
- stream.on('data', (chunk: any) => {
14
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
15
- accumulated += buffer.length;
16
- if (accumulated > maxBytes) {
17
- stream.destroy();
18
- if (onOversized) onOversized(accumulated);
19
- reject(new Error('Oversized response'));
20
- return;
21
- }
22
- chunks.push(buffer);
23
- });
24
-
25
- stream.on('end', () => {
26
- resolve(Buffer.concat(chunks).toString('utf-8'));
27
- });
28
-
29
- stream.on('error', (err) => {
30
- reject(err);
31
- });
32
- });
33
- }
34
- }
@@ -1,57 +0,0 @@
1
- export interface RetryConfig {
2
- maxRetries: number;
3
- baseDelay: number;
4
- }
5
-
6
- export class RetryPolicy {
7
- static DEFAULT_CONFIG: RetryConfig = {
8
- maxRetries: 3,
9
- baseDelay: 500
10
- };
11
-
12
- static async execute<T>(
13
- operation: (attempt: number) => Promise<T>,
14
- isRetryable: (error: any) => boolean,
15
- config: RetryConfig = RetryPolicy.DEFAULT_CONFIG
16
- ): Promise<T> {
17
- let lastError: any;
18
-
19
- for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
20
- try {
21
- return await operation(attempt);
22
- } catch (error) {
23
- lastError = error;
24
-
25
- if (attempt === config.maxRetries || !isRetryable(error)) {
26
- throw error;
27
- }
28
-
29
- const delay = config.baseDelay * Math.pow(2, attempt);
30
- const jitter = delay * 0.1 * (Math.random() * 2 - 1);
31
- const finalDelay = Math.max(0, delay + jitter);
32
-
33
- await new Promise(resolve => setTimeout(resolve, finalDelay));
34
- }
35
- }
36
-
37
- throw lastError;
38
- }
39
-
40
- static isRetryableStatus(status: number): boolean {
41
- return status === 429 || (status >= 500 && status <= 599);
42
- }
43
-
44
- static isNetworkError(error: any): boolean {
45
- const code = error?.code || error?.cause?.code;
46
- return [
47
- 'ETIMEDOUT',
48
- 'ECONNRESET',
49
- 'EADDRINUSE',
50
- 'ECONNREFUSED',
51
- 'EPIPE',
52
- 'ENOTFOUND',
53
- 'ENETUNREACH',
54
- 'EAI_AGAIN'
55
- ].includes(code);
56
- }
57
- }
@@ -1,45 +0,0 @@
1
- export class DomainFilter {
2
- private allowed: Set<string>;
3
- private denied: Set<string>;
4
-
5
- constructor(allowed: string[] = [], denied: string[] = []) {
6
- this.allowed = new Set(allowed.map(d => this.normalize(d)));
7
- this.denied = new Set(denied.map(d => this.normalize(d)));
8
- }
9
-
10
- /**
11
- * Normalizes a hostname: lowercase, strip trailing dot.
12
- * Note: We expect hostnames, not URLs.
13
- */
14
- private normalize(hostname: string): string {
15
- let h = hostname.toLowerCase().trim();
16
- if (h.endsWith('.')) {
17
- h = h.slice(0, -1);
18
- }
19
- // Use URL to handle punycode and basic validation if possible
20
- try {
21
- // We wrap it in a dummy URL to let the browser/node logic normalize it
22
- const url = new URL(`http://${h}`);
23
- return url.hostname;
24
- } catch {
25
- return h;
26
- }
27
- }
28
-
29
- isAllowed(hostname: string): boolean {
30
- const normalized = this.normalize(hostname);
31
-
32
- // 1. Deny list match -> Reject
33
- if (this.denied.has(normalized)) {
34
- return false;
35
- }
36
-
37
- // 2. Allow list not empty AND no match -> Reject
38
- if (this.allowed.size > 0 && !this.allowed.has(normalized)) {
39
- return false;
40
- }
41
-
42
- // 3. Otherwise -> Allow
43
- return true;
44
- }
45
- }
@@ -1,52 +0,0 @@
1
- import { DomainFilter } from './domainFilter.js';
2
- import { SubdomainPolicy } from './subdomainPolicy.js';
3
-
4
- export interface ScopeOptions {
5
- allowedDomains?: string[];
6
- deniedDomains?: string[];
7
- includeSubdomains?: boolean;
8
- rootUrl: string;
9
- }
10
-
11
- export type EligibilityResult = 'allowed' | 'blocked_by_domain_filter' | 'blocked_subdomain';
12
-
13
- export class ScopeManager {
14
- private domainFilter: DomainFilter;
15
- private subdomainPolicy: SubdomainPolicy;
16
- private explicitAllowed: Set<string>;
17
-
18
- constructor(options: ScopeOptions) {
19
- this.domainFilter = new DomainFilter(options.allowedDomains, options.deniedDomains);
20
- this.subdomainPolicy = new SubdomainPolicy(options.rootUrl, options.includeSubdomains);
21
- this.explicitAllowed = new Set((options.allowedDomains || []).map(d => {
22
- let h = d.toLowerCase().trim();
23
- if (h.endsWith('.')) h = h.slice(0, -1);
24
- return h;
25
- }));
26
- }
27
-
28
- isUrlEligible(url: string): EligibilityResult {
29
- let hostname: string;
30
- try {
31
- hostname = new URL(url).hostname.toLowerCase();
32
- if (hostname.endsWith('.')) hostname = hostname.slice(0, -1);
33
- } catch {
34
- return 'blocked_by_domain_filter'; // Invalid URL is effectively blocked
35
- }
36
-
37
- if (!this.domainFilter.isAllowed(hostname)) {
38
- return 'blocked_by_domain_filter';
39
- }
40
-
41
- // If explicit whitelist is used, and this domain is in it, allow it
42
- if (this.explicitAllowed.has(hostname)) {
43
- return 'allowed';
44
- }
45
-
46
- if (!this.subdomainPolicy.isAllowed(hostname)) {
47
- return 'blocked_subdomain';
48
- }
49
-
50
- return 'allowed';
51
- }
52
- }
@@ -1,39 +0,0 @@
1
- export class SubdomainPolicy {
2
- private rootHost: string;
3
- private includeSubdomains: boolean;
4
-
5
- constructor(rootUrl: string, includeSubdomains: boolean = false) {
6
- try {
7
- this.rootHost = new URL(rootUrl).hostname.toLowerCase();
8
- if (this.rootHost.endsWith('.')) {
9
- this.rootHost = this.rootHost.slice(0, -1);
10
- }
11
- } catch {
12
- this.rootHost = '';
13
- }
14
- this.includeSubdomains = includeSubdomains;
15
- }
16
-
17
- isAllowed(hostname: string): boolean {
18
- let target = hostname.toLowerCase().trim();
19
- if (target.endsWith('.')) {
20
- target = target.slice(0, -1);
21
- }
22
-
23
- // Exact match is always allowed if rootHost is set
24
- if (target === this.rootHost) {
25
- return true;
26
- }
27
-
28
- if (!this.includeSubdomains) {
29
- return false;
30
- }
31
-
32
- // Label-based check for subdomains
33
- // target must end with .rootHost
34
- if (!target.endsWith(`.${this.rootHost}`)) {
35
- return false;
36
- }
37
- return true;
38
- }
39
- }
@@ -1,92 +0,0 @@
1
- import * as dns from 'dns';
2
- import * as net from 'net';
3
- import { promisify } from 'util';
4
-
5
- const resolve4 = promisify(dns.resolve4);
6
- const resolve6 = promisify(dns.resolve6);
7
-
8
- export class IPGuard {
9
- /**
10
- * Checks if an IP address is internal/private
11
- */
12
- static isInternal(ip: string): boolean {
13
- if (net.isIPv4(ip)) {
14
- const parts = ip.split('.').map(Number);
15
-
16
- // 127.0.0.0/8
17
- if (parts[0] === 127) return true;
18
-
19
- // 10.0.0.0/8
20
- if (parts[0] === 10) return true;
21
-
22
- // 192.168.0.0/16
23
- if (parts[0] === 192 && parts[1] === 168) return true;
24
-
25
- // 172.16.0.0 – 172.31.255.255
26
- if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return true;
27
-
28
- // 169.254.0.0/16
29
- if (parts[0] === 169 && parts[1] === 254) return true;
30
-
31
- // 0.0.0.0/8
32
- if (parts[0] === 0) return true;
33
-
34
- return false;
35
- }
36
-
37
- if (net.isIPv6(ip)) {
38
- // Normalize IPv6
39
- const expanded = IPGuard.expandIPv6(ip);
40
-
41
- // ::1
42
- if (expanded === '0000:0000:0000:0000:0000:0000:0000:0001') return true;
43
-
44
- // fc00::/7 (Unique Local Address) -> fc or fd
45
- const firstWord = parseInt(expanded.split(':')[0], 16);
46
- if ((firstWord & 0xfe00) === 0xfc00) return true;
47
-
48
- // fe80::/10 (Link Local)
49
- if ((firstWord & 0xffc0) === 0xfe80) return true;
50
-
51
- return false;
52
- }
53
-
54
- return true; // Unknown format, block it for safety
55
- }
56
-
57
- /**
58
- * Resolves a hostname and validates all result IPs
59
- */
60
- static async validateHost(host: string): Promise<boolean> {
61
- if (net.isIP(host)) {
62
- return !IPGuard.isInternal(host);
63
- }
64
-
65
- try {
66
- const res4 = await resolve4(host).catch(() => [] as string[]);
67
- const res6 = await resolve6(host).catch(() => [] as string[]);
68
- const ips = [...res4, ...res6];
69
-
70
- if (ips.length === 0) return true; // Let the fetcher handle DNS failures
71
-
72
- return ips.every(ip => !IPGuard.isInternal(ip));
73
- } catch (_e) {
74
- // If resolution fails drastically, we block for safety or let fetcher try
75
- return false;
76
- }
77
- }
78
-
79
- private static expandIPv6(ip: string): string {
80
- if (ip === '::') return '0000:0000:0000:0000:0000:0000:0000:0000';
81
- let full = ip;
82
- if (ip.includes('::')) {
83
- const parts = ip.split('::');
84
- const left = parts[0].split(':').filter(x => x !== '');
85
- const right = parts[1].split(':').filter(x => x !== '');
86
- const missing = 8 - (left.length + right.length);
87
- const middle = Array(missing).fill('0000');
88
- full = [...left, ...middle, ...right].join(':');
89
- }
90
- return full.split(':').map(part => part.padStart(4, '0')).join(':');
91
- }
92
- }