@ever-works/urlbox-plugin 1.0.0 → 1.0.3
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/dist/index.cjs +394 -0
- package/dist/index.d.cts +32 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +385 -0
- package/package.json +94 -81
- package/LICENSE +0 -661
- package/src/__tests__/urlbox.plugin.spec.ts +0 -353
- package/src/index.ts +0 -2
- package/src/urlbox.plugin.ts +0 -406
- package/tsconfig.json +0 -23
- package/tsup.config.ts +0 -12
- package/vitest.config.ts +0 -13
package/src/urlbox.plugin.ts
DELETED
|
@@ -1,406 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
IPlugin,
|
|
3
|
-
IScreenshotPlugin,
|
|
4
|
-
PluginContext,
|
|
5
|
-
PluginCategory,
|
|
6
|
-
PluginManifest,
|
|
7
|
-
PluginHealthCheck,
|
|
8
|
-
JsonSchema,
|
|
9
|
-
PluginSettings,
|
|
10
|
-
ScreenshotOptions,
|
|
11
|
-
ScreenshotResult,
|
|
12
|
-
ScreenshotFormat,
|
|
13
|
-
ScreenshotValidationResult,
|
|
14
|
-
ConnectionValidationResult
|
|
15
|
-
} from '@ever-works/plugin';
|
|
16
|
-
|
|
17
|
-
import Urlbox, { type RenderOptions } from 'urlbox';
|
|
18
|
-
|
|
19
|
-
// Security (SSRF): shared lexical guard rejecting non-HTTP(S) schemes and
|
|
20
|
-
// literal private/loopback/link-local/cloud-metadata IPs. Imported directly
|
|
21
|
-
// from the helper subpath because the plugin barrel intentionally does not
|
|
22
|
-
// re-export it (it pulls in node:net / node:dns).
|
|
23
|
-
import { isSafeWebhookUrl } from '@ever-works/plugin/helpers/ssrf-guard';
|
|
24
|
-
|
|
25
|
-
interface UrlboxSettings {
|
|
26
|
-
readonly apiKey?: string;
|
|
27
|
-
readonly apiSecret?: string;
|
|
28
|
-
readonly viewportWidth?: number;
|
|
29
|
-
readonly viewportHeight?: number;
|
|
30
|
-
readonly format?: ScreenshotFormat;
|
|
31
|
-
readonly fullPage?: boolean;
|
|
32
|
-
readonly quality?: number;
|
|
33
|
-
readonly retina?: boolean;
|
|
34
|
-
readonly blockAds?: boolean;
|
|
35
|
-
readonly hideCookieBanners?: boolean;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class UrlboxPlugin implements IPlugin, IScreenshotPlugin {
|
|
39
|
-
readonly id = 'urlbox';
|
|
40
|
-
readonly name = 'Urlbox';
|
|
41
|
-
readonly version = '1.0.0';
|
|
42
|
-
readonly category: PluginCategory = 'screenshot';
|
|
43
|
-
readonly capabilities: readonly string[] = ['screenshot'];
|
|
44
|
-
readonly providerName = 'Urlbox';
|
|
45
|
-
|
|
46
|
-
readonly settingsSchema: JsonSchema = {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: {
|
|
49
|
-
apiKey: {
|
|
50
|
-
type: 'string',
|
|
51
|
-
title: 'API Key',
|
|
52
|
-
description: 'Your Urlbox API key. Get one at https://urlbox.com',
|
|
53
|
-
'x-secret': true,
|
|
54
|
-
'x-envVar': 'PLUGIN_URLBOX_API_KEY',
|
|
55
|
-
'x-scope': 'user'
|
|
56
|
-
},
|
|
57
|
-
apiSecret: {
|
|
58
|
-
type: 'string',
|
|
59
|
-
title: 'API Secret',
|
|
60
|
-
description: 'Your Urlbox API secret for signed render links (recommended for security)',
|
|
61
|
-
'x-secret': true,
|
|
62
|
-
'x-envVar': 'PLUGIN_URLBOX_API_SECRET',
|
|
63
|
-
'x-scope': 'user'
|
|
64
|
-
},
|
|
65
|
-
viewportWidth: {
|
|
66
|
-
type: 'number',
|
|
67
|
-
title: 'Viewport Width',
|
|
68
|
-
description: 'Default viewport width in pixels',
|
|
69
|
-
default: 1280,
|
|
70
|
-
minimum: 320,
|
|
71
|
-
maximum: 3840,
|
|
72
|
-
'x-envVar': 'PLUGIN_URLBOX_VIEWPORT_WIDTH'
|
|
73
|
-
},
|
|
74
|
-
viewportHeight: {
|
|
75
|
-
type: 'number',
|
|
76
|
-
title: 'Viewport Height',
|
|
77
|
-
description: 'Default viewport height in pixels',
|
|
78
|
-
default: 1024,
|
|
79
|
-
minimum: 200,
|
|
80
|
-
maximum: 2160,
|
|
81
|
-
'x-envVar': 'PLUGIN_URLBOX_VIEWPORT_HEIGHT'
|
|
82
|
-
},
|
|
83
|
-
format: {
|
|
84
|
-
type: 'string',
|
|
85
|
-
title: 'Image Format',
|
|
86
|
-
description: 'Default output image format',
|
|
87
|
-
enum: ['png', 'jpg', 'jpeg', 'webp'],
|
|
88
|
-
default: 'png',
|
|
89
|
-
'x-envVar': 'PLUGIN_URLBOX_FORMAT'
|
|
90
|
-
},
|
|
91
|
-
fullPage: {
|
|
92
|
-
type: 'boolean',
|
|
93
|
-
title: 'Full Page',
|
|
94
|
-
description: 'Capture full page by default',
|
|
95
|
-
default: false
|
|
96
|
-
},
|
|
97
|
-
quality: {
|
|
98
|
-
type: 'number',
|
|
99
|
-
title: 'Image Quality',
|
|
100
|
-
description: 'Image quality for lossy formats (1-100)',
|
|
101
|
-
default: 80,
|
|
102
|
-
minimum: 1,
|
|
103
|
-
maximum: 100
|
|
104
|
-
},
|
|
105
|
-
retina: {
|
|
106
|
-
type: 'boolean',
|
|
107
|
-
title: 'Retina',
|
|
108
|
-
description: 'Enable retina/HiDPI rendering (2x device scale factor)',
|
|
109
|
-
default: false
|
|
110
|
-
},
|
|
111
|
-
blockAds: {
|
|
112
|
-
type: 'boolean',
|
|
113
|
-
title: 'Block Ads',
|
|
114
|
-
description: 'Block ads when capturing screenshots',
|
|
115
|
-
default: true
|
|
116
|
-
},
|
|
117
|
-
hideCookieBanners: {
|
|
118
|
-
type: 'boolean',
|
|
119
|
-
title: 'Hide Cookie Banners',
|
|
120
|
-
description: 'Hide cookie consent banners when capturing screenshots',
|
|
121
|
-
default: true
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
required: ['apiKey']
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
readonly configurationMode: 'admin-only' | 'user-required' | 'hybrid' = 'hybrid';
|
|
128
|
-
|
|
129
|
-
private context?: PluginContext;
|
|
130
|
-
|
|
131
|
-
async capture(options: ScreenshotOptions): Promise<ScreenshotResult> {
|
|
132
|
-
const settings = this.mergeSettings(options.settings);
|
|
133
|
-
|
|
134
|
-
// Security (SSRF): `options.url` is attacker-controllable (work source
|
|
135
|
-
// URLs, LLM-generated item URLs) and is forwarded to Urlbox, which
|
|
136
|
-
// renders it in a real headless browser and returns a screenshot of the
|
|
137
|
-
// response. Block non-HTTP(S) schemes and literal
|
|
138
|
-
// private/loopback/link-local/cloud-metadata targets (e.g.
|
|
139
|
-
// http://169.254.169.254/...) before generating the render link.
|
|
140
|
-
// Legitimate https item URLs are unaffected.
|
|
141
|
-
if (!isSafeWebhookUrl(options.url)) {
|
|
142
|
-
return {
|
|
143
|
-
success: false,
|
|
144
|
-
error: `URL host is not safe to capture (SSRF guard blocked: ${options.url})`
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const client = this.createClient(settings);
|
|
150
|
-
const renderOptions = this.buildOptions(options, settings);
|
|
151
|
-
const renderUrl = client.generateRenderLink(renderOptions);
|
|
152
|
-
const response = await fetch(renderUrl);
|
|
153
|
-
|
|
154
|
-
if (!response.ok) {
|
|
155
|
-
throw new Error(`Urlbox render failed with status ${response.status}`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
159
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
success: true,
|
|
163
|
-
imageBuffer: buffer,
|
|
164
|
-
imageBase64: buffer.toString('base64'),
|
|
165
|
-
imageUrl: renderUrl,
|
|
166
|
-
width: options.viewportWidth ?? settings.viewportWidth ?? 1280,
|
|
167
|
-
height: options.viewportHeight ?? settings.viewportHeight ?? 1024,
|
|
168
|
-
fileSize: buffer.length
|
|
169
|
-
};
|
|
170
|
-
} catch (error) {
|
|
171
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
172
|
-
this.context?.logger.error(`Urlbox capture failed: ${errorMessage}`);
|
|
173
|
-
|
|
174
|
-
return {
|
|
175
|
-
success: false,
|
|
176
|
-
error: errorMessage
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async getScreenshotUrl(options: ScreenshotOptions): Promise<string | null> {
|
|
182
|
-
const settings = this.mergeSettings(options.settings);
|
|
183
|
-
|
|
184
|
-
// Security (SSRF): same guard as capture() — `options.url` is
|
|
185
|
-
// attacker-controllable and ends up in the generated Urlbox render
|
|
186
|
-
// link. Refuse non-HTTP(S) schemes and private/loopback/link-local/
|
|
187
|
-
// cloud-metadata targets; return null (the existing failure shape).
|
|
188
|
-
if (!isSafeWebhookUrl(options.url)) {
|
|
189
|
-
this.context?.logger.error(`Urlbox URL generation blocked by SSRF guard for unsafe URL: ${options.url}`);
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const client = this.createClient(settings);
|
|
195
|
-
const renderOptions = this.buildOptions(options, settings);
|
|
196
|
-
|
|
197
|
-
return client.generateRenderLink(renderOptions);
|
|
198
|
-
} catch (error) {
|
|
199
|
-
this.context?.logger.error(
|
|
200
|
-
`Urlbox URL generation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
201
|
-
);
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async isAvailable(): Promise<boolean> {
|
|
207
|
-
if (!this.context) return false;
|
|
208
|
-
const settings = await this.context.getSettings();
|
|
209
|
-
return Boolean(settings?.apiKey);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async validateConnection(settings: Record<string, unknown>): Promise<ConnectionValidationResult> {
|
|
213
|
-
const apiKey = settings.apiKey as string | undefined;
|
|
214
|
-
if (!apiKey) {
|
|
215
|
-
return { success: false, message: 'Urlbox API key is not configured.' };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
try {
|
|
219
|
-
const resolvedSettings = this.mergeSettings(settings);
|
|
220
|
-
const client = this.createClient(resolvedSettings);
|
|
221
|
-
const url = client.generateRenderLink({ url: 'https://example.com' });
|
|
222
|
-
|
|
223
|
-
if (url && url.includes('api.urlbox.com')) {
|
|
224
|
-
return { success: true, message: 'Urlbox connection verified.' };
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return { success: false, message: 'Urlbox credentials appear invalid.' };
|
|
228
|
-
} catch (error) {
|
|
229
|
-
return {
|
|
230
|
-
success: false,
|
|
231
|
-
message: `Urlbox connection failed: ${error instanceof Error ? error.message : String(error)}`
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
async validateCredentials(): Promise<ScreenshotValidationResult> {
|
|
237
|
-
if (!this.context) {
|
|
238
|
-
return {
|
|
239
|
-
valid: false,
|
|
240
|
-
message: 'Plugin not initialized'
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const settings = await this.context.getSettings();
|
|
246
|
-
const resolvedSettings = this.mergeSettings(settings);
|
|
247
|
-
|
|
248
|
-
if (!resolvedSettings.apiKey) {
|
|
249
|
-
return {
|
|
250
|
-
valid: false,
|
|
251
|
-
message: 'API key is not configured'
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const client = this.createClient(resolvedSettings);
|
|
256
|
-
const url = client.generateRenderLink({ url: 'https://example.com' });
|
|
257
|
-
|
|
258
|
-
if (url && url.includes('api.urlbox.com')) {
|
|
259
|
-
return {
|
|
260
|
-
valid: true,
|
|
261
|
-
message: 'Credentials are valid'
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
return {
|
|
266
|
-
valid: false,
|
|
267
|
-
message: 'Invalid credentials format'
|
|
268
|
-
};
|
|
269
|
-
} catch (error) {
|
|
270
|
-
return {
|
|
271
|
-
valid: false,
|
|
272
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
getSupportedFormats(): readonly ScreenshotFormat[] {
|
|
278
|
-
return ['png', 'jpg', 'jpeg', 'webp'] as const;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
getMaxDimensions(): { width: number; height: number } {
|
|
282
|
-
return { width: 3840, height: 2160 };
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
private createClient(settings: UrlboxSettings) {
|
|
286
|
-
const apiKey = settings.apiKey;
|
|
287
|
-
const apiSecret = settings.apiSecret ?? '';
|
|
288
|
-
|
|
289
|
-
if (!apiKey) {
|
|
290
|
-
throw new Error(
|
|
291
|
-
'Urlbox API key not configured. ' +
|
|
292
|
-
'Set it in plugin settings or via PLUGIN_URLBOX_API_KEY environment variable.'
|
|
293
|
-
);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return Urlbox(apiKey, apiSecret);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private buildOptions(options: ScreenshotOptions, settings: UrlboxSettings): RenderOptions {
|
|
300
|
-
const format = options.format ?? settings.format ?? 'png';
|
|
301
|
-
|
|
302
|
-
const renderOptions: RenderOptions = {
|
|
303
|
-
url: options.url,
|
|
304
|
-
width: options.viewportWidth ?? settings.viewportWidth ?? 1280,
|
|
305
|
-
height: options.viewportHeight ?? settings.viewportHeight ?? 1024,
|
|
306
|
-
format: format as RenderOptions['format'],
|
|
307
|
-
full_page: options.fullPage ?? settings.fullPage ?? false,
|
|
308
|
-
quality: settings.quality ?? 80,
|
|
309
|
-
retina: settings.retina ?? false,
|
|
310
|
-
block_ads: options.blockAds ?? settings.blockAds ?? true,
|
|
311
|
-
hide_cookie_banners: options.blockCookieBanners ?? settings.hideCookieBanners ?? true
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
if (options.delay !== undefined) {
|
|
315
|
-
renderOptions.delay = options.delay;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (options.waitForSelector) {
|
|
319
|
-
renderOptions.selector = options.waitForSelector;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (options.userAgent) {
|
|
323
|
-
renderOptions.user_agent = options.userAgent;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
return renderOptions;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private mergeSettings(settings?: PluginSettings): UrlboxSettings {
|
|
330
|
-
return {
|
|
331
|
-
apiKey: settings?.apiKey as string | undefined,
|
|
332
|
-
apiSecret: settings?.apiSecret as string | undefined,
|
|
333
|
-
viewportWidth: (settings?.viewportWidth as number | undefined) ?? 1280,
|
|
334
|
-
viewportHeight: (settings?.viewportHeight as number | undefined) ?? 1024,
|
|
335
|
-
format: (settings?.format as ScreenshotFormat | undefined) ?? 'png',
|
|
336
|
-
fullPage: (settings?.fullPage as boolean | undefined) ?? false,
|
|
337
|
-
quality: (settings?.quality as number | undefined) ?? 80,
|
|
338
|
-
retina: (settings?.retina as boolean | undefined) ?? false,
|
|
339
|
-
blockAds: (settings?.blockAds as boolean | undefined) ?? true,
|
|
340
|
-
hideCookieBanners: (settings?.hideCookieBanners as boolean | undefined) ?? true
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
async onLoad(context: PluginContext): Promise<void> {
|
|
345
|
-
this.context = context;
|
|
346
|
-
context.logger.log('Urlbox Plugin loaded');
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async onUnload(): Promise<void> {
|
|
350
|
-
this.context = undefined;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
async healthCheck(): Promise<PluginHealthCheck> {
|
|
354
|
-
return {
|
|
355
|
-
status: 'healthy',
|
|
356
|
-
message: 'Urlbox plugin is ready (API key required for operations)',
|
|
357
|
-
checkedAt: Date.now()
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
getManifest(): PluginManifest {
|
|
362
|
-
return {
|
|
363
|
-
id: this.id,
|
|
364
|
-
name: this.name,
|
|
365
|
-
version: this.version,
|
|
366
|
-
description: 'Automatically capture website screenshots for your work items',
|
|
367
|
-
category: this.category,
|
|
368
|
-
capabilities: [...this.capabilities],
|
|
369
|
-
author: { name: 'Ever Works Team' },
|
|
370
|
-
license: 'AGPL-3.0',
|
|
371
|
-
builtIn: false,
|
|
372
|
-
systemPlugin: false,
|
|
373
|
-
readme: [
|
|
374
|
-
'## What does Urlbox do?',
|
|
375
|
-
'',
|
|
376
|
-
'Urlbox is a screenshot capture API. When work items include source URLs, this plugin automatically generates preview images by capturing a screenshot of each page.',
|
|
377
|
-
'',
|
|
378
|
-
'## Why use it?',
|
|
379
|
-
'',
|
|
380
|
-
'- **Automated capture** — preview images are generated for each work item without manual effort',
|
|
381
|
-
'- **Consistent output** — every screenshot uses the same viewport size, format, and rendering settings',
|
|
382
|
-
'- **Ad and cookie banner blocking** — captures clean screenshots free of ads and cookie banners',
|
|
383
|
-
'- **Retina rendering** — supports HiDPI output for crisp images on high-resolution displays',
|
|
384
|
-
'- **Multiple formats** — supports PNG, JPG, and WebP output with configurable quality',
|
|
385
|
-
'',
|
|
386
|
-
'## How it works in Ever Works',
|
|
387
|
-
'',
|
|
388
|
-
'During work generation, the screenshot facade sends capture requests to Urlbox for items that have a source URL. The resulting images are used as item preview thumbnails. You can configure viewport dimensions, image format, quality, retina rendering, and ad/cookie blocking behavior.',
|
|
389
|
-
'',
|
|
390
|
-
'## Getting started',
|
|
391
|
-
'',
|
|
392
|
-
'1. Sign up at [urlbox.com](https://urlbox.com)',
|
|
393
|
-
'2. Copy your API key and API secret',
|
|
394
|
-
'3. Enable the Urlbox plugin on this page',
|
|
395
|
-
'4. Enter your credentials in the settings below'
|
|
396
|
-
].join('\n'),
|
|
397
|
-
homepage: 'https://urlbox.com',
|
|
398
|
-
icon: {
|
|
399
|
-
type: 'url',
|
|
400
|
-
value: 'https://urlbox.com/apple-touch-icon.png'
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export default UrlboxPlugin;
|
package/tsconfig.json
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"module": "ESNext",
|
|
4
|
-
"moduleResolution": "bundler",
|
|
5
|
-
"target": "ES2021",
|
|
6
|
-
"types": ["node"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"strictNullChecks": true,
|
|
9
|
-
"noImplicitAny": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "./dist",
|
|
14
|
-
"rootDir": "./src",
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"forceConsistentCasingInFileNames": true,
|
|
18
|
-
"isolatedModules": true,
|
|
19
|
-
"noEmit": true
|
|
20
|
-
},
|
|
21
|
-
"include": ["src/**/*"],
|
|
22
|
-
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
|
23
|
-
}
|
package/tsup.config.ts
DELETED
package/vitest.config.ts
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vitest/config';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
test: {
|
|
5
|
-
globals: true,
|
|
6
|
-
environment: 'node',
|
|
7
|
-
include: ['src/**/*.{test,spec}.ts'],
|
|
8
|
-
coverage: {
|
|
9
|
-
reporter: ['text', 'json', 'html'],
|
|
10
|
-
exclude: ['node_modules/', 'dist/']
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
});
|