@miketromba/screenshot-service 0.2.2 → 0.2.6

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/README.md CHANGED
@@ -24,25 +24,39 @@ npx @miketromba/screenshot-service --port 3001
24
24
  SCREENSHOT_AUTH_TOKEN=secret npx @miketromba/screenshot-service
25
25
  ```
26
26
 
27
- ### Production (Vercel)
27
+ ### Production (Vercel + Next.js)
28
28
 
29
- Add the screenshot service to your existing Vercel/Next.js project:
29
+ Deploy the screenshot service as part of your Next.js project on Vercel.
30
+
31
+ > **Note:** This package ships raw TypeScript files. You must configure Next.js to transpile it.
30
32
 
31
33
  ```bash
32
34
  npm install @miketromba/screenshot-service
33
35
  ```
34
36
 
35
- **Next.js App Router** (`app/api/screenshot/route.ts`):
37
+ **Step 1: Configure Next.js to transpile the package** (`next.config.js` or `next.config.ts`):
38
+ ```js
39
+ /** @type {import('next').NextConfig} */
40
+ const nextConfig = {
41
+ transpilePackages: ['@miketromba/screenshot-service'],
42
+ }
43
+
44
+ module.exports = nextConfig
45
+ ```
46
+
47
+ **Step 2: Create the API route**
48
+
49
+ Next.js App Router (`app/api/screenshot/route.ts`):
36
50
  ```ts
37
51
  export { GET } from '@miketromba/screenshot-service/vercel'
38
52
  ```
39
53
 
40
- **Vercel API Routes** (`api/screenshot.ts`):
54
+ Or for Pages Router (`pages/api/screenshot.ts`):
41
55
  ```ts
42
56
  export { GET } from '@miketromba/screenshot-service/vercel'
43
57
  ```
44
58
 
45
- Add function configuration to your `vercel.json`:
59
+ **Step 3: Add function configuration** (`vercel.json`):
46
60
  ```json
