mcp-scraper 0.38.2 → 0.40.1

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 (111) hide show
  1. package/README.md +5 -2
  2. package/package.json +5 -6
  3. package/dist/bin/api-server.cjs +0 -58752
  4. package/dist/bin/api-server.cjs.map +0 -1
  5. package/dist/bin/api-server.d.cts +0 -1
  6. package/dist/bin/api-server.d.ts +0 -1
  7. package/dist/bin/api-server.js +0 -38
  8. package/dist/bin/api-server.js.map +0 -1
  9. package/dist/bin/mcp-scraper-cli.cjs +0 -2671
  10. package/dist/bin/mcp-scraper-cli.cjs.map +0 -1
  11. package/dist/bin/mcp-scraper-cli.d.cts +0 -1
  12. package/dist/bin/mcp-scraper-cli.d.ts +0 -1
  13. package/dist/bin/mcp-scraper-cli.js +0 -742
  14. package/dist/bin/mcp-scraper-cli.js.map +0 -1
  15. package/dist/bin/mcp-scraper-install.cjs +0 -129
  16. package/dist/bin/mcp-scraper-install.cjs.map +0 -1
  17. package/dist/bin/mcp-scraper-install.d.cts +0 -1
  18. package/dist/bin/mcp-scraper-install.d.ts +0 -1
  19. package/dist/bin/mcp-scraper-install.js +0 -27
  20. package/dist/bin/mcp-scraper-install.js.map +0 -1
  21. package/dist/bin/mcp-stdio-server.cjs +0 -12264
  22. package/dist/bin/mcp-stdio-server.cjs.map +0 -1
  23. package/dist/bin/mcp-stdio-server.d.cts +0 -1
  24. package/dist/bin/mcp-stdio-server.d.ts +0 -1
  25. package/dist/bin/mcp-stdio-server.js +0 -135
  26. package/dist/bin/mcp-stdio-server.js.map +0 -1
  27. package/dist/bin/paa-harvest.cjs +0 -3808
  28. package/dist/bin/paa-harvest.cjs.map +0 -1
  29. package/dist/bin/paa-harvest.d.cts +0 -1
  30. package/dist/bin/paa-harvest.d.ts +0 -1
  31. package/dist/bin/paa-harvest.js +0 -44
  32. package/dist/bin/paa-harvest.js.map +0 -1
  33. package/dist/chunk-345BQXZH.js +0 -712
  34. package/dist/chunk-345BQXZH.js.map +0 -1
  35. package/dist/chunk-44HZLHDV.js +0 -52
  36. package/dist/chunk-44HZLHDV.js.map +0 -1
  37. package/dist/chunk-AZRPG43B.js +0 -617
  38. package/dist/chunk-AZRPG43B.js.map +0 -1
  39. package/dist/chunk-CB5C3BPB.js +0 -135
  40. package/dist/chunk-CB5C3BPB.js.map +0 -1
  41. package/dist/chunk-EGJKUB4Q.js +0 -276
  42. package/dist/chunk-EGJKUB4Q.js.map +0 -1
  43. package/dist/chunk-FQI5PFE7.js +0 -1866
  44. package/dist/chunk-FQI5PFE7.js.map +0 -1
  45. package/dist/chunk-FRYT3ID4.js +0 -684
  46. package/dist/chunk-FRYT3ID4.js.map +0 -1
  47. package/dist/chunk-G3P3ZDB4.js +0 -69
  48. package/dist/chunk-G3P3ZDB4.js.map +0 -1
  49. package/dist/chunk-K443GQY5.js +0 -24
  50. package/dist/chunk-K443GQY5.js.map +0 -1
  51. package/dist/chunk-N7KUTTCC.js +0 -3007
  52. package/dist/chunk-N7KUTTCC.js.map +0 -1
  53. package/dist/chunk-NGM237OO.js +0 -3410
  54. package/dist/chunk-NGM237OO.js.map +0 -1
  55. package/dist/chunk-NKCCGADE.js +0 -11285
  56. package/dist/chunk-NKCCGADE.js.map +0 -1
  57. package/dist/chunk-NNW3O6ZD.js +0 -108
  58. package/dist/chunk-NNW3O6ZD.js.map +0 -1
  59. package/dist/chunk-QZXKQB7Y.js +0 -414
  60. package/dist/chunk-QZXKQB7Y.js.map +0 -1
  61. package/dist/chunk-SFRMFGQ6.js +0 -158
  62. package/dist/chunk-SFRMFGQ6.js.map +0 -1
  63. package/dist/chunk-YCI2PNCS.js +0 -499
  64. package/dist/chunk-YCI2PNCS.js.map +0 -1
  65. package/dist/chunk-YODBNTTN.js +0 -7
  66. package/dist/chunk-YODBNTTN.js.map +0 -1
  67. package/dist/db-C5KVCOYT.js +0 -239
  68. package/dist/db-C5KVCOYT.js.map +0 -1
  69. package/dist/extract-bundle-KUBX6N6Z.js +0 -568
  70. package/dist/extract-bundle-KUBX6N6Z.js.map +0 -1
  71. package/dist/index.cjs +0 -4160
  72. package/dist/index.cjs.map +0 -1
  73. package/dist/index.d.cts +0 -413
  74. package/dist/index.d.ts +0 -413
  75. package/dist/index.js +0 -338
  76. package/dist/index.js.map +0 -1
  77. package/dist/location-data-repository-TTWF3OTM.js +0 -35
  78. package/dist/location-data-repository-TTWF3OTM.js.map +0 -1
  79. package/dist/server-5EX6XBIA.js +0 -33596
  80. package/dist/server-5EX6XBIA.js.map +0 -1
  81. package/dist/site-extract-repository-XSPJTCIL.js +0 -62
  82. package/dist/site-extract-repository-XSPJTCIL.js.map +0 -1
  83. package/dist/worker-XCPU4YSN.js +0 -142
  84. package/dist/worker-XCPU4YSN.js.map +0 -1
  85. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
  86. package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
  87. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
  88. package/docs/adr/README.md +0 -13
  89. package/docs/final-tooling-spec.md +0 -206
  90. package/docs/hosted-location-data.md +0 -108
  91. package/docs/kernel-proxy-future-enhancements.md +0 -80
  92. package/docs/mcp-tool-craft-lint.generated.md +0 -183
  93. package/docs/mcp-tool-design-guide.md +0 -225
  94. package/docs/mcp-tool-manifest.generated.json +0 -22871
  95. package/docs/mcp-tool-quality-spec.md +0 -240
  96. package/docs/oauth-legal-review.md +0 -38
  97. package/docs/seo-crawl-report-spec.md +0 -287
  98. package/docs/specs/api-forge-spec.md +0 -234
  99. package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
  100. package/docs/specs/deferred-work-spec.md +0 -86
  101. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
  102. package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
  103. package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
  104. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
  105. package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
  106. package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
  107. package/docs/specs/oauth-mcp-spec.md +0 -213
  108. package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
  109. package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
  110. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
  111. package/docs/tool-catalog-spec.md +0 -388
