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.
Files changed (86) hide show
  1. package/dist/server/app.d.ts +14 -0
  2. package/dist/server/app.js +384 -0
  3. package/dist/server/auth-store.d.ts +27 -0
  4. package/dist/server/auth-store.js +88 -0
  5. package/dist/server/email-service.d.ts +21 -0
  6. package/dist/server/email-service.js +79 -0
  7. package/dist/server/job-queue.d.ts +100 -0
  8. package/dist/server/job-queue.js +145 -0
  9. package/dist/server/logger.d.ts +10 -0
  10. package/dist/server/logger.js +37 -0
  11. package/dist/server/middleware/auth.d.ts +28 -0
  12. package/dist/server/middleware/auth.js +221 -0
  13. package/dist/server/middleware/rate-limit.d.ts +24 -0
  14. package/dist/server/middleware/rate-limit.js +167 -0
  15. package/dist/server/middleware/url-validator.d.ts +15 -0
  16. package/dist/server/middleware/url-validator.js +186 -0
  17. package/dist/server/openapi.yaml +6418 -0
  18. package/dist/server/pg-auth-store.d.ts +132 -0
  19. package/dist/server/pg-auth-store.js +472 -0
  20. package/dist/server/pg-job-queue.d.ts +59 -0
  21. package/dist/server/pg-job-queue.js +375 -0
  22. package/dist/server/premium/domain-intel.d.ts +16 -0
  23. package/dist/server/premium/domain-intel.js +133 -0
  24. package/dist/server/premium/index.d.ts +17 -0
  25. package/dist/server/premium/index.js +35 -0
  26. package/dist/server/premium/swr-cache.d.ts +14 -0
  27. package/dist/server/premium/swr-cache.js +34 -0
  28. package/dist/server/routes/activity.d.ts +6 -0
  29. package/dist/server/routes/activity.js +74 -0
  30. package/dist/server/routes/answer.d.ts +5 -0
  31. package/dist/server/routes/answer.js +125 -0
  32. package/dist/server/routes/ask.d.ts +28 -0
  33. package/dist/server/routes/ask.js +229 -0
  34. package/dist/server/routes/batch.d.ts +6 -0
  35. package/dist/server/routes/batch.js +493 -0
  36. package/dist/server/routes/cli-usage.d.ts +6 -0
  37. package/dist/server/routes/cli-usage.js +127 -0
  38. package/dist/server/routes/compat.d.ts +23 -0
  39. package/dist/server/routes/compat.js +652 -0
  40. package/dist/server/routes/deep-fetch.d.ts +8 -0
  41. package/dist/server/routes/deep-fetch.js +57 -0
  42. package/dist/server/routes/demo.d.ts +24 -0
  43. package/dist/server/routes/demo.js +517 -0
  44. package/dist/server/routes/do.d.ts +8 -0
  45. package/dist/server/routes/do.js +72 -0
  46. package/dist/server/routes/extract.d.ts +8 -0
  47. package/dist/server/routes/extract.js +235 -0
  48. package/dist/server/routes/fetch.d.ts +7 -0
  49. package/dist/server/routes/fetch.js +999 -0
  50. package/dist/server/routes/health.d.ts +7 -0
  51. package/dist/server/routes/health.js +19 -0
  52. package/dist/server/routes/jobs.d.ts +7 -0
  53. package/dist/server/routes/jobs.js +573 -0
  54. package/dist/server/routes/mcp.d.ts +14 -0
  55. package/dist/server/routes/mcp.js +141 -0
  56. package/dist/server/routes/oauth.d.ts +9 -0
  57. package/dist/server/routes/oauth.js +396 -0
  58. package/dist/server/routes/playground.d.ts +17 -0
  59. package/dist/server/routes/playground.js +283 -0
  60. package/dist/server/routes/screenshot.d.ts +22 -0
  61. package/dist/server/routes/screenshot.js +816 -0
  62. package/dist/server/routes/search.d.ts +6 -0
  63. package/dist/server/routes/search.js +303 -0
  64. package/dist/server/routes/session.d.ts +15 -0
  65. package/dist/server/routes/session.js +397 -0
  66. package/dist/server/routes/stats.d.ts +6 -0
  67. package/dist/server/routes/stats.js +71 -0
  68. package/dist/server/routes/stripe.d.ts +15 -0
  69. package/dist/server/routes/stripe.js +294 -0
  70. package/dist/server/routes/users.d.ts +8 -0
  71. package/dist/server/routes/users.js +1671 -0
  72. package/dist/server/routes/watch.d.ts +15 -0
  73. package/dist/server/routes/watch.js +309 -0
  74. package/dist/server/routes/webhooks.d.ts +26 -0
  75. package/dist/server/routes/webhooks.js +170 -0
  76. package/dist/server/routes/youtube.d.ts +6 -0
  77. package/dist/server/routes/youtube.js +130 -0
  78. package/dist/server/sentry.d.ts +13 -0
  79. package/dist/server/sentry.js +38 -0
  80. package/dist/server/types.d.ts +15 -0
  81. package/dist/server/types.js +7 -0
  82. package/dist/server/utils/response.d.ts +44 -0
  83. package/dist/server/utils/response.js +69 -0
  84. package/dist/server/utils/sse.d.ts +22 -0
  85. package/dist/server/utils/sse.js +38 -0
  86. package/package.json +2 -1
