webspresso 0.0.94 → 0.0.95
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/package.json +15 -4
- package/plugins/polar/CHANGELOG.md +18 -0
- package/plugins/polar/README.md +30 -7
- package/plugins/polar/index.js +6 -1
- package/plugins/polar/src/billing-hooks.js +51 -0
- package/plugins/polar/src/config.js +2 -0
- package/plugins/polar/src/rate-limit.js +96 -28
- package/plugins/polar/src/routes.js +3 -2
- package/plugins/polar/src/urls.js +26 -2
- package/plugins/polar/src/user-resolver.js +5 -1
- package/plugins/rate-limit/index.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.95",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -14,10 +14,21 @@
|
|
|
14
14
|
"default": "./core/errors/index.js"
|
|
15
15
|
},
|
|
16
16
|
"./plugins": "./plugins/index.js",
|
|
17
|
-
"./plugins
|
|
17
|
+
"./plugins/polar": "./plugins/polar/index.js",
|
|
18
|
+
"./plugins/polar/src/*": "./plugins/polar/src/*.js",
|
|
19
|
+
"./plugins/*": "./plugins/*/index.js",
|
|
18
20
|
"./realtime": "./core/realtime/index.js",
|
|
19
|
-
"./core
|
|
20
|
-
"./
|
|
21
|
+
"./core/auth": "./core/auth/index.js",
|
|
22
|
+
"./core/auth/*": "./core/auth/*.js",
|
|
23
|
+
"./core/errors": "./core/errors/index.js",
|
|
24
|
+
"./core/orm": "./core/orm/index.js",
|
|
25
|
+
"./core/queue": "./core/queue/index.js",
|
|
26
|
+
"./core/*.js": "./core/*.js",
|
|
27
|
+
"./core/*": "./core/*/index.js",
|
|
28
|
+
"./core/*/*.js": "./core/*/*.js",
|
|
29
|
+
"./core/*/*": "./core/*/*/index.js",
|
|
30
|
+
"./core/*/*/*.js": "./core/*/*/*.js",
|
|
31
|
+
"./src/*": "./src/*.js",
|
|
21
32
|
"./views/*": "./views/*",
|
|
22
33
|
"./templates/*": "./templates/*",
|
|
23
34
|
"./package.json": "./package.json"
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog — `webspresso/plugins/polar`
|
|
2
2
|
|
|
3
|
+
## 1.0.1 — Unreleased
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **package exports**: `webspresso/core/auth`, `webspresso/plugins/polar`, and `webspresso/plugins/polar/src/*` resolve without manual `/index.js` suffix
|
|
8
|
+
- **Rate limiting**: Polar limiters use `express-rate-limit` v8 `ipKeyGenerator` (fixes production startup `ValidationError`)
|
|
9
|
+
- **Webhook user lookup**: `metadata.user_id` supports nanoid/UUID string primary keys
|
|
10
|
+
- **CSP**: `polarCspDirectives()` includes `'self'` in `formAction` and `connectSrc`
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- `hooks.isPaidUser({ user, knex, config })` for external tier tables (legacy `(user, knex, config)` still supported)
|
|
15
|
+
- `syncBeforeCheckout` option (default `true`)
|
|
16
|
+
- `polar.api.syncBillingForAppUser(user, knex, { hooks })` for dashboard page-load sync
|
|
17
|
+
- `mergePolarCspDirectives(existing)` for Helmet CSP union merge
|
|
18
|
+
- Production checklist: `docs/guides/polar-production-checklist.md`
|
|
19
|
+
- Integration tests: rate-limit startup, IPv6 key validation, `isPaidUser` hook
|
|
20
|
+
|
|
3
21
|
## 1.0.0 — 2026-09-10
|
|
4
22
|
|
|
5
23
|
First production-ready release.
|
package/plugins/polar/README.md
CHANGED
|
@@ -58,10 +58,19 @@ That's it — routes, webhooks, checkout, portal, and status API are auto-mounte
|
|
|
58
58
|
| Method | Path | Auth | Behavior |
|
|
59
59
|
|--------|------|------|----------|
|
|
60
60
|
| POST | `/api/v1/polar/webhook` | HMAC signature | Process Polar events |
|
|
61
|
-
| GET/POST | `/settings/billing/checkout` | session/JWT | Sync → Polar checkout redirect |
|
|
61
|
+
| GET/POST | `/settings/billing/checkout` | session/JWT | Sync → Polar checkout redirect (see POST vs GET below) |
|
|
62
62
|
| GET | `/settings/billing/portal` | session/JWT | Customer portal redirect |
|
|
63
63
|
| GET | `/api/v1/billing/status` | session/JWT | Sync + JSON billing status |
|
|
64
64
|
|
|
65
|
+
## Checkout: POST vs GET
|
|
66
|
+
|
|
67
|
+
Both `/settings/billing/checkout` methods run the same handler:
|
|
68
|
+
|
|
69
|
+
| Method | Typical use | CSP note |
|
|
70
|
+
|--------|-------------|----------|
|
|
71
|
+
| **POST** | `<form method="post" action="…">` | Requires `form-action 'self'` (included in `polarCspDirectives()`) |
|
|
72
|
+
| **GET** | `<a href="…">Upgrade</a>` | No form submission — simpler when CSP is strict |
|
|
73
|
+
|
|
65
74
|
## Template usage
|
|
66
75
|
|
|
67
76
|
```njk
|
|
@@ -81,6 +90,7 @@ That's it — routes, webhooks, checkout, portal, and status API are auto-mounte
|
|
|
81
90
|
const polar = polarPlugin({ db });
|
|
82
91
|
polar.api.verifyPolarWebhook(raw, headers, secret);
|
|
83
92
|
polar.api.syncPolarBillingForUser(user, knex);
|
|
93
|
+
polar.api.syncBillingForAppUser(user, knex, { hooks }); // dashboard page-load sync
|
|
84
94
|
polar.api.getPolarCheckoutUrl({ user, plan: 'pro_monthly', baseUrl });
|
|
85
95
|
polar.api.handlePolarWebhookEvent(event, knex);
|
|
86
96
|
polar.api.generateMigration();
|
|
@@ -118,12 +128,20 @@ polarPlugin({ db, rateLimit: { checkout: { limit: 3 } } }); // override
|
|
|
118
128
|
|
|
119
129
|
## Optional hooks
|
|
120
130
|
|
|
131
|
+
Billing columns on `users` work out of the box. If your paid tier lives elsewhere (e.g. `subscriptions.plan_tier`), use hooks:
|
|
132
|
+
|
|
121
133
|
```js
|
|
122
134
|
polarPlugin({
|
|
123
135
|
db,
|
|
136
|
+
syncBeforeCheckout: false, // skip Polar API sync before redirect (default: true)
|
|
124
137
|
hooks: {
|
|
138
|
+
/** Override paid check at checkout (default: users.tier vs tierMapping) */
|
|
139
|
+
async isPaidUser({ user, knex }) {
|
|
140
|
+
const sub = await knex('subscriptions').where('user_id', user.id).first();
|
|
141
|
+
return sub?.plan_tier === 'pro';
|
|
142
|
+
},
|
|
125
143
|
onSubscriptionChange({ user, tier, polarStatus, subscription, knex, update }) {
|
|
126
|
-
//
|
|
144
|
+
// Sync external tables when webhooks fire
|
|
127
145
|
return knex('users').where('id', user.id).update(update);
|
|
128
146
|
},
|
|
129
147
|
},
|
|
@@ -133,23 +151,28 @@ polarPlugin({
|
|
|
133
151
|
});
|
|
134
152
|
```
|
|
135
153
|
|
|
154
|
+
Checkout metadata sends `user_id` as a string (supports nanoid/UUID primary keys, not only integers).
|
|
155
|
+
|
|
136
156
|
## CSP (Helmet)
|
|
137
157
|
|
|
158
|
+
`polarCspDirectives()` always includes `'self'` so local login/register/checkout forms keep working. Polar domains are added for hosted checkout redirects.
|
|
159
|
+
|
|
138
160
|
```js
|
|
139
|
-
const {
|
|
161
|
+
const { mergePolarCspDirectives } = require('webspresso/plugins/polar/src/urls');
|
|
140
162
|
|
|
141
163
|
createApp({
|
|
142
164
|
helmet: {
|
|
143
165
|
contentSecurityPolicy: {
|
|
144
|
-
directives: {
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
directives: mergePolarCspDirectives({
|
|
167
|
+
formAction: ["'self'"],
|
|
168
|
+
scriptSrc: ["'self'"],
|
|
169
|
+
}),
|
|
147
170
|
},
|
|
148
171
|
},
|
|
149
172
|
});
|
|
150
173
|
```
|
|
151
174
|
|
|
152
|
-
Or rely on the plugin's built-in `csp` export
|
|
175
|
+
Or rely on the plugin's built-in `csp` export — the plugin manager **unions** sources per directive (does not replace your Helmet config).
|
|
153
176
|
|
|
154
177
|
## Non-goals (v1)
|
|
155
178
|
|
package/plugins/polar/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const { verifyPolarWebhook, handlePolarWebhookEvent, createWebhookRawBodyMiddlew
|
|
|
12
12
|
const { getPolarCheckoutUrl } = require('./src/checkout');
|
|
13
13
|
const { getPolarPortalUrl } = require('./src/portal');
|
|
14
14
|
const { syncPolarBillingForUser } = require('./src/sync');
|
|
15
|
+
const { syncBillingForAppUser } = require('./src/billing-hooks');
|
|
15
16
|
const {
|
|
16
17
|
resolveUserFromPolarData,
|
|
17
18
|
buildCheckoutMetadata,
|
|
@@ -24,6 +25,7 @@ const {
|
|
|
24
25
|
isValidPortalRedirectUrl,
|
|
25
26
|
extractCheckoutUrlFromResponse,
|
|
26
27
|
polarCspDirectives,
|
|
28
|
+
mergePolarCspDirectives,
|
|
27
29
|
} = require('./src/urls');
|
|
28
30
|
const { generatePolarMigration } = require('./src/migration');
|
|
29
31
|
const { polarSyncMiddleware } = require('./src/middleware');
|
|
@@ -66,6 +68,8 @@ function polarPlugin(options = {}) {
|
|
|
66
68
|
getPolarCheckoutUrl: (params) => getPolarCheckoutUrl({ ...params, config: runtimeConfig }),
|
|
67
69
|
getPolarPortalUrl: (user, baseUrl) => getPolarPortalUrl(user, baseUrl, runtimeConfig),
|
|
68
70
|
syncPolarBillingForUser: (user, knex) => syncPolarBillingForUser(user, knex, runtimeConfig),
|
|
71
|
+
/** Dashboard/page-load sync — optional per-call hooks override */
|
|
72
|
+
syncBillingForAppUser: (user, knex, opts) => syncBillingForAppUser(user, knex, runtimeConfig, opts),
|
|
69
73
|
handlePolarWebhookEvent: (event, knex, hooks) => handlePolarWebhookEvent(event, knex, runtimeConfig, hooks),
|
|
70
74
|
resolveUserFromPolarData: (data, knex) => resolveUserFromPolarData(data, knex, runtimeConfig),
|
|
71
75
|
extractCheckoutUrlFromResponse: (data) => extractCheckoutUrlFromResponse(data, runtimeConfig.sandbox),
|
|
@@ -78,6 +82,7 @@ function polarPlugin(options = {}) {
|
|
|
78
82
|
generateMigration: (opts) => generatePolarMigration({ ...opts, fields: runtimeConfig.fields }),
|
|
79
83
|
polarSyncMiddleware: (opts) => polarSyncMiddleware(runtimeConfig, opts),
|
|
80
84
|
polarCspDirectives,
|
|
85
|
+
mergePolarCspDirectives,
|
|
81
86
|
createWebhookRawBodyMiddleware,
|
|
82
87
|
registerNunjucksFilters,
|
|
83
88
|
dateLabel,
|
|
@@ -89,7 +94,7 @@ function polarPlugin(options = {}) {
|
|
|
89
94
|
|
|
90
95
|
return {
|
|
91
96
|
name: 'polar',
|
|
92
|
-
version: '1.0.
|
|
97
|
+
version: '1.0.1',
|
|
93
98
|
description: 'Polar.sh subscription billing — checkout, portal, webhooks, and tier sync',
|
|
94
99
|
|
|
95
100
|
csp: polarCspDirectives(),
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { userHasProTier } = require('./user-resolver');
|
|
4
|
+
const { syncPolarBillingForUser } = require('./sync');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Invoke hooks.isPaidUser — supports ({ user, knex, config }) or legacy (user, knex, config).
|
|
8
|
+
* @param {Function} hook
|
|
9
|
+
* @param {Object} user
|
|
10
|
+
* @param {Object} knex
|
|
11
|
+
* @param {import('./config').PolarConfig} config
|
|
12
|
+
*/
|
|
13
|
+
async function invokeIsPaidUserHook(hook, user, knex, config) {
|
|
14
|
+
const ctx = { user, knex, config };
|
|
15
|
+
// Legacy (user, knex, config) when declared with 2+ params; default is ({ user, knex, config })
|
|
16
|
+
const result = hook.length >= 2
|
|
17
|
+
? await hook(user, knex, config)
|
|
18
|
+
: await hook(ctx);
|
|
19
|
+
return Boolean(result);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Paid-user check at checkout: hooks.isPaidUser override, else users.tier via userHasProTier.
|
|
24
|
+
*/
|
|
25
|
+
async function userIsPaid(user, knex, config) {
|
|
26
|
+
if (typeof config.hooks?.isPaidUser === 'function') {
|
|
27
|
+
return invokeIsPaidUserHook(config.hooks.isPaidUser, user, knex, config);
|
|
28
|
+
}
|
|
29
|
+
return userHasProTier(user, config);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* App-level billing sync for dashboard loaders — merges optional hook overrides per call.
|
|
34
|
+
* @param {Object} user
|
|
35
|
+
* @param {Object} knex
|
|
36
|
+
* @param {import('./config').PolarConfig} config
|
|
37
|
+
* @param {{ hooks?: Object }} [opts]
|
|
38
|
+
*/
|
|
39
|
+
async function syncBillingForAppUser(user, knex, config, opts = {}) {
|
|
40
|
+
const merged = {
|
|
41
|
+
...config,
|
|
42
|
+
hooks: { ...config.hooks, ...(opts.hooks || {}) },
|
|
43
|
+
};
|
|
44
|
+
return syncPolarBillingForUser(user, knex, merged);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
invokeIsPaidUserHook,
|
|
49
|
+
userIsPaid,
|
|
50
|
+
syncBillingForAppUser,
|
|
51
|
+
};
|
|
@@ -67,6 +67,8 @@ function resolvePolarConfig(options = {}) {
|
|
|
67
67
|
hooks: options.hooks || {},
|
|
68
68
|
requireAuth: options.requireAuth || null,
|
|
69
69
|
syncMiddleware: options.syncMiddleware || null,
|
|
70
|
+
/** Sync from Polar API before checkout redirect (default true). Set false to skip. */
|
|
71
|
+
syncBeforeCheckout: options.syncBeforeCheckout !== false,
|
|
70
72
|
rateLimit: options.rateLimit !== undefined ? options.rateLimit : true,
|
|
71
73
|
env,
|
|
72
74
|
accessToken: options.accessToken || env.POLAR_ACCESS_TOKEN || null,
|
|
@@ -1,36 +1,86 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
const { loadPeer } = require('../../rate-limit/index.js');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_IPV6_SUBNET = 56;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {Function} ipKeyGenerator
|
|
9
|
+
* @param {string} prefix
|
|
10
|
+
*/
|
|
11
|
+
function polarIpKey(ipKeyGenerator, prefix) {
|
|
12
|
+
return (req) => `${prefix}:${ipKeyGenerator(req.ip, DEFAULT_IPV6_SUBNET)}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prefer authenticated user id; fall back to IPv6-safe IP key (express-rate-limit v8).
|
|
17
|
+
* @param {Function} ipKeyGenerator
|
|
18
|
+
* @param {string} prefix
|
|
19
|
+
*/
|
|
20
|
+
function polarUserOrIpKey(ipKeyGenerator, prefix) {
|
|
21
|
+
const ipKey = polarIpKey(ipKeyGenerator, prefix);
|
|
22
|
+
return (req) => (req.user?.id != null ? `${prefix}:${req.user.id}` : ipKey(req));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {Function} ipKeyGenerator
|
|
27
|
+
*/
|
|
28
|
+
function buildDefaultRateLimits(ipKeyGenerator) {
|
|
29
|
+
return {
|
|
30
|
+
webhook: {
|
|
31
|
+
limit: 120,
|
|
32
|
+
windowMs: 60_000,
|
|
33
|
+
message: { error: 'Too many webhook requests' },
|
|
34
|
+
keyGenerator: polarIpKey(ipKeyGenerator, 'polar:webhook'),
|
|
35
|
+
},
|
|
36
|
+
checkout: {
|
|
37
|
+
limit: 5,
|
|
38
|
+
windowMs: 60_000,
|
|
39
|
+
message: { error: 'Too many checkout attempts. Please try again later.' },
|
|
40
|
+
keyGenerator: polarUserOrIpKey(ipKeyGenerator, 'polar:checkout'),
|
|
41
|
+
},
|
|
42
|
+
portal: {
|
|
43
|
+
limit: 10,
|
|
44
|
+
windowMs: 60_000,
|
|
45
|
+
message: { error: 'Too many portal requests. Please try again later.' },
|
|
46
|
+
keyGenerator: polarUserOrIpKey(ipKeyGenerator, 'polar:portal'),
|
|
47
|
+
},
|
|
48
|
+
status: {
|
|
49
|
+
limit: 60,
|
|
50
|
+
windowMs: 60_000,
|
|
51
|
+
message: { error: 'Too many billing status requests' },
|
|
52
|
+
keyGenerator: polarUserOrIpKey(ipKeyGenerator, 'polar:status'),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @type {ReturnType<typeof buildDefaultRateLimits>|null} */
|
|
58
|
+
let cachedDefaults = null;
|
|
59
|
+
|
|
60
|
+
function loadIpKeyGenerator() {
|
|
61
|
+
try {
|
|
62
|
+
const { ipKeyGenerator } = loadPeer();
|
|
63
|
+
return ipKeyGenerator;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getDefaultRateLimits() {
|
|
70
|
+
const ipKeyGenerator = loadIpKeyGenerator();
|
|
71
|
+
if (!ipKeyGenerator) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (!cachedDefaults) {
|
|
75
|
+
cachedDefaults = buildDefaultRateLimits(ipKeyGenerator);
|
|
76
|
+
}
|
|
77
|
+
return cachedDefaults;
|
|
78
|
+
}
|
|
28
79
|
|
|
29
80
|
/**
|
|
30
81
|
* Build per-route limiter middleware from rateLimitPlugin factory.
|
|
31
82
|
* @param {Object} ctx - plugin onRoutesReady context
|
|
32
83
|
* @param {import('./config').PolarConfig} config
|
|
33
|
-
* @returns {{ webhook?: Function, checkout?: Function, portal?: Function, status?: Function }}
|
|
34
84
|
*/
|
|
35
85
|
function resolvePolarRateLimiters(ctx, config) {
|
|
36
86
|
const rlOpt = config.rateLimit;
|
|
@@ -48,6 +98,14 @@ function resolvePolarRateLimiters(ctx, config) {
|
|
|
48
98
|
return {};
|
|
49
99
|
}
|
|
50
100
|
|
|
101
|
+
const defaults = getDefaultRateLimits();
|
|
102
|
+
if (!defaults) {
|
|
103
|
+
console.warn(
|
|
104
|
+
'[polar] rateLimit is enabled but express-rate-limit >= 8 is not installed — skipping polar rate limiters'
|
|
105
|
+
);
|
|
106
|
+
return {};
|
|
107
|
+
}
|
|
108
|
+
|
|
51
109
|
const overrides = rlOpt === true ? {} : (rlOpt || {});
|
|
52
110
|
const routes = ['webhook', 'checkout', 'portal', 'status'];
|
|
53
111
|
const out = {};
|
|
@@ -55,7 +113,7 @@ function resolvePolarRateLimiters(ctx, config) {
|
|
|
55
113
|
for (const route of routes) {
|
|
56
114
|
if (overrides[route] === false) continue;
|
|
57
115
|
const opts = {
|
|
58
|
-
...
|
|
116
|
+
...defaults[route],
|
|
59
117
|
...(typeof overrides[route] === 'object' ? overrides[route] : {}),
|
|
60
118
|
};
|
|
61
119
|
out[route] = factory(opts);
|
|
@@ -65,6 +123,16 @@ function resolvePolarRateLimiters(ctx, config) {
|
|
|
65
123
|
}
|
|
66
124
|
|
|
67
125
|
module.exports = {
|
|
68
|
-
|
|
126
|
+
loadIpKeyGenerator,
|
|
127
|
+
polarIpKey,
|
|
128
|
+
polarUserOrIpKey,
|
|
129
|
+
buildDefaultRateLimits,
|
|
130
|
+
getDefaultRateLimits,
|
|
69
131
|
resolvePolarRateLimiters,
|
|
132
|
+
/** @deprecated use getDefaultRateLimits() */
|
|
133
|
+
get DEFAULT_RATE_LIMITS() {
|
|
134
|
+
return getDefaultRateLimits() || buildDefaultRateLimits(
|
|
135
|
+
loadIpKeyGenerator() || ((ip) => String(ip))
|
|
136
|
+
);
|
|
137
|
+
},
|
|
70
138
|
};
|
|
@@ -5,6 +5,7 @@ const { getPolarCheckoutUrl } = require('./checkout');
|
|
|
5
5
|
const { getPolarPortalUrl } = require('./portal');
|
|
6
6
|
const { syncPolarBillingForUser } = require('./sync');
|
|
7
7
|
const { userHasProTier } = require('./user-resolver');
|
|
8
|
+
const { userIsPaid } = require('./billing-hooks');
|
|
8
9
|
|
|
9
10
|
function resolveKnex(db) {
|
|
10
11
|
return db?.knex || (typeof db?.raw === 'function' ? db : null);
|
|
@@ -81,12 +82,12 @@ function createCheckoutHandler(config) {
|
|
|
81
82
|
if (knex) {
|
|
82
83
|
const dbUser = await knex(config.userTable).where(f.id, req.user[f.id] || req.user.id).first();
|
|
83
84
|
if (dbUser) user = dbUser;
|
|
84
|
-
if (config.accessToken) {
|
|
85
|
+
if (config.accessToken && config.syncBeforeCheckout) {
|
|
85
86
|
user = await syncPolarBillingForUser(user, knex, config);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
|
|
89
|
-
if (
|
|
90
|
+
if (await userIsPaid(user, knex, config)) {
|
|
90
91
|
const alreadyUrl = config.urls.alreadySubscribed.startsWith('http')
|
|
91
92
|
? config.urls.alreadySubscribed
|
|
92
93
|
: config.urls.alreadySubscribed;
|
|
@@ -92,6 +92,7 @@ function extractCheckoutUrlFromResponse(data, sandbox = false) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
function polarCspDirectives(opts = {}) {
|
|
95
|
+
const self = "'self'";
|
|
95
96
|
const domains = opts.domains || [
|
|
96
97
|
'https://buy.polar.sh',
|
|
97
98
|
'https://sandbox-buy.polar.sh',
|
|
@@ -101,8 +102,30 @@ function polarCspDirectives(opts = {}) {
|
|
|
101
102
|
'https://sandbox-api.polar.sh',
|
|
102
103
|
];
|
|
103
104
|
return {
|
|
104
|
-
formAction: domains,
|
|
105
|
-
connectSrc: domains,
|
|
105
|
+
formAction: [self, ...domains],
|
|
106
|
+
connectSrc: [self, ...domains],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Union Polar CSP sources into an existing Helmet/plugin CSP config (does not replace).
|
|
112
|
+
* @param {Object} [existing]
|
|
113
|
+
* @param {Object} [opts] - passed to polarCspDirectives
|
|
114
|
+
*/
|
|
115
|
+
function mergePolarCspDirectives(existing = {}, opts = {}) {
|
|
116
|
+
const polar = polarCspDirectives(opts);
|
|
117
|
+
const mergeList = (key) => {
|
|
118
|
+
const base = existing[key];
|
|
119
|
+
const items = [
|
|
120
|
+
...(Array.isArray(base) ? base : base ? [base] : []),
|
|
121
|
+
...polar[key],
|
|
122
|
+
];
|
|
123
|
+
return [...new Set(items)];
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
...existing,
|
|
127
|
+
formAction: mergeList('formAction'),
|
|
128
|
+
connectSrc: mergeList('connectSrc'),
|
|
106
129
|
};
|
|
107
130
|
}
|
|
108
131
|
|
|
@@ -114,4 +137,5 @@ module.exports = {
|
|
|
114
137
|
isValidPortalRedirectUrl,
|
|
115
138
|
extractCheckoutUrlFromResponse,
|
|
116
139
|
polarCspDirectives,
|
|
140
|
+
mergePolarCspDirectives,
|
|
117
141
|
};
|
|
@@ -113,7 +113,11 @@ async function resolveUserFromPolarData(data, knex, config) {
|
|
|
113
113
|
const table = config.userTable;
|
|
114
114
|
|
|
115
115
|
if (metadata.user_id) {
|
|
116
|
-
const
|
|
116
|
+
const uid = String(metadata.user_id).trim();
|
|
117
|
+
let user = await knex(table).where(f.id, uid).first();
|
|
118
|
+
if (!user && /^\d+$/.test(uid)) {
|
|
119
|
+
user = await knex(table).where(f.id, Number(uid)).first();
|
|
120
|
+
}
|
|
117
121
|
if (user) return user;
|
|
118
122
|
}
|
|
119
123
|
|