47
61
  {
48
62
  "functions": {
@@ -205,7 +219,8 @@ The service provides several options to control when the screenshot is taken, en
205
219
  - Input validation using [Zod](https://zod.dev/)
206
220
  - Requires [Bun](https://bun.sh) runtime
207
221
 
208
- ### Vercel Serverless
222
+ ### Vercel Serverless (Next.js)
223
+ - Requires Next.js with `transpilePackages` configured (ships raw TypeScript)
209
224
  - Serverless function with [puppeteer-core](https://pptr.dev/)
210
225
  - Uses [@sparticuz/chromium](https://github.com/Sparticuz/chromium) for serverless-optimized Chromium
211
226
  - Shared validation and screenshot logic
@@ -277,15 +292,16 @@ Note: The `--add-host` flag is required for `host.docker.internal` to work. Make
277
292
  - **No custom fonts**: Unlike local development, Vercel functions don't include custom fonts. Screenshots may render with different fonts.
278
293
  - **Scaling**: Vercel handles scaling automatically via parallel function invocations
279
294
 
280
- ### Local Dev vs Vercel
295
+ ### Local Dev vs Vercel + Next.js
281
296
 
282
- | Feature | Local (npx) | Vercel |
283
- |---------|-------------|--------|
297
+ | Feature | Local (npx) | Vercel + Next.js |
298
+ |---------|-------------|------------------|
284
299
  | Concurrency | puppeteer-cluster (configurable) | Horizontal scaling |
285
300
  | Cold start | None (always running) | 5-10 seconds |
286
301
  | Custom fonts | System fonts available | Limited |
287
302
  | Max timeout | Unlimited | 60-300 seconds |
288
303
  | Cost | Free (local) | Pay per invocation |
304
+ | Setup | None | Requires `transpilePackages` config |
289
305
 
290
306
  ## Example Usage
291
307
 
package/bin/cli.ts CHANGED
File without changes
package/package.json CHANGED
@@ -1,35 +1,32 @@
1
1
  {
2
2
  "name": "@miketromba/screenshot-service",
3
- "version": "0.2.2",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "screenshot-service": "bin/cli.ts"
7
7
  },
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.js",
11
- "types": "./dist/index.d.ts"
10
+ "import": "./src/index.ts",
11
+ "types": "./src/index.ts"
12
12
  },
13
13
  "./vercel": {
14
- "import": "./dist/vercel.js",
15
- "types": "./dist/vercel.d.ts"
14
+ "import": "./src/vercel.ts",
15
+ "types": "./src/vercel.ts"
16
16
  }
17
17
  },
18
18
  "files": [
19
- "dist",
20
19
  "bin",
21
20
  "src"
22
21
  ],
23
22
  "scripts": {
24
- "build": "tsc",
25
- "prepublishOnly": "npm run build",
26
23
  "dev": "bun --watch src/server.ts",
27
24
  "start": "bun run src/server.ts",
28
25
  "docker:build": "docker build -t screenshot-service ."
29
26
  },
30
27
  "dependencies": {
31
28
  "@hono/zod-validator": "^0.4.3",
32
- "@sparticuz/chromium-min": "^133.0.0",
29
+ "@sparticuz/chromium": "^143.0.4",
33
30
  "hono": "^4.7.4",
34
31
  "puppeteer-core": "^24.36.0",
35
32
  "zod": "^3.24.2"
@@ -39,8 +36,7 @@
39
36
  "puppeteer-cluster": "^0.24.0"
40
37
  },
41
38
  "devDependencies": {
42
- "@types/bun": "latest",
43
- "typescript": "^5.0.0"
39
+ "@types/bun": "latest"
44
40
  },
45
41
  "trustedDependencies": [
46
42
  "puppeteer"
package/src/vercel.ts CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  import type { Browser, Page } from 'puppeteer-core'
13
13
  import puppeteer from 'puppeteer-core'
14
- import chromium from '@sparticuz/chromium-min'
14
+ import chromium from '@sparticuz/chromium'
15
15
  import {
16
16
  screenshotQuerySchema,
17
17
  isUrlHostnameAllowed,
@@ -23,11 +23,6 @@ import {
23
23
  CHROME_ARGS
24
24
  } from './shared'
25
25
 
26
- // Remote chromium URL - uses @sparticuz/chromium releases on GitHub
27
- // This avoids bundling issues on Vercel since chromium is downloaded at runtime
28
- const CHROMIUM_REMOTE_URL =
29
- 'https://github.com/Sparticuz/chromium/releases/download/v133.0.0/chromium-v133.0.0-pack.tar'
30
-
31
26
  // Cache browser instance for warm invocations
32
27
  let browser: Browser | null = null
33
28
 
@@ -39,7 +34,7 @@ async function getBrowser(): Promise<Browser> {
39
34
  browser = await puppeteer.launch({
40
35
  args: [...chromium.args, ...CHROME_ARGS],
41
36
  defaultViewport: null,
42
- executablePath: await chromium.executablePath(CHROMIUM_REMOTE_URL),
37
+ executablePath: await chromium.executablePath(),
43
38
  headless: true
44
39
  })
45
40
 
package/dist/index.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export { screenshotQuerySchema, type ScreenshotQuery, type ScreenshotOptions, type AuthResult, type Page, getHostWhitelist, getAuthToken, isUrlHostnameAllowed, queryToScreenshotOptions, validateBearerToken, captureScreenshot, CHROME_ARGS, USER_AGENT, SCREENSHOT_CSS } from './shared';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAEN,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,IAAI,EAGT,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,EACpB,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,EAGjB,WAAW,EACX,UAAU,EACV,cAAc,EACd,MAAM,UAAU,CAAA"}
package/dist/index.js DELETED
@@ -1,8 +0,0 @@
1
- // Main package exports - shared utilities, types, and schemas
2
- export {
3
- // Schema and types
4
- screenshotQuerySchema,
5
- // Utilities
6
- getHostWhitelist, getAuthToken, isUrlHostnameAllowed, queryToScreenshotOptions, validateBearerToken, captureScreenshot,
7
- // Constants
8
- CHROME_ARGS, USER_AGENT, SCREENSHOT_CSS } from './shared';
package/dist/server.d.ts DELETED
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=server.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":""}
package/dist/server.js DELETED
@@ -1,84 +0,0 @@
1
- import { Hono } from 'hono';
2
- import { cors } from 'hono/cors';
3
- import { logger } from 'hono/logger';
4
- import { Cluster } from 'puppeteer-cluster';
5
- import puppeteer from 'puppeteer';
6
- import { zValidator } from '@hono/zod-validator';
7
- import { screenshotQuerySchema, isUrlHostnameAllowed, queryToScreenshotOptions, validateBearerToken, captureScreenshot, getHostWhitelist, getAuthToken, CHROME_ARGS } from './shared';
8
- const MAX_CONCURRENCY = process.env.MAX_CONCURRENCY
9
- ? parseInt(process.env.MAX_CONCURRENCY)
10
- : 10;
11
- const SCREENSHOT_HOST_WHITELIST = getHostWhitelist();
12
- const SCREENSHOT_AUTH_TOKEN = getAuthToken();
13
- const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000;
14
- const DEV_MODE = process.env.NODE_ENV === 'development';
15
- let cluster;
16
- async function getCluster() {
17
- if (!cluster) {
18
- cluster = await Cluster.launch({
19
- concurrency: Cluster.CONCURRENCY_CONTEXT,
20
- maxConcurrency: MAX_CONCURRENCY,
21
- puppeteerOptions: {
22
- timeout: 300_000, // 5 minutes -- give it very generous timeout since loading browsers all at once on resource-bound VM is slow
23
- headless: true,
24
- args: CHROME_ARGS
25
- }
26
- });
27
- }
28
- return cluster;
29
- }
30
- async function takeScreenshot(opts) {
31
- const cluster = await getCluster();
32
- return cluster.execute(null, async ({ page }) => {
33
- return captureScreenshot(page, opts, SCREENSHOT_AUTH_TOKEN);
34
- });
35
- }
36
- const app = new Hono();
37
- // Add common middleware
38
- // Only use logger middleware in development environment
39
- if (DEV_MODE) {
40
- app.use('*', logger());
41
- }
42
- app.use('*', cors());
43
- // Authentication middleware
44
- const auth = async (c, next) => {
45
- // Skip auth for health check endpoint
46
- if (c.req.path === '/') {
47
- return next();
48
- }
49
- const authResult = validateBearerToken(c.req.header('Authorization'), SCREENSHOT_AUTH_TOKEN);
50
- if (authResult.valid === false) {
51
- return c.json({ error: authResult.error }, authResult.status);
52
- }
53
- return next();
54
- };
55
- if (SCREENSHOT_AUTH_TOKEN) {
56
- app.use('*', auth);
57
- }
58
- app.get('/', c => {
59
- return c.json({ online: true });
60
- });
61
- app.get('/screenshot', zValidator('query', screenshotQuerySchema), async (c) => {
62
- const query = c.req.valid('query');
63
- // Prod logging
64
- if (!DEV_MODE)
65
- console.log('CAPTURE:', query.url);
66
- // Check if the URL's hostname is allowed
67
- if (!isUrlHostnameAllowed(query.url, SCREENSHOT_HOST_WHITELIST)) {
68
- return c.json({
69
- error: `Hostname not allowed. Must be one of: ${SCREENSHOT_HOST_WHITELIST.join(', ')}`
70
- }, 403);
71
- }
72
- const screenshot = await takeScreenshot(queryToScreenshotOptions(query));
73
- return new Response(Buffer.from(screenshot), {
74
- headers: {
75
- 'Content-Type': `image/${query.type}`
76
- }
77
- });
78
- });
79
- Bun.serve({
80
- port: PORT,
81
- fetch: app.fetch,
82
- idleTimeout: 255 // max value, 5 mins
83
- });
84
- console.log(`Screenshot service is online on port ${PORT}`);
package/dist/shared.d.ts DELETED
@@ -1,66 +0,0 @@
1
- import { z } from 'zod';
2
- import type { Page as PuppeteerPage } from 'puppeteer';
3
- import type { Page as PuppeteerCorePage } from 'puppeteer-core';
4
- export declare const getHostWhitelist: () => string[];
5
- export declare const getAuthToken: () => string | undefined;
6
- export declare const screenshotQuerySchema: z.ZodObject<{
7
- url: z.ZodString;
8
- fullPage: z.ZodDefault<z.ZodOptional<z.ZodEnum<["true", "false"]>>>;
9
- quality: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
10
- type: z.ZodDefault<z.ZodOptional<z.ZodEnum<["png", "webp", "jpeg"]>>>;
11
- width: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
12
- height: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
13
- waitUntil: z.ZodDefault<z.ZodOptional<z.ZodEnum<["load", "domcontentloaded", "networkidle0", "networkidle2"]>>>;
14
- waitForSelector: z.ZodOptional<z.ZodString>;
15
- delay: z.ZodOptional<z.ZodNumber>;
16
- }, "strip", z.ZodTypeAny, {
17
- url: string;
18
- fullPage: "true" | "false";
19
- quality: number;
20
- type: "png" | "webp" | "jpeg";
21
- width: number;
22
- height: number;
23
- waitUntil: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
24
- waitForSelector?: string | undefined;
25
- delay?: number | undefined;
26
- }, {
27
- url: string;
28
- fullPage?: "true" | "false" | undefined;
29
- quality?: number | undefined;
30
- type?: "png" | "webp" | "jpeg" | undefined;
31
- width?: number | undefined;
32
- height?: number | undefined;
33
- waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2" | undefined;
34
- waitForSelector?: string | undefined;
35
- delay?: number | undefined;
36
- }>;
37
- export type ScreenshotQuery = z.infer<typeof screenshotQuerySchema>;
38
- export type ScreenshotOptions = {
39
- url: string;
40
- fullPage: boolean;
41
- quality: number;
42
- type: 'png' | 'webp' | 'jpeg';
43
- dimensions: {
44
- width: number;
45
- height: number;
46
- };
47
- waitUntil: 'load' | 'domcontentloaded' | 'networkidle0' | 'networkidle2';
48
- waitForSelector?: string;
49
- delay?: number;
50
- };
51
- export declare function isUrlHostnameAllowed(url: string, whitelist: string[]): boolean;
52
- export declare function queryToScreenshotOptions(query: ScreenshotQuery): ScreenshotOptions;
53
- export declare const CHROME_ARGS: string[];
54
- export declare const USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
55
- export declare const SCREENSHOT_CSS = "\n\t* {\n\t\t-webkit-print-color-adjust: exact !important;\n\t\ttext-rendering: geometricprecision !important;\n\t\t-webkit-font-smoothing: antialiased !important;\n\t}\n";
56
- export type Page = PuppeteerPage | PuppeteerCorePage;
57
- export declare function captureScreenshot(page: Page, opts: ScreenshotOptions, authToken?: string): Promise<Uint8Array>;
58
- export type AuthResult = {
59
- valid: true;
60
- } | {
61
- valid: false;
62
- error: string;
63
- status: 401 | 403;
64
- };
65
- export declare function validateBearerToken(authHeader: string | null | undefined, expectedToken: string): AuthResult;
66
- //# sourceMappingURL=shared.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../src/shared.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,IAAI,IAAI,aAAa,EAAE,MAAM,WAAW,CAAA;AACtD,OAAO,KAAK,EAAE,IAAI,IAAI,iBAAiB,EAAE,MAAM,gBAAgB,CAAA;AAG/D,eAAO,MAAM,gBAAgB,gBAC8C,CAAA;AAE3E,eAAO,MAAM,YAAY,0BAA0C,CAAA;AAGnE,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAahC,CAAA;AAEF,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAA;AAGnE,MAAM,MAAM,iBAAiB,GAAG;IAC/B,GAAG,EAAE,MAAM,CAAA;IACX,QAAQ,EAAE,OAAO,CAAA;IACjB,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;IAC7B,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAA;IAC7C,SAAS,EAAE,MAAM,GAAG,kBAAkB,GAAG,cAAc,GAAG,cAAc,CAAA;IACxE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAGD,wBAAgB,oBAAoB,CACnC,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,EAAE,GACjB,OAAO,CAYT;AAGD,wBAAgB,wBAAwB,CACvC,KAAK,EAAE,eAAe,GACpB,iBAAiB,CAcnB;AAGD,eAAO,MAAM,WAAW,UAMvB,CAAA;AAGD,eAAO,MAAM,UAAU,0HACiG,CAAA;AAGxH,eAAO,MAAM,cAAc,+KAM1B,CAAA;AAGD,MAAM,MAAM,IAAI,GAAG,aAAa,GAAG,iBAAiB,CAAA;AAIpD,wBAAsB,iBAAiB,CACtC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,iBAAiB,EACvB,SAAS,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,CAAC,CAiDrB;AAGD,MAAM,MAAM,UAAU,GACnB;IAAE,KAAK,EAAE,IAAI,CAAA;CAAE,GACf;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAA;CAAE,CAAA;AAGrD,wBAAgB,mBAAmB,CAClC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EACrC,aAAa,EAAE,MAAM,GACnB,UAAU,CAaZ"}
package/dist/shared.js DELETED
@@ -1,123 +0,0 @@
1
- import { z } from 'zod';
2
- // Environment variables
3
- export const getHostWhitelist = () => process.env.SCREENSHOT_HOST_WHITELIST?.split(',').map(h => h.trim()) || [];
4
- export const getAuthToken = () => process.env.SCREENSHOT_AUTH_TOKEN;
5
- // Query parameter schema for screenshot endpoint
6
- export const screenshotQuerySchema = z.object({
7
- url: z.string().url(),
8
- fullPage: z.enum(['true', 'false']).optional().default('false'),
9
- quality: z.coerce.number().min(1).max(100).optional().default(100),
10
- type: z.enum(['png', 'webp', 'jpeg']).optional().default('png'),
11
- width: z.coerce.number().min(1).max(1920).optional().default(1440),
12
- height: z.coerce.number().min(1).max(10000).optional().default(900),
13
- waitUntil: z
14
- .enum(['load', 'domcontentloaded', 'networkidle0', 'networkidle2'])
15
- .optional()
16
- .default('networkidle2'),
17
- waitForSelector: z.string().optional(),
18
- delay: z.coerce.number().min(0).max(30000).optional()
19
- });
20
- // Check if URL hostname is in the whitelist
21
- export function isUrlHostnameAllowed(url, whitelist) {
22
- try {
23
- const hostname = new URL(url).hostname;
24
- if (!whitelist.length) {
25
- return true;
26
- }
27
- return whitelist.some(allowed => hostname === allowed || hostname.endsWith(`.${allowed}`));
28
- }
29
- catch {
30
- return false;
31
- }
32
- }
33
- // Convert validated query params to screenshot options
34
- export function queryToScreenshotOptions(query) {
35
- return {
36
- url: query.url,
37
- fullPage: query.fullPage === 'true',
38
- quality: query.quality,
39
- type: query.type,
40
- dimensions: {
41
- width: query.width,
42
- height: query.height
43
- },
44
- waitUntil: query.waitUntil,
45
- waitForSelector: query.waitForSelector,
46
- delay: query.delay
47
- };
48
- }
49
- // Common Chrome launch arguments for consistent rendering
50
- export const CHROME_ARGS = [
51
- '--no-sandbox',
52
- '--disable-setuid-sandbox',
53
- '--font-render-hinting=none',
54
- '--disable-font-subpixel-positioning',
55
- '--force-color-profile=srgb'
56
- ];
57
- // User agent string
58
- export const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36';
59
- // CSS for consistent font rendering
60
- export const SCREENSHOT_CSS = `
61
- * {
62
- -webkit-print-color-adjust: exact !important;
63
- text-rendering: geometricprecision !important;
64
- -webkit-font-smoothing: antialiased !important;
65
- }
66
- `;
67
- // Capture screenshot using a page instance (shared logic for both implementations)
68
- // Uses PuppeteerCorePage internally since both packages have identical runtime APIs
69
- export async function captureScreenshot(page, opts, authToken) {
70
- // Cast to PuppeteerCorePage to avoid union type signature conflicts
71
- // Both puppeteer and puppeteer-core have identical APIs at runtime
72
- const p = page;
73
- // Use string format for compatibility with both puppeteer and puppeteer-core
74
- await p.setUserAgent(USER_AGENT);
75
- // Set authorization header for all requests
76
- if (authToken) {
77
- await p.setExtraHTTPHeaders({
78
- Authorization: `Bearer ${authToken}`
79
- });
80
- }
81
- await p.setViewport({
82
- width: opts.dimensions.width,
83
- height: opts.dimensions.height
84
- });
85
- await p.goto(opts.url, {
86
- waitUntil: opts.waitUntil
87
- });
88
- // Set custom CSS to ensure consistent font rendering
89
- await p.addStyleTag({
90
- content: SCREENSHOT_CSS
91
- });
92
- // Wait for specific selector if provided
93
- if (opts.waitForSelector) {
94
- await p.waitForSelector(opts.waitForSelector, { timeout: 30000 });
95
- }
96
- // Wait for fonts to load
97
- await p.evaluateHandle('document.fonts.ready');
98
- // Additional delay if specified
99
- if (opts.delay) {
100
- await new Promise(resolve => setTimeout(resolve, opts.delay));
101
- }
102
- const screenshot = await p.screenshot({
103
- type: opts.type,
104
- ...(opts.type !== 'png' ? { quality: opts.quality } : {}),
105
- fullPage: opts.fullPage
106
- });
107
- return screenshot;
108
- }
109
- // Validate Bearer token from Authorization header
110
- export function validateBearerToken(authHeader, expectedToken) {
111
- if (!authHeader || !authHeader.startsWith('Bearer ')) {
112
- return {
113
- valid: false,
114
- error: 'Authorization header missing or invalid format',
115
- status: 401
116
- };
117
- }
118
- const token = authHeader.split(' ')[1];
119
- if (token !== expectedToken) {
120
- return { valid: false, error: 'Invalid token', status: 403 };
121
- }
122
- return { valid: true };
123
- }
package/dist/vercel.d.ts DELETED
@@ -1,16 +0,0 @@
1
- /**
2
- * Vercel serverless function handler for screenshot service.
3
- *
4
- * Usage in your Vercel project:
5
- *
6
- * ```ts
7
- * // api/screenshot.ts (or app/api/screenshot/route.ts for Next.js App Router)
8
- * export { GET } from '@miketromba/screenshot-service/vercel'
9
- * ```
10
- */
11
- /**
12
- * GET handler for Vercel serverless functions.
13
- * Re-export this from your API route.
14
- */
15
- export declare function GET(request: Request): Promise<Response>;
16
- //# sourceMappingURL=vercel.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"vercel.d.ts","sourceRoot":"","sources":["../src/vercel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAqDH;;;GAGG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAkE7D"}
package/dist/vercel.js DELETED
@@ -1,89 +0,0 @@
1
- /**
2
- * Vercel serverless function handler for screenshot service.
3
- *
4
- * Usage in your Vercel project:
5
- *
6
- * ```ts
7
- * // api/screenshot.ts (or app/api/screenshot/route.ts for Next.js App Router)
8
- * export { GET } from '@miketromba/screenshot-service/vercel'
9
- * ```
10
- */
11
- import puppeteer from 'puppeteer-core';
12
- import chromium from '@sparticuz/chromium-min';
13
- import { screenshotQuerySchema, isUrlHostnameAllowed, queryToScreenshotOptions, validateBearerToken, captureScreenshot, getHostWhitelist, getAuthToken, CHROME_ARGS } from './shared';
14
- // Remote chromium URL - uses @sparticuz/chromium releases on GitHub
15
- // This avoids bundling issues on Vercel since chromium is downloaded at runtime
16
- const CHROMIUM_REMOTE_URL = 'https://github.com/Sparticuz/chromium/releases/download/v133.0.0/chromium-v133.0.0-pack.tar';
17
- // Cache browser instance for warm invocations
18
- let browser = null;
19
- async function getBrowser() {
20
- if (browser) {
21
- return browser;
22
- }
23
- browser = await puppeteer.launch({
24
- args: [...chromium.args, ...CHROME_ARGS],
25
- defaultViewport: null,
26
- executablePath: await chromium.executablePath(CHROMIUM_REMOTE_URL),
27
- headless: true
28
- });
29
- return browser;
30
- }
31
- async function takeScreenshot(opts, authToken) {
32
- const browser = await getBrowser();
33
- const page = await browser.newPage();
34
- try {
35
- return await captureScreenshot(page, opts, authToken);
36
- }
37
- finally {
38
- await page.close();
39
- }
40
- }
41
- /**
42
- * GET handler for Vercel serverless functions.
43
- * Re-export this from your API route.
44
- */
45
- export async function GET(request) {
46
- const url = new URL(request.url);
47
- const params = Object.fromEntries(url.searchParams.entries());
48
- // Validate query parameters
49
- const result = screenshotQuerySchema.safeParse(params);
50
- if (!result.success) {
51
- return Response.json({
52
- error: 'Invalid query parameters',
53
- details: result.error.flatten()
54
- }, { status: 400 });
55
- }
56
- const query = result.data;
57
- const authToken = getAuthToken();
58
- const hostWhitelist = getHostWhitelist();
59
- // Check authentication if SCREENSHOT_AUTH_TOKEN is set
60
- if (authToken) {
61
- const authResult = validateBearerToken(request.headers.get('Authorization'), authToken);
62
- if (authResult.valid === false) {
63
- return Response.json({ error: authResult.error }, { status: authResult.status });
64
- }
65
- }
66
- // Check if the URL's hostname is allowed
67
- if (!isUrlHostnameAllowed(query.url, hostWhitelist)) {
68
- return Response.json({
69
- error: `Hostname not allowed. Must be one of: ${hostWhitelist.join(', ')}`
70
- }, { status: 403 });
71
- }
72
- console.log('CAPTURE:', query.url);
73
- try {
74
- const opts = queryToScreenshotOptions(query);
75
- const screenshot = await takeScreenshot(opts, authToken);
76
- return new Response(Buffer.from(screenshot), {
77
- headers: {
78
- 'Content-Type': `image/${query.type}`
79
- }
80
- });
81
- }
82
- catch (error) {
83
- console.error('Screenshot error:', error);
84
- return Response.json({
85
- error: 'Failed to capture screenshot',
86
- message: error instanceof Error ? error.message : 'Unknown error'
87
- }, { status: 500 });
88
- }
89
- }