@@ -0,0 +1,375 @@
1
+ /**
2
+ * PostgreSQL-backed job queue for production deployments
3
+ * Uses same Pool pattern as pg-auth-store.ts
4
+ */
5
+ import pg from 'pg';
6
+ import { randomUUID } from 'crypto';
7
+ const { Pool } = pg;
8
+ export class PostgresJobQueue {
9
+ pool;
10
+ cleanupInterval;
11
+ constructor(connectionString) {
12
+ const dbUrl = connectionString || process.env.DATABASE_URL;
13
+ if (!dbUrl) {
14
+ throw new Error('DATABASE_URL environment variable is required for PostgresJobQueue');
15
+ }
16
+ this.pool = new Pool({
17
+ connectionString: dbUrl,
18
+ // TLS: enabled when DATABASE_URL contains sslmode=require.
19
+ // Secure by default (rejectUnauthorized: true); set PG_REJECT_UNAUTHORIZED=false
20
+ // only for managed DBs (Render/Neon/Supabase) that use self-signed certs.
21
+ ssl: process.env.DATABASE_URL?.includes('sslmode=require')
22
+ ? { rejectUnauthorized: process.env.PG_REJECT_UNAUTHORIZED !== 'false' }
23
+ : undefined,
24
+ max: 20,
25
+ idleTimeoutMillis: 30000,
26
+ connectionTimeoutMillis: 10000,
27
+ });
28
+ // Initialize table
29
+ this.initTable().catch(err => {
30
+ console.error('Failed to initialize jobs table:', err);
31
+ });
32
+ // Clean up old completed/failed jobs every hour
33
+ this.cleanupInterval = setInterval(() => {
34
+ this.cleanupOldJobs().catch(err => {
35
+ console.error('Failed to cleanup old jobs:', err);
36
+ });
37
+ }, 60 * 60 * 1000);
38
+ }
39
+ /**
40
+ * Create jobs table if it doesn't exist
41
+ */
42
+ async initTable() {
43
+ try {
44
+ await this.pool.query(`
45
+ CREATE TABLE IF NOT EXISTS jobs (
46
+ id TEXT PRIMARY KEY,
47
+ type TEXT NOT NULL,
48
+ status TEXT NOT NULL DEFAULT 'pending',
49
+ progress INTEGER DEFAULT 0,
50
+ data JSONB,
51
+ error TEXT,
52
+ total INTEGER DEFAULT 0,
53
+ completed INTEGER DEFAULT 0,
54
+ credits_used INTEGER DEFAULT 0,
55
+ owner_id TEXT,
56
+ webhook_url TEXT,
57
+ webhook_events JSONB,
58
+ webhook_metadata JSONB,
59
+ webhook_secret TEXT,
60
+ created_at TIMESTAMPTZ DEFAULT NOW(),
61
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
62
+ expires_at TIMESTAMPTZ
63
+ )
64
+ `);
65
+ // Add owner_id column if it doesn't exist (migration for existing tables)
66
+ await this.pool.query(`
67
+ DO $$ BEGIN
68
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS owner_id TEXT;
69
+ ALTER TABLE jobs ADD COLUMN IF NOT EXISTS webhook_delivery JSONB;
70
+ EXCEPTION WHEN others THEN NULL;
71
+ END $$;
72
+ `);
73
+ // Add index on status and created_at for faster queries
74
+ await this.pool.query(`
75
+ CREATE INDEX IF NOT EXISTS idx_jobs_status_created
76
+ ON jobs(status, created_at DESC)
77
+ `);
78
+ // Add index on type for filtering
79
+ await this.pool.query(`
80
+ CREATE INDEX IF NOT EXISTS idx_jobs_type
81
+ ON jobs(type)
82
+ `);
83
+ // Add index on expires_at for cleanup
84
+ await this.pool.query(`
85
+ CREATE INDEX IF NOT EXISTS idx_jobs_expires
86
+ ON jobs(expires_at)
87
+ `);
88
+ // Add index on owner_id for per-user job filtering
89
+ await this.pool.query(`
90
+ CREATE INDEX IF NOT EXISTS idx_jobs_owner
91
+ ON jobs(owner_id)
92
+ `);
93
+ }
94
+ catch (error) {
95
+ console.error('Failed to create jobs table:', error);
96
+ throw error;
97
+ }
98
+ }
99
+ /**
100
+ * Create a new job
101
+ */
102
+ async createJob(type, webhook, ownerId) {
103
+ const id = randomUUID();
104
+ const now = new Date();
105
+ const expiresAt = new Date(now.getTime() + 25 * 60 * 60 * 1000); // 25h from now
106
+ try {
107
+ await this.pool.query(`INSERT INTO jobs (
108
+ id, type, status, progress, data, total, completed, credits_used,
109
+ owner_id, webhook_url, webhook_events, webhook_metadata, webhook_secret,
110
+ created_at, updated_at, expires_at
111
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)`, [
112
+ id,
113
+ type,
114
+ 'queued',
115
+ 0,
116
+ JSON.stringify([]),
117
+ 0,
118
+ 0,
119
+ 0,
120
+ ownerId || null,
121
+ webhook?.url || null,
122
+ webhook?.events ? JSON.stringify(webhook.events) : null,
123
+ webhook?.metadata ? JSON.stringify(webhook.metadata) : null,
124
+ webhook?.secret || null,
125
+ now,
126
+ now,
127
+ expiresAt,
128
+ ]);
129
+ return {
130
+ id,
131
+ type,
132
+ status: 'queued',
133
+ progress: 0,
134
+ total: 0,
135
+ completed: 0,
136
+ creditsUsed: 0,
137
+ data: [],
138
+ webhook,
139
+ ownerId,
140
+ createdAt: now.toISOString(),
141
+ updatedAt: now.toISOString(),
142
+ expiresAt: expiresAt.toISOString(),
143
+ };
144
+ }
145
+ catch (error) {
146
+ console.error('Failed to create job:', error);
147
+ throw error;
148
+ }
149
+ }
150
+ /**
151
+ * Get a job by ID
152
+ */
153
+ async getJob(id) {
154
+ try {
155
+ const result = await this.pool.query(`SELECT * FROM jobs WHERE id = $1`, [id]);
156
+ if (result.rows.length === 0) {
157
+ return null;
158
+ }
159
+ return this.mapRowToJob(result.rows[0]);
160
+ }
161
+ catch (error) {
162
+ console.error('Failed to get job:', error);
163
+ return null;
164
+ }
165
+ }
166
+ /**
167
+ * Update a job
168
+ */
169
+ async updateJob(id, update) {
170
+ try {
171
+ const job = await this.getJob(id);
172
+ if (!job)
173
+ return;
174
+ const updates = [];
175
+ const values = [];
176
+ let paramIndex = 1;
177
+ // Map Job fields to database columns
178
+ if (update.status !== undefined) {
179
+ updates.push(`status = $${paramIndex++}`);
180
+ values.push(update.status);
181
+ }
182
+ if (update.progress !== undefined) {
183
+ updates.push(`progress = $${paramIndex++}`);
184
+ values.push(update.progress);
185
+ }
186
+ if (update.total !== undefined) {
187
+ updates.push(`total = $${paramIndex++}`);
188
+ values.push(update.total);
189
+ }
190
+ if (update.completed !== undefined) {
191
+ updates.push(`completed = $${paramIndex++}`);
192
+ values.push(update.completed);
193
+ }
194
+ if (update.creditsUsed !== undefined) {
195
+ updates.push(`credits_used = $${paramIndex++}`);
196
+ values.push(update.creditsUsed);
197
+ }
198
+ if (update.data !== undefined) {
199
+ updates.push(`data = $${paramIndex++}`);
200
+ values.push(JSON.stringify(update.data));
201
+ }
202
+ if (update.error !== undefined) {
203
+ updates.push(`error = $${paramIndex++}`);
204
+ values.push(update.error);
205
+ }
206
+ if (update.webhookDelivery !== undefined) {
207
+ updates.push(`webhook_delivery = $${paramIndex++}`);
208
+ values.push(JSON.stringify(update.webhookDelivery));
209
+ }
210
+ // Always update updated_at
211
+ updates.push(`updated_at = $${paramIndex++}`);
212
+ values.push(new Date());
213
+ // When job completes/fails, set expiration to 24h from now
214
+ if (update.status === 'completed' || update.status === 'failed' || update.status === 'cancelled') {
215
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
216
+ updates.push(`expires_at = $${paramIndex++}`);
217
+ values.push(expiresAt);
218
+ }
219
+ // Calculate progress percentage if total and completed are provided
220
+ const newTotal = update.total ?? job.total;
221
+ const newCompleted = update.completed ?? job.completed;
222
+ if (newTotal > 0) {
223
+ const progress = Math.round((newCompleted / newTotal) * 100);
224
+ if (!updates.some(u => u.startsWith('progress'))) {
225
+ updates.push(`progress = $${paramIndex++}`);
226
+ values.push(progress);
227
+ }
228
+ }
229
+ if (updates.length === 0)
230
+ return;
231
+ // Add job ID as the last parameter
232
+ values.push(id);
233
+ const sql = `UPDATE jobs SET ${updates.join(', ')} WHERE id = $${paramIndex}`;
234
+ await this.pool.query(sql, values);
235
+ }
236
+ catch (error) {
237
+ console.error('Failed to update job:', error);
238
+ throw error;
239
+ }
240
+ }
241
+ /**
242
+ * Cancel a job
243
+ */
244
+ async cancelJob(id) {
245
+ try {
246
+ const job = await this.getJob(id);
247
+ if (!job)
248
+ return false;
249
+ // Can only cancel queued or processing jobs
250
+ if (job.status !== 'queued' && job.status !== 'processing') {
251
+ return false;
252
+ }
253
+ await this.updateJob(id, {
254
+ status: 'cancelled',
255
+ });
256
+ return true;
257
+ }
258
+ catch (error) {
259
+ console.error('Failed to cancel job:', error);
260
+ return false;
261
+ }
262
+ }
263
+ /**
264
+ * List jobs with optional filters
265
+ */
266
+ async listJobs(options) {
267
+ try {
268
+ const conditions = [];
269
+ const values = [];
270
+ let paramIndex = 1;
271
+ if (options?.ownerId) {
272
+ conditions.push(`owner_id = $${paramIndex++}`);
273
+ values.push(options.ownerId);
274
+ }
275
+ if (options?.type) {
276
+ conditions.push(`type = $${paramIndex++}`);
277
+ values.push(options.type);
278
+ }
279
+ if (options?.status) {
280
+ conditions.push(`status = $${paramIndex++}`);
281
+ values.push(options.status);
282
+ }
283
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
284
+ const limit = options?.limit || 50;
285
+ const sql = `
286
+ SELECT * FROM jobs
287
+ ${whereClause}
288
+ ORDER BY created_at DESC
289
+ LIMIT $${paramIndex}
290
+ `;
291
+ values.push(limit);
292
+ const result = await this.pool.query(sql, values);
293
+ return result.rows.map(row => this.mapRowToJob(row));
294
+ }
295
+ catch (error) {
296
+ console.error('Failed to list jobs:', error);
297
+ return [];
298
+ }
299
+ }
300
+ /**
301
+ * Remove expired jobs (called periodically)
302
+ */
303
+ async cleanExpired() {
304
+ try {
305
+ await this.pool.query(`DELETE FROM jobs WHERE expires_at < NOW()`);
306
+ }
307
+ catch (error) {
308
+ console.error('Failed to clean expired jobs:', error);
309
+ }
310
+ }
311
+ /**
312
+ * Remove old completed/failed jobs (>7 days)
313
+ */
314
+ async cleanupOldJobs() {
315
+ try {
316
+ // Remove expired jobs
317
+ await this.cleanExpired();
318
+ // Remove completed/failed jobs older than 7 days
319
+ await this.pool.query(`DELETE FROM jobs
320
+ WHERE (status = 'completed' OR status = 'failed' OR status = 'cancelled')
321
+ AND updated_at < NOW() - INTERVAL '7 days'`);
322
+ }
323
+ catch (error) {
324
+ console.error('Failed to cleanup old jobs:', error);
325
+ }
326
+ }
327
+ /**
328
+ * Map database row to Job object
329
+ */
330
+ mapRowToJob(row) {
331
+ const webhook = row.webhook_url
332
+ ? {
333
+ url: row.webhook_url,
334
+ events: row.webhook_events || [],
335
+ metadata: row.webhook_metadata || undefined,
336
+ secret: row.webhook_secret || undefined,
337
+ }
338
+ : undefined;
339
+ const webhookDelivery = row.webhook_delivery
340
+ ? (typeof row.webhook_delivery === 'string'
341
+ ? JSON.parse(row.webhook_delivery)
342
+ : row.webhook_delivery)
343
+ : undefined;
344
+ return {
345
+ id: row.id,
346
+ type: row.type,
347
+ status: row.status,
348
+ progress: row.progress || 0,
349
+ total: row.total || 0,
350
+ completed: row.completed || 0,
351
+ creditsUsed: row.credits_used || 0,
352
+ data: row.data || [],
353
+ error: row.error || undefined,
354
+ webhook,
355
+ webhookDelivery,
356
+ ownerId: row.owner_id || undefined,
357
+ createdAt: row.created_at.toISOString(),
358
+ updatedAt: row.updated_at.toISOString(),
359
+ expiresAt: row.expires_at.toISOString(),
360
+ };
361
+ }
362
+ /**
363
+ * Clean up interval on shutdown
364
+ */
365
+ destroy() {
366
+ clearInterval(this.cleanupInterval);
367
+ }
368
+ /**
369
+ * Close the database pool
370
+ */
371
+ async close() {
372
+ this.destroy();
373
+ await this.pool.end();
374
+ }
375
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Domain Intelligence — premium server-only optimisation.
3
+ *
4
+ * Learns from historical fetch outcomes which domains require browser or
5
+ * stealth mode, so subsequent requests skip the slow simple→browser
6
+ * escalation path and go straight to the right strategy.
7
+ *
8
+ * Uses an exponential moving average for latency tracking and requires a
9
+ * minimum sample count before issuing recommendations to avoid false
10
+ * positives from one-off failures.
11
+ *
12
+ * This module is NOT shipped in the npm package.
13
+ */
14
+ import type { StrategyHooks } from '../../core/strategy-hooks.js';
15
+ export declare function clearDomainIntel(): void;
16
+ export declare function createDomainIntelHooks(): Pick<StrategyHooks, 'getDomainRecommendation' | 'recordDomainResult'>;
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Domain Intelligence — premium server-only optimisation.
3
+ *
4
+ * Learns from historical fetch outcomes which domains require browser or
5
+ * stealth mode, so subsequent requests skip the slow simple→browser
6
+ * escalation path and go straight to the right strategy.
7
+ *
8
+ * Uses an exponential moving average for latency tracking and requires a
9
+ * minimum sample count before issuing recommendations to avoid false
10
+ * positives from one-off failures.
11
+ *
12
+ * This module is NOT shipped in the npm package.
13
+ */
14
+ /* ---------- configuration ----------------------------------------------- */
15
+ const MAX_DOMAINS = 500;
16
+ const TTL_MS = 60 * 60 * 1000; // 1 hour
17
+ const EMA_ALPHA = 0.3;
18
+ const MIN_SAMPLES = 3;
19
+ /* ---------- state ------------------------------------------------------- */
20
+ const domainIntel = new Map();
21
+ const methodCounts = new Map();
22
+ /* ---------- internals --------------------------------------------------- */
23
+ function domainKey(url) {
24
+ try {
25
+ return new URL(url).hostname.toLowerCase();
26
+ }
27
+ catch {
28
+ return '';
29
+ }
30
+ }
31
+ function prune(now) {
32
+ for (const [key, intel] of domainIntel) {
33
+ if (now - intel.lastSeen > TTL_MS) {
34
+ domainIntel.delete(key);
35
+ methodCounts.delete(key);
36
+ }
37
+ }
38
+ }
39
+ /* ---------- hook implementations ---------------------------------------- */
40
+ function getDomainRecommendation(url) {
41
+ const key = domainKey(url);
42
+ if (!key)
43
+ return null;
44
+ const intel = domainIntel.get(key);
45
+ if (!intel)
46
+ return null;
47
+ const now = Date.now();
48
+ if (now - intel.lastSeen > TTL_MS) {
49
+ domainIntel.delete(key);
50
+ methodCounts.delete(key);
51
+ return null;
52
+ }
53
+ if (intel.sampleCount < MIN_SAMPLES)
54
+ return null;
55
+ const counts = methodCounts.get(key);
56
+ if (!counts)
57
+ return null;
58
+ // LRU touch
59
+ domainIntel.delete(key);
60
+ domainIntel.set(key, intel);
61
+ // All samples needed stealth → recommend stealth
62
+ if (counts.stealth === intel.sampleCount && intel.needsStealth) {
63
+ return { mode: 'stealth' };
64
+ }
65
+ // All samples needed browser (never succeeded with simple) → recommend browser
66
+ if (counts.simple === 0 &&
67
+ counts.browser + counts.stealth === intel.sampleCount &&
68
+ intel.needsBrowser) {
69
+ return { mode: 'browser' };
70
+ }
71
+ return null;
72
+ }
73
+ function recordDomainResult(url, method, latencyMs) {
74
+ const key = domainKey(url);
75
+ if (!key)
76
+ return;
77
+ const now = Date.now();
78
+ prune(now);
79
+ const existing = domainIntel.get(key);
80
+ const sanitizedLatency = Number.isFinite(latencyMs) && latencyMs > 0
81
+ ? latencyMs
82
+ : (existing?.avgLatencyMs ?? 0);
83
+ const next = existing
84
+ ? {
85
+ needsBrowser: existing.needsBrowser ||
86
+ method === 'browser' ||
87
+ method === 'stealth',
88
+ needsStealth: existing.needsStealth || method === 'stealth',
89
+ avgLatencyMs: existing.avgLatencyMs === 0
90
+ ? sanitizedLatency
91
+ : existing.avgLatencyMs * (1 - EMA_ALPHA) +
92
+ sanitizedLatency * EMA_ALPHA,
93
+ lastSeen: now,
94
+ sampleCount: existing.sampleCount + 1,
95
+ }
96
+ : {
97
+ needsBrowser: method === 'browser' || method === 'stealth',
98
+ needsStealth: method === 'stealth',
99
+ avgLatencyMs: sanitizedLatency,
100
+ lastSeen: now,
101
+ sampleCount: 1,
102
+ };
103
+ const existingCounts = methodCounts.get(key) ?? {
104
+ simple: 0,
105
+ browser: 0,
106
+ stealth: 0,
107
+ };
108
+ existingCounts[method] += 1;
109
+ // Delete-then-set for LRU ordering
110
+ domainIntel.delete(key);
111
+ domainIntel.set(key, next);
112
+ methodCounts.set(key, existingCounts);
113
+ // Evict oldest when over capacity
114
+ while (domainIntel.size > MAX_DOMAINS) {
115
+ const oldest = domainIntel.keys().next().value;
116
+ if (!oldest)
117
+ break;
118
+ domainIntel.delete(oldest);
119
+ methodCounts.delete(oldest);
120
+ }
121
+ }
122
+ /* ---------- cleanup ----------------------------------------------------- */
123
+ export function clearDomainIntel() {
124
+ domainIntel.clear();
125
+ methodCounts.clear();
126
+ }
127
+ /* ---------- public export ----------------------------------------------- */
128
+ export function createDomainIntelHooks() {
129
+ return {
130
+ getDomainRecommendation,
131
+ recordDomainResult,
132
+ };
133
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Premium strategy hooks — server-only optimisations.
3
+ *
4
+ * Call `registerPremiumHooks()` once at server startup to activate:
5
+ * • SWR (stale-while-revalidate) response cache
6
+ * • Domain intelligence (learns which sites need browser/stealth)
7
+ * • Parallel race strategy (starts browser if simple fetch is slow)
8
+ *
9
+ * These modules are NOT shipped in the npm package.
10
+ */
11
+ export { clearDomainIntel } from './domain-intel.js';
12
+ /**
13
+ * Wire all premium hooks into the core strategy layer.
14
+ *
15
+ * Must be called before any request is served.
16
+ */
17
+ export declare function registerPremiumHooks(): void;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Premium strategy hooks — server-only optimisations.
3
+ *
4
+ * Call `registerPremiumHooks()` once at server startup to activate:
5
+ * • SWR (stale-while-revalidate) response cache
6
+ * • Domain intelligence (learns which sites need browser/stealth)
7
+ * • Parallel race strategy (starts browser if simple fetch is slow)
8
+ *
9
+ * These modules are NOT shipped in the npm package.
10
+ */
11
+ import { registerStrategyHooks } from '../../core/strategy-hooks.js';
12
+ import { createSWRCacheHooks } from './swr-cache.js';
13
+ import { createDomainIntelHooks } from './domain-intel.js';
14
+ export { clearDomainIntel } from './domain-intel.js';
15
+ /**
16
+ * Wire all premium hooks into the core strategy layer.
17
+ *
18
+ * Must be called before any request is served.
19
+ */
20
+ export function registerPremiumHooks() {
21
+ const cacheHooks = createSWRCacheHooks();
22
+ const intelHooks = createDomainIntelHooks();
23
+ registerStrategyHooks({
24
+ // SWR cache
25
+ checkCache: cacheHooks.checkCache,
26
+ markRevalidating: cacheHooks.markRevalidating,
27
+ setCache: cacheHooks.setCache,
28
+ // Domain intelligence
29
+ getDomainRecommendation: intelHooks.getDomainRecommendation,
30
+ recordDomainResult: intelHooks.recordDomainResult,
31
+ // Parallel race strategy
32
+ shouldRace: () => true,
33
+ getRaceTimeoutMs: () => 2000,
34
+ });
35
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Stale-While-Revalidate cache — premium server-only optimisation.
3
+ *
4
+ * Wraps the core LRU cache with SWR semantics:
5
+ * • Fresh entries are served immediately.
6
+ * • Stale entries (within the SWR window) are served AND trigger a
7
+ * background revalidation so the next caller gets a fresh result.
8
+ * • Expired entries (past the SWR window) are evicted.
9
+ *
10
+ * This module is NOT shipped in the npm package — it lives under
11
+ * `src/server/` which is excluded from the package.json `files` list.
12
+ */
13
+ import type { StrategyHooks } from '../../core/strategy-hooks.js';
14
+ export declare function createSWRCacheHooks(): Pick<StrategyHooks, 'checkCache' | 'markRevalidating' | 'setCache'>;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Stale-While-Revalidate cache — premium server-only optimisation.
3
+ *
4
+ * Wraps the core LRU cache with SWR semantics:
5
+ * • Fresh entries are served immediately.
6
+ * • Stale entries (within the SWR window) are served AND trigger a
7
+ * background revalidation so the next caller gets a fresh result.
8
+ * • Expired entries (past the SWR window) are evicted.
9
+ *
10
+ * This module is NOT shipped in the npm package — it lives under
11
+ * `src/server/` which is excluded from the package.json `files` list.
12
+ */
13
+ import { getCachedWithSWR, markRevalidating, setCached, } from '../../core/cache.js';
14
+ /* ---------- hook implementations ---------------------------------------- */
15
+ function checkCache(url) {
16
+ const entry = getCachedWithSWR(url);
17
+ if (!entry)
18
+ return null;
19
+ return { value: entry.value, stale: entry.stale };
20
+ }
21
+ function markRevalidatingHook(url) {
22
+ return markRevalidating(url);
23
+ }
24
+ function setCache(url, result) {
25
+ setCached(url, result);
26
+ }
27
+ /* ---------- public export ----------------------------------------------- */
28
+ export function createSWRCacheHooks() {
29
+ return {
30
+ checkCache,
31
+ markRevalidating: markRevalidatingHook,
32
+ setCache,
33
+ };
34
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Activity endpoint - provides recent API request history
3
+ */
4
+ import { Router } from 'express';
5
+ import { AuthStore } from '../auth-store.js';
6
+ export declare function createActivityRouter(authStore: AuthStore): Router;