webspresso 0.0.102 → 0.0.104
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/.agents/ORM.md +13 -4
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/README.md +2 -2
- package/bin/commands/dev.js +3 -2
- package/bin/utils/orm-map-html.js +2 -8
- package/core/auth/jwt.js +12 -21
- package/core/auth/oauth.js +38 -14
- package/core/auth/totp.js +3 -15
- package/core/auth/user-agent.js +59 -60
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/content/renderer.js +1 -13
- package/core/errors/middleware.js +2 -8
- package/core/events/dispatcher.js +56 -33
- package/core/forms/form-builder.js +1 -9
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/index.js +5 -0
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/shutdown/shutdown-manager.js +16 -16
- package/core/utils/crypto.js +73 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/fetch.js +120 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +33 -6
- package/index.js +10 -0
- package/package.json +2 -1
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +1 -75
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/api-key/crypto.js +1 -24
- package/plugins/audit-log/api-handlers.js +11 -1
- package/plugins/audit-log/middleware.js +50 -6
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +3 -15
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/core.js +34 -4
- package/plugins/file-manager/index.js +11 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +9 -2
- package/plugins/polar/src/webhooks.js +3 -8
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +51 -3
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +265 -63
- package/plugins/sitemap.js +18 -15
- package/plugins/swagger.js +9 -14
- package/plugins/webhooks/crypto.js +1 -18
- package/plugins/webhooks/dispatcher.js +6 -1
- package/plugins/webhooks/matcher.js +25 -10
- package/plugins/webhooks/sender.js +32 -2
- package/src/api/api-loader.js +1 -1
- package/src/dev-reload/index.js +47 -6
- package/src/discovery/scan-routes.js +14 -4
- package/src/file-router.js +62 -6
- package/src/helpers.js +157 -32
- package/src/plugin-manager.js +34 -13
- package/src/routing/route-table.js +14 -4
- package/src/server.js +25 -9
- package/src/services/validator.js +57 -32
|
@@ -173,10 +173,12 @@ function generateFileManagerComponent(options = {}) {
|
|
|
173
173
|
|
|
174
174
|
var uploadUrl = (window.__ADMIN_PATH__ || '/_admin') + '/api' + API + '/upload?path=' + encodeURIComponent(currentPath);
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
var fetchFn = (typeof window !== 'undefined' && window.Admin && window.Admin.fetch) ? window.Admin.fetch : fetch;
|
|
177
|
+
fetchFn(uploadUrl, {
|
|
177
178
|
method: 'POST',
|
|
178
179
|
body: fd,
|
|
179
180
|
credentials: 'include',
|
|
181
|
+
timeout: 60000,
|
|
180
182
|
}).then(function(r) { return r.json(); }).then(function(res) {
|
|
181
183
|
uploading = false;
|
|
182
184
|
uploadStatus = '';
|
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const multer = require('multer');
|
|
7
|
-
const
|
|
8
|
-
listFiles,
|
|
9
|
-
createDirectory,
|
|
10
|
-
renameItem,
|
|
11
|
-
deleteItem,
|
|
12
|
-
moveItem,
|
|
13
|
-
saveUploadedFile,
|
|
14
|
-
calculateStorageUsage,
|
|
15
|
-
} = require('./core');
|
|
7
|
+
const core = require('./core');
|
|
16
8
|
|
|
17
9
|
/**
|
|
18
10
|
* Create Multer middleware for in-memory upload parsing
|
|
@@ -71,7 +63,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
71
63
|
const permissions = options.permissions || null;
|
|
72
64
|
const securityOpts = options.security || {};
|
|
73
65
|
|
|
74
|
-
const parseUpload = createUploadMiddleware(maxUploadBytes);
|
|
66
|
+
const parseUpload = options.uploadMiddleware || createUploadMiddleware(maxUploadBytes);
|
|
75
67
|
|
|
76
68
|
return {
|
|
77
69
|
async list(req, res) {
|
|
@@ -88,7 +80,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
88
80
|
const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
|
|
89
81
|
const perPage = req.query.perPage ? parseInt(req.query.perPage, 10) : undefined;
|
|
90
82
|
|
|
91
|
-
const result = await listFiles(baseDir, relPath, {
|
|
83
|
+
const result = await core.listFiles(baseDir, relPath, {
|
|
92
84
|
search,
|
|
93
85
|
type,
|
|
94
86
|
sort,
|
|
@@ -115,7 +107,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
115
107
|
if (!name) {
|
|
116
108
|
return res.status(400).json({ success: false, error: 'Directory name is required' });
|
|
117
109
|
}
|
|
118
|
-
const result = await createDirectory(baseDir, relPath, name);
|
|
110
|
+
const result = await core.createDirectory(baseDir, relPath, name);
|
|
119
111
|
return res.status(201).json({ success: true, data: result });
|
|
120
112
|
} catch (err) {
|
|
121
113
|
const status = err.statusCode || err.status || 400;
|
|
@@ -133,7 +125,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
133
125
|
if (!itemPath || !newName) {
|
|
134
126
|
return res.status(400).json({ success: false, error: 'Path and newName are required' });
|
|
135
127
|
}
|
|
136
|
-
const result = await renameItem(baseDir, itemPath, newName);
|
|
128
|
+
const result = await core.renameItem(baseDir, itemPath, newName);
|
|
137
129
|
return res.json({ success: true, data: result });
|
|
138
130
|
} catch (err) {
|
|
139
131
|
const status = err.statusCode || err.status || 400;
|
|
@@ -151,7 +143,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
151
143
|
if (!source) {
|
|
152
144
|
return res.status(400).json({ success: false, error: 'Source path is required' });
|
|
153
145
|
}
|
|
154
|
-
const result = await moveItem(baseDir, source, destination);
|
|
146
|
+
const result = await core.moveItem(baseDir, source, destination);
|
|
155
147
|
return res.json({ success: true, data: result });
|
|
156
148
|
} catch (err) {
|
|
157
149
|
const status = err.statusCode || err.status || 400;
|
|
@@ -169,7 +161,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
169
161
|
if (!itemPath) {
|
|
170
162
|
return res.status(400).json({ success: false, error: 'Path is required' });
|
|
171
163
|
}
|
|
172
|
-
const result = await deleteItem(baseDir, itemPath);
|
|
164
|
+
const result = await core.deleteItem(baseDir, itemPath);
|
|
173
165
|
return res.json({ success: true, data: result });
|
|
174
166
|
} catch (err) {
|
|
175
167
|
const status = err.statusCode || err.status || 400;
|
|
@@ -199,7 +191,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
199
191
|
|
|
200
192
|
// Storage Quota Check
|
|
201
193
|
if (maxTotalStorageBytes && typeof maxTotalStorageBytes === 'number') {
|
|
202
|
-
const currentUsage = await calculateStorageUsage(baseDir);
|
|
194
|
+
const currentUsage = await core.calculateStorageUsage(baseDir);
|
|
203
195
|
const incomingBytes = files.reduce((acc, f) => acc + (f.size || f.buffer?.length || 0), 0);
|
|
204
196
|
if (currentUsage + incomingBytes > maxTotalStorageBytes) {
|
|
205
197
|
return res.status(507).json({
|
|
@@ -229,7 +221,7 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
229
221
|
try {
|
|
230
222
|
const uploaded = [];
|
|
231
223
|
for (const file of files) {
|
|
232
|
-
const saved = await saveUploadedFile(
|
|
224
|
+
const saved = await core.saveUploadedFile(
|
|
233
225
|
baseDir,
|
|
234
226
|
targetRelPath,
|
|
235
227
|
file.buffer,
|
|
@@ -257,5 +249,6 @@ function createFileManagerApiHandlers(options = {}) {
|
|
|
257
249
|
|
|
258
250
|
module.exports = {
|
|
259
251
|
createFileManagerApiHandlers,
|
|
252
|
+
createUploadMiddleware,
|
|
260
253
|
isActionAllowed,
|
|
261
254
|
};
|
|
@@ -137,7 +137,12 @@ function resolveSafePath(baseDir, relPath = '') {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
// Prevent URL decoded traversal attempts
|
|
140
|
-
|
|
140
|
+
let decodedPath;
|
|
141
|
+
try {
|
|
142
|
+
decodedPath = decodeURIComponent(relPath);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
throw new ValidationError('Malformed URI path sequence', 400);
|
|
145
|
+
}
|
|
141
146
|
if (decodedPath.includes('\0')) {
|
|
142
147
|
throw new SecurityError('Null byte detected in path', 400);
|
|
143
148
|
}
|
|
@@ -237,7 +242,7 @@ function buildBreadcrumbs(relPath) {
|
|
|
237
242
|
*/
|
|
238
243
|
async function listFiles(baseDir, relPath = '', options = {}) {
|
|
239
244
|
const targetDir = resolveSafePath(baseDir, relPath);
|
|
240
|
-
const normalizedRel = path.relative(path.resolve(baseDir), targetDir).replace(/\\/g, '');
|
|
245
|
+
const normalizedRel = path.relative(path.resolve(baseDir), targetDir).replace(/\\/g, '/');
|
|
241
246
|
const publicBase = normalizePublicBase(options.publicBasePath);
|
|
242
247
|
|
|
243
248
|
let dirents;
|
|
@@ -456,6 +461,7 @@ async function deleteItem(baseDir, itemRelPath) {
|
|
|
456
461
|
await fs.unlink(targetFullPath);
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
invalidateStorageCache(baseDir);
|
|
459
465
|
return { success: true, path: itemRelPath };
|
|
460
466
|
}
|
|
461
467
|
|
|
@@ -487,12 +493,33 @@ async function moveItem(baseDir, sourceRelPath, destDirRelPath) {
|
|
|
487
493
|
return { success: true, oldPath: sourceRelPath, newPath: newRelPath };
|
|
488
494
|
}
|
|
489
495
|
|
|
496
|
+
const storageCache = new Map();
|
|
497
|
+
const STORAGE_CACHE_TTL = 15000; // 15 seconds
|
|
498
|
+
|
|
499
|
+
function invalidateStorageCache(baseDir) {
|
|
500
|
+
if (baseDir) {
|
|
501
|
+
storageCache.delete(path.resolve(baseDir));
|
|
502
|
+
} else {
|
|
503
|
+
storageCache.clear();
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
490
507
|
/**
|
|
491
508
|
* Calculate total disk usage in bytes under baseDir
|
|
492
509
|
* @param {string} baseDir
|
|
510
|
+
* @param {Object} [options]
|
|
511
|
+
* @param {boolean} [options.forceRefresh=false]
|
|
493
512
|
* @returns {Promise<number>}
|
|
494
513
|
*/
|
|
495
|
-
async function calculateStorageUsage(baseDir) {
|
|
514
|
+
async function calculateStorageUsage(baseDir, options = {}) {
|
|
515
|
+
const resolvedBase = path.resolve(baseDir);
|
|
516
|
+
const now = Date.now();
|
|
517
|
+
const cached = storageCache.get(resolvedBase);
|
|
518
|
+
|
|
519
|
+
if (!options.forceRefresh && cached && (now - cached.timestamp) < STORAGE_CACHE_TTL) {
|
|
520
|
+
return cached.bytes;
|
|
521
|
+
}
|
|
522
|
+
|
|
496
523
|
let totalBytes = 0;
|
|
497
524
|
async function scan(dir) {
|
|
498
525
|
try {
|
|
@@ -509,7 +536,8 @@ async function calculateStorageUsage(baseDir) {
|
|
|
509
536
|
}
|
|
510
537
|
} catch (e) {}
|
|
511
538
|
}
|
|
512
|
-
await scan(
|
|
539
|
+
await scan(resolvedBase);
|
|
540
|
+
storageCache.set(resolvedBase, { bytes: totalBytes, timestamp: now });
|
|
513
541
|
return totalBytes;
|
|
514
542
|
}
|
|
515
543
|
|
|
@@ -568,6 +596,7 @@ async function saveUploadedFile(baseDir, targetDirRelPath, buffer, originalName,
|
|
|
568
596
|
resolveSafePath(baseDir, path.relative(baseDir, targetPath));
|
|
569
597
|
|
|
570
598
|
await fs.writeFile(targetPath, finalBuffer);
|
|
599
|
+
invalidateStorageCache(baseDir);
|
|
571
600
|
|
|
572
601
|
const relFilePath = path.relative(path.resolve(baseDir), targetPath).replace(/\\/g, '/');
|
|
573
602
|
const publicBase = normalizePublicBase(publicBasePath);
|
|
@@ -598,4 +627,5 @@ module.exports = {
|
|
|
598
627
|
moveItem,
|
|
599
628
|
saveUploadedFile,
|
|
600
629
|
calculateStorageUsage,
|
|
630
|
+
invalidateStorageCache,
|
|
601
631
|
};
|
|
@@ -114,15 +114,17 @@ function fileManagerPlugin(options = {}) {
|
|
|
114
114
|
// so window.__openFileManagerPicker is available across all admin pages (models, records, rich-text)
|
|
115
115
|
if (typeof adminApi.getRegistry === 'function') {
|
|
116
116
|
const registry = adminApi.getRegistry();
|
|
117
|
-
registry
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
117
|
+
if (registry) {
|
|
118
|
+
registry.configure({
|
|
119
|
+
fileManager: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
apiPrefix,
|
|
122
|
+
publicBasePath,
|
|
123
|
+
pageTitle,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
registry.registerClientComponent('files', componentCode);
|
|
127
|
+
}
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
adminApi.registerModule({
|
|
@@ -24,6 +24,16 @@ const DANGEROUS_EXTENSIONS = new Set([
|
|
|
24
24
|
'htaccess', 'htpasswd', 'ini', 'env', 'config', 'conf', 'passwd', 'shadow',
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Common safe extensions allowed when passed as raw extensions (without filename dot)
|
|
29
|
+
*/
|
|
30
|
+
const COMMON_SAFE_EXTENSIONS = new Set([
|
|
31
|
+
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp', 'ico', 'tiff',
|
|
32
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf',
|
|
33
|
+
'txt', 'csv', 'md', 'json', 'xml', 'html', 'css', 'js', 'ts', 'jsx', 'tsx',
|
|
34
|
+
'sql', 'yaml', 'yml', 'zip', 'tar', 'gz', 'mp3', 'wav', 'mp4', 'mov', 'webm',
|
|
35
|
+
]);
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Known magic numbers table for high-speed signature verification.
|
|
29
39
|
* Only validates when extension matches known binary format.
|
|
@@ -118,9 +128,8 @@ function validateSafeExtension(filename) {
|
|
|
118
128
|
throw new ValidationError('Filename must have a valid extension');
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
// Check all
|
|
122
|
-
for (
|
|
123
|
-
const segment = parts[i];
|
|
131
|
+
// Check all segments against dangerous extensions to block double extensions and dangerous dotfiles (e.g. .env, payload.php.jpg)
|
|
132
|
+
for (const segment of parts) {
|
|
124
133
|
if (DANGEROUS_EXTENSIONS.has(segment)) {
|
|
125
134
|
throw new SecurityError(
|
|
126
135
|
`Dangerous extension or double-extension detected: .${segment}`,
|
|
@@ -129,11 +138,14 @@ function validateSafeExtension(filename) {
|
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
if (parts.length < 2 && !cleanName.startsWith('.')) {
|
|
142
|
+
if (COMMON_SAFE_EXTENSIONS.has(parts[0])) {
|
|
143
|
+
return parts[0];
|
|
144
|
+
}
|
|
145
|
+
throw new ValidationError('Filename must have a valid extension');
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const finalExt = parts[parts.length - 1];
|
|
137
149
|
return finalExt;
|
|
138
150
|
}
|
|
139
151
|
|
package/plugins/mcp/auth.js
CHANGED
|
@@ -6,19 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const crypto = require('crypto');
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
* Constant-time string comparison to avoid timing attacks
|
|
11
|
-
* @param {string} a
|
|
12
|
-
* @param {string} b
|
|
13
|
-
* @returns {boolean}
|
|
14
|
-
*/
|
|
15
|
-
function safeEqual(a, b) {
|
|
16
|
-
if (typeof a !== 'string' || typeof b !== 'string') return false;
|
|
17
|
-
const bufA = Buffer.from(a);
|
|
18
|
-
const bufB = Buffer.from(b);
|
|
19
|
-
if (bufA.length !== bufB.length) return false;
|
|
20
|
-
return crypto.timingSafeEqual(bufA, bufB);
|
|
21
|
-
}
|
|
9
|
+
const { safeCompare: safeEqual } = require('../../core/utils/crypto');
|
|
22
10
|
|
|
23
11
|
/**
|
|
24
12
|
* Check if incoming request is from localhost
|
package/plugins/mcp/index.js
CHANGED
|
@@ -52,6 +52,7 @@ function mcpPlugin(options = {}) {
|
|
|
52
52
|
} = options;
|
|
53
53
|
|
|
54
54
|
let serverInstance = null;
|
|
55
|
+
let sseTransport = null;
|
|
55
56
|
|
|
56
57
|
return {
|
|
57
58
|
name: 'mcp',
|
|
@@ -63,6 +64,19 @@ function mcpPlugin(options = {}) {
|
|
|
63
64
|
registerTool: (tool) => serverInstance && serverInstance.registerTool(tool),
|
|
64
65
|
registerResource: (res) => serverInstance && serverInstance.registerResource(res),
|
|
65
66
|
registerPrompt: (prompt) => serverInstance && serverInstance.registerPrompt(prompt),
|
|
67
|
+
closeTransport: () => sseTransport && sseTransport.close && sseTransport.close(),
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
dispose() {
|
|
71
|
+
if (sseTransport && typeof sseTransport.close === 'function') {
|
|
72
|
+
sseTransport.close();
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
shutdown() {
|
|
77
|
+
if (sseTransport && typeof sseTransport.close === 'function') {
|
|
78
|
+
sseTransport.close();
|
|
79
|
+
}
|
|
66
80
|
},
|
|
67
81
|
|
|
68
82
|
register(ctx) {
|
|
@@ -180,12 +194,20 @@ function mcpPlugin(options = {}) {
|
|
|
180
194
|
const authMiddleware =
|
|
181
195
|
auth.token || auth.localhostOnly || auth.verify ? createMcpAuthMiddleware(auth) : null;
|
|
182
196
|
|
|
183
|
-
mountSseTransport({
|
|
197
|
+
sseTransport = mountSseTransport({
|
|
184
198
|
app,
|
|
185
199
|
server: serverInstance,
|
|
186
200
|
path,
|
|
187
201
|
authMiddleware,
|
|
188
202
|
});
|
|
203
|
+
|
|
204
|
+
if (typeof ctx.onDispose === 'function') {
|
|
205
|
+
ctx.onDispose(() => {
|
|
206
|
+
if (sseTransport && typeof sseTransport.close === 'function') {
|
|
207
|
+
sseTransport.close();
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
189
211
|
},
|
|
190
212
|
};
|
|
191
213
|
}
|
|
@@ -150,7 +150,16 @@ function mountSseTransport(options) {
|
|
|
150
150
|
}
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
const close = () => {
|
|
154
|
+
for (const [id, session] of sessions.entries()) {
|
|
155
|
+
try {
|
|
156
|
+
session.res.end();
|
|
157
|
+
} catch {}
|
|
158
|
+
}
|
|
159
|
+
sessions.clear();
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return { sessions, close };
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
module.exports = {
|
|
@@ -8,12 +8,13 @@ function generateOrmCacheAdminComponent() {
|
|
|
8
8
|
(function() {
|
|
9
9
|
|
|
10
10
|
function ormCacheApi(method, path, body) {
|
|
11
|
-
var opts = { method: method, credentials: 'same-origin' };
|
|
11
|
+
var opts = { method: method, credentials: 'same-origin', timeout: 30000 };
|
|
12
12
|
if (body !== undefined) {
|
|
13
13
|
opts.headers = { 'Content-Type': 'application/json' };
|
|
14
14
|
opts.body = JSON.stringify(body);
|
|
15
15
|
}
|
|
16
|
-
|
|
16
|
+
var fetchFn = (typeof window !== 'undefined' && window.Admin && window.Admin.fetch) ? window.Admin.fetch : fetch;
|
|
17
|
+
return fetchFn('/_admin/api/orm-cache' + path, opts).then(function(r) {
|
|
17
18
|
return r.json().then(function(j) {
|
|
18
19
|
if (!r.ok) throw new Error(j.error || r.statusText);
|
|
19
20
|
return j;
|
package/plugins/polar/src/api.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { fetchWithTimeout } = require('../../../core/utils/fetch');
|
|
4
|
+
|
|
3
5
|
function getPolarApiBase(sandbox) {
|
|
4
6
|
return sandbox ? 'https://sandbox-api.polar.sh/v1' : 'https://api.polar.sh/v1';
|
|
5
7
|
}
|
|
@@ -11,14 +13,17 @@ function getPolarApiBase(sandbox) {
|
|
|
11
13
|
* @param {Object} [opts.body]
|
|
12
14
|
* @param {string} opts.accessToken
|
|
13
15
|
* @param {boolean} [opts.sandbox=false]
|
|
16
|
+
* @param {number} [opts.timeoutMs=15000]
|
|
17
|
+
* @param {AbortSignal} [opts.signal]
|
|
14
18
|
*/
|
|
15
|
-
async function polarApiRequest(path, { method = 'GET', body, accessToken, sandbox = false } = {}) {
|
|
19
|
+
async function polarApiRequest(path, { method = 'GET', body, accessToken, sandbox = false, timeoutMs = 15000, signal } = {}) {
|
|
16
20
|
if (!accessToken) {
|
|
17
21
|
throw new Error('POLAR_ACCESS_TOKEN is not configured');
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
21
|
-
|
|
25
|
+
|
|
26
|
+
const res = await fetchWithTimeout(`${getPolarApiBase(sandbox)}${normalizedPath}`, {
|
|
22
27
|
method,
|
|
23
28
|
headers: {
|
|
24
29
|
Authorization: `Bearer ${accessToken}`,
|
|
@@ -26,6 +31,8 @@ async function polarApiRequest(path, { method = 'GET', body, accessToken, sandbo
|
|
|
26
31
|
Accept: 'application/json',
|
|
27
32
|
},
|
|
28
33
|
body: body ? JSON.stringify(body) : undefined,
|
|
34
|
+
timeout: timeoutMs,
|
|
35
|
+
signal,
|
|
29
36
|
});
|
|
30
37
|
|
|
31
38
|
const raw = await res.text();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
|
+
const { safeCompare } = require('../../../core/utils/crypto');
|
|
4
5
|
const { syncPolarBillingForUser, applyPolarProSubscription } = require('./sync');
|
|
5
6
|
const {
|
|
6
7
|
collectPolarMetadata,
|
|
@@ -46,14 +47,8 @@ function verifyPolarWebhook(rawPayload, headers, secret) {
|
|
|
46
47
|
|
|
47
48
|
for (const candidate of candidates) {
|
|
48
49
|
for (const exp of expectedList) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const b = Buffer.from(exp);
|
|
52
|
-
if (a.length === b.length && crypto.timingSafeEqual(a, b)) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
} catch {
|
|
56
|
-
// continue
|
|
50
|
+
if (safeCompare(candidate, exp)) {
|
|
51
|
+
return true;
|
|
57
52
|
}
|
|
58
53
|
}
|
|
59
54
|
}
|
|
@@ -72,13 +72,7 @@ function recaptchaV3ExecuteScript(opts = {}) {
|
|
|
72
72
|
return hidden + script;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
return String(s)
|
|
77
|
-
.replace(/&/g, '&')
|
|
78
|
-
.replace(/"/g, '"')
|
|
79
|
-
.replace(/</g, '<')
|
|
80
|
-
.replace(/>/g, '>');
|
|
81
|
-
}
|
|
75
|
+
const { escapeHtml } = require('../../core/utils/html');
|
|
82
76
|
|
|
83
77
|
function escapeJsInline(s) {
|
|
84
78
|
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* @module plugins/recaptcha/verify
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
const { fetchWithTimeout } = require('../../core/utils/fetch');
|
|
7
|
+
const { getClientIp } = require('../../core/utils/ip');
|
|
8
|
+
|
|
6
9
|
const SITEVERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
|
7
10
|
|
|
8
11
|
/**
|
|
@@ -20,7 +23,8 @@ const SITEVERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify';
|
|
|
20
23
|
* @param {VerifyOptions} options
|
|
21
24
|
* @returns {Promise<{ success: boolean, score?: number, action?: string, hostname?: string, challenge_ts?: string, errorCodes?: string[], raw: object }>}
|
|
22
25
|
*/
|
|
23
|
-
async function verifyRecaptcha(options) {
|
|
26
|
+
async function verifyRecaptcha(options = {}) {
|
|
27
|
+
const opts = options || {};
|
|
24
28
|
const {
|
|
25
29
|
secret,
|
|
26
30
|
token,
|
|
@@ -28,7 +32,8 @@ async function verifyRecaptcha(options) {
|
|
|
28
32
|
version = 'v2',
|
|
29
33
|
minScore = 0.5,
|
|
30
34
|
expectedAction,
|
|
31
|
-
|
|
35
|
+
timeoutMs = 10000,
|
|
36
|
+
} = opts;
|
|
32
37
|
|
|
33
38
|
if (!secret || typeof secret !== 'string') {
|
|
34
39
|
throw new Error('recaptcha: secret is required');
|
|
@@ -49,13 +54,24 @@ async function verifyRecaptcha(options) {
|
|
|
49
54
|
body.set('remoteip', remoteIp);
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
let res;
|
|
58
|
+
try {
|
|
59
|
+
res = await fetchWithTimeout(SITEVERIFY_URL, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
63
|
+
},
|
|
64
|
+
body: body.toString(),
|
|
65
|
+
timeout: timeoutMs,
|
|
66
|
+
});
|
|
67
|
+
} catch (netErr) {
|
|
68
|
+
const isTimeout = netErr.name === 'AbortError' || netErr.name === 'TimeoutError' || netErr.code === 'ETIMEDOUT';
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
errorCodes: [isTimeout ? 'timeout' : 'network-error'],
|
|
72
|
+
raw: {},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
59
75
|
|
|
60
76
|
if (!res.ok) {
|
|
61
77
|
return {
|
|
@@ -66,8 +82,17 @@ async function verifyRecaptcha(options) {
|
|
|
66
82
|
}
|
|
67
83
|
|
|
68
84
|
/** @type {Record<string, unknown>} */
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = await res.json();
|
|
88
|
+
} catch (jsonErr) {
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
errorCodes: ['invalid-json-response'],
|
|
92
|
+
raw: {},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const googleSuccess = raw && raw.success === true;
|
|
71
96
|
|
|
72
97
|
if (!googleSuccess) {
|
|
73
98
|
return {
|
|
@@ -129,18 +154,7 @@ async function verifyRecaptcha(options) {
|
|
|
129
154
|
* @returns {string|undefined}
|
|
130
155
|
*/
|
|
131
156
|
function getRemoteIp(req) {
|
|
132
|
-
|
|
133
|
-
const xf = req.headers && req.headers['x-forwarded-for'];
|
|
134
|
-
if (typeof xf === 'string' && xf.length) {
|
|
135
|
-
return xf.split(',')[0].trim();
|
|
136
|
-
}
|
|
137
|
-
if (req.ip) {
|
|
138
|
-
return req.ip;
|
|
139
|
-
}
|
|
140
|
-
if (req.socket && req.socket.remoteAddress) {
|
|
141
|
-
return req.socket.remoteAddress;
|
|
142
|
-
}
|
|
143
|
-
return undefined;
|
|
157
|
+
return getClientIp(req, undefined);
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
module.exports = {
|
|
@@ -151,6 +151,7 @@ function methodMatches(compiled, method) {
|
|
|
151
151
|
* @returns {{ name: string, version: string, description: string, register: Function }}
|
|
152
152
|
*/
|
|
153
153
|
function redirectPlugin(options = {}) {
|
|
154
|
+
const opts = options || {};
|
|
154
155
|
const {
|
|
155
156
|
rules = [],
|
|
156
157
|
defaultStatus = 302,
|
|
@@ -158,7 +159,7 @@ function redirectPlugin(options = {}) {
|
|
|
158
159
|
allowExternal = false,
|
|
159
160
|
trailingSlash = false,
|
|
160
161
|
defaultMethods = ['GET', 'HEAD'],
|
|
161
|
-
} =
|
|
162
|
+
} = opts;
|
|
162
163
|
|
|
163
164
|
const pluginOpts = {
|
|
164
165
|
defaultStatus: REDIRECT_STATUSES.has(Number(defaultStatus)) ? Number(defaultStatus) : 302,
|
|
@@ -169,16 +170,63 @@ function redirectPlugin(options = {}) {
|
|
|
169
170
|
};
|
|
170
171
|
|
|
171
172
|
const compiled = [];
|
|
172
|
-
|
|
173
|
+
const stringRulesMap = new Map();
|
|
174
|
+
let hasRegexRules = false;
|
|
175
|
+
|
|
176
|
+
const ruleList = Array.isArray(rules) ? rules : [];
|
|
177
|
+
for (const rule of ruleList) {
|
|
173
178
|
const c = compileRule(rule, pluginOpts);
|
|
174
|
-
if (c)
|
|
179
|
+
if (!c) continue;
|
|
180
|
+
compiled.push(c);
|
|
181
|
+
if (c.kind === 'string') {
|
|
182
|
+
const keys = [c.fromNorm];
|
|
183
|
+
if (!c.trailing) {
|
|
184
|
+
const alt = c.fromRaw.replace(/\/$/, '') || '/';
|
|
185
|
+
if (!keys.includes(alt)) keys.push(alt);
|
|
186
|
+
}
|
|
187
|
+
for (const k of keys) {
|
|
188
|
+
let list = stringRulesMap.get(k);
|
|
189
|
+
if (!list) {
|
|
190
|
+
list = [];
|
|
191
|
+
stringRulesMap.set(k, list);
|
|
192
|
+
}
|
|
193
|
+
list.push(c);
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
hasRegexRules = true;
|
|
197
|
+
}
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
function redirectMiddleware(req, res, next) {
|
|
201
|
+
if (!req) return next();
|
|
178
202
|
const rawPath = req.path || '/';
|
|
179
203
|
const pathForMatch = normalizePathForTrailingSlash(pluginOpts.trailingSlash, rawPath);
|
|
180
204
|
const method = req.method || 'GET';
|
|
181
205
|
|
|
206
|
+
// Fast O(1) path when all configured rules are string based
|
|
207
|
+
if (!hasRegexRules) {
|
|
208
|
+
const candidates = stringRulesMap.get(pathForMatch) || (!pluginOpts.trailingSlash ? stringRulesMap.get(rawPath.replace(/\/$/, '') || '/') : null);
|
|
209
|
+
if (candidates) {
|
|
210
|
+
for (const c of candidates) {
|
|
211
|
+
if (!methodMatches(c, method)) continue;
|
|
212
|
+
if (!pathMatches(c, pathForMatch, rawPath)) continue;
|
|
213
|
+
|
|
214
|
+
let location = c.to;
|
|
215
|
+
if (pluginOpts.preserveQuery && !location.includes('?')) {
|
|
216
|
+
const full = req.originalUrl || req.url || '';
|
|
217
|
+
const qi = full.indexOf('?');
|
|
218
|
+
if (qi !== -1) {
|
|
219
|
+
location += full.slice(qi);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
res.redirect(c.status, location);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return next();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Linear path when regex rules are present to preserve definition order
|
|
182
230
|
for (const c of compiled) {
|
|
183
231
|
if (!methodMatches(c, method)) continue;
|
|
184
232
|
if (!pathMatches(c, pathForMatch, rawPath)) continue;
|