@link-assistant/web-capture 1.11.2 → 2.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.
Files changed (61) hide show
  1. package/.changeset/config.json +1 -1
  2. package/.dockerignore +6 -0
  3. package/.jscpd.json +4 -5
  4. package/CHANGELOG.md +12 -0
  5. package/Dockerfile +13 -11
  6. package/README.md +23 -4
  7. package/bin/web-capture.js +3 -2
  8. package/examples/js/capture_cache.js +12 -0
  9. package/examples/js/playwright_example.js +1 -1
  10. package/examples/python/capture_client.py +20 -0
  11. package/examples/python/html_download.py +2 -14
  12. package/examples/python/image_download.py +3 -7
  13. package/examples/python/markdown_download.py +2 -14
  14. package/examples/python/playwright_example.py +6 -6
  15. package/jest.config.mjs +1 -4
  16. package/package.json +54 -39
  17. package/src/archive.js +1 -1
  18. package/src/binary-download.js +14 -0
  19. package/src/cache.js +313 -0
  20. package/src/docx.js +1 -1
  21. package/src/fetch.js +1 -1
  22. package/src/figures.js +2 -12
  23. package/src/gdocs-model-wait.js +2 -4
  24. package/src/gdocs.js +1 -1
  25. package/src/index.js +7 -0
  26. package/src/lib.js +2 -2
  27. package/src/localize-images.js +2 -12
  28. package/src/search.js +1 -1
  29. package/src/shared-dialog.js +1 -1
  30. package/src/transport.js +2 -2
  31. package/tests/ci/dependency-freshness.test.js +74 -0
  32. package/tests/ci/detect-code-changes.test.js +36 -23
  33. package/tests/ci/docker-install.test.js +31 -0
  34. package/tests/ci/workflow-policy.test.js +18 -2
  35. package/tests/e2e/process.test.js +2 -1
  36. package/tests/helpers/nock.js +10 -0
  37. package/tests/helpers/screenshot.js +10 -0
  38. package/tests/helpers/search-fixture.cjs +1 -1
  39. package/tests/helpers/temporary-directory.js +12 -0
  40. package/tests/integration/api-endpoints.test.js +1 -1
  41. package/tests/integration/browser-engines.test.js +4 -52
  42. package/tests/integration/browser-packaged-path.test.js +45 -0
  43. package/tests/integration/deepwiki-rendered-page.mock.test.js +1 -1
  44. package/tests/integration/formalai-contract.test.js +1 -1
  45. package/tests/integration/github-readme.test.js +2 -6
  46. package/tests/integration/google-drive-image.test.js +1 -1
  47. package/tests/integration/search-endpoint.test.js +1 -1
  48. package/tests/integration/wikipedia-download.test.js +3 -11
  49. package/tests/mock/index.test.js +1 -1
  50. package/tests/unit/browser-launch-options.test.js +42 -0
  51. package/tests/unit/cache.test.js +182 -0
  52. package/tests/unit/fetch-html.test.js +1 -1
  53. package/tests/unit/gdocs-export-headers.test.js +1 -1
  54. package/tests/unit/gdocs.test.js +1 -1
  55. package/tests/unit/github.test.js +1 -1
  56. package/tests/unit/publish-verification.test.js +13 -16
  57. package/tests/unit/stream.test.js +8 -10
  58. package/tests/xpaste/xpaste-screenshot-markdown.mock.test.js +1 -1
  59. package/tests/xpaste/xpaste.e2e.test.js +1 -1
  60. package/tests/xpaste/xpaste.mock.test.js +1 -1
  61. package/babel.config.cjs +0 -13
