webspresso 0.0.91 → 0.0.94

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/README.md +103 -2864
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/polar-migrate.js +50 -0
  9. package/bin/commands/seed.js +7 -1
  10. package/bin/commands/service.js +8 -2
  11. package/bin/commands/skill.js +7 -1
  12. package/bin/webspresso.js +4 -0
  13. package/core/auth/tokens.js +16 -0
  14. package/core/compileSchema.js +1 -5
  15. package/core/compression/compressible.js +17 -5
  16. package/core/compression/compression-stream.js +1 -1
  17. package/core/compression/middleware.js +9 -0
  18. package/core/compression/negotiate.js +23 -11
  19. package/core/kernel/app.js +2 -1
  20. package/core/kernel/index.js +5 -1
  21. package/core/orm/cache/fingerprint.js +23 -5
  22. package/core/orm/cache/layer.js +1 -1
  23. package/core/orm/eager-loader.js +54 -9
  24. package/core/orm/index.js +42 -3
  25. package/core/orm/model.js +11 -2
  26. package/core/orm/transaction.js +3 -1
  27. package/core/queue/adapters/database.js +46 -13
  28. package/core/queue/adapters/memory.js +21 -0
  29. package/core/queue/adapters/redis.js +21 -0
  30. package/core/queue/manager.js +46 -20
  31. package/core/ssr/stream.js +58 -14
  32. package/core/validation/index.js +70 -0
  33. package/index.d.ts +99 -5
  34. package/index.js +7 -1
  35. package/package.json +9 -1
  36. package/plugins/admin-panel/auth.js +10 -0
  37. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +6 -1
  38. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +18 -8
  39. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  40. package/plugins/admin-panel/client/parts/08-record-list.js +20 -2
  41. package/plugins/admin-panel/index.js +65 -6
  42. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  43. package/plugins/admin-panel/modules/dashboard.js +8 -1
  44. package/plugins/content/index.js +5 -3
  45. package/plugins/data-exchange/export-xlsx.js +28 -6
  46. package/plugins/data-exchange/import.js +120 -58
  47. package/plugins/data-exchange/index.js +5 -0
  48. package/plugins/data-exchange/parse-table.js +14 -5
  49. package/plugins/data-exchange/record-selection.js +2 -0
  50. package/plugins/index.js +6 -0
  51. package/plugins/mcp/auth.js +104 -0
  52. package/plugins/mcp/discovery.js +88 -0
  53. package/plugins/mcp/index.js +208 -0
  54. package/plugins/mcp/prompts.js +149 -0
  55. package/plugins/mcp/providers/orm.js +325 -0
  56. package/plugins/mcp/providers/services.js +190 -0
  57. package/plugins/mcp/providers/system.js +87 -0
  58. package/plugins/mcp/server.js +472 -0
  59. package/plugins/mcp/transports/sse.js +156 -0
  60. package/plugins/mcp/transports/stdio.js +108 -0
  61. package/plugins/polar/.env.example +12 -0
  62. package/plugins/polar/CHANGELOG.md +38 -0
  63. package/plugins/polar/README.md +158 -0
  64. package/plugins/polar/index.js +144 -0
  65. package/plugins/polar/src/api.js +54 -0
  66. package/plugins/polar/src/checkout.js +111 -0
  67. package/plugins/polar/src/config.js +88 -0
  68. package/plugins/polar/src/env.schema.js +36 -0
  69. package/plugins/polar/src/middleware.js +37 -0
  70. package/plugins/polar/src/migration.js +58 -0
  71. package/plugins/polar/src/nunjucks-filters.js +56 -0
  72. package/plugins/polar/src/portal.js +51 -0
  73. package/plugins/polar/src/rate-limit.js +70 -0
  74. package/plugins/polar/src/routes.js +212 -0
  75. package/plugins/polar/src/sync.js +138 -0
  76. package/plugins/polar/src/urls.js +117 -0
  77. package/plugins/polar/src/user-resolver.js +195 -0
  78. package/plugins/polar/src/webhooks.js +248 -0
  79. package/src/file-router.js +203 -125
  80. package/src/helpers.js +60 -13
  81. package/src/njk-frontmatter.js +24 -14
  82. package/src/server.js +30 -12
  83. package/src/services/discovery.js +2 -1
  84. package/src/services/registry.js +2 -0
  85. package/src/services/validator.js +2 -53
  86. package/templates/skills/webspresso-usage/REFERENCE-framework.md +7 -4
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const { parsePolarEnv } = require('./env.schema');
4
+
5
+ const DEFAULT_FIELDS = {
6
+ id: 'id',
7
+ email: 'email',
8
+ publicId: 'public_id',
9
+ tier: 'tier',
10
+ polarCustomerId: 'polar_customer_id',
11
+ polarSubscriptionId: 'polar_subscription_id',
12
+ polarStatus: 'polar_status',
13
+ polarCurrentPeriodEnd: 'polar_current_period_end',
14
+ polarCancelAtPeriodEnd: 'polar_cancel_at_period_end',
15
+ };
16
+
17
+ const DEFAULT_ROUTES = {
18
+ webhook: '/api/v1/polar/webhook',
19
+ checkout: '/settings/billing/checkout',
20
+ portal: '/settings/billing/portal',
21
+ status: '/api/v1/billing/status',
22
+ };
23
+
24
+ const DEFAULT_URLS = {
25
+ success: '/settings?tab=billing&upgraded=1',
26
+ return: '/settings?tab=billing',
27
+ alreadySubscribed: '/settings?tab=billing&success=already_pro',
28
+ login: '/login?error=required',
29
+ };
30
+
31
+ /**
32
+ * @param {Object} options - polarPlugin options
33
+ * @returns {import('./types').PolarConfig}
34
+ */
35
+ function resolvePolarConfig(options = {}) {
36
+ const env = parsePolarEnv({ ...process.env, ...options.env });
37
+
38
+ const fields = { ...DEFAULT_FIELDS, ...(options.fields || {}) };
39
+ const tierMapping = options.tierMapping || { pro: ['pro_monthly'], free: [] };
40
+ const plans = {
41
+ ...(options.plans || {}),
42
+ };
43
+
44
+ if (!plans.pro_monthly && env.POLAR_PRO_PRODUCT_ID) {
45
+ plans.pro_monthly = env.POLAR_PRO_PRODUCT_ID;
46
+ }
47
+ if (!plans.pro_yearly && env.POLAR_PRO_YEARLY_PRODUCT_ID) {
48
+ plans.pro_yearly = env.POLAR_PRO_YEARLY_PRODUCT_ID;
49
+ }
50
+
51
+ const proTiers = Object.keys(tierMapping).filter(
52
+ (t) => t !== 'free' && Array.isArray(tierMapping[t]) && tierMapping[t].length > 0
53
+ );
54
+
55
+ return {
56
+ db: options.db || null,
57
+ userModel: options.userModel || 'User',
58
+ userTable: options.userTable || 'users',
59
+ fields,
60
+ plans,
61
+ tierMapping,
62
+ proTiers,
63
+ proTierName: options.proTierName || proTiers[0] || 'pro',
64
+ freeTierName: options.freeTierName || 'free',
65
+ routes: { ...DEFAULT_ROUTES, ...(options.routes || {}) },
66
+ urls: { ...DEFAULT_URLS, ...(options.urls || {}) },
67
+ hooks: options.hooks || {},
68
+ requireAuth: options.requireAuth || null,
69
+ syncMiddleware: options.syncMiddleware || null,
70
+ rateLimit: options.rateLimit !== undefined ? options.rateLimit : true,
71
+ env,
72
+ accessToken: options.accessToken || env.POLAR_ACCESS_TOKEN || null,
73
+ webhookSecret: options.webhookSecret || env.POLAR_WEBHOOK_SECRET || null,
74
+ orgSlug: options.orgSlug || env.POLAR_ORG_SLUG || null,
75
+ checkoutUrl: options.checkoutUrl || env.POLAR_CHECKOUT_URL || null,
76
+ priceId: options.priceId || env.POLAR_PRO_PRICE_ID || null,
77
+ sandbox: options.sandbox !== undefined ? options.sandbox : env.POLAR_SANDBOX === true,
78
+ baseUrl: options.baseUrl || env.BASE_URL || null,
79
+ enabled: options.enabled !== false,
80
+ };
81
+ }
82
+
83
+ module.exports = {
84
+ DEFAULT_FIELDS,
85
+ DEFAULT_ROUTES,
86
+ DEFAULT_URLS,
87
+ resolvePolarConfig,
88
+ };
@@ -0,0 +1,36 @@
1
+ 'use strict';
2
+
3
+ const { z } = require('zod');
4
+
5
+ const polarEnvSchema = z.object({
6
+ POLAR_ACCESS_TOKEN: z.string().min(1).optional(),
7
+ POLAR_WEBHOOK_SECRET: z.string().min(1).optional(),
8
+ POLAR_PRO_PRODUCT_ID: z.string().optional(),
9
+ POLAR_PRO_YEARLY_PRODUCT_ID: z.string().optional(),
10
+ POLAR_PRO_PRICE_ID: z.string().optional(),
11
+ POLAR_ORG_SLUG: z.string().optional(),
12
+ POLAR_SANDBOX: z
13
+ .union([z.literal('true'), z.literal('false'), z.boolean()])
14
+ .optional()
15
+ .transform((v) => v === true || v === 'true'),
16
+ POLAR_CHECKOUT_URL: z.string().url().optional(),
17
+ BASE_URL: z.string().url().optional(),
18
+ });
19
+
20
+ /**
21
+ * Parse Polar-related env vars (process.env or overrides).
22
+ * @param {Record<string, string|undefined>} [source=process.env]
23
+ */
24
+ function parsePolarEnv(source = process.env) {
25
+ const parsed = polarEnvSchema.safeParse(source);
26
+ if (!parsed.success) {
27
+ const msg = parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
28
+ throw new Error(`Invalid Polar env: ${msg}`);
29
+ }
30
+ return parsed.data;
31
+ }
32
+
33
+ module.exports = {
34
+ polarEnvSchema,
35
+ parsePolarEnv,
36
+ };
@@ -0,0 +1,37 @@
1
+ 'use strict';
2
+
3
+ const { syncPolarBillingForUser } = require('./sync');
4
+
5
+ /**
6
+ * Optional middleware: sync billing from Polar on matching requests (webhook delay fallback).
7
+ * @param {import('./config').PolarConfig} config
8
+ * @param {Object} [opts]
9
+ * @param {(req: import('express').Request) => boolean} [opts.when]
10
+ */
11
+ function polarSyncMiddleware(config, opts = {}) {
12
+ const when = opts.when || config.syncMiddleware?.when || (() => false);
13
+
14
+ return async function polarSync(req, res, next) {
15
+ try {
16
+ if (!when(req) || !req.user) return next();
17
+
18
+ const knex = config.db?.knex || config.db;
19
+ if (!knex || !config.accessToken) return next();
20
+
21
+ const f = config.fields;
22
+ let user = req.user;
23
+ const dbUser = await knex(config.userTable).where(f.id, req.user[f.id] || req.user.id).first();
24
+ if (dbUser) user = dbUser;
25
+
26
+ req.user = await syncPolarBillingForUser(user, knex, config);
27
+ next();
28
+ } catch (err) {
29
+ console.error('[polar] Sync middleware error:', err.message);
30
+ next();
31
+ }
32
+ };
33
+ }
34
+
35
+ module.exports = {
36
+ polarSyncMiddleware,
37
+ };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { DEFAULT_FIELDS } = require('./config');
4
+
5
+ /**
6
+ * Generate idempotent Knex migration for Polar billing columns.
7
+ * @param {Object} [opts]
8
+ * @param {string} [opts.tableName='users']
9
+ * @param {Object} [opts.fields]
10
+ */
11
+ function generatePolarMigration(opts = {}) {
12
+ const tableName = opts.tableName || 'users';
13
+ const f = { ...DEFAULT_FIELDS, ...(opts.fields || {}) };
14
+
15
+ return `/**
16
+ * Migration: Add Polar.sh billing fields to ${tableName}
17
+ * Auto-generated by polar plugin — idempotent (safe to re-run)
18
+ */
19
+
20
+ exports.up = async function up(knex) {
21
+ const hasTable = await knex.schema.hasTable('${tableName}');
22
+ if (!hasTable) return;
23
+
24
+ const hasTier = await knex.schema.hasColumn('${tableName}', '${f.tier}');
25
+ if (!hasTier) {
26
+ await knex.schema.alterTable('${tableName}', (table) => {
27
+ table.string('${f.tier}', 32).notNullable().defaultTo('free').index();
28
+ table.string('${f.polarCustomerId}', 128).nullable().index();
29
+ table.string('${f.polarSubscriptionId}', 128).nullable().index();
30
+ table.string('${f.polarStatus}', 32).notNullable().defaultTo('none').index();
31
+ table.string('${f.polarCurrentPeriodEnd}', 64).nullable();
32
+ table.boolean('${f.polarCancelAtPeriodEnd}').notNullable().defaultTo(false);
33
+ });
34
+ }
35
+ };
36
+
37
+ exports.down = async function down(knex) {
38
+ const hasTable = await knex.schema.hasTable('${tableName}');
39
+ if (!hasTable) return;
40
+
41
+ const hasTier = await knex.schema.hasColumn('${tableName}', '${f.tier}');
42
+ if (hasTier) {
43
+ await knex.schema.alterTable('${tableName}', (table) => {
44
+ table.dropColumn('${f.tier}');
45
+ table.dropColumn('${f.polarCustomerId}');
46
+ table.dropColumn('${f.polarSubscriptionId}');
47
+ table.dropColumn('${f.polarStatus}');
48
+ table.dropColumn('${f.polarCurrentPeriodEnd}');
49
+ table.dropColumn('${f.polarCancelAtPeriodEnd}');
50
+ });
51
+ }
52
+ };
53
+ `;
54
+ }
55
+
56
+ module.exports = {
57
+ generatePolarMigration,
58
+ };
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ function dateLabel(value) {
4
+ if (!value) return '';
5
+ if (value instanceof Date) {
6
+ return Number.isNaN(value.getTime()) ? '' : value.toISOString().slice(0, 10);
7
+ }
8
+ const str = String(value);
9
+ if (/^\d{4}-\d{2}-\d{2}/.test(str)) return str.slice(0, 10);
10
+ const parsed = new Date(str);
11
+ return Number.isNaN(parsed.getTime()) ? str.slice(0, 10) : parsed.toISOString().slice(0, 10);
12
+ }
13
+
14
+ function createSafeTruncateFilter(originalTruncate) {
15
+ return function safeTruncate(value, length, end) {
16
+ if (value == null) return '';
17
+ if (value instanceof Date) {
18
+ return dateLabel(value);
19
+ }
20
+ if (typeof value === 'object') {
21
+ try {
22
+ return originalTruncate(JSON.stringify(value), length, end);
23
+ } catch {
24
+ return originalTruncate(String(value), length, end);
25
+ }
26
+ }
27
+ return originalTruncate(String(value), length, end);
28
+ };
29
+ }
30
+
31
+ function defaultTruncate(value, length = 20, end = '…') {
32
+ const str = value instanceof Date ? dateLabel(value) : String(value ?? '');
33
+ if (str.length <= length) return str;
34
+ return `${str.slice(0, length)}${end}`;
35
+ }
36
+
37
+ function registerNunjucksFilters(nunjucksEnv) {
38
+ nunjucksEnv.addFilter('dateLabel', dateLabel);
39
+
40
+ let existing = null;
41
+ if (typeof nunjucksEnv.getFilter === 'function') {
42
+ existing = nunjucksEnv.getFilter('truncate');
43
+ }
44
+
45
+ const truncateFn = typeof existing === 'function'
46
+ ? createSafeTruncateFilter(existing)
47
+ : defaultTruncate;
48
+
49
+ nunjucksEnv.addFilter('truncate', truncateFn, true);
50
+ }
51
+
52
+ module.exports = {
53
+ dateLabel,
54
+ createSafeTruncateFilter,
55
+ registerNunjucksFilters,
56
+ };
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const { polarApiRequest } = require('./api');
4
+ const { isValidPortalRedirectUrl } = require('./urls');
5
+
6
+ function getDefaultPortalUrl(config) {
7
+ if (!config.orgSlug) return null;
8
+ const host = config.sandbox ? 'https://sandbox.polar.sh' : 'https://polar.sh';
9
+ return `${host}/${config.orgSlug}/portal`;
10
+ }
11
+
12
+ async function getPolarPortalUrl(user, baseUrl, config) {
13
+ const defaultPortal = getDefaultPortalUrl(config);
14
+ const f = config.fields;
15
+
16
+ if (config.accessToken && user[f.polarCustomerId]) {
17
+ try {
18
+ const payload = { customer_id: user[f.polarCustomerId] };
19
+ if (baseUrl) {
20
+ const returnPath = config.urls.return.startsWith('http')
21
+ ? config.urls.return
22
+ : `${baseUrl}${config.urls.return}`;
23
+ payload.return_url = returnPath;
24
+ }
25
+
26
+ const data = await polarApiRequest('/customer-sessions/', {
27
+ method: 'POST',
28
+ body: payload,
29
+ accessToken: config.accessToken,
30
+ sandbox: config.sandbox,
31
+ });
32
+
33
+ if (data.customer_portal_url && isValidPortalRedirectUrl(data.customer_portal_url)) {
34
+ return data.customer_portal_url;
35
+ }
36
+ } catch (err) {
37
+ console.error('[polar] Portal API error:', err.message);
38
+ }
39
+ }
40
+
41
+ if (defaultPortal && isValidPortalRedirectUrl(defaultPortal)) {
42
+ return defaultPortal;
43
+ }
44
+
45
+ throw new Error('Polar customer portal is not configured. Set POLAR_ORG_SLUG to your Polar organization slug.');
46
+ }
47
+
48
+ module.exports = {
49
+ getDefaultPortalUrl,
50
+ getPolarPortalUrl,
51
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const DEFAULT_RATE_LIMITS = {
4
+ webhook: {
5
+ limit: 120,
6
+ windowMs: 60_000,
7
+ message: { error: 'Too many webhook requests' },
8
+ },
9
+ checkout: {
10
+ limit: 5,
11
+ windowMs: 60_000,
12
+ message: { error: 'Too many checkout attempts. Please try again later.' },
13
+ keyGenerator: (req) => `polar:checkout:${req.user?.id ?? req.ip}`,
14
+ },
15
+ portal: {
16
+ limit: 10,
17
+ windowMs: 60_000,
18
+ message: { error: 'Too many portal requests. Please try again later.' },
19
+ keyGenerator: (req) => `polar:portal:${req.user?.id ?? req.ip}`,
20
+ },
21
+ status: {
22
+ limit: 60,
23
+ windowMs: 60_000,
24
+ message: { error: 'Too many billing status requests' },
25
+ keyGenerator: (req) => `polar:status:${req.user?.id ?? req.ip}`,
26
+ },
27
+ };
28
+
29
+ /**
30
+ * Build per-route limiter middleware from rateLimitPlugin factory.
31
+ * @param {Object} ctx - plugin onRoutesReady context
32
+ * @param {import('./config').PolarConfig} config
33
+ * @returns {{ webhook?: Function, checkout?: Function, portal?: Function, status?: Function }}
34
+ */
35
+ function resolvePolarRateLimiters(ctx, config) {
36
+ const rlOpt = config.rateLimit;
37
+ if (rlOpt === false || rlOpt?.enabled === false) {
38
+ return {};
39
+ }
40
+
41
+ const factory = ctx.middlewares?.rateLimit;
42
+ if (typeof factory !== 'function') {
43
+ if (rlOpt === true || (rlOpt && typeof rlOpt === 'object')) {
44
+ console.warn(
45
+ '[polar] rateLimit is enabled but rateLimitPlugin is not loaded — add rateLimitPlugin() before polarPlugin in plugins[]'
46
+ );
47
+ }
48
+ return {};
49
+ }
50
+
51
+ const overrides = rlOpt === true ? {} : (rlOpt || {});
52
+ const routes = ['webhook', 'checkout', 'portal', 'status'];
53
+ const out = {};
54
+
55
+ for (const route of routes) {
56
+ if (overrides[route] === false) continue;
57
+ const opts = {
58
+ ...DEFAULT_RATE_LIMITS[route],
59
+ ...(typeof overrides[route] === 'object' ? overrides[route] : {}),
60
+ };
61
+ out[route] = factory(opts);
62
+ }
63
+
64
+ return out;
65
+ }
66
+
67
+ module.exports = {
68
+ DEFAULT_RATE_LIMITS,
69
+ resolvePolarRateLimiters,
70
+ };
@@ -0,0 +1,212 @@
1
+ 'use strict';
2
+
3
+ const { verifyPolarWebhook, handlePolarWebhookEvent } = require('./webhooks');
4
+ const { getPolarCheckoutUrl } = require('./checkout');
5
+ const { getPolarPortalUrl } = require('./portal');
6
+ const { syncPolarBillingForUser } = require('./sync');
7
+ const { userHasProTier } = require('./user-resolver');
8
+
9
+ function resolveKnex(db) {
10
+ return db?.knex || (typeof db?.raw === 'function' ? db : null);
11
+ }
12
+
13
+ function wantsJson(req) {
14
+ return req.xhr
15
+ || req.headers.accept?.includes('application/json')
16
+ || req.is('json');
17
+ }
18
+
19
+ function resolveBaseUrl(req, config) {
20
+ return config.baseUrl || `${req.protocol}://${req.get('host')}`;
21
+ }
22
+
23
+ function createInjectDb(db) {
24
+ return (req, res, next) => {
25
+ if (db && !req.db) req.db = db;
26
+ next();
27
+ };
28
+ }
29
+
30
+ function createWebhookHandler(config) {
31
+ return async function polarWebhookHandler(req, res) {
32
+ try {
33
+ const secret = config.webhookSecret;
34
+ if (!secret) {
35
+ return res.status(500).json({ error: 'POLAR_WEBHOOK_SECRET is not configured' });
36
+ }
37
+
38
+ const rawPayload = req.rawBody
39
+ ? (Buffer.isBuffer(req.rawBody) ? req.rawBody.toString('utf8') : String(req.rawBody))
40
+ : (typeof req.body === 'string' ? req.body : JSON.stringify(req.body));
41
+
42
+ if (!verifyPolarWebhook(rawPayload, req.headers, secret)) {
43
+ return res.status(401).json({ error: 'Invalid webhook signature', received: false });
44
+ }
45
+
46
+ const knex = resolveKnex(config.db);
47
+ if (!knex) {
48
+ return res.status(500).json({ error: 'Database unavailable' });
49
+ }
50
+
51
+ const event = typeof req.body === 'object' && req.body && !Buffer.isBuffer(req.body)
52
+ ? req.body
53
+ : JSON.parse(rawPayload);
54
+
55
+ const result = await handlePolarWebhookEvent(event, knex, config);
56
+ return res.status(200).json({
57
+ received: true,
58
+ processed: result.processed,
59
+ action: result.action,
60
+ userId: result.userId,
61
+ });
62
+ } catch (err) {
63
+ console.error('[polar] Webhook error:', err);
64
+ return res.status(400).json({ error: err.message || 'Webhook processing failed' });
65
+ }
66
+ };
67
+ }
68
+
69
+ function createCheckoutHandler(config) {
70
+ return async function polarCheckoutHandler(req, res) {
71
+ try {
72
+ if (!req.user) {
73
+ if (wantsJson(req)) return res.status(401).json({ error: 'Authentication required' });
74
+ return res.redirect(config.urls.login);
75
+ }
76
+
77
+ const knex = resolveKnex(config.db);
78
+ const f = config.fields;
79
+ let user = req.user;
80
+
81
+ if (knex) {
82
+ const dbUser = await knex(config.userTable).where(f.id, req.user[f.id] || req.user.id).first();
83
+ if (dbUser) user = dbUser;
84
+ if (config.accessToken) {
85
+ user = await syncPolarBillingForUser(user, knex, config);
86
+ }
87
+ }
88
+
89
+ if (userHasProTier(user, config)) {
90
+ const alreadyUrl = config.urls.alreadySubscribed.startsWith('http')
91
+ ? config.urls.alreadySubscribed
92
+ : config.urls.alreadySubscribed;
93
+ if (wantsJson(req)) {
94
+ return res.status(409).json({
95
+ error: 'You already have an active subscription.',
96
+ tier: user[f.tier],
97
+ polarStatus: user[f.polarStatus] || 'active',
98
+ });
99
+ }
100
+ return res.redirect(alreadyUrl);
101
+ }
102
+
103
+ const plan = req.body?.plan || req.query?.plan || 'pro_monthly';
104
+ const baseUrl = resolveBaseUrl(req, config);
105
+ const checkoutUrl = await getPolarCheckoutUrl({ user, plan, baseUrl, config });
106
+
107
+ if (wantsJson(req)) {
108
+ return res.json({ url: checkoutUrl });
109
+ }
110
+ return res.redirect(checkoutUrl);
111
+ } catch (err) {
112
+ console.error('[polar] Checkout error:', err.message);
113
+ if (wantsJson(req)) {
114
+ return res.status(503).json({ error: 'Checkout is temporarily unavailable. Please try again later.' });
115
+ }
116
+ const errUrl = config.urls.return.includes('?')
117
+ ? `${config.urls.return}&error=checkout_unavailable`
118
+ : `${config.urls.return}?error=checkout_unavailable`;
119
+ return res.redirect(errUrl);
120
+ }
121
+ };
122
+ }
123
+
124
+ function createPortalHandler(config) {
125
+ return async function polarPortalHandler(req, res) {
126
+ try {
127
+ if (!req.user) {
128
+ return res.redirect(config.urls.login);
129
+ }
130
+
131
+ const baseUrl = resolveBaseUrl(req, config);
132
+ const portalUrl = await getPolarPortalUrl(req.user, baseUrl, config);
133
+ return res.redirect(portalUrl);
134
+ } catch (err) {
135
+ console.error('[polar] Portal error:', err.message);
136
+ const errUrl = config.urls.return.includes('?')
137
+ ? `${config.urls.return}&error=portal_unavailable`
138
+ : `${config.urls.return}?error=portal_unavailable`;
139
+ return res.redirect(errUrl);
140
+ }
141
+ };
142
+ }
143
+
144
+ function createStatusHandler(config) {
145
+ return async function polarStatusHandler(req, res, next) {
146
+ try {
147
+ const knex = resolveKnex(config.db);
148
+ const f = config.fields;
149
+ let user = req.user;
150
+
151
+ if (!user) {
152
+ return res.status(401).json({ error: 'Unauthorized' });
153
+ }
154
+
155
+ if (knex) {
156
+ const dbUser = await knex(config.userTable).where(f.id, user[f.id] || user.id).first();
157
+ if (dbUser) user = dbUser;
158
+ if (config.accessToken) {
159
+ user = await syncPolarBillingForUser(user, knex, config);
160
+ }
161
+ }
162
+
163
+ const tier = user[f.tier] || config.freeTierName;
164
+ const isPro = userHasProTier(user, config);
165
+
166
+ return res.json({
167
+ userId: user[f.id],
168
+ publicId: user[f.publicId],
169
+ tier,
170
+ isPro,
171
+ polarStatus: user[f.polarStatus] || 'none',
172
+ polarCustomerId: user[f.polarCustomerId] || null,
173
+ polarSubscriptionId: user[f.polarSubscriptionId] || null,
174
+ polarCurrentPeriodEnd: user[f.polarCurrentPeriodEnd] || null,
175
+ polarCancelAtPeriodEnd: Boolean(user[f.polarCancelAtPeriodEnd]),
176
+ });
177
+ } catch (err) {
178
+ next(err);
179
+ }
180
+ };
181
+ }
182
+
183
+ function createRequireAuth(config, ctx) {
184
+ if (typeof config.requireAuth === 'function') {
185
+ return config.requireAuth;
186
+ }
187
+
188
+ const authFactory = ctx.middlewares?.auth;
189
+ if (typeof authFactory === 'function') {
190
+ return (req, res, next) => {
191
+ const isApi = req.path.startsWith('/api/') || wantsJson(req);
192
+ return authFactory({ api: isApi })(req, res, next);
193
+ };
194
+ }
195
+
196
+ return (req, res, next) => {
197
+ if (req.user) return next();
198
+ if (wantsJson(req) || req.path.startsWith('/api/')) {
199
+ return res.status(401).json({ error: 'Authentication required' });
200
+ }
201
+ return res.redirect(config.urls.login);
202
+ };
203
+ }
204
+
205
+ module.exports = {
206
+ createInjectDb,
207
+ createWebhookHandler,
208
+ createCheckoutHandler,
209
+ createPortalHandler,
210
+ createStatusHandler,
211
+ createRequireAuth,
212
+ };