@@ -1,108 +0,0 @@
1
- // src/install-terminal.ts
2
- var CODES = {
3
- reset: "\x1B[0m",
4
- cyan: "\x1B[36m",
5
- lime: "\x1B[32m",
6
- amber: "\x1B[33m",
7
- red: "\x1B[31m",
8
- muted: "\x1B[90m",
9
- bold: "\x1B[1m"
10
- };
11
- function colorize(value, tone, enabled) {
12
- if (!enabled) return value;
13
- return `${CODES[tone]}${value}${CODES.reset}`;
14
- }
15
- function toolRow(label, tools, enabled) {
16
- const padded = label.padEnd(9, " ");
17
- return ` ${colorize(padded, "muted", enabled)} ${tools.join(colorize(" . ", "muted", enabled))}`;
18
- }
19
- function renderInstallTerminal(options) {
20
- const color = options.color ?? true;
21
- const apiKeyValue = options.apiKeyConfigured ? "$MCP_SCRAPER_API_KEY" : "sk_live_your_key";
22
- const ascii = String.raw`
23
- __ __ ____ ____
24
- | \/ |/ ___| _ \
25
- | |\/| | | | |_) |
26
- | | | | |___| __/
27
- |_| |_|\____|_|
28
-
29
- ____ ____ ____ _ ____ _____ ____
30
- / ___| / ___| _ \ / \ | _ \| ____| _ \
31
- \___ \| | | |_) | / _ \ | |_) | _| | |_) |
32
- ___) | |___| _ < / ___ \| __/| |___| _ <
33
- |____/ \____|_| \_\/_/ \_\_| |_____|_| \_\
34
- `;
35
- const claudeCommand = [
36
- `MCP_SCRAPER_API_KEY=${apiKeyValue} npx -y -p mcp-scraper@latest \\`,
37
- " mcp-scraper-cli agent install claude --apply"
38
- ].join("\n");
39
- const codexConfig = [
40
- "[mcp_servers.mcp-scraper]",
41
- 'command = "npx"',
42
- 'args = ["-y", "-p", "mcp-scraper@latest", "mcp-scraper"]',
43
- `env = { MCP_SCRAPER_API_KEY = "${apiKeyValue}" }`
44
- ].join("\n");
45
- return [
46
- colorize(`mcp-scraper v${options.version}`, "bold", color),
47
- colorize("> mcp-scraper-install", "muted", color),
48
- colorize(ascii, "amber", color),
49
- `${colorize("MCP Scraper Agent", "cyan", color)} . v${options.version} . mcpscraper.dev`,
50
- "1/1 install surfaces ready",
51
- colorize("Newest: any approved connection read can become an indexed Memory snapshot in one call. OAuth stays tenant-isolated and provider content is redacted and marked untrusted.", "lime", color),
52
- "",
53
- `${colorize("Tools", "cyan", color)} ${colorize("(172 MCP tools)", "muted", color)}`,
54
- toolRow("search", ["harvest_paa", "search_serp", "maps_search", "maps_place_intel"], color),
55
- toolRow("extract", ["extract_url", "map_site_urls", "extract_site", "audit_site", "directory_workflow"], color),
56
- toolRow("build", ["rank_tracker_workflow", "cron plan", "database prompt"], color),
57
- toolRow("media", ["youtube_harvest", "youtube_transcribe", "facebook_ad_search", "facebook_page_intel", "facebook_ad_transcribe", "facebook_video_transcribe", "instagram_profile_content", "instagram_media_download", "reddit_thread"], color),
58
- toolRow("browser", ["browser_open", "browser_profile_connect", "browser_profile_list", "browser_close", "browser_screenshot", "browser_read", "browser_locate", "browser_replay_mark", "browser_replay_annotate"], color),
59
- toolRow("connect", ["list_service_connections", "describe_service_connection_tool", "import_service_connection_to_memory", "export_connected_service_data", "renew_connected_data_download", "read_service_connection", "call_service_connection_action"], color),
60
- toolRow("account", ["credits_info", "reports", "MCP resources"], color),
61
- toolRow("memory", ["memory-put", "memory-get", "memory-search", "list-vaults", "record-fact", "list-scheduled-actions"], color),
62
- `${colorize("Workflows", "cyan", color)} ${colorize("(MCP + CLI + API)", "muted", color)}`,
63
- toolRow("route", ["workflow_list", "workflow_suggest", "workflow_run", "workflow_step", "workflow_status", "workflow_artifact_read"], color),
64
- toolRow("seo", ["directory", "agent-packet", "competitive audit", "map/serp comparison", "PAA/AIO briefs", "scheduled runs"], color),
65
- "",
66
- colorize("Usage tips:", "amber", color),
67
- "Run mcp-scraper-install for this visible card. Run mcp-scraper-cli for setup utilities and subcommands.",
68
- "Run mcp-scraper in a human terminal to print this card; MCP clients get the same command as a silent stdio server.",
69
- "Explicit card command: npx -y -p mcp-scraper@latest mcp-scraper-install",
70
- "Hosted browser sessions use direct/no-proxy egress by default.",
71
- "Customer auth setup: run browser_profile_connect, send the watch_url, let the user sign in, then call browser_profile_list until AUTHENTICATED.",
72
- "Connected account ranges: call export_connected_service_data once. It handles Gmail, Calendar, Zoom, and Resend pagination; do not loop read_service_connection over individual records.",
73
- "Connected account RAG: call import_service_connection_to_memory for one bounded approved read. It writes a redacted, untrusted snapshot to a stable Memory path and embeds it for search.",
74
- "Stack logins / reconnect: run browser_profile_connect again with the same profile name and another domain to add accounts or refresh a login.",
75
- "Start with workflow_suggest for broad jobs like market analysis, ICP research, CRO audits, brand briefs, content gaps, and AI visibility.",
76
- "For MCP clients, use mcp-scraper so one install can mix SERP, Maps, browser, reports, and saved MCP resources.",
77
- "If you hit the concurrency limit, add an extra slot for $5/month with mcp-scraper-cli billing concurrency checkout.",
78
- "",
79
- `${colorize("Ready.", "lime", color)} Install the combined MCP server with one command:`,
80
- "",
81
- colorize("Setup doctor", "amber", color),
82
- "npx -y -p mcp-scraper@latest mcp-scraper-cli doctor",
83
- "",
84
- colorize("Hosted profile setup", "amber", color),
85
- 'In your MCP client, call browser_profile_connect with email="seo@example.com" and domain="chatgpt.com".',
86
- "Give the returned watch_url to the user. After they sign in, call browser_profile_list, then browser_open with the returned profile. Add more logins by calling browser_profile_connect again with the same profile and a new domain.",
87
- "",
88
- colorize("Claude Code one-command setup", "amber", color),
89
- claudeCommand,
90
- "Then fully exit Claude Code and open a new Claude terminal. Check with: claude mcp list",
91
- "",
92
- colorize("Codex config", "amber", color),
93
- codexConfig,
94
- "",
95
- colorize("Claude Desktop Extension", "amber", color),
96
- "Download: https://mcpscraper.dev/downloads/mcp-scraper.mcpb",
97
- "",
98
- colorize("Safety note:", "muted", color),
99
- "mcp-scraper prints this card only when stdin/stdout are an interactive TTY. In MCP clients it writes only JSON-RPC to stdout.",
100
- "Use --stdio or MCP_SCRAPER_FORCE_STDIO=1 to force server mode from a terminal.",
101
- ""
102
- ].join("\n");
103
- }
104
-
105
- export {
106
- renderInstallTerminal
107
- };
108
- //# sourceMappingURL=chunk-NNW3O6ZD.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/install-terminal.ts"],"sourcesContent":["export interface InstallTerminalOptions {\n color?: boolean\n version: string\n apiKeyConfigured?: boolean\n}\n\ntype Tone = 'cyan' | 'lime' | 'amber' | 'red' | 'muted' | 'bold'\n\nconst CODES: Record<Tone | 'reset', string> = {\n reset: '\\x1b[0m',\n cyan: '\\x1b[36m',\n lime: '\\x1b[32m',\n amber: '\\x1b[33m',\n red: '\\x1b[31m',\n muted: '\\x1b[90m',\n bold: '\\x1b[1m',\n}\n\nfunction colorize(value: string, tone: Tone, enabled: boolean): string {\n if (!enabled) return value\n return `${CODES[tone]}${value}${CODES.reset}`\n}\n\nfunction toolRow(label: string, tools: string[], enabled: boolean): string {\n const padded = label.padEnd(9, ' ')\n return ` ${colorize(padded, 'muted', enabled)} ${tools.join(colorize(' . ', 'muted', enabled))}`\n}\n\nexport function renderInstallTerminal(options: InstallTerminalOptions): string {\n const color = options.color ?? true\n const apiKeyValue = options.apiKeyConfigured ? '$MCP_SCRAPER_API_KEY' : 'sk_live_your_key'\n const ascii = String.raw`\n __ __ ____ ____ \n| \\/ |/ ___| _ \\\n| |\\/| | | | |_) |\n| | | | |___| __/\n|_| |_|\\____|_|\n\n ____ ____ ____ _ ____ _____ ____\n/ ___| / ___| _ \\ / \\ | _ \\| ____| _ \\\n\\___ \\| | | |_) | / _ \\ | |_) | _| | |_) |\n ___) | |___| _ < / ___ \\| __/| |___| _ <\n|____/ \\____|_| \\_\\/_/ \\_\\_| |_____|_| \\_\\\n`\n\n const claudeCommand = [\n `MCP_SCRAPER_API_KEY=${apiKeyValue} npx -y -p mcp-scraper@latest \\\\`,\n ' mcp-scraper-cli agent install claude --apply',\n ].join('\\n')\n\n const codexConfig = [\n '[mcp_servers.mcp-scraper]',\n 'command = \"npx\"',\n 'args = [\"-y\", \"-p\", \"mcp-scraper@latest\", \"mcp-scraper\"]',\n `env = { MCP_SCRAPER_API_KEY = \"${apiKeyValue}\" }`,\n ].join('\\n')\n\n return [\n colorize(`mcp-scraper v${options.version}`, 'bold', color),\n colorize('> mcp-scraper-install', 'muted', color),\n colorize(ascii, 'amber', color),\n `${colorize('MCP Scraper Agent', 'cyan', color)} . v${options.version} . mcpscraper.dev`,\n '1/1 install surfaces ready',\n colorize('Newest: any approved connection read can become an indexed Memory snapshot in one call. OAuth stays tenant-isolated and provider content is redacted and marked untrusted.', 'lime', color),\n '',\n `${colorize('Tools', 'cyan', color)} ${colorize('(172 MCP tools)', 'muted', color)}`,\n toolRow('search', ['harvest_paa', 'search_serp', 'maps_search', 'maps_place_intel'], color),\n toolRow('extract', ['extract_url', 'map_site_urls', 'extract_site', 'audit_site', 'directory_workflow'], color),\n toolRow('build', ['rank_tracker_workflow', 'cron plan', 'database prompt'], color),\n toolRow('media', ['youtube_harvest', 'youtube_transcribe', 'facebook_ad_search', 'facebook_page_intel', 'facebook_ad_transcribe', 'facebook_video_transcribe', 'instagram_profile_content', 'instagram_media_download', 'reddit_thread'], color),\n toolRow('browser', ['browser_open', 'browser_profile_connect', 'browser_profile_list', 'browser_close', 'browser_screenshot', 'browser_read', 'browser_locate', 'browser_replay_mark', 'browser_replay_annotate'], color),\n toolRow('connect', ['list_service_connections', 'describe_service_connection_tool', 'import_service_connection_to_memory', 'export_connected_service_data', 'renew_connected_data_download', 'read_service_connection', 'call_service_connection_action'], color),\n toolRow('account', ['credits_info', 'reports', 'MCP resources'], color),\n toolRow('memory', ['memory-put', 'memory-get', 'memory-search', 'list-vaults', 'record-fact', 'list-scheduled-actions'], color),\n `${colorize('Workflows', 'cyan', color)} ${colorize('(MCP + CLI + API)', 'muted', color)}`,\n toolRow('route', ['workflow_list', 'workflow_suggest', 'workflow_run', 'workflow_step', 'workflow_status', 'workflow_artifact_read'], color),\n toolRow('seo', ['directory', 'agent-packet', 'competitive audit', 'map/serp comparison', 'PAA/AIO briefs', 'scheduled runs'], color),\n '',\n colorize('Usage tips:', 'amber', color),\n 'Run mcp-scraper-install for this visible card. Run mcp-scraper-cli for setup utilities and subcommands.',\n 'Run mcp-scraper in a human terminal to print this card; MCP clients get the same command as a silent stdio server.',\n 'Explicit card command: npx -y -p mcp-scraper@latest mcp-scraper-install',\n 'Hosted browser sessions use direct/no-proxy egress by default.',\n 'Customer auth setup: run browser_profile_connect, send the watch_url, let the user sign in, then call browser_profile_list until AUTHENTICATED.',\n 'Connected account ranges: call export_connected_service_data once. It handles Gmail, Calendar, Zoom, and Resend pagination; do not loop read_service_connection over individual records.',\n 'Connected account RAG: call import_service_connection_to_memory for one bounded approved read. It writes a redacted, untrusted snapshot to a stable Memory path and embeds it for search.',\n 'Stack logins / reconnect: run browser_profile_connect again with the same profile name and another domain to add accounts or refresh a login.',\n 'Start with workflow_suggest for broad jobs like market analysis, ICP research, CRO audits, brand briefs, content gaps, and AI visibility.',\n 'For MCP clients, use mcp-scraper so one install can mix SERP, Maps, browser, reports, and saved MCP resources.',\n 'If you hit the concurrency limit, add an extra slot for $5/month with mcp-scraper-cli billing concurrency checkout.',\n '',\n `${colorize('Ready.', 'lime', color)} Install the combined MCP server with one command:`,\n '',\n colorize('Setup doctor', 'amber', color),\n 'npx -y -p mcp-scraper@latest mcp-scraper-cli doctor',\n '',\n colorize('Hosted profile setup', 'amber', color),\n 'In your MCP client, call browser_profile_connect with email=\"seo@example.com\" and domain=\"chatgpt.com\".',\n 'Give the returned watch_url to the user. After they sign in, call browser_profile_list, then browser_open with the returned profile. Add more logins by calling browser_profile_connect again with the same profile and a new domain.',\n '',\n colorize('Claude Code one-command setup', 'amber', color),\n claudeCommand,\n 'Then fully exit Claude Code and open a new Claude terminal. Check with: claude mcp list',\n '',\n colorize('Codex config', 'amber', color),\n codexConfig,\n '',\n colorize('Claude Desktop Extension', 'amber', color),\n 'Download: https://mcpscraper.dev/downloads/mcp-scraper.mcpb',\n '',\n colorize('Safety note:', 'muted', color),\n 'mcp-scraper prints this card only when stdin/stdout are an interactive TTY. In MCP clients it writes only JSON-RPC to stdout.',\n 'Use --stdio or MCP_SCRAPER_FORCE_STDIO=1 to force server mode from a terminal.',\n '',\n ].join('\\n')\n}\n"],"mappings":"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","names":[]}
@@ -1,414 +0,0 @@
1
- import {
2
- createPrivateArtifactFromStream,
3
- renewPrivateArtifactDownload,
4
- validatePublicHttpUrl
5
- } from "./chunk-345BQXZH.js";
6
-
7
- // src/lib/media-extractor.ts
8
- import { createWriteStream, mkdirSync, rmSync } from "fs";
9
- import { homedir } from "os";
10
- import { join, extname, basename } from "path";
11
- import { pipeline } from "stream/promises";
12
- import { Readable, Transform } from "stream";
13
- var AD_PATTERNS = [
14
- "doubleclick.net",
15
- "googlesyndication.com",
16
- "googletagmanager.com",
17
- "google-analytics.com",
18
- "googletagservices.com",
19
- "adservice.google",
20
- "googletag.",
21
- "pagead2.googlesyndication",
22
- "facebook.net/tr",
23
- "connect.facebook.net",
24
- "fbcdn.net/rsrc",
25
- "analytics.twitter.com",
26
- "static.ads-twitter.com",
27
- "ads.twitter.com",
28
- "t.co/i/adsct",
29
- "pixel.advertising.com",
30
- "hotjar.com",
31
- "clarity.ms",
32
- "quantserve.com",
33
- "scorecardresearch.com",
34
- "newrelic.com",
35
- "nr-data.net",
36
- "segment.io",
37
- "segment.com",
38
- "amplitude.com",
39
- "mixpanel.com",
40
- "heap.io",
41
- "fullstory.com",
42
- "moatads.com",
43
- "criteo.com",
44
- "adsrvr.org",
45
- "rubiconproject.com",
46
- "pubmatic.com",
47
- "openx.net",
48
- "appnexus.com",
49
- "amazon-adsystem.com",
50
- "media.net",
51
- "yieldmo.com",
52
- "triplelift.com",
53
- "sharethrough.com",
54
- "prebid.",
55
- "smaato.net",
56
- "indexworm.com",
57
- "casalemedia.com",
58
- "outbrain.com",
59
- "taboola.com",
60
- "revcontent.com",
61
- "mgid.com",
62
- "tawk.to",
63
- "intercom.io",
64
- "drift.com",
65
- "hs-scripts.com",
66
- "zopim.com",
67
- "livechatinc.com",
68
- "userlike.com",
69
- "onetrust.com",
70
- "cookielaw.org",
71
- "cookieinformation.com",
72
- "trustarc.com",
73
- "/ads/",
74
- "/ad/",
75
- "/banner/",
76
- "/banners/",
77
- "/pixel/",
78
- "/beacon/",
79
- "/tracking/",
80
- "/tracker/",
81
- "/remarketing/",
82
- "/conversion/",
83
- "1x1.gif",
84
- "spacer.gif",
85
- "blank.gif",
86
- "transparent.gif"
87
- ];
88
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif", ".svg", ".tiff"]);
89
- var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".m4v", ".ogv", ".mkv"]);
90
- var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".aac", ".m4a", ".flac", ".opus"]);
91
- function isAdUrl(url) {
92
- const lower = url.toLowerCase();
93
- return AD_PATTERNS.some((p) => lower.includes(p));
94
- }
95
- function isDataUri(url) {
96
- return url.startsWith("data:");
97
- }
98
- function typeFromUrl(url) {
99
- try {
100
- const ext = extname(new URL(url).pathname).toLowerCase();
101
- if (IMAGE_EXTS.has(ext)) return "image";
102
- if (VIDEO_EXTS.has(ext)) return "video";
103
- if (AUDIO_EXTS.has(ext)) return "audio";
104
- } catch {
105
- }
106
- return null;
107
- }
108
- function typeFromMime(mime) {
109
- const lower = mime.toLowerCase();
110
- if (lower.startsWith("image/")) return "image";
111
- if (lower.startsWith("video/")) return "video";
112
- if (lower.startsWith("audio/")) return "audio";
113
- return null;
114
- }
115
- function resolveUrl(raw, base) {
116
- if (!raw || isDataUri(raw)) return null;
117
- try {
118
- return new URL(raw, base).href;
119
- } catch {
120
- return null;
121
- }
122
- }
123
- function safeFilename(url, index) {
124
- try {
125
- const u = new URL(url);
126
- const base = basename(u.pathname).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
127
- return base || `asset-${index}`;
128
- } catch {
129
- return `asset-${index}`;
130
- }
131
- }
132
- function extractMediaUrls(html, baseUrl) {
133
- const seen = /* @__PURE__ */ new Set();
134
- const urls = [];
135
- const add = (raw) => {
136
- const resolved = resolveUrl(raw.trim(), baseUrl);
137
- if (!resolved || seen.has(resolved) || isAdUrl(resolved)) return;
138
- seen.add(resolved);
139
- urls.push(resolved);
140
- };
141
- for (const m of html.matchAll(/<img\s[^>]*>/gi)) {
142
- const tag = m[0];
143
- for (const attr of ["src", "data-src", "data-lazy-src", "data-original"]) {
144
- const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
145
- if (v) add(v);
146
- }
147
- const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
148
- if (srcset) {
149
- for (const part of srcset.split(",")) {
150
- const u = part.trim().split(/\s+/)[0];
151
- if (u) add(u);
152
- }
153
- }
154
- }
155
- for (const m of html.matchAll(/<source\s[^>]*>/gi)) {
156
- const tag = m[0];
157
- const src = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
158
- if (src) add(src);
159
- const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
160
- if (srcset) {
161
- for (const part of srcset.split(",")) {
162
- const u = part.trim().split(/\s+/)[0];
163
- if (u) add(u);
164
- }
165
- }
166
- }
167
- for (const m of html.matchAll(/<video\s[^>]*>/gi)) {
168
- const tag = m[0];
169
- for (const attr of ["src", "poster"]) {
170
- const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
171
- if (v) add(v);
172
- }
173
- }
174
- for (const m of html.matchAll(/<audio\s[^>]*>/gi)) {
175
- const tag = m[0];
176
- const v = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
177
- if (v) add(v);
178
- }
179
- for (const m of html.matchAll(/<meta\s[^>]*>/gi)) {
180
- const tag = m[0];
181
- const prop = (tag.match(/\b(?:property|name)\s*=\s*["']([^"']+)["']/i) ?? [])[1]?.toLowerCase();
182
- if (prop === "og:image" || prop === "twitter:image") {
183
- const content = (tag.match(/\bcontent\s*=\s*["']([^"']+)["']/i) ?? [])[1];
184
- if (content) add(content);
185
- }
186
- }
187
- for (const m of html.matchAll(/background(?:-image)?\s*:\s*url\(\s*["']?([^"')]+)["']?\s*\)/gi)) {
188
- add(m[1]);
189
- }
190
- return urls;
191
- }
192
- async function downloadAsset(url, destDir, filename, options = {}) {
193
- const maxBytes = Math.max(1, Math.min(options.maxBytes ?? 50 * 1024 * 1024, 100 * 1024 * 1024));
194
- let target = url;
195
- let res = null;
196
- for (let redirects = 0; redirects <= 5; redirects++) {
197
- const checked = await validatePublicHttpUrl(target, { field: "media URL" });
198
- if (checked.error || !checked.parsed) throw new Error(checked.error ?? "Media URL was rejected");
199
- res = await fetch(checked.parsed.href, {
200
- headers: { "User-Agent": "Mozilla/5.0 (compatible; ThorbitBot/1.0)" },
201
- signal: AbortSignal.timeout(15e3),
202
- redirect: "manual"
203
- });
204
- if (res.status >= 300 && res.status < 400) {
205
- const location = res.headers.get("location");
206
- if (!location) throw new Error(`HTTP ${res.status} redirect did not include Location`);
207
- target = new URL(location, checked.parsed.href).href;
208
- res = null;
209
- continue;
210
- }
211
- break;
212
- }
213
- if (!res) throw new Error("Media download exceeded five redirects");
214
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
215
- if (!res.body) throw new Error("Empty response body");
216
- const mimeType = res.headers.get("content-type")?.split(";")[0].trim() ?? null;
217
- if (options.expectedType && (!mimeType || typeFromMime(mimeType) !== options.expectedType)) {
218
- throw new Error(`Expected ${options.expectedType} content but received ${mimeType ?? "no content-type"}`);
219
- }
220
- const declaredBytes = Number(res.headers.get("content-length") ?? 0);
221
- if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
222
- throw new Error(`Media asset exceeds ${maxBytes} byte limit`);
223
- }
224
- let dest = join(destDir, filename);
225
- if (mimeType && !extname(filename)) {
226
- const mimeExt = {
227
- "image/jpeg": ".jpg",
228
- "image/png": ".png",
229
- "image/webp": ".webp",
230
- "image/gif": ".gif",
231
- "image/svg+xml": ".svg",
232
- "image/avif": ".avif",
233
- "video/mp4": ".mp4",
234
- "video/webm": ".webm",
235
- "audio/mpeg": ".mp3",
236
- "audio/ogg": ".ogg",
237
- "audio/wav": ".wav"
238
- };
239
- const ext = mimeExt[mimeType];
240
- if (ext) dest = dest + ext;
241
- }
242
- const writer = createWriteStream(dest);
243
- await new Promise((resolve, reject) => {
244
- writer.once("open", () => resolve());
245
- writer.once("error", reject);
246
- });
247
- let streamedBytes = 0;
248
- const limiter = new Transform({
249
- transform(chunk, _encoding, callback) {
250
- const bytes = chunk.length;
251
- if (streamedBytes + bytes > maxBytes) {
252
- callback(new Error(`Media asset exceeds ${maxBytes} byte limit`));
253
- return;
254
- }
255
- if (options.consumeBytes && !options.consumeBytes(bytes)) {
256
- callback(new Error("Media export total byte limit exceeded"));
257
- return;
258
- }
259
- streamedBytes += bytes;
260
- callback(null, chunk);
261
- }
262
- });
263
- try {
264
- await pipeline(Readable.fromWeb(res.body), limiter, writer);
265
- } catch (error) {
266
- writer.destroy();
267
- rmSync(dest, { force: true });
268
- throw error;
269
- }
270
- const { statSync } = await import("fs");
271
- const sizeBytes = statSync(dest).size;
272
- return { savedPath: dest, sizeBytes, mimeType };
273
- }
274
- async function harvestPageMedia(html, pageUrl, options = {}) {
275
- const types = options.types ?? ["image", "video", "audio"];
276
- const typesSet = new Set(types);
277
- const rawUrls = extractMediaUrls(html, pageUrl);
278
- const totalFound = rawUrls.length;
279
- const filteredUrls = [];
280
- const kept = [];
281
- for (const url of rawUrls) {
282
- const type = typeFromUrl(url);
283
- if (!type || !typesSet.has(type)) {
284
- filteredUrls.push(url);
285
- continue;
286
- }
287
- kept.push({ url, type });
288
- }
289
- if (options.outputDir === null) {
290
- return {
291
- pageUrl,
292
- outputDir: null,
293
- assets: kept.map(({ url, type }, i) => ({
294
- url,
295
- type,
296
- mimeType: null,
297
- filename: safeFilename(url, i),
298
- savedPath: null,
299
- sizeBytes: null
300
- })),
301
- filteredCount: filteredUrls.length,
302
- totalFound
303
- };
304
- }
305
- const domain = (() => {
306
- try {
307
- return new URL(pageUrl).hostname.replace(/^www\./, "");
308
- } catch {
309
- return "unknown";
310
- }
311
- })();
312
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
313
- const outDir = options.outputDir ?? join(homedir(), "Downloads", "mcp-scraper", "media", `${stamp}-${domain}`);
314
- mkdirSync(outDir, { recursive: true });
315
- const assets = [];
316
- await Promise.allSettled(
317
- kept.map(async ({ url, type }, i) => {
318
- const filename = safeFilename(url, i);
319
- try {
320
- const { savedPath, sizeBytes, mimeType } = await downloadAsset(url, outDir, filename);
321
- const resolvedType = mimeType ? typeFromMime(mimeType) ?? type : type;
322
- assets.push({ url, type: resolvedType, mimeType, filename: basename(savedPath), savedPath, sizeBytes });
323
- } catch {
324
- assets.push({ url, type, mimeType: null, filename, savedPath: null, sizeBytes: null });
325
- }
326
- })
327
- );
328
- assets.sort((a, b) => {
329
- if (a.savedPath && !b.savedPath) return -1;
330
- if (!a.savedPath && b.savedPath) return 1;
331
- return a.url.localeCompare(b.url);
332
- });
333
- return { pageUrl, outputDir: outDir, assets, filteredCount: filteredUrls.length, totalFound };
334
- }
335
-
336
- // src/api/site-extract-artifacts.ts
337
- var SITE_EXTRACT_ARTIFACT_PREFIX = "site-extracts/";
338
- var SITE_EXTRACT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
339
- var SITE_EXTRACT_DOWNLOAD_TTL_MS = 15 * 60 * 1e3;
340
- function siteExtractArtifactToken() {
341
- return process.env.SITE_EXTRACT_ARTIFACT_READ_WRITE_TOKEN?.trim() || process.env.PRIVATE_ARTIFACT_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim() || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim() || null;
342
- }
343
- function hostedByEnvironment() {
344
- return process.env.VERCEL === "1" || process.env.NODE_ENV === "production";
345
- }
346
- function policy() {
347
- return {
348
- prefix: SITE_EXTRACT_ARTIFACT_PREFIX,
349
- artifactTtlMs: SITE_EXTRACT_ARTIFACT_TTL_MS,
350
- downloadTtlMs: SITE_EXTRACT_DOWNLOAD_TTL_MS,
351
- token: siteExtractArtifactToken()
352
- };
353
- }
354
- async function createSiteExtractBundleArtifactStream(args) {
355
- const pointer = await createPrivateArtifactFromStream({
356
- policy: policy(),
357
- ownerId: args.ownerId,
358
- artifactKey: `${args.jobId}.zip`,
359
- createdAt: args.createdAt,
360
- filename: `${args.jobId}-site-export.zip`,
361
- contentType: "application/zip",
362
- content: args.content
363
- });
364
- return {
365
- key: pointer.artifactId,
366
- url: pointer.downloadUrl ?? "",
367
- bytes: pointer.bytes,
368
- contentType: pointer.contentType,
369
- filename: pointer.filename,
370
- sha256: pointer.sha256,
371
- expiresAt: pointer.expiresAt,
372
- downloadUrlExpiresAt: pointer.downloadUrlExpiresAt
373
- };
374
- }
375
- async function renewSiteExtractArtifactDownload(args) {
376
- return renewPrivateArtifactDownload({
377
- policy: policy(),
378
- artifactId: args.artifactId,
379
- ownerId: args.ownerId
380
- });
381
- }
382
- async function cleanupExpiredSiteExtractArtifacts(args = {}) {
383
- const now = args.now ?? /* @__PURE__ */ new Date();
384
- const token = siteExtractArtifactToken();
385
- if (!token) return { deleted: 0, store: hostedByEnvironment() ? "none" : "local" };
386
- if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 21)) {
387
- return { deleted: 0, store: "private-vercel-blob", skipped: true };
388
- }
389
- const cutoff = now.getTime() - SITE_EXTRACT_ARTIFACT_TTL_MS;
390
- const { list, del } = await import("@vercel/blob");
391
- let cursor;
392
- let deleted = 0;
393
- for (let page = 0; page < 20; page += 1) {
394
- const result = await list({ prefix: SITE_EXTRACT_ARTIFACT_PREFIX, token, limit: 1e3, cursor });
395
- const expired = result.blobs.filter((blob) => new Date(blob.uploadedAt).getTime() <= cutoff);
396
- if (expired.length > 0) {
397
- await del(expired.map((blob) => blob.pathname), { token });
398
- deleted += expired.length;
399
- }
400
- if (!result.hasMore || !result.cursor) break;
401
- cursor = result.cursor;
402
- }
403
- return { deleted, store: "private-vercel-blob" };
404
- }
405
-
406
- export {
407
- downloadAsset,
408
- harvestPageMedia,
409
- SITE_EXTRACT_ARTIFACT_PREFIX,
410
- createSiteExtractBundleArtifactStream,
411
- renewSiteExtractArtifactDownload,
412
- cleanupExpiredSiteExtractArtifacts
413
- };
414
- //# sourceMappingURL=chunk-QZXKQB7Y.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/lib/media-extractor.ts","../src/api/site-extract-artifacts.ts"],"sourcesContent":["import { createWriteStream, mkdirSync, rmSync } from 'node:fs'\nimport { homedir } from 'node:os'\nimport { join, extname, basename } from 'node:path'\nimport { pipeline } from 'node:stream/promises'\nimport { Readable, Transform } from 'node:stream'\nimport { validatePublicHttpUrl } from '../api/url-utils.js'\n\nconst AD_PATTERNS: string[] = [\n 'doubleclick.net', 'googlesyndication.com', 'googletagmanager.com',\n 'google-analytics.com', 'googletagservices.com', 'adservice.google',\n 'googletag.', 'pagead2.googlesyndication',\n 'facebook.net/tr', 'connect.facebook.net', 'fbcdn.net/rsrc',\n 'analytics.twitter.com', 'static.ads-twitter.com', 'ads.twitter.com',\n 't.co/i/adsct', 'pixel.advertising.com',\n 'hotjar.com', 'clarity.ms', 'quantserve.com', 'scorecardresearch.com',\n 'newrelic.com', 'nr-data.net', 'segment.io', 'segment.com',\n 'amplitude.com', 'mixpanel.com', 'heap.io', 'fullstory.com',\n 'moatads.com', 'criteo.com', 'adsrvr.org', 'rubiconproject.com',\n 'pubmatic.com', 'openx.net', 'appnexus.com', 'amazon-adsystem.com',\n 'media.net', 'yieldmo.com', 'triplelift.com', 'sharethrough.com',\n 'prebid.', 'smaato.net', 'indexworm.com', 'casalemedia.com',\n 'outbrain.com', 'taboola.com', 'revcontent.com', 'mgid.com',\n 'tawk.to', 'intercom.io', 'drift.com', 'hs-scripts.com',\n 'zopim.com', 'livechatinc.com', 'userlike.com',\n 'onetrust.com', 'cookielaw.org', 'cookieinformation.com', 'trustarc.com',\n '/ads/', '/ad/', '/banner/', '/banners/', '/pixel/', '/beacon/',\n '/tracking/', '/tracker/', '/remarketing/', '/conversion/',\n '1x1.gif', 'spacer.gif', 'blank.gif', 'transparent.gif',\n]\n\nconst IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif', '.avif', '.svg', '.tiff'])\nconst VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi', '.m4v', '.ogv', '.mkv'])\nconst AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg', '.aac', '.m4a', '.flac', '.opus'])\n\nexport type MediaType = 'image' | 'video' | 'audio'\n\nexport interface MediaAsset {\n url: string\n type: MediaType\n mimeType: string | null\n filename: string\n savedPath: string | null\n sizeBytes: number | null\n}\n\nexport interface MediaManifest {\n pageUrl: string\n outputDir: string | null\n assets: MediaAsset[]\n filteredCount: number\n totalFound: number\n}\n\nexport interface MediaExtractOptions {\n types?: MediaType[]\n outputDir?: string | null\n}\n\nfunction isAdUrl(url: string): boolean {\n const lower = url.toLowerCase()\n return AD_PATTERNS.some(p => lower.includes(p))\n}\n\nfunction isDataUri(url: string): boolean {\n return url.startsWith('data:')\n}\n\nfunction typeFromUrl(url: string): MediaType | null {\n try {\n const ext = extname(new URL(url).pathname).toLowerCase()\n if (IMAGE_EXTS.has(ext)) return 'image'\n if (VIDEO_EXTS.has(ext)) return 'video'\n if (AUDIO_EXTS.has(ext)) return 'audio'\n } catch { /* non-parseable URL */ }\n return null\n}\n\nfunction typeFromMime(mime: string): MediaType | null {\n const lower = mime.toLowerCase()\n if (lower.startsWith('image/')) return 'image'\n if (lower.startsWith('video/')) return 'video'\n if (lower.startsWith('audio/')) return 'audio'\n return null\n}\n\nfunction resolveUrl(raw: string, base: string): string | null {\n if (!raw || isDataUri(raw)) return null\n try { return new URL(raw, base).href } catch { return null }\n}\n\nfunction safeFilename(url: string, index: number): string {\n try {\n const u = new URL(url)\n const base = basename(u.pathname).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80)\n return base || `asset-${index}`\n } catch {\n return `asset-${index}`\n }\n}\n\nexport function extractMediaUrls(html: string, baseUrl: string): string[] {\n const seen = new Set<string>()\n const urls: string[] = []\n\n const add = (raw: string) => {\n const resolved = resolveUrl(raw.trim(), baseUrl)\n if (!resolved || seen.has(resolved) || isAdUrl(resolved)) return\n seen.add(resolved)\n urls.push(resolved)\n }\n\n for (const m of html.matchAll(/<img\\s[^>]*>/gi)) {\n const tag = m[0]\n for (const attr of ['src', 'data-src', 'data-lazy-src', 'data-original']) {\n const v = (tag.match(new RegExp(`\\\\b${attr}\\\\s*=\\\\s*[\"']([^\"']+)[\"']`, 'i')) ?? [])[1]\n if (v) add(v)\n }\n const srcset = (tag.match(/\\bsrcset\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (srcset) {\n for (const part of srcset.split(',')) {\n const u = part.trim().split(/\\s+/)[0]\n if (u) add(u)\n }\n }\n }\n\n for (const m of html.matchAll(/<source\\s[^>]*>/gi)) {\n const tag = m[0]\n const src = (tag.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (src) add(src)\n const srcset = (tag.match(/\\bsrcset\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (srcset) {\n for (const part of srcset.split(',')) {\n const u = part.trim().split(/\\s+/)[0]\n if (u) add(u)\n }\n }\n }\n\n for (const m of html.matchAll(/<video\\s[^>]*>/gi)) {\n const tag = m[0]\n for (const attr of ['src', 'poster']) {\n const v = (tag.match(new RegExp(`\\\\b${attr}\\\\s*=\\\\s*[\"']([^\"']+)[\"']`, 'i')) ?? [])[1]\n if (v) add(v)\n }\n }\n\n for (const m of html.matchAll(/<audio\\s[^>]*>/gi)) {\n const tag = m[0]\n const v = (tag.match(/\\bsrc\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (v) add(v)\n }\n\n for (const m of html.matchAll(/<meta\\s[^>]*>/gi)) {\n const tag = m[0]\n const prop = (tag.match(/\\b(?:property|name)\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]?.toLowerCase()\n if (prop === 'og:image' || prop === 'twitter:image') {\n const content = (tag.match(/\\bcontent\\s*=\\s*[\"']([^\"']+)[\"']/i) ?? [])[1]\n if (content) add(content)\n }\n }\n\n for (const m of html.matchAll(/background(?:-image)?\\s*:\\s*url\\(\\s*[\"']?([^\"')]+)[\"']?\\s*\\)/gi)) {\n add(m[1])\n }\n\n return urls\n}\n\nexport async function downloadAsset(\n url: string,\n destDir: string,\n filename: string,\n options: {\n expectedType?: MediaType\n maxBytes?: number\n consumeBytes?: (bytes: number) => boolean\n } = {},\n): Promise<{ savedPath: string; sizeBytes: number; mimeType: string | null }> {\n const maxBytes = Math.max(1, Math.min(options.maxBytes ?? 50 * 1024 * 1024, 100 * 1024 * 1024))\n let target = url\n let res: Response | null = null\n for (let redirects = 0; redirects <= 5; redirects++) {\n const checked = await validatePublicHttpUrl(target, { field: 'media URL' })\n if (checked.error || !checked.parsed) throw new Error(checked.error ?? 'Media URL was rejected')\n res = await fetch(checked.parsed.href, {\n headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ThorbitBot/1.0)' },\n signal: AbortSignal.timeout(15_000),\n redirect: 'manual',\n })\n if (res.status >= 300 && res.status < 400) {\n const location = res.headers.get('location')\n if (!location) throw new Error(`HTTP ${res.status} redirect did not include Location`)\n target = new URL(location, checked.parsed.href).href\n res = null\n continue\n }\n break\n }\n if (!res) throw new Error('Media download exceeded five redirects')\n if (!res.ok) throw new Error(`HTTP ${res.status}`)\n if (!res.body) throw new Error('Empty response body')\n\n const mimeType = res.headers.get('content-type')?.split(';')[0].trim() ?? null\n if (options.expectedType && (!mimeType || typeFromMime(mimeType) !== options.expectedType)) {\n throw new Error(`Expected ${options.expectedType} content but received ${mimeType ?? 'no content-type'}`)\n }\n const declaredBytes = Number(res.headers.get('content-length') ?? 0)\n if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {\n throw new Error(`Media asset exceeds ${maxBytes} byte limit`)\n }\n\n let dest = join(destDir, filename)\n if (mimeType && !extname(filename)) {\n const mimeExt: Record<string, string> = {\n 'image/jpeg': '.jpg', 'image/png': '.png', 'image/webp': '.webp',\n 'image/gif': '.gif', 'image/svg+xml': '.svg', 'image/avif': '.avif',\n 'video/mp4': '.mp4', 'video/webm': '.webm',\n 'audio/mpeg': '.mp3', 'audio/ogg': '.ogg', 'audio/wav': '.wav',\n }\n const ext = mimeExt[mimeType]\n if (ext) dest = dest + ext\n }\n\n const writer = createWriteStream(dest)\n await new Promise<void>((resolve, reject) => {\n writer.once('open', () => resolve())\n writer.once('error', reject)\n })\n let streamedBytes = 0\n const limiter = new Transform({\n transform(chunk: Buffer, _encoding, callback) {\n const bytes = chunk.length\n if (streamedBytes + bytes > maxBytes) {\n callback(new Error(`Media asset exceeds ${maxBytes} byte limit`))\n return\n }\n if (options.consumeBytes && !options.consumeBytes(bytes)) {\n callback(new Error('Media export total byte limit exceeded'))\n return\n }\n streamedBytes += bytes\n callback(null, chunk)\n },\n })\n try {\n await pipeline(Readable.fromWeb(res.body as import('stream/web').ReadableStream), limiter, writer)\n } catch (error) {\n writer.destroy()\n rmSync(dest, { force: true })\n throw error\n }\n\n const { statSync } = await import('node:fs')\n const sizeBytes = statSync(dest).size\n\n return { savedPath: dest, sizeBytes, mimeType }\n}\n\nexport async function harvestPageMedia(\n html: string,\n pageUrl: string,\n options: MediaExtractOptions = {},\n): Promise<MediaManifest> {\n const types = options.types ?? ['image', 'video', 'audio']\n const typesSet = new Set(types)\n const rawUrls = extractMediaUrls(html, pageUrl)\n const totalFound = rawUrls.length\n\n const filteredUrls: string[] = []\n const kept: Array<{ url: string; type: MediaType }> = []\n\n for (const url of rawUrls) {\n const type = typeFromUrl(url)\n if (!type || !typesSet.has(type)) { filteredUrls.push(url); continue }\n kept.push({ url, type })\n }\n\n if (options.outputDir === null) {\n return {\n pageUrl,\n outputDir: null,\n assets: kept.map(({ url, type }, i) => ({\n url, type, mimeType: null, filename: safeFilename(url, i), savedPath: null, sizeBytes: null,\n })),\n filteredCount: filteredUrls.length,\n totalFound,\n }\n }\n\n const domain = (() => { try { return new URL(pageUrl).hostname.replace(/^www\\./, '') } catch { return 'unknown' } })()\n const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)\n const outDir = options.outputDir ?? join(homedir(), 'Downloads', 'mcp-scraper', 'media', `${stamp}-${domain}`)\n mkdirSync(outDir, { recursive: true })\n\n const assets: MediaAsset[] = []\n\n await Promise.allSettled(\n kept.map(async ({ url, type }, i) => {\n const filename = safeFilename(url, i)\n try {\n const { savedPath, sizeBytes, mimeType } = await downloadAsset(url, outDir, filename)\n const resolvedType = mimeType ? (typeFromMime(mimeType) ?? type) : type\n assets.push({ url, type: resolvedType, mimeType, filename: basename(savedPath), savedPath, sizeBytes })\n } catch {\n assets.push({ url, type, mimeType: null, filename, savedPath: null, sizeBytes: null })\n }\n })\n )\n\n assets.sort((a, b) => {\n if (a.savedPath && !b.savedPath) return -1\n if (!a.savedPath && b.savedPath) return 1\n return a.url.localeCompare(b.url)\n })\n\n return { pageUrl, outputDir: outDir, assets, filteredCount: filteredUrls.length, totalFound }\n}\n","import {\n createPrivateArtifact,\n createPrivateArtifactFromStream,\n privateArtifactOwnerId,\n renewPrivateArtifactDownload,\n type PrivateArtifactPolicy,\n} from './private-artifacts.js'\nimport type { ExtractJobArtifact } from './site-extract-repository.js'\nimport type { Readable } from 'node:stream'\n\nexport const SITE_EXTRACT_ARTIFACT_PREFIX = 'site-extracts/'\nexport const SITE_EXTRACT_ARTIFACT_TTL_MS = 7 * 24 * 60 * 60 * 1_000\nexport const SITE_EXTRACT_DOWNLOAD_TTL_MS = 15 * 60 * 1_000\n\nfunction siteExtractArtifactToken(): string | null {\n return process.env.SITE_EXTRACT_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.PRIVATE_ARTIFACT_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_READ_WRITE_TOKEN?.trim()\n || process.env.CONNECTED_DATA_BLOB_READ_WRITE_TOKEN?.trim()\n || null\n}\n\nfunction hostedByEnvironment(): boolean {\n return process.env.VERCEL === '1' || process.env.NODE_ENV === 'production'\n}\n\nfunction policy(): PrivateArtifactPolicy {\n return {\n prefix: SITE_EXTRACT_ARTIFACT_PREFIX,\n artifactTtlMs: SITE_EXTRACT_ARTIFACT_TTL_MS,\n downloadTtlMs: SITE_EXTRACT_DOWNLOAD_TTL_MS,\n token: siteExtractArtifactToken(),\n }\n}\n\nexport function siteExtractArtifactOwnerId(artifactId: string): string | null {\n return privateArtifactOwnerId(artifactId, SITE_EXTRACT_ARTIFACT_PREFIX)\n}\n\nexport async function createSiteExtractBundleArtifact(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Buffer\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifact({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n }\n}\n\nexport async function createSiteExtractBundleArtifactStream(args: {\n ownerId: string\n jobId: string\n createdAt: Date | string | number\n content: Readable\n}): Promise<ExtractJobArtifact> {\n const pointer = await createPrivateArtifactFromStream({\n policy: policy(),\n ownerId: args.ownerId,\n artifactKey: `${args.jobId}.zip`,\n createdAt: args.createdAt,\n filename: `${args.jobId}-site-export.zip`,\n contentType: 'application/zip',\n content: args.content,\n })\n return {\n key: pointer.artifactId,\n url: pointer.downloadUrl ?? '',\n bytes: pointer.bytes,\n contentType: pointer.contentType,\n filename: pointer.filename,\n sha256: pointer.sha256,\n expiresAt: pointer.expiresAt,\n downloadUrlExpiresAt: pointer.downloadUrlExpiresAt,\n }\n}\n\nexport async function renewSiteExtractArtifactDownload(args: {\n artifactId: string\n ownerId: string\n}): Promise<{ downloadUrl: string; downloadUrlExpiresAt: string; expiresAt: string } | null> {\n return renewPrivateArtifactDownload({\n policy: policy(),\n artifactId: args.artifactId,\n ownerId: args.ownerId,\n })\n}\n\nexport async function cleanupExpiredSiteExtractArtifacts(args: {\n now?: Date\n force?: boolean\n} = {}): Promise<{ deleted: number; store: 'private-vercel-blob' | 'local' | 'none'; skipped?: boolean }> {\n const now = args.now ?? new Date()\n const token = siteExtractArtifactToken()\n if (!token) return { deleted: 0, store: hostedByEnvironment() ? 'none' : 'local' }\n if (!args.force && !(now.getUTCHours() === 3 && now.getUTCMinutes() === 21)) {\n return { deleted: 0, store: 'private-vercel-blob', skipped: true }\n }\n const cutoff = now.getTime() - SITE_EXTRACT_ARTIFACT_TTL_MS\n const { list, del } = await import('@vercel/blob')\n let cursor: string | undefined\n let deleted = 0\n for (let page = 0; page < 20; page += 1) {\n const result = await list({ prefix: SITE_EXTRACT_ARTIFACT_PREFIX, token, limit: 1_000, cursor })\n const expired = result.blobs.filter(blob => new Date(blob.uploadedAt).getTime() <= cutoff)\n if (expired.length > 0) {\n await del(expired.map(blob => blob.pathname), { token })\n deleted += expired.length\n }\n if (!result.hasMore || !result.cursor) break\n cursor = result.cursor\n }\n return { deleted, store: 'private-vercel-blob' }\n}\n"],"mappings":"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","names":[]}