@@ -1,5 +1,5 @@
1
1
  {
2
- "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
2
+ "$schema": "../node_modules/@changesets/config/schema.json",
3
3
  "changelog": "@changesets/cli/changelog",
4
4
  "commit": false,
5
5
  "fixed": [],
package/.dockerignore ADDED
@@ -0,0 +1,6 @@
1
+ node_modules
2
+ reports
3
+ coverage
4
+ *.log
5
+ .captures
6
+ capture-replay.json
package/.jscpd.json CHANGED
@@ -1,8 +1,6 @@
1
1
  {
2
- "threshold": 0,
3
2
  "minTokens": 30,
4
3
  "minLines": 5,
5
- "skipComments": true,
6
4
  "ignore": [
7
5
  "**/node_modules/**",
8
6
  "**/build/**",
@@ -12,9 +10,10 @@
12
10
  "**/.changeset/**",
13
11
  "**/package-lock.json",
14
12
  "**/pnpm-lock.yaml",
15
- "**/yarn.lock"
13
+ "**/yarn.lock",
14
+ "**/reports/**"
16
15
  ],
17
- "format": "console",
18
16
  "reporters": ["console", "html"],
19
- "output": "./reports/jscpd"
17
+ "output": "./reports/jscpd",
18
+ "mode": "weak"
20
19
  }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - b055a30: Require Node.js 26.10 or newer and update Playwright to 1.64. Use current Trixie browser images, native Jest ESM and reproducibly locked release tooling. Add locked Python examples and weekly dependency/security maintenance across all ecosystems.
8
+
9
+ ## 1.12.0
10
+
11
+ ### Minor Changes
12
+
13
+ - a6c74ba: Add a SHA-256 disk capture cache with TTL, offline stale replay, portable receipts, listing and pruning. Upgrade Puppeteer to 25 and forward packaged browser paths through browser-commander 0.10.0. Update other direct dependencies and require audit/freshness checks in CI. Node.js 22.22.1 or later is now required. Bundled consumers must externalize Playwright and Playwright Core.
14
+
3
15
  ## 1.11.2
4
16
 
5
17
  ### Patch Changes
package/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM node:22-bullseye
1
+ FROM node:26.10.0-trixie
2
2
 
3
3
  WORKDIR /app
4
4
 
@@ -9,15 +9,15 @@ RUN apt-get update && \
9
9
  apt-get install -y \
10
10
  chromium \
11
11
  fonts-liberation \
12
- libasound2 \
13
- libatk-bridge2.0-0 \
14
- libatk1.0-0 \
15
- libatspi2.0-0 \
16
- libcups2 \
12
+ libasound2t64 \
13
+ libatk-bridge2.0-0t64 \
14
+ libatk1.0-0t64 \
15
+ libatspi2.0-0t64 \
16
+ libcups2t64 \
17
17
  libdbus-1-3 \
18
18
  libdrm2 \
19
19
  libgbm1 \
20
- libgtk-3-0 \
20
+ libgtk-3-0t64 \
21
21
  libnspr4 \
22
22
  libnss3 \
23
23
  libxcomposite1 \
@@ -27,12 +27,14 @@ RUN apt-get update && \
27
27
  libxshmfence1 \
28
28
  xdg-utils \
29
29
  --no-install-recommends && break || \
30
- { echo "apt-get install attempt $i failed, retrying..."; sleep 5; }; \
30
+ { if [ "$i" -eq 3 ]; then exit 1; fi; \
31
+ echo "apt-get install attempt $i failed, retrying..."; \
32
+ apt-get update && sleep 5; }; \
31
33
  done && \
32
34
  rm -rf /var/lib/apt/lists/*
33
35
 
34
36
  # Set Puppeteer environment variables
35
- ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
37
+ ENV PUPPETEER_SKIP_DOWNLOAD=true
36
38
  ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
37
39
  ENV PUPPETEER_ARGS="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage"
38
40
 
@@ -40,8 +42,8 @@ ENV PUPPETEER_ARGS="--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usag
40
42
  ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
41
43
  ENV PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
42
44
 
43
- COPY package.json yarn.lock ./
44
- RUN yarn install --frozen-lockfile
45
+ COPY package.json package-lock.json ./
46
+ RUN npm install --global npm@12.2.0 && npm ci
45
47
 
46
48
  COPY . .
47
49
 
package/README.md CHANGED
@@ -19,6 +19,17 @@ A CLI and microservice to fetch URLs and render them as:
19
19
 
20
20
  ## Installation
21
21
 
22
+ Node.js 26.10.0 or newer is required. CI uses 26.11.1 and the latest published
23
+ Node Docker image uses 26.10.0 on Debian Trixie. Use `npm ci`
24
+ for a locked repository install; Docker and CI share `package-lock.json` and its
25
+ security overrides.
26
+
27
+ Bundled applications must externalize `playwright` and `playwright-core` and
28
+ ship them as production packages. Playwright does not support bundling its
29
+ internal modules. Packaged Chromium is supported through
30
+ `createBrowser(engine, { executablePath: packagedPath, channel: 'chrome' })`
31
+ for both engines. See [the upstream bundling discussion](https://github.com/microsoft/playwright/issues/33031).
32
+
22
33
  ### From npm
23
34
 
24
35
  ```bash
@@ -518,14 +529,14 @@ with environment variables:
518
529
 
519
530
  ```bash
520
531
  # Download the Wikipedia page (markdown + image) in every supported engine
521
- WIKIPEDIA_INTEGRATION=true npm test -- --testPathPattern="wikipedia-download"
532
+ WIKIPEDIA_INTEGRATION=true npm test -- --testPathPatterns="wikipedia-download"
522
533
 
523
534
  # Download a GitHub repository page as compact txt/markdown, original HTML, and screenshots
524
- GITHUB_REPOSITORY_INTEGRATION=true npm test -- --testPathPattern="github-readme"
535
+ GITHUB_REPOSITORY_INTEGRATION=true npm test -- --testPathPatterns="github-readme"
525
536
 
526
537
  # Habr articles and public Google Docs live suites
527
- HABR_INTEGRATION=true npm test -- --testPathPattern="habr-article"
528
- GDOCS_INTEGRATION=true npm test -- --testPathPattern="gdocs-public-doc"
538
+ HABR_INTEGRATION=true npm test -- --testPathPatterns="habr-article"
539
+ GDOCS_INTEGRATION=true npm test -- --testPathPatterns="gdocs-public-doc"
529
540
  ```
530
541
 
531
542
  ## Built With
@@ -598,3 +609,11 @@ JSON response does not duplicate the full provider body.
598
609
  ## License
599
610
 
600
611
  [Unlicense](../LICENSE) — This is free and unencumbered software released into the public domain. You are free to copy, modify, publish, use, compile, sell, or distribute this software for any purpose, commercial or non-commercial, and by any means. See [https://unlicense.org](https://unlicense.org) for details.
612
+
613
+ ## Capture cache
614
+
615
+ Both packages provide `CaptureStore` and `CachedTransport` over caller-owned transports. Captures keep the exact body bytes, final URL, status, response headers and diagnostics. Request keys include the exact URL, method and every request header in sorted order. Identical bodies share a SHA-256 file. The default TTL is 60 days; expiry is measured from fetch completion, in Unix milliseconds.
616
+
617
+ Online mode reuses fresh captures and refetches expired entries. Offline mode never invokes the transport: it returns cached captures with `cached: true` and a `stale` flag, or an `offline_cache_miss` error. Use `capture` to read these flags; the transport adapter returns the original receipt unchanged. Transport errors are not stored. Corrupt or unsupported records produce cache errors instead of returning unverified bytes.
618
+
619
+ `list` returns request metadata; `prune(now, ttl)` removes expired request records and unreferenced body files. `export(request)` and `import(json)` exchange version 1 JSON replay fixtures with byte arrays, shared between Rust and JavaScript. See [the cache format and examples](../docs/capture-cache.md).
@@ -1213,8 +1213,9 @@ async function captureUrl(url, options) {
1213
1213
  baseDelay: 500,
1214
1214
  });
1215
1215
  if (resp.ok) {
1216
- const buffer = await resp.buffer();
1217
- archive.append(buffer, { name: localPath });
1216
+ archive.append(Buffer.from(await resp.arrayBuffer()), {
1217
+ name: localPath,
1218
+ });
1218
1219
  }
1219
1220
  } catch {
1220
1221
  /* skip failed image downloads */
@@ -0,0 +1,12 @@
1
+ import { CaptureStore, CachedTransport } from '../../src/cache.js';
2
+ import { fetchTransport } from '../../src/transport.js';
3
+
4
+ const store = new CaptureStore('.captures');
5
+ const request = { url: 'https://example.com', method: 'GET', headers: {} };
6
+ const online = new CachedTransport(fetchTransport, store);
7
+ const first = await online.capture(request);
8
+ const offline = new CachedTransport(fetchTransport, store, { offline: true });
9
+ const replay = await offline.capture(request);
10
+ console.log(replay.sha256, replay.cached, replay.stale);
11
+ console.log('Exact replay:', first.receipt.body.equals(replay.receipt.body));
12
+ console.log('Portable fixture:', await store.export(request));
@@ -16,7 +16,7 @@ console.log('Fetching screenshot using Playwright engine...');
16
16
  const imageResponse = await fetch(
17
17
  `${baseUrl}/image?url=${encodeURIComponent(targetUrl)}&engine=playwright`
18
18
  );
19
- const imageBuffer = await imageResponse.buffer();
19
+ const imageBuffer = Buffer.from(await imageResponse.arrayBuffer());
20
20
  fs.writeFileSync('output/playwright_screenshot.png', imageBuffer);
21
21
  console.log('Screenshot saved to output/playwright_screenshot.png');
22
22
 
@@ -0,0 +1,20 @@
1
+ """Shared request policy for the capture-service examples."""
2
+ import os
3
+ from pathlib import Path
4
+ import sys
5
+ import requests
6
+
7
+
8
+ def capture(kind, url=None, **options):
9
+ if url is None:
10
+ url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
11
+ endpoint = os.environ.get('WEB_CAPTURE_URL', 'http://localhost:3000').rstrip('/')
12
+ response = requests.get(f'{endpoint}/{kind}', params={'url': url, **options}, timeout=30)
13
+ response.raise_for_status()
14
+ return response
15
+
16
+
17
+ def download_text(kind, filename):
18
+ output = Path(__file__).with_name(filename)
19
+ output.write_text(capture(kind).text, encoding='utf-8')
20
+ print(f'{kind.title()} saved to {output}')
@@ -1,15 +1,3 @@
1
- import sys
2
- import requests
3
- import os
1
+ from capture_client import download_text
4
2
 
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/html?url={url}'
7
-
8
- response = requests.get(endpoint)
9
- response.raise_for_status()
10
-
11
- output_path = os.path.join(os.path.dirname(__file__), 'downloaded.html')
12
- with open(output_path, 'w', encoding='utf-8') as f:
13
- f.write(response.text)
14
-
15
- print(f'HTML saved to {output_path}')
3
+ download_text('html', 'downloaded.html')
@@ -1,11 +1,7 @@
1
- import sys
2
- import requests
1
+ from capture_client import capture
3
2
  import os
4
3
 
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/image?url={url}'
7
-
8
- response = requests.get(endpoint)
4
+ response = capture('image')
9
5
  print('Status:', response.status_code)
10
6
  print('Content-Type:', response.headers.get('content-type'))
11
7
 
@@ -17,4 +13,4 @@ if response.content[:8] == png_signature:
17
13
  f.write(response.content)
18
14
  print(f'Image saved to {output_path} (valid PNG)')
19
15
  else:
20
- print('Response is not a valid PNG! First bytes:', response.content[:16])
16
+ print('Response is not a valid PNG! First bytes:', response.content[:16])
@@ -1,15 +1,3 @@
1
- import sys
2
- import requests
3
- import os
1
+ from capture_client import download_text
4
2
 
5
- url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
- endpoint = f'http://localhost:3000/markdown?url={url}'
7
-
8
- response = requests.get(endpoint)
9
- response.raise_for_status()
10
-
11
- output_path = os.path.join(os.path.dirname(__file__), 'downloaded.md')
12
- with open(output_path, 'w', encoding='utf-8') as f:
13
- f.write(response.text)
14
-
15
- print(f'Markdown saved to {output_path}')
3
+ download_text('markdown', 'downloaded.md')
@@ -1,20 +1,20 @@
1
- import requests
1
+ import sys
2
+ from capture_client import capture
2
3
  import os
3
4
 
4
- base_url = 'http://localhost:3000'
5
- target_url = 'https://example.com'
5
+ target_url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
6
6
 
7
7
  # Create output directory if it doesn't exist
8
8
  os.makedirs('output', exist_ok=True)
9
9
 
10
10
  print('Fetching HTML using Playwright engine...')
11
- html_response = requests.get(f'{base_url}/html', params={'url': target_url, 'engine': 'playwright'})
12
- with open('output/playwright_html.html', 'w') as f:
11
+ html_response = capture('html', target_url, engine='playwright')
12
+ with open('output/playwright_html.html', 'w', encoding='utf-8') as f:
13
13
  f.write(html_response.text)
14
14
  print('HTML saved to output/playwright_html.html')
15
15
 
16
16
  print('Fetching screenshot using Playwright engine...')
17
- image_response = requests.get(f'{base_url}/image', params={'url': target_url, 'engine': 'playwright'})
17
+ image_response = capture('image', target_url, engine='playwright')
18
18
  with open('output/playwright_screenshot.png', 'wb') as f:
19
19
  f.write(image_response.content)
20
20
  print('Screenshot saved to output/playwright_screenshot.png')
package/jest.config.mjs CHANGED
@@ -1,8 +1,5 @@
1
1
  export default {
2
- transform: {
3
- '^.+\\.js$': ['babel-jest', { configFile: './babel.config.cjs' }],
4
- },
5
- transformIgnorePatterns: ['node_modules/(?!(turndown)/)'],
2
+ transform: {},
6
3
  moduleNameMapper: {
7
4
  '^(\\.{1,2}/.*)\\.js$': '$1',
8
5
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.11.2",
3
+ "version": "2.0.0",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,22 +8,22 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "docker compose up -d",
11
- "dev": "nodemon bin/web-capture.js -- --serve",
11
+ "dev": "node --watch bin/web-capture.js --serve",
12
12
  "test": "node --experimental-vm-modules ./node_modules/.bin/jest",
13
13
  "test:watch": "node --experimental-vm-modules ./node_modules/.bin/jest --watch",
14
- "test:e2e:process": "node --experimental-vm-modules ./node_modules/.bin/jest tests/e2e.process.test.js",
15
- "test:e2e:docker": "node --experimental-vm-modules ./node_modules/.bin/jest tests/e2e.docker.test.js",
16
- "test:e2e": "yarn test:e2e:process && yarn test:e2e:docker",
17
- "test:all": "yarn build && yarn test && yarn test:e2e",
18
- "build": "docker compose down && docker compose build && yarn start",
19
- "examples:python": "python3 examples/python/html_download.py && python3 examples/python/markdown_download.py && python3 examples/python/image_download.py",
14
+ "test:e2e:process": "node --experimental-vm-modules ./node_modules/.bin/jest tests/e2e/process.test.js",
15
+ "test:e2e:docker": "node --experimental-vm-modules ./node_modules/.bin/jest tests/e2e/docker.test.js",
16
+ "test:e2e": "npm run test:e2e:process && npm run test:e2e:docker",
17
+ "test:all": "npm run build && npm test && npm run test:e2e",
18
+ "build": "docker compose down && docker compose build && npm start",
19
+ "examples:python": "uv run --project .. --locked python examples/python/html_download.py && uv run --project .. --locked python examples/python/markdown_download.py && uv run --project .. --locked python examples/python/image_download.py",
20
20
  "examples:javascript": "node examples/js/html_download.js && node examples/js/markdown_download.js && node examples/js/image_download.js",
21
- "examples": "yarn build && yarn examples:javascript && yarn examples:python",
21
+ "examples": "npm run build && npm run examples:javascript && npm run examples:python",
22
22
  "lint": "eslint .",
23
23
  "lint:fix": "eslint . --fix",
24
24
  "format": "prettier --write .",
25
25
  "format:check": "prettier --check .",
26
- "check:duplication": "jscpd .",
26
+ "check:duplication": "jscpd . --baseline-from-ref origin/main --fail-on-new-clones --fail-on-empty",
27
27
  "check": "npm run lint && npm run format:check && npm run check:duplication",
28
28
  "prepare": "husky || true",
29
29
  "changeset": "changeset",
@@ -32,42 +32,40 @@
32
32
  "changeset:status": "changeset status --since=origin/main"
33
33
  },
34
34
  "dependencies": {
35
- "@kreuzberg/html-to-markdown-node": "^3.5.5",
35
+ "@kreuzberg/html-to-markdown-node": "^3.7.2",
36
36
  "archiver": "^8.0.0",
37
- "browser-commander": "^0.8.0",
38
- "cheerio": "^1.0.0",
39
- "docx": "^9.6.1",
40
- "express": "^4.18.2",
41
- "he": "^1.2.0",
42
- "iconv-lite": "^0.6.3",
37
+ "browser-commander": "0.10.0",
38
+ "cheerio": "^1.2.0",
39
+ "docx": "^9.9.0",
40
+ "express": "^5.2.1",
41
+ "he": "^2.0.0",
42
+ "iconv-lite": "^0.7.3",
43
43
  "lino-arguments": "^0.3.0",
44
44
  "log-lazy": "^1.0.4",
45
- "node-fetch": "^2.7.0",
46
- "playwright": "^1.49.0",
47
- "puppeteer": "^24.8.2",
48
- "puppeteer-core": "^24.40.0",
49
- "turndown": "^7.1.1",
45
+ "node-fetch": "^3.3.2",
46
+ "playwright": "^1.64.0",
47
+ "puppeteer": "^25.12.0",
48
+ "puppeteer-core": "^25.12.0",
49
+ "turndown": "^7.2.4",
50
50
  "turndown-plugin-gfm": "^1.0.2"
51
51
  },
52
52
  "devDependencies": {
53
- "@babel/core": "^7.24.0",
54
- "@babel/preset-env": "^7.24.0",
55
- "@changesets/cli": "^2.29.7",
56
- "@types/archiver": "^7.0.0",
57
- "babel-jest": "^29.7.0",
58
- "eslint": "^9.38.0",
53
+ "@changesets/cli": "^3.0.3",
54
+ "@types/archiver": "^8.0.0",
55
+ "eslint": "^10.12.0",
59
56
  "eslint-config-prettier": "^10.1.8",
60
- "eslint-plugin-prettier": "^5.5.4",
61
- "get-port": "^7.1.0",
57
+ "eslint-plugin-prettier": "^5.5.6",
58
+ "get-port": "^7.2.0",
62
59
  "husky": "^9.1.7",
63
- "jest": "^29.7.0",
64
- "jscpd": "^4.0.5",
65
- "lint-staged": "^16.2.6",
66
- "markdown-it": "^14.1.1",
67
- "nock": "^13.5.4",
68
- "prettier": "^3.6.2",
69
- "supertest": "^6.3.4",
70
- "unzipper": "^0.12.3"
60
+ "jest": "^30.5.2",
61
+ "jscpd": "^5.4.0",
62
+ "lint-staged": "^17.6.0",
63
+ "markdown-it": "^15.0.2",
64
+ "nock": "^15.0.1",
65
+ "prettier": "^3.9.9",
66
+ "supertest": "^7.3.1",
67
+ "unzipper": "^0.12.5",
68
+ "@eslint/js": "^10.0.1"
71
69
  },
72
70
  "type": "module",
73
71
  "keywords": [
@@ -82,7 +80,7 @@
82
80
  "url": "https://github.com/link-assistant/web-capture"
83
81
  },
84
82
  "engines": {
85
- "node": ">=22.0.0 <23.0.0"
83
+ "node": ">=26.10.0"
86
84
  },
87
85
  "lint-staged": {
88
86
  "*.{js,mjs,cjs}": [
@@ -94,5 +92,22 @@
94
92
  "prettier --write",
95
93
  "prettier --check"
96
94
  ]
95
+ },
96
+ "dependencyBlockers": {
97
+ "browser-commander": {
98
+ "issue": "https://github.com/link-assistant/web-capture/issues/160",
99
+ "reason": "Keep the first compatible packaged-browser release; later versions add native better-sqlite3 dependencies."
100
+ }
101
+ },
102
+ "overrides": {
103
+ "@istanbuljs/load-nyc-config": {
104
+ "js-yaml": "^4.1.1"
105
+ }
106
+ },
107
+ "packageManager": "npm@12.2.0",
108
+ "allowScripts": {
109
+ "@parcel/watcher": true,
110
+ "puppeteer": true,
111
+ "unrs-resolver": true
97
112
  }
98
113
  }
package/src/archive.js CHANGED
@@ -200,7 +200,7 @@ export async function archiveHandler(req, res) {
200
200
  baseDelay: 500,
201
201
  });
202
202
  if (imgResp.ok) {
203
- const buffer = await imgResp.buffer();
203
+ const buffer = Buffer.from(await imgResp.arrayBuffer());
204
204
  archive.append(buffer, { name: localPath });
205
205
  }
206
206
  } catch {
@@ -0,0 +1,14 @@
1
+ import fetch from 'node-fetch';
2
+ import { retry } from './retry.js';
3
+
4
+ // Shared image-download policy; keep HTTP errors distinct from network retries.
5
+ export async function downloadBinary(url) {
6
+ const response = await retry(() => fetch(url), {
7
+ retries: 3,
8
+ baseDelay: 1000,
9
+ });
10
+ if (!response.ok) {
11
+ throw new Error(`HTTP ${response.status}`);
12
+ }
13
+ return Buffer.from(await response.arrayBuffer());
14
+ }