@riligar/agents-kit 1.12.0 → 1.13.0

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.
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Stripe Server Snippets for Elysia
3
+ * Copy-paste ready code for backend Stripe integration
4
+ */
5
+
6
+ // ============================================
7
+ // 1. SETUP & PLUGIN
8
+ // ============================================
9
+
10
+ // plugins/stripe.js
11
+ import { Elysia } from 'elysia'
12
+ import Stripe from 'stripe'
13
+
14
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
15
+
16
+ export const stripePlugin = new Elysia({ name: 'stripe' })
17
+ .decorate('stripe', stripe)
18
+
19
+ // ============================================
20
+ // 2. BILLING ROUTES
21
+ // ============================================
22
+
23
+ // routes/billing.js
24
+ import { Elysia, t } from 'elysia'
25
+ import { stripePlugin } from '../plugins/stripe'
26
+ import { billingService } from '../services/billing'
27
+
28
+ export const billingRoutes = new Elysia({ prefix: '/billing' })
29
+ .use(stripePlugin)
30
+
31
+ // Create checkout session
32
+ .post('/checkout', async ({ stripe, body, user }) => {
33
+ // Get or create Stripe customer
34
+ let customerId = user.stripeCustomerId
35
+ if (!customerId) {
36
+ const customer = await stripe.customers.create({
37
+ email: user.email,
38
+ name: user.name,
39
+ metadata: { userId: user.id }
40
+ })
41
+ customerId = customer.id
42
+ await billingService.linkStripeCustomer(user.id, customerId)
43
+ }
44
+
45
+ const session = await stripe.checkout.sessions.create({
46
+ mode: body.mode,
47
+ customer: customerId,
48
+ line_items: [{ price: body.priceId, quantity: 1 }],
49
+ success_url: `${process.env.APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
50
+ cancel_url: `${process.env.APP_URL}/pricing`,
51
+ metadata: { userId: user.id }
52
+ })
53
+
54
+ return { url: session.url }
55
+ }, {
56
+ body: t.Object({
57
+ priceId: t.String(),
58
+ mode: t.Union([t.Literal('subscription'), t.Literal('payment')])
59
+ })
60
+ })
61
+
62
+ // Get subscription status
63
+ .get('/subscription', async ({ stripe, user }) => {
64
+ if (!user.stripeSubscriptionId) {
65
+ return { status: 'none', plan: null }
66
+ }
67
+
68
+ const subscription = await stripe.subscriptions.retrieve(user.stripeSubscriptionId)
69
+
70
+ return {
71
+ status: subscription.status,
72
+ plan: subscription.items.data[0].price.id,
73
+ currentPeriodEnd: subscription.current_period_end,
74
+ cancelAtPeriodEnd: subscription.cancel_at_period_end
75
+ }
76
+ })
77
+
78
+ // Customer portal
79
+ .post('/portal', async ({ stripe, user, set }) => {
80
+ if (!user.stripeCustomerId) {
81
+ set.status = 400
82
+ return { error: 'No billing account' }
83
+ }
84
+
85
+ const portal = await stripe.billingPortal.sessions.create({
86
+ customer: user.stripeCustomerId,
87
+ return_url: `${process.env.APP_URL}/account`
88
+ })
89
+
90
+ return { url: portal.url }
91
+ })
92
+
93
+ // Cancel subscription
94
+ .post('/subscription/cancel', async ({ stripe, user, set }) => {
95
+ if (!user.stripeSubscriptionId) {
96
+ set.status = 400
97
+ return { error: 'No active subscription' }
98
+ }
99
+
100
+ const subscription = await stripe.subscriptions.update(
101
+ user.stripeSubscriptionId,
102
+ { cancel_at_period_end: true }
103
+ )
104
+
105
+ return {
106
+ status: subscription.status,
107
+ cancelAt: subscription.cancel_at
108
+ }
109
+ })
110
+
111
+ // Resume subscription
112
+ .post('/subscription/resume', async ({ stripe, user, set }) => {
113
+ if (!user.stripeSubscriptionId) {
114
+ set.status = 400
115
+ return { error: 'No subscription to resume' }
116
+ }
117
+
118
+ const subscription = await stripe.subscriptions.update(
119
+ user.stripeSubscriptionId,
120
+ { cancel_at_period_end: false }
121
+ )
122
+
123
+ return { status: subscription.status }
124
+ })
125
+
126
+ // List invoices
127
+ .get('/invoices', async ({ stripe, user, query }) => {
128
+ if (!user.stripeCustomerId) {
129
+ return { data: [], hasMore: false }
130
+ }
131
+
132
+ const invoices = await stripe.invoices.list({
133
+ customer: user.stripeCustomerId,
134
+ limit: query.limit
135
+ })
136
+
137
+ return {
138
+ data: invoices.data.map(inv => ({
139
+ id: inv.id,
140
+ amount: inv.amount_paid,
141
+ status: inv.status,
142
+ date: inv.created,
143
+ pdfUrl: inv.invoice_pdf
144
+ })),
145
+ hasMore: invoices.has_more
146
+ }
147
+ }, {
148
+ query: t.Object({
149
+ limit: t.Optional(t.Numeric({ default: 10 }))
150
+ })
151
+ })
152
+
153
+ // ============================================
154
+ // 3. WEBHOOK ROUTES
155
+ // ============================================
156
+
157
+ // routes/webhook.js
158
+ import { Elysia } from 'elysia'
159
+ import Stripe from 'stripe'
160
+ import { billingService } from '../services/billing'
161
+
162
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
163
+
164
+ export const webhookRoutes = new Elysia()
165
+ .post('/webhook/stripe', async ({ request, set }) => {
166
+ const sig = request.headers.get('stripe-signature')
167
+ const body = await request.text()
168
+
169
+ let event
170
+
171
+ try {
172
+ event = stripe.webhooks.constructEvent(
173
+ body,
174
+ sig,
175
+ process.env.STRIPE_WEBHOOK_SECRET
176
+ )
177
+ } catch (err) {
178
+ console.error('Webhook signature failed:', err.message)
179
+ set.status = 400
180
+ return { error: 'Invalid signature' }
181
+ }
182
+
183
+ // Handle events
184
+ switch (event.type) {
185
+ case 'checkout.session.completed': {
186
+ const session = event.data.object
187
+ const userId = session.metadata.userId
188
+
189
+ await billingService.activateSubscription(userId, {
190
+ subscriptionId: session.subscription,
191
+ plan: 'pro', // Map from price ID
192
+ periodEnd: new Date(session.expires_at * 1000)
193
+ })
194
+ break
195
+ }
196
+
197
+ case 'customer.subscription.updated': {
198
+ const subscription = event.data.object
199
+ // Handle plan changes, status updates
200
+ break
201
+ }
202
+
203
+ case 'customer.subscription.deleted': {
204
+ const subscription = event.data.object
205
+ await billingService.deactivateSubscription(subscription.id)
206
+ break
207
+ }
208
+
209
+ case 'invoice.paid': {
210
+ const invoice = event.data.object
211
+ // Record successful payment
212
+ break
213
+ }
214
+
215
+ case 'invoice.payment_failed': {
216
+ const invoice = event.data.object
217
+ // Handle failed payment - notify user
218
+ break
219
+ }
220
+ }
221
+
222
+ return { received: true }
223
+ })
224
+
225
+ // ============================================
226
+ // 4. BILLING SERVICE
227
+ // ============================================
228
+
229
+ // services/billing.js
230
+ import { db } from '../database'
231
+ import { users } from '../database/schema'
232
+ import { eq } from 'drizzle-orm'
233
+
234
+ export const billingService = {
235
+ async linkStripeCustomer(userId, stripeCustomerId) {
236
+ await db.update(users)
237
+ .set({ stripeCustomerId, updatedAt: new Date() })
238
+ .where(eq(users.id, userId))
239
+ },
240
+
241
+ async activateSubscription(userId, { subscriptionId, plan, periodEnd }) {
242
+ await db.update(users)
243
+ .set({
244
+ stripeSubscriptionId: subscriptionId,
245
+ plan,
246
+ subscriptionStatus: 'active',
247
+ currentPeriodEnd: periodEnd,
248
+ cancelAtPeriodEnd: false,
249
+ updatedAt: new Date()
250
+ })
251
+ .where(eq(users.id, userId))
252
+ },
253
+
254
+ async deactivateSubscription(subscriptionId) {
255
+ await db.update(users)
256
+ .set({
257
+ plan: 'free',
258
+ subscriptionStatus: 'canceled',
259
+ stripeSubscriptionId: null,
260
+ updatedAt: new Date()
261
+ })
262
+ .where(eq(users.stripeSubscriptionId, subscriptionId))
263
+ },
264
+
265
+ async canAccessFeature(userId, requiredPlan) {
266
+ const [user] = await db.select()
267
+ .from(users)
268
+ .where(eq(users.id, userId))
269
+ .limit(1)
270
+
271
+ if (!user) return false
272
+
273
+ const planHierarchy = { free: 0, starter: 1, pro: 2, enterprise: 3 }
274
+ return (planHierarchy[user.plan] || 0) >= (planHierarchy[requiredPlan] || 0)
275
+ }
276
+ }
277
+
278
+ // ============================================
279
+ // 5. MIDDLEWARE
280
+ // ============================================
281
+
282
+ // middleware/require-subscription.js
283
+ export const requireSubscription = async ({ user, set }) => {
284
+ if (!user?.stripeSubscriptionId) {
285
+ set.status = 403
286
+ return { error: 'Subscription required' }
287
+ }
288
+ }
289
+
290
+ export const requirePlan = (plan) => async ({ user, set }) => {
291
+ const canAccess = await billingService.canAccessFeature(user.id, plan)
292
+ if (!canAccess) {
293
+ set.status = 403
294
+ return { error: `Plan ${plan} required` }
295
+ }
296
+ }
297
+
298
+ // Usage:
299
+ // app.guard({ beforeHandle: requireSubscription }, app => app.get('/premium', handler))
300
+ // app.get('/enterprise-only', handler, { beforeHandle: requirePlan('enterprise') })
@@ -0,0 +1,369 @@
1
+ # Stripe Database Patterns (Drizzle)
2
+
3
+ Database schemas and queries for Stripe billing with SQLite and Drizzle ORM.
4
+
5
+ ## Schema
6
+
7
+ ### Users Table (with Stripe fields)
8
+
9
+ ```javascript
10
+ // database/schema.js
11
+ import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
12
+
13
+ export const users = sqliteTable('users', {
14
+ id: text('id').primaryKey(),
15
+ email: text('email').notNull().unique(),
16
+ name: text('name'),
17
+
18
+ // Stripe fields
19
+ stripeCustomerId: text('stripe_customer_id').unique(),
20
+ stripeSubscriptionId: text('stripe_subscription_id').unique(),
21
+ plan: text('plan').default('free'), // free, starter, pro, enterprise
22
+ subscriptionStatus: text('subscription_status'), // active, past_due, canceled, trialing
23
+ currentPeriodEnd: integer('current_period_end', { mode: 'timestamp' }),
24
+ cancelAtPeriodEnd: integer('cancel_at_period_end', { mode: 'boolean' }).default(false),
25
+
26
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
27
+ updatedAt: integer('updated_at', { mode: 'timestamp' })
28
+ })
29
+ ```
30
+
31
+ ### Invoices Table (optional)
32
+
33
+ ```javascript
34
+ export const invoices = sqliteTable('invoices', {
35
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
36
+ userId: text('user_id').references(() => users.id).notNull(),
37
+ stripeInvoiceId: text('stripe_invoice_id').unique().notNull(),
38
+ amount: integer('amount').notNull(), // in cents
39
+ status: text('status').notNull(), // paid, open, void
40
+ url: text('url'), // hosted invoice URL
41
+ createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date())
42
+ })
43
+ ```
44
+
45
+ ### Webhook Events Table (for idempotency)
46
+
47
+ ```javascript
48
+ export const webhookEvents = sqliteTable('webhook_events', {
49
+ id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
50
+ stripeEventId: text('stripe_event_id').unique().notNull(),
51
+ type: text('type').notNull(),
52
+ processedAt: integer('processed_at', { mode: 'timestamp' }).notNull()
53
+ })
54
+ ```
55
+
56
+ ### Products Table (for local price cache)
57
+
58
+ ```javascript
59
+ export const products = sqliteTable('products', {
60
+ id: text('id').primaryKey(),
61
+ stripePriceId: text('stripe_price_id').unique().notNull(),
62
+ name: text('name').notNull(),
63
+ description: text('description'),
64
+ amount: integer('amount').notNull(), // in cents
65
+ interval: text('interval'), // month, year, null for one-time
66
+ active: integer('active', { mode: 'boolean' }).default(true),
67
+ features: text('features', { mode: 'json' }) // ["feature1", "feature2"]
68
+ })
69
+ ```
70
+
71
+ ## Queries
72
+
73
+ ### Get User with Subscription
74
+
75
+ ```javascript
76
+ import { db } from '../database'
77
+ import { users } from '../database/schema'
78
+ import { eq } from 'drizzle-orm'
79
+
80
+ export async function getUserWithSubscription(userId) {
81
+ const [user] = await db.select()
82
+ .from(users)
83
+ .where(eq(users.id, userId))
84
+ .limit(1)
85
+
86
+ return user
87
+ }
88
+ ```
89
+
90
+ ### Check Active Subscription
91
+
92
+ ```javascript
93
+ export async function hasActiveSubscription(userId) {
94
+ const [user] = await db.select({
95
+ status: users.subscriptionStatus,
96
+ periodEnd: users.currentPeriodEnd
97
+ })
98
+ .from(users)
99
+ .where(eq(users.id, userId))
100
+ .limit(1)
101
+
102
+ if (!user) return false
103
+
104
+ const activeStatuses = ['active', 'trialing']
105
+ return activeStatuses.includes(user.status)
106
+ }
107
+ ```
108
+
109
+ ### Get User by Stripe Customer
110
+
111
+ ```javascript
112
+ export async function getUserByStripeCustomer(stripeCustomerId) {
113
+ const [user] = await db.select()
114
+ .from(users)
115
+ .where(eq(users.stripeCustomerId, stripeCustomerId))
116
+ .limit(1)
117
+
118
+ return user
119
+ }
120
+ ```
121
+
122
+ ### Get User by Subscription
123
+
124
+ ```javascript
125
+ export async function getUserBySubscription(stripeSubscriptionId) {
126
+ const [user] = await db.select()
127
+ .from(users)
128
+ .where(eq(users.stripeSubscriptionId, stripeSubscriptionId))
129
+ .limit(1)
130
+
131
+ return user
132
+ }
133
+ ```
134
+
135
+ ### Update Subscription
136
+
137
+ ```javascript
138
+ export async function updateUserSubscription(userId, data) {
139
+ await db.update(users)
140
+ .set({
141
+ stripeCustomerId: data.customerId,
142
+ stripeSubscriptionId: data.subscriptionId,
143
+ plan: data.plan,
144
+ subscriptionStatus: data.status,
145
+ currentPeriodEnd: data.periodEnd,
146
+ cancelAtPeriodEnd: data.cancelAtPeriodEnd,
147
+ updatedAt: new Date()
148
+ })
149
+ .where(eq(users.id, userId))
150
+ }
151
+ ```
152
+
153
+ ### Cancel Subscription (in DB)
154
+
155
+ ```javascript
156
+ export async function cancelSubscriptionInDb(subscriptionId) {
157
+ await db.update(users)
158
+ .set({
159
+ plan: 'free',
160
+ subscriptionStatus: 'canceled',
161
+ stripeSubscriptionId: null,
162
+ updatedAt: new Date()
163
+ })
164
+ .where(eq(users.stripeSubscriptionId, subscriptionId))
165
+ }
166
+ ```
167
+
168
+ ### Get User Invoices
169
+
170
+ ```javascript
171
+ import { desc } from 'drizzle-orm'
172
+
173
+ export async function getUserInvoices(userId, limit = 10) {
174
+ return db.select()
175
+ .from(invoices)
176
+ .where(eq(invoices.userId, userId))
177
+ .orderBy(desc(invoices.createdAt))
178
+ .limit(limit)
179
+ }
180
+ ```
181
+
182
+ ### Check Webhook Processed
183
+
184
+ ```javascript
185
+ export async function isWebhookProcessed(eventId) {
186
+ const [event] = await db.select()
187
+ .from(webhookEvents)
188
+ .where(eq(webhookEvents.stripeEventId, eventId))
189
+ .limit(1)
190
+
191
+ return !!event
192
+ }
193
+
194
+ export async function markWebhookProcessed(eventId, type) {
195
+ await db.insert(webhookEvents).values({
196
+ stripeEventId: eventId,
197
+ type,
198
+ processedAt: new Date()
199
+ })
200
+ }
201
+ ```
202
+
203
+ ## Migrations
204
+
205
+ ### Initial Migration
206
+
207
+ ```javascript
208
+ // database/migrations/0001_add_stripe_fields.js
209
+ import { sql } from 'drizzle-orm'
210
+
211
+ export async function up(db) {
212
+ await db.run(sql`
213
+ ALTER TABLE users ADD COLUMN stripe_customer_id TEXT UNIQUE;
214
+ ALTER TABLE users ADD COLUMN stripe_subscription_id TEXT UNIQUE;
215
+ ALTER TABLE users ADD COLUMN plan TEXT DEFAULT 'free';
216
+ ALTER TABLE users ADD COLUMN subscription_status TEXT;
217
+ ALTER TABLE users ADD COLUMN current_period_end INTEGER;
218
+ ALTER TABLE users ADD COLUMN cancel_at_period_end INTEGER DEFAULT 0;
219
+ `)
220
+ }
221
+
222
+ export async function down(db) {
223
+ // SQLite doesn't support DROP COLUMN easily
224
+ // Would need to recreate table
225
+ }
226
+ ```
227
+
228
+ ### Create Invoices Table
229
+
230
+ ```javascript
231
+ // database/migrations/0002_create_invoices.js
232
+ import { sql } from 'drizzle-orm'
233
+
234
+ export async function up(db) {
235
+ await db.run(sql`
236
+ CREATE TABLE invoices (
237
+ id TEXT PRIMARY KEY,
238
+ user_id TEXT NOT NULL REFERENCES users(id),
239
+ stripe_invoice_id TEXT UNIQUE NOT NULL,
240
+ amount INTEGER NOT NULL,
241
+ status TEXT NOT NULL,
242
+ url TEXT,
243
+ created_at INTEGER NOT NULL
244
+ )
245
+ `)
246
+ }
247
+
248
+ export async function down(db) {
249
+ await db.run(sql`DROP TABLE invoices`)
250
+ }
251
+ ```
252
+
253
+ ## Helper Service
254
+
255
+ ```javascript
256
+ // services/billing.js
257
+ import { db } from '../database'
258
+ import { users, invoices } from '../database/schema'
259
+ import { eq, and } from 'drizzle-orm'
260
+
261
+ export const billingService = {
262
+ // Check if user can access feature
263
+ async canAccessFeature(userId, requiredPlan) {
264
+ const user = await this.getUser(userId)
265
+
266
+ if (!user) return false
267
+
268
+ const planHierarchy = { free: 0, starter: 1, pro: 2, enterprise: 3 }
269
+ const userLevel = planHierarchy[user.plan] || 0
270
+ const requiredLevel = planHierarchy[requiredPlan] || 0
271
+
272
+ return userLevel >= requiredLevel
273
+ },
274
+
275
+ // Get user
276
+ async getUser(userId) {
277
+ const [user] = await db.select()
278
+ .from(users)
279
+ .where(eq(users.id, userId))
280
+ .limit(1)
281
+ return user
282
+ },
283
+
284
+ // Link Stripe customer
285
+ async linkStripeCustomer(userId, stripeCustomerId) {
286
+ await db.update(users)
287
+ .set({ stripeCustomerId, updatedAt: new Date() })
288
+ .where(eq(users.id, userId))
289
+ },
290
+
291
+ // Activate subscription
292
+ async activateSubscription(userId, { subscriptionId, plan, periodEnd }) {
293
+ await db.update(users)
294
+ .set({
295
+ stripeSubscriptionId: subscriptionId,
296
+ plan,
297
+ subscriptionStatus: 'active',
298
+ currentPeriodEnd: periodEnd,
299
+ cancelAtPeriodEnd: false,
300
+ updatedAt: new Date()
301
+ })
302
+ .where(eq(users.id, userId))
303
+ },
304
+
305
+ // Deactivate subscription
306
+ async deactivateSubscription(subscriptionId) {
307
+ await db.update(users)
308
+ .set({
309
+ plan: 'free',
310
+ subscriptionStatus: 'canceled',
311
+ stripeSubscriptionId: null,
312
+ updatedAt: new Date()
313
+ })
314
+ .where(eq(users.stripeSubscriptionId, subscriptionId))
315
+ },
316
+
317
+ // Record invoice
318
+ async recordInvoice(userId, stripeInvoice) {
319
+ await db.insert(invoices).values({
320
+ userId,
321
+ stripeInvoiceId: stripeInvoice.id,
322
+ amount: stripeInvoice.amount_paid,
323
+ status: stripeInvoice.status,
324
+ url: stripeInvoice.hosted_invoice_url
325
+ })
326
+ }
327
+ }
328
+ ```
329
+
330
+ ## Plan Feature Checks
331
+
332
+ ```javascript
333
+ // services/features.js
334
+ const planFeatures = {
335
+ free: {
336
+ projects: 1,
337
+ storage: 100 * 1024 * 1024, // 100MB
338
+ apiCalls: 100,
339
+ support: 'community'
340
+ },
341
+ starter: {
342
+ projects: 5,
343
+ storage: 1 * 1024 * 1024 * 1024, // 1GB
344
+ apiCalls: 1000,
345
+ support: 'email'
346
+ },
347
+ pro: {
348
+ projects: Infinity,
349
+ storage: 10 * 1024 * 1024 * 1024, // 10GB
350
+ apiCalls: 10000,
351
+ support: 'priority'
352
+ },
353
+ enterprise: {
354
+ projects: Infinity,
355
+ storage: Infinity,
356
+ apiCalls: Infinity,
357
+ support: 'dedicated'
358
+ }
359
+ }
360
+
361
+ export function getFeatureLimits(plan) {
362
+ return planFeatures[plan] || planFeatures.free
363
+ }
364
+
365
+ export function canCreateProject(plan, currentCount) {
366
+ const limits = getFeatureLimits(plan)
367
+ return currentCount < limits.projects
368
+ }
369
+ ```