@link-assistant/web-capture 1.7.3 → 1.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 1.7.5
4
+
5
+ ### Patch Changes
6
+
7
+ - dfaf007: Remove the unsupported `/gdocs` HTTP API route from the JavaScript and Rust server surfaces while keeping Google Docs URL capture through normal output formats.
8
+
9
+ ## 1.7.4
10
+
11
+ ### Patch Changes
12
+
13
+ - 494da4e: Move turndown-plugin-gfm into runtime dependencies so published CLI installs include the GFM plugin used by Markdown conversion.
14
+
3
15
  ## 1.7.3
4
16
 
5
17
  ### Patch Changes
@@ -230,7 +230,7 @@ const config = makeConfig({
230
230
  'Capture private Google Doc with API token'
231
231
  )
232
232
  .epilogue(
233
- 'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&format=png|jpeg&theme=light|dark Screenshot\n GET /archive?url=<URL>&localImages=true&documentFormat=markdown|html ZIP archive\n GET /pdf?url=<URL>&theme=light|dark PDF with embedded images\n GET /docx?url=<URL> DOCX with embedded images\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy\n GET /gdocs?url=<URL>&format=markdown|html|txt Google Docs capture'
233
+ 'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&format=png|jpeg&theme=light|dark Screenshot\n GET /archive?url=<URL>&localImages=true&documentFormat=markdown|html ZIP archive\n GET /pdf?url=<URL>&theme=light|dark PDF with embedded images\n GET /docx?url=<URL> DOCX with embedded images\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy'
234
234
  )
235
235
  .strict(),
236
236
  lenv: {
@@ -271,7 +271,6 @@ async function startServer(port) {
271
271
  console.log(` GET /docx?url=<URL> - DOCX with embedded images`);
272
272
  console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
273
273
  console.log(` GET /stream?url=<URL> - Stream content`);
274
- console.log(` GET /gdocs?url=<URL> - Capture Google Docs document`);
275
274
  console.log('');
276
275
  console.log('Press Ctrl+C to stop the server');
277
276
  resolve(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -44,7 +44,8 @@
44
44
  "playwright": "^1.49.0",
45
45
  "puppeteer": "^24.8.2",
46
46
  "puppeteer-core": "^24.40.0",
47
- "turndown": "^7.1.1"
47
+ "turndown": "^7.1.1",
48
+ "turndown-plugin-gfm": "^1.0.2"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@babel/core": "^7.24.0",
@@ -62,8 +63,7 @@
62
63
  "lint-staged": "^16.2.6",
63
64
  "nock": "^13.5.4",
64
65
  "prettier": "^3.6.2",
65
- "supertest": "^6.3.4",
66
- "turndown-plugin-gfm": "^1.0.2"
66
+ "supertest": "^6.3.4"
67
67
  },
68
68
  "type": "module",
69
69
  "keywords": [
package/src/gdocs.js CHANGED
@@ -6,8 +6,7 @@
6
6
 
7
7
  import fetch from 'node-fetch';
8
8
  import he from 'he';
9
- import archiver from 'archiver';
10
- import { convertHtmlToMarkdown, prettyPrintHtml } from './lib.js';
9
+ import { convertHtmlToMarkdown } from './lib.js';
11
10
 
12
11
  const GDOCS_URL_PATTERN = /docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/;
13
12
 
@@ -223,113 +222,3 @@ export async function fetchGoogleDocAsArchive(url, options = {}) {
223
222
  images,
224
223
  };
225
224
  }
226
-
227
- /**
228
- * Express handler for the /gdocs API endpoint.
229
- *
230
- * Query parameters:
231
- * - url (required): Google Docs URL
232
- * - format (optional): Export format (html, markdown, md, txt, pdf, docx)
233
- * - apiToken (optional): API token for private documents
234
- *
235
- * The API token can also be provided via:
236
- * - Authorization header: Bearer <token>
237
- * - X-Api-Token header: <token>
238
- *
239
- * @param {import('express').Request} req
240
- * @param {import('express').Response} res
241
- */
242
- export async function gdocsHandler(req, res) {
243
- const url = req.query.url;
244
- if (!url) {
245
- return res.status(400).send('Missing `url` parameter');
246
- }
247
-
248
- if (!isGoogleDocsUrl(url)) {
249
- return res.status(400).send('URL is not a Google Docs document URL');
250
- }
251
-
252
- // Resolve API token from query, headers, or environment
253
- const apiToken =
254
- req.query.apiToken ||
255
- extractBearerToken(req.headers.authorization) ||
256
- req.headers['x-api-token'] ||
257
- undefined;
258
-
259
- const format = (req.query.format || 'markdown').toLowerCase();
260
-
261
- try {
262
- if (format === 'archive' || format === 'zip') {
263
- return await sendGDocsArchive(res, url, apiToken);
264
- }
265
-
266
- if (format === 'markdown' || format === 'md') {
267
- const result = await fetchGoogleDocAsMarkdown(url, { apiToken });
268
- return res.type('text/markdown').send(result.markdown);
269
- }
270
-
271
- return await sendGDocsExport(res, url, format, apiToken);
272
- } catch (err) {
273
- console.error('Google Docs capture error:', err.message);
274
- return res.status(500).send(`Error capturing Google Doc: ${err.message}`);
275
- }
276
- }
277
-
278
- async function sendGDocsArchive(res, url, apiToken) {
279
- const archiveResult = await fetchGoogleDocAsArchive(url, { apiToken });
280
-
281
- res.set('Content-Type', 'application/zip');
282
- res.set(
283
- 'Content-Disposition',
284
- `attachment; filename="gdoc-${archiveResult.documentId}.zip"`
285
- );
286
-
287
- const archive = archiver('zip', { zlib: { level: 9 } });
288
- archive.pipe(res);
289
- archive.append(archiveResult.markdown, { name: 'document.md' });
290
- archive.append(prettyPrintHtml(archiveResult.html), {
291
- name: 'document.html',
292
- });
293
- for (const img of archiveResult.images) {
294
- archive.append(img.data, { name: `images/${img.filename}` });
295
- }
296
- await archive.finalize();
297
- }
298
-
299
- const FORMAT_CONTENT_TYPES = {
300
- html: 'text/html',
301
- txt: 'text/plain',
302
- pdf: 'application/pdf',
303
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
304
- };
305
-
306
- const FORMAT_DISPOSITIONS = {
307
- pdf: 'inline; filename="document.pdf"',
308
- docx: 'inline; filename="document.docx"',
309
- };
310
-
311
- async function sendGDocsExport(res, url, format, apiToken) {
312
- const result = await fetchGoogleDoc(url, { format, apiToken });
313
- const contentType = FORMAT_CONTENT_TYPES[format] || 'text/html';
314
- const disposition = FORMAT_DISPOSITIONS[format];
315
-
316
- res.type(contentType);
317
- if (disposition) {
318
- res.set('Content-Disposition', disposition);
319
- }
320
- return res.send(result.content);
321
- }
322
-
323
- /**
324
- * Extract Bearer token from Authorization header value.
325
- *
326
- * @param {string|undefined} authHeader - Authorization header value
327
- * @returns {string|undefined} Token or undefined
328
- */
329
- function extractBearerToken(authHeader) {
330
- if (!authHeader || typeof authHeader !== 'string') {
331
- return undefined;
332
- }
333
- const match = authHeader.match(/^Bearer\s+(.+)$/i);
334
- return match ? match[1] : undefined;
335
- }
package/src/index.js CHANGED
@@ -11,7 +11,6 @@ import { docxHandler } from './docx.js';
11
11
  import { animationHandler } from './animation.js';
12
12
  import { figuresHandler } from './figures.js';
13
13
  import { themedImageHandler } from './themed-image.js';
14
- import { gdocsHandler } from './gdocs.js';
15
14
 
16
15
  const app = express();
17
16
  const port = process.env.PORT || 3000;
@@ -28,7 +27,6 @@ app.get('/docx', docxHandler);
28
27
  app.get('/animation', animationHandler);
29
28
  app.get('/figures', figuresHandler);
30
29
  app.get('/themed-image', themedImageHandler);
31
- app.get('/gdocs', gdocsHandler);
32
30
 
33
31
  // Start the server if this is the main module
34
32
  const isMainModule =
@@ -28,6 +28,17 @@ afterEach(() => {
28
28
  });
29
29
 
30
30
  describe('API Endpoint Tests', () => {
31
+ describe('unsupported routes', () => {
32
+ it('does not expose /gdocs as an output-format endpoint', async () => {
33
+ await request(app)
34
+ .get('/gdocs')
35
+ .query({
36
+ url: 'https://docs.google.com/document/d/test-doc/edit',
37
+ })
38
+ .expect(404);
39
+ });
40
+ });
41
+
31
42
  describe('GET /image', () => {
32
43
  it('rejects invalid format', async () => {
33
44
  await request(app)
@@ -222,6 +222,7 @@ describe('CLI', () => {
222
222
  expect(result.stdout).toContain('/html');
223
223
  expect(result.stdout).toContain('/markdown');
224
224
  expect(result.stdout).toContain('/image');
225
+ expect(result.stdout).not.toContain('/gdocs');
225
226
  });
226
227
  });
227
228
 
@@ -0,0 +1,48 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const jsRoot = resolve(__dirname, '../..');
8
+
9
+ function readJson(relativePath) {
10
+ return JSON.parse(readFileSync(resolve(jsRoot, relativePath), 'utf8'));
11
+ }
12
+
13
+ const runtimeImports = [
14
+ {
15
+ packageName: 'turndown-plugin-gfm',
16
+ sourceFile: 'src/lib.js',
17
+ },
18
+ ];
19
+
20
+ describe('runtime package dependencies', () => {
21
+ test.each(runtimeImports)(
22
+ '$packageName is declared as a production dependency',
23
+ ({ packageName, sourceFile }) => {
24
+ const source = readFileSync(resolve(jsRoot, sourceFile), 'utf8');
25
+ const packageJson = readJson('package.json');
26
+
27
+ expect(source).toContain(`from '${packageName}'`);
28
+ expect(packageJson.dependencies).toHaveProperty(packageName);
29
+ expect(packageJson.devDependencies || {}).not.toHaveProperty(packageName);
30
+ }
31
+ );
32
+
33
+ test.each(runtimeImports)(
34
+ '$packageName is not marked dev-only in package-lock.json',
35
+ ({ packageName }) => {
36
+ const packageLock = readJson('package-lock.json');
37
+
38
+ expect(packageLock.packages[''].dependencies).toHaveProperty(packageName);
39
+ expect(packageLock.packages[''].devDependencies || {}).not.toHaveProperty(
40
+ packageName
41
+ );
42
+ expect(packageLock.packages[`node_modules/${packageName}`]).toBeDefined();
43
+ expect(packageLock.packages[`node_modules/${packageName}`].dev).not.toBe(
44
+ true
45
+ );
46
+ }
47
+ );
48
+ });