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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe webhook handler for subscription management
|
|
3
|
+
*/
|
|
4
|
+
import { Router } from 'express';
|
|
5
|
+
import Stripe from 'stripe';
|
|
6
|
+
import pg from 'pg';
|
|
7
|
+
import { createLogger } from '../logger.js';
|
|
8
|
+
const log = createLogger('stripe');
|
|
9
|
+
const { Pool } = pg;
|
|
10
|
+
/**
|
|
11
|
+
* Tier configuration (weekly usage model)
|
|
12
|
+
*/
|
|
13
|
+
const TIER_LIMITS = {
|
|
14
|
+
free: { weekly_limit: 500, burst_limit: 50, rate_limit: 10 },
|
|
15
|
+
pro: { weekly_limit: 1250, burst_limit: 100, rate_limit: 60 },
|
|
16
|
+
max: { weekly_limit: 6250, burst_limit: 500, rate_limit: 200 },
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Create Stripe Billing Portal router
|
|
20
|
+
* POST /v1/billing/portal — create a Stripe Customer Portal session
|
|
21
|
+
* Requires global auth middleware to already have run (req.user or req.auth set).
|
|
22
|
+
*/
|
|
23
|
+
export function createBillingPortalRouter(pool) {
|
|
24
|
+
const router = Router();
|
|
25
|
+
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
26
|
+
if (!stripeSecretKey) {
|
|
27
|
+
log.warn('STRIPE_SECRET_KEY not configured - billing portal disabled');
|
|
28
|
+
return router;
|
|
29
|
+
}
|
|
30
|
+
const stripe = new Stripe(stripeSecretKey);
|
|
31
|
+
router.post('/v1/billing/portal', async (req, res) => {
|
|
32
|
+
try {
|
|
33
|
+
const userId = req.user?.userId || req.auth?.keyInfo?.accountId;
|
|
34
|
+
if (!userId) {
|
|
35
|
+
res.status(401).json({ success: false, error: { type: 'unauthorized', message: 'Authentication required.', docs: 'https://webpeel.dev/docs/authentication' }, requestId: req.requestId });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (!pool) {
|
|
39
|
+
res.status(503).json({
|
|
40
|
+
success: false,
|
|
41
|
+
error: {
|
|
42
|
+
type: 'db_unavailable',
|
|
43
|
+
message: 'Database not configured',
|
|
44
|
+
docs: 'https://webpeel.dev/docs/errors#db_unavailable',
|
|
45
|
+
},
|
|
46
|
+
requestId: req.requestId,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// Get user's stripe_customer_id from DB
|
|
51
|
+
const result = await pool.query('SELECT stripe_customer_id FROM users WHERE id = $1', [userId]);
|
|
52
|
+
const stripeCustomerId = result.rows[0]?.stripe_customer_id;
|
|
53
|
+
if (!stripeCustomerId) {
|
|
54
|
+
res.status(400).json({ success: false, error: { type: 'no_subscription', message: 'No active subscription found. Upgrade to Pro or Max to manage billing.', hint: 'Upgrade at https://webpeel.dev/pricing', docs: 'https://webpeel.dev/docs/errors#no_subscription' }, requestId: req.requestId });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Create portal session
|
|
58
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
59
|
+
customer: stripeCustomerId,
|
|
60
|
+
return_url: 'https://app.webpeel.dev/billing',
|
|
61
|
+
});
|
|
62
|
+
res.json({ url: session.url });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
log.error('Failed to create portal session:', err);
|
|
66
|
+
res.status(500).json({
|
|
67
|
+
success: false,
|
|
68
|
+
error: {
|
|
69
|
+
type: 'portal_failed',
|
|
70
|
+
message: 'Failed to create billing portal session',
|
|
71
|
+
docs: 'https://webpeel.dev/docs/errors#portal_failed',
|
|
72
|
+
},
|
|
73
|
+
requestId: req.requestId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
return router;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create Stripe webhook router
|
|
81
|
+
*/
|
|
82
|
+
export function createStripeRouter() {
|
|
83
|
+
const router = Router();
|
|
84
|
+
const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
|
|
85
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
86
|
+
const dbUrl = process.env.DATABASE_URL;
|
|
87
|
+
if (!stripeSecretKey) {
|
|
88
|
+
log.warn('STRIPE_SECRET_KEY not configured - Stripe webhooks disabled');
|
|
89
|
+
return router;
|
|
90
|
+
}
|
|
91
|
+
if (!webhookSecret) {
|
|
92
|
+
log.warn('STRIPE_WEBHOOK_SECRET not configured - Stripe webhooks disabled');
|
|
93
|
+
return router;
|
|
94
|
+
}
|
|
95
|
+
if (!dbUrl) {
|
|
96
|
+
throw new Error('DATABASE_URL environment variable is required');
|
|
97
|
+
}
|
|
98
|
+
const stripe = new Stripe(stripeSecretKey);
|
|
99
|
+
const pool = new Pool({
|
|
100
|
+
connectionString: dbUrl,
|
|
101
|
+
// TLS: enabled when DATABASE_URL contains sslmode=require.
|
|
102
|
+
// Secure by default (rejectUnauthorized: true); set PG_REJECT_UNAUTHORIZED=false
|
|
103
|
+
// only for managed DBs (Render/Neon/Supabase) that use self-signed certs.
|
|
104
|
+
ssl: process.env.DATABASE_URL?.includes('sslmode=require')
|
|
105
|
+
? { rejectUnauthorized: process.env.PG_REJECT_UNAUTHORIZED !== 'false' }
|
|
106
|
+
: undefined,
|
|
107
|
+
});
|
|
108
|
+
/**
|
|
109
|
+
* POST /v1/webhooks/stripe
|
|
110
|
+
* Handle Stripe webhook events
|
|
111
|
+
* SECURITY: Verifies webhook signature
|
|
112
|
+
*/
|
|
113
|
+
router.post('/', async (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const sig = req.headers['stripe-signature'];
|
|
116
|
+
if (!sig || typeof sig !== 'string') {
|
|
117
|
+
res.status(400).json({ success: false, error: { type: 'missing_signature', message: 'Stripe signature header missing', hint: 'Ensure the request includes the stripe-signature header', docs: 'https://webpeel.dev/docs/errors#missing_signature' }, requestId: req.requestId });
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
// SECURITY: Verify webhook signature
|
|
121
|
+
let event;
|
|
122
|
+
try {
|
|
123
|
+
event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
log.error('Webhook signature verification failed', { message: err.message });
|
|
127
|
+
res.status(400).json({ success: false, error: { type: 'invalid_signature', message: 'Webhook signature verification failed', hint: 'Verify your STRIPE_WEBHOOK_SECRET matches the Stripe dashboard', docs: 'https://webpeel.dev/docs/errors#invalid_signature' }, requestId: req.requestId });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Handle different event types
|
|
131
|
+
switch (event.type) {
|
|
132
|
+
case 'checkout.session.completed': {
|
|
133
|
+
const session = event.data.object;
|
|
134
|
+
await handleCheckoutCompleted(pool, session);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case 'customer.subscription.updated': {
|
|
138
|
+
const subscription = event.data.object;
|
|
139
|
+
await handleSubscriptionUpdated(pool, subscription);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'customer.subscription.deleted': {
|
|
143
|
+
const subscription = event.data.object;
|
|
144
|
+
await handleSubscriptionDeleted(pool, subscription);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'invoice.payment_failed': {
|
|
148
|
+
const invoice = event.data.object;
|
|
149
|
+
await handlePaymentFailed(pool, invoice);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
default:
|
|
153
|
+
log.warn(`Unhandled Stripe event type: ${event.type}`);
|
|
154
|
+
}
|
|
155
|
+
res.json({ received: true });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
log.error('Webhook error', { error: error instanceof Error ? error.message : String(error) });
|
|
159
|
+
res.status(500).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: {
|
|
162
|
+
type: 'webhook_failed',
|
|
163
|
+
message: 'Failed to process webhook',
|
|
164
|
+
docs: 'https://webpeel.dev/docs/errors#webhook_failed',
|
|
165
|
+
},
|
|
166
|
+
requestId: req.requestId,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
return router;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Handle checkout.session.completed
|
|
174
|
+
* Upgrade user tier and set limits
|
|
175
|
+
*/
|
|
176
|
+
async function handleCheckoutCompleted(pool, session) {
|
|
177
|
+
try {
|
|
178
|
+
const customerId = session.customer;
|
|
179
|
+
const subscriptionId = session.subscription;
|
|
180
|
+
// Get subscription to determine tier
|
|
181
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
|
|
182
|
+
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
|
183
|
+
// Determine tier from price ID (you'll need to configure these)
|
|
184
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
185
|
+
const tier = getTierFromPriceId(priceId);
|
|
186
|
+
const limits = TIER_LIMITS[tier];
|
|
187
|
+
// Update user
|
|
188
|
+
await pool.query(`UPDATE users
|
|
189
|
+
SET
|
|
190
|
+
stripe_customer_id = $1,
|
|
191
|
+
stripe_subscription_id = $2,
|
|
192
|
+
tier = $3,
|
|
193
|
+
weekly_limit = $4,
|
|
194
|
+
burst_limit = $5,
|
|
195
|
+
rate_limit = $6,
|
|
196
|
+
updated_at = now()
|
|
197
|
+
WHERE stripe_customer_id = $1 OR email = $7`, [
|
|
198
|
+
customerId,
|
|
199
|
+
subscriptionId,
|
|
200
|
+
tier,
|
|
201
|
+
limits.weekly_limit,
|
|
202
|
+
limits.burst_limit,
|
|
203
|
+
limits.rate_limit,
|
|
204
|
+
session.customer_email,
|
|
205
|
+
]);
|
|
206
|
+
log.info(`Checkout completed for customer ${customerId}: upgraded to ${tier}`);
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
log.error('Failed to handle checkout completion', { error: error instanceof Error ? error.message : String(error) });
|
|
210
|
+
throw error;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Handle customer.subscription.updated
|
|
215
|
+
* Update user tier based on subscription changes
|
|
216
|
+
*/
|
|
217
|
+
async function handleSubscriptionUpdated(pool, subscription) {
|
|
218
|
+
try {
|
|
219
|
+
const customerId = subscription.customer;
|
|
220
|
+
const priceId = subscription.items.data[0]?.price.id;
|
|
221
|
+
const tier = getTierFromPriceId(priceId);
|
|
222
|
+
const limits = TIER_LIMITS[tier];
|
|
223
|
+
await pool.query(`UPDATE users
|
|
224
|
+
SET
|
|
225
|
+
tier = $1,
|
|
226
|
+
weekly_limit = $2,
|
|
227
|
+
burst_limit = $3,
|
|
228
|
+
rate_limit = $4,
|
|
229
|
+
stripe_subscription_id = $5,
|
|
230
|
+
updated_at = now()
|
|
231
|
+
WHERE stripe_customer_id = $6`, [tier, limits.weekly_limit, limits.burst_limit, limits.rate_limit, subscription.id, customerId]);
|
|
232
|
+
log.info(`Subscription updated for customer ${customerId}: tier=${tier}`);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
log.error('Failed to handle subscription update', { error: error instanceof Error ? error.message : String(error) });
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Handle customer.subscription.deleted
|
|
241
|
+
* Downgrade user to free tier
|
|
242
|
+
*/
|
|
243
|
+
async function handleSubscriptionDeleted(pool, subscription) {
|
|
244
|
+
try {
|
|
245
|
+
const customerId = subscription.customer;
|
|
246
|
+
const limits = TIER_LIMITS.free;
|
|
247
|
+
await pool.query(`UPDATE users
|
|
248
|
+
SET
|
|
249
|
+
tier = 'free',
|
|
250
|
+
weekly_limit = $1,
|
|
251
|
+
burst_limit = $2,
|
|
252
|
+
rate_limit = $3,
|
|
253
|
+
stripe_subscription_id = NULL,
|
|
254
|
+
updated_at = now()
|
|
255
|
+
WHERE stripe_customer_id = $4`, [limits.weekly_limit, limits.burst_limit, limits.rate_limit, customerId]);
|
|
256
|
+
log.info(`Subscription deleted for customer ${customerId}: downgraded to free`);
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
log.error('Failed to handle subscription deletion', { error: error instanceof Error ? error.message : String(error) });
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Handle invoice.payment_failed
|
|
265
|
+
* Log payment failure (could add email notification here)
|
|
266
|
+
*/
|
|
267
|
+
async function handlePaymentFailed(pool, invoice) {
|
|
268
|
+
try {
|
|
269
|
+
const customerId = invoice.customer;
|
|
270
|
+
// Get user email for logging
|
|
271
|
+
const result = await pool.query('SELECT email FROM users WHERE stripe_customer_id = $1', [customerId]);
|
|
272
|
+
if (result.rows.length > 0) {
|
|
273
|
+
log.warn(`Payment failed for customer ${customerId}`, { email: result.rows[0].email });
|
|
274
|
+
// Note: Email notification not implemented. Log only for now.
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
log.error('Failed to handle payment failure', { error: error instanceof Error ? error.message : String(error) });
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Map Stripe price ID to tier
|
|
284
|
+
* Maps Stripe price IDs to tiers (configured via STRIPE_PRICE_PRO and STRIPE_PRICE_MAX env vars)
|
|
285
|
+
*/
|
|
286
|
+
function getTierFromPriceId(priceId) {
|
|
287
|
+
// Map price IDs to tiers
|
|
288
|
+
const priceMap = {
|
|
289
|
+
// Add your Stripe price IDs here
|
|
290
|
+
[process.env.STRIPE_PRICE_PRO || '']: 'pro',
|
|
291
|
+
[process.env.STRIPE_PRICE_MAX || '']: 'max',
|
|
292
|
+
};
|
|
293
|
+
return priceMap[priceId] || 'free';
|
|
294
|
+
}
|