webpeel 0.20.2 → 0.20.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/server/app.d.ts +14 -0
- package/dist/server/app.js +384 -0
- package/dist/server/auth-store.d.ts +27 -0
- package/dist/server/auth-store.js +88 -0
- package/dist/server/email-service.d.ts +21 -0
- package/dist/server/email-service.js +79 -0
- package/dist/server/job-queue.d.ts +100 -0
- package/dist/server/job-queue.js +145 -0
- package/dist/server/logger.d.ts +10 -0
- package/dist/server/logger.js +37 -0
- package/dist/server/middleware/auth.d.ts +28 -0
- package/dist/server/middleware/auth.js +221 -0
- package/dist/server/middleware/rate-limit.d.ts +24 -0
- package/dist/server/middleware/rate-limit.js +167 -0
- package/dist/server/middleware/url-validator.d.ts +15 -0
- package/dist/server/middleware/url-validator.js +186 -0
- package/dist/server/openapi.yaml +6418 -0
- package/dist/server/pg-auth-store.d.ts +132 -0
- package/dist/server/pg-auth-store.js +472 -0
- package/dist/server/pg-job-queue.d.ts +59 -0
- package/dist/server/pg-job-queue.js +375 -0
- package/dist/server/premium/domain-intel.d.ts +16 -0
- package/dist/server/premium/domain-intel.js +133 -0
- package/dist/server/premium/index.d.ts +17 -0
- package/dist/server/premium/index.js +35 -0
- package/dist/server/premium/swr-cache.d.ts +14 -0
- package/dist/server/premium/swr-cache.js +34 -0
- package/dist/server/routes/activity.d.ts +6 -0
- package/dist/server/routes/activity.js +74 -0
- package/dist/server/routes/answer.d.ts +5 -0
- package/dist/server/routes/answer.js +125 -0
- package/dist/server/routes/ask.d.ts +28 -0
- package/dist/server/routes/ask.js +229 -0
- package/dist/server/routes/batch.d.ts +6 -0
- package/dist/server/routes/batch.js +493 -0
- package/dist/server/routes/cli-usage.d.ts +6 -0
- package/dist/server/routes/cli-usage.js +127 -0
- package/dist/server/routes/compat.d.ts +23 -0
- package/dist/server/routes/compat.js +652 -0
- package/dist/server/routes/deep-fetch.d.ts +8 -0
- package/dist/server/routes/deep-fetch.js +57 -0
- package/dist/server/routes/demo.d.ts +24 -0
- package/dist/server/routes/demo.js +517 -0
- package/dist/server/routes/do.d.ts +8 -0
- package/dist/server/routes/do.js +72 -0
- package/dist/server/routes/extract.d.ts +8 -0
- package/dist/server/routes/extract.js +235 -0
- package/dist/server/routes/fetch.d.ts +7 -0
- package/dist/server/routes/fetch.js +999 -0
- package/dist/server/routes/health.d.ts +7 -0
- package/dist/server/routes/health.js +19 -0
- package/dist/server/routes/jobs.d.ts +7 -0
- package/dist/server/routes/jobs.js +573 -0
- package/dist/server/routes/mcp.d.ts +14 -0
- package/dist/server/routes/mcp.js +141 -0
- package/dist/server/routes/oauth.d.ts +9 -0
- package/dist/server/routes/oauth.js +396 -0
- package/dist/server/routes/playground.d.ts +17 -0
- package/dist/server/routes/playground.js +283 -0
- package/dist/server/routes/screenshot.d.ts +22 -0
- package/dist/server/routes/screenshot.js +816 -0
- package/dist/server/routes/search.d.ts +6 -0
- package/dist/server/routes/search.js +303 -0
- package/dist/server/routes/session.d.ts +15 -0
- package/dist/server/routes/session.js +397 -0
- package/dist/server/routes/stats.d.ts +6 -0
- package/dist/server/routes/stats.js +71 -0
- package/dist/server/routes/stripe.d.ts +15 -0
- package/dist/server/routes/stripe.js +294 -0
- package/dist/server/routes/users.d.ts +8 -0
- package/dist/server/routes/users.js +1671 -0
- package/dist/server/routes/watch.d.ts +15 -0
- package/dist/server/routes/watch.js +309 -0
- package/dist/server/routes/webhooks.d.ts +26 -0
- package/dist/server/routes/webhooks.js +170 -0
- package/dist/server/routes/youtube.d.ts +6 -0
- package/dist/server/routes/youtube.js +130 -0
- package/dist/server/sentry.d.ts +13 -0
- package/dist/server/sentry.js +38 -0
- package/dist/server/types.d.ts +15 -0
- package/dist/server/types.js +7 -0
- package/dist/server/utils/response.d.ts +44 -0
- package/dist/server/utils/response.js +69 -0
- package/dist/server/utils/sse.d.ts +22 -0
- package/dist/server/utils/sse.js +38 -0
- package/package.json +2 -1
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playground endpoint — GET /v1/playground?url=<encoded_url>
|
|
3
|
+
* GET /v1/playground/search?q=<query>
|
|
4
|
+
*
|
|
5
|
+
* Unauthenticated endpoints for the WebPeel playground page.
|
|
6
|
+
* Lets visitors try the product without signing up.
|
|
7
|
+
*
|
|
8
|
+
* Security:
|
|
9
|
+
* - CORS-locked to webpeel.dev and localhost
|
|
10
|
+
* - IP-based rate limit: 10 requests per 15 minutes (shared across /fetch and /search)
|
|
11
|
+
* - Simple HTTP-only fetch (no browser rendering)
|
|
12
|
+
* - 5-second timeout
|
|
13
|
+
* - Content truncated to 5,000 chars
|
|
14
|
+
* - No screenshots
|
|
15
|
+
*/
|
|
16
|
+
import { Router } from 'express';
|
|
17
|
+
import { peel } from '../../index.js';
|
|
18
|
+
import { getBestSearchProvider } from '../../core/search-provider.js';
|
|
19
|
+
import { createLogger } from '../logger.js';
|
|
20
|
+
const log = createLogger('playground');
|
|
21
|
+
// ── IP-based rate limiter ─────────────────────────────────────────────────────
|
|
22
|
+
const MAX_PER_WINDOW = 10;
|
|
23
|
+
const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
|
|
24
|
+
const ipHits = new Map();
|
|
25
|
+
function checkRateLimit(ip) {
|
|
26
|
+
const now = Date.now();
|
|
27
|
+
const entry = ipHits.get(ip);
|
|
28
|
+
if (!entry || now > entry.resetAt) {
|
|
29
|
+
const resetAt = now + WINDOW_MS;
|
|
30
|
+
ipHits.set(ip, { count: 1, resetAt });
|
|
31
|
+
return { allowed: true, remaining: MAX_PER_WINDOW - 1, resetAt };
|
|
32
|
+
}
|
|
33
|
+
if (entry.count >= MAX_PER_WINDOW) {
|
|
34
|
+
return { allowed: false, remaining: 0, resetAt: entry.resetAt };
|
|
35
|
+
}
|
|
36
|
+
entry.count++;
|
|
37
|
+
return { allowed: true, remaining: MAX_PER_WINDOW - entry.count, resetAt: entry.resetAt };
|
|
38
|
+
}
|
|
39
|
+
// Clean up expired entries every 5 minutes
|
|
40
|
+
setInterval(() => {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
for (const [ip, entry] of ipHits) {
|
|
43
|
+
if (now > entry.resetAt)
|
|
44
|
+
ipHits.delete(ip);
|
|
45
|
+
}
|
|
46
|
+
}, 5 * 60 * 1000).unref();
|
|
47
|
+
// ── CORS helper ───────────────────────────────────────────────────────────────
|
|
48
|
+
function setCorsHeaders(req, res) {
|
|
49
|
+
const origin = req.headers.origin || '';
|
|
50
|
+
if (origin === 'https://webpeel.dev' ||
|
|
51
|
+
/^http:\/\/localhost(:\d+)?$/.test(origin) ||
|
|
52
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?$/.test(origin)) {
|
|
53
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
54
|
+
res.setHeader('Vary', 'Origin');
|
|
55
|
+
}
|
|
56
|
+
else if (!origin) {
|
|
57
|
+
// Allow curl and server-to-server (no Origin header)
|
|
58
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
59
|
+
}
|
|
60
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
|
|
61
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
62
|
+
}
|
|
63
|
+
// ── IP extraction ─────────────────────────────────────────────────────────────
|
|
64
|
+
function getClientIp(req) {
|
|
65
|
+
const forwardedFor = req.headers['x-forwarded-for'];
|
|
66
|
+
const firstForwardedIp = typeof forwardedFor === 'string'
|
|
67
|
+
? forwardedFor.split(',')[0].trim()
|
|
68
|
+
: Array.isArray(forwardedFor) ? forwardedFor[0] : undefined;
|
|
69
|
+
return req.headers['cf-connecting-ip']
|
|
70
|
+
|| firstForwardedIp
|
|
71
|
+
|| req.headers['x-real-ip']
|
|
72
|
+
|| req.ip
|
|
73
|
+
|| req.socket?.remoteAddress
|
|
74
|
+
|| 'unknown';
|
|
75
|
+
}
|
|
76
|
+
// ── CORS origin check ─────────────────────────────────────────────────────────
|
|
77
|
+
function isAllowedOrigin(req) {
|
|
78
|
+
const origin = req.headers.origin || req.headers.referer || '';
|
|
79
|
+
if (!origin)
|
|
80
|
+
return true; // Allow curl / server-to-server (no Origin header)
|
|
81
|
+
return (origin === 'https://webpeel.dev' ||
|
|
82
|
+
origin.startsWith('https://webpeel.dev/') ||
|
|
83
|
+
/^http:\/\/localhost(:\d+)?/.test(origin) ||
|
|
84
|
+
/^http:\/\/127\.0\.0\.1(:\d+)?/.test(origin));
|
|
85
|
+
}
|
|
86
|
+
// ── Router ────────────────────────────────────────────────────────────────────
|
|
87
|
+
const MAX_CONTENT_LENGTH = 5000;
|
|
88
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
89
|
+
const SIGN_UP_URL = 'https://app.webpeel.dev';
|
|
90
|
+
export function createPlaygroundRouter() {
|
|
91
|
+
const router = Router();
|
|
92
|
+
// ── CORS preflight ─────────────────────────────────────────────────────────
|
|
93
|
+
router.options('/', (req, res) => {
|
|
94
|
+
setCorsHeaders(req, res);
|
|
95
|
+
res.status(204).end();
|
|
96
|
+
});
|
|
97
|
+
router.options('/search', (req, res) => {
|
|
98
|
+
setCorsHeaders(req, res);
|
|
99
|
+
res.status(204).end();
|
|
100
|
+
});
|
|
101
|
+
// ── GET /v1/playground?url=... ─────────────────────────────────────────────
|
|
102
|
+
router.get('/', async (req, res) => {
|
|
103
|
+
setCorsHeaders(req, res);
|
|
104
|
+
// CORS check
|
|
105
|
+
if (!isAllowedOrigin(req)) {
|
|
106
|
+
res.status(403).json({
|
|
107
|
+
success: false,
|
|
108
|
+
error: {
|
|
109
|
+
type: 'cors_denied',
|
|
110
|
+
message: 'Playground is only available from webpeel.dev',
|
|
111
|
+
hint: `Sign up at ${SIGN_UP_URL} for full API access.`,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const url = (req.query.url || '').trim();
|
|
117
|
+
if (!url) {
|
|
118
|
+
res.status(400).json({
|
|
119
|
+
success: false,
|
|
120
|
+
error: {
|
|
121
|
+
type: 'missing_url',
|
|
122
|
+
message: 'URL parameter is required',
|
|
123
|
+
hint: 'GET /v1/playground?url=https://example.com',
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Basic URL validation
|
|
129
|
+
let parsedUrl;
|
|
130
|
+
try {
|
|
131
|
+
parsedUrl = new URL(url);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
res.status(400).json({
|
|
135
|
+
success: false,
|
|
136
|
+
error: {
|
|
137
|
+
type: 'invalid_url',
|
|
138
|
+
message: 'Invalid URL format',
|
|
139
|
+
hint: 'Ensure the URL is well-formed: https://example.com',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
145
|
+
res.status(400).json({
|
|
146
|
+
success: false,
|
|
147
|
+
error: {
|
|
148
|
+
type: 'invalid_url',
|
|
149
|
+
message: 'Only HTTP and HTTPS URLs are allowed',
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Rate limit by IP
|
|
155
|
+
const ip = getClientIp(req);
|
|
156
|
+
const rl = checkRateLimit(ip);
|
|
157
|
+
res.setHeader('X-RateLimit-Limit', String(MAX_PER_WINDOW));
|
|
158
|
+
res.setHeader('X-RateLimit-Remaining', String(rl.remaining));
|
|
159
|
+
res.setHeader('X-RateLimit-Reset', String(Math.ceil(rl.resetAt / 1000)));
|
|
160
|
+
if (!rl.allowed) {
|
|
161
|
+
res.status(429).json({
|
|
162
|
+
success: false,
|
|
163
|
+
error: {
|
|
164
|
+
type: 'rate_limited',
|
|
165
|
+
message: 'Playground limit reached (10 requests per 15 minutes)',
|
|
166
|
+
hint: `Sign up for a free API key for unlimited access: ${SIGN_UP_URL}`,
|
|
167
|
+
},
|
|
168
|
+
playground: true,
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
log.info('Playground fetch', { url, ip });
|
|
174
|
+
const startMs = Date.now();
|
|
175
|
+
const result = await peel(url, {
|
|
176
|
+
timeout: FETCH_TIMEOUT_MS,
|
|
177
|
+
render: false,
|
|
178
|
+
noEscalate: true,
|
|
179
|
+
});
|
|
180
|
+
const fullContent = result.content || '';
|
|
181
|
+
const content = fullContent.slice(0, MAX_CONTENT_LENGTH);
|
|
182
|
+
const truncated = fullContent.length > MAX_CONTENT_LENGTH;
|
|
183
|
+
res.json({
|
|
184
|
+
success: true,
|
|
185
|
+
url: result.url,
|
|
186
|
+
title: result.title,
|
|
187
|
+
content,
|
|
188
|
+
tokens: result.tokens,
|
|
189
|
+
method: result.method,
|
|
190
|
+
elapsed: Date.now() - startMs,
|
|
191
|
+
truncated,
|
|
192
|
+
...(truncated && {
|
|
193
|
+
upgrade: `Full content available with a free API key → ${SIGN_UP_URL}`,
|
|
194
|
+
}),
|
|
195
|
+
playground: true,
|
|
196
|
+
rateLimitRemaining: rl.remaining,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
log.warn('Playground fetch error', { url, error: err?.message });
|
|
201
|
+
res.status(502).json({
|
|
202
|
+
success: false,
|
|
203
|
+
error: {
|
|
204
|
+
type: 'fetch_failed',
|
|
205
|
+
message: err?.message || 'Failed to fetch URL',
|
|
206
|
+
hint: 'Check that the URL is publicly accessible.',
|
|
207
|
+
},
|
|
208
|
+
playground: true,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
// ── GET /v1/playground/search?q=... ───────────────────────────────────────
|
|
213
|
+
router.get('/search', async (req, res) => {
|
|
214
|
+
setCorsHeaders(req, res);
|
|
215
|
+
// CORS check
|
|
216
|
+
if (!isAllowedOrigin(req)) {
|
|
217
|
+
res.status(403).json({
|
|
218
|
+
success: false,
|
|
219
|
+
error: {
|
|
220
|
+
type: 'cors_denied',
|
|
221
|
+
message: 'Playground is only available from webpeel.dev',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const q = (req.query.q || '').trim();
|
|
227
|
+
if (!q) {
|
|
228
|
+
res.status(400).json({
|
|
229
|
+
success: false,
|
|
230
|
+
error: {
|
|
231
|
+
type: 'missing_query',
|
|
232
|
+
message: 'Query parameter is required',
|
|
233
|
+
hint: 'GET /v1/playground/search?q=your+query',
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Rate limit by IP (shared counter with fetch)
|
|
239
|
+
const ip = getClientIp(req);
|
|
240
|
+
const rl = checkRateLimit(ip);
|
|
241
|
+
res.setHeader('X-RateLimit-Limit', String(MAX_PER_WINDOW));
|
|
242
|
+
res.setHeader('X-RateLimit-Remaining', String(rl.remaining));
|
|
243
|
+
res.setHeader('X-RateLimit-Reset', String(Math.ceil(rl.resetAt / 1000)));
|
|
244
|
+
if (!rl.allowed) {
|
|
245
|
+
res.status(429).json({
|
|
246
|
+
success: false,
|
|
247
|
+
error: {
|
|
248
|
+
type: 'rate_limited',
|
|
249
|
+
message: 'Playground limit reached (10 requests per 15 minutes)',
|
|
250
|
+
hint: `Sign up for a free API key for unlimited access: ${SIGN_UP_URL}`,
|
|
251
|
+
},
|
|
252
|
+
playground: true,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
log.info('Playground search', { q, ip });
|
|
258
|
+
const startMs = Date.now();
|
|
259
|
+
const { provider, apiKey } = getBestSearchProvider();
|
|
260
|
+
const results = await provider.searchWeb(q, { count: 5, apiKey });
|
|
261
|
+
res.json({
|
|
262
|
+
success: true,
|
|
263
|
+
query: q,
|
|
264
|
+
results,
|
|
265
|
+
elapsed: Date.now() - startMs,
|
|
266
|
+
playground: true,
|
|
267
|
+
rateLimitRemaining: rl.remaining,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
log.warn('Playground search error', { q, error: err?.message });
|
|
272
|
+
res.status(502).json({
|
|
273
|
+
success: false,
|
|
274
|
+
error: {
|
|
275
|
+
type: 'search_failed',
|
|
276
|
+
message: err?.message || 'Search failed',
|
|
277
|
+
},
|
|
278
|
+
playground: true,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
return router;
|
|
283
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot endpoint — POST /v1/screenshot
|
|
3
|
+
*
|
|
4
|
+
* Takes a screenshot of a URL and returns base64-encoded image data.
|
|
5
|
+
* Uses the same rate limiting / credit system as the fetch endpoint (1 credit).
|
|
6
|
+
*
|
|
7
|
+
* The main endpoint accepts an optional `mode` parameter to select behaviour:
|
|
8
|
+
* - "screenshot" (default) — basic screenshot
|
|
9
|
+
* - "filmstrip" — multiple frames over time
|
|
10
|
+
* - "audit" — accessibility / section audit
|
|
11
|
+
* - "viewports" — multi-viewport screenshots
|
|
12
|
+
* - "design" — design analysis (audit + tokens merged)
|
|
13
|
+
* - "diff" — visual diff between url and compareUrl
|
|
14
|
+
* - "compare" — design comparison between url and compareUrl/ref
|
|
15
|
+
*
|
|
16
|
+
* All legacy sub-endpoints (/filmstrip, /audit, /viewports, …) are kept as
|
|
17
|
+
* thin wrappers that delegate to the same named handler functions.
|
|
18
|
+
* /animation is deprecated and returns 410 Gone.
|
|
19
|
+
*/
|
|
20
|
+
import { Router } from 'express';
|
|
21
|
+
import type { AuthStore } from '../auth-store.js';
|
|
22
|
+
export declare function createScreenshotRouter(authStore: AuthStore): Router;
|