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
package/src/plugin-manager.js
CHANGED
|
@@ -7,20 +7,29 @@
|
|
|
7
7
|
* Simple semver comparison utilities
|
|
8
8
|
* (Lightweight alternative to full semver package)
|
|
9
9
|
*/
|
|
10
|
+
const semverCache = new Map();
|
|
11
|
+
|
|
10
12
|
const semver = {
|
|
11
13
|
/**
|
|
12
14
|
* Parse version string to components
|
|
13
15
|
*/
|
|
14
16
|
parse(version) {
|
|
15
17
|
if (!version) return null;
|
|
18
|
+
const cached = semverCache.get(version);
|
|
19
|
+
if (cached !== undefined) return cached;
|
|
16
20
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
17
|
-
if (!match)
|
|
18
|
-
|
|
21
|
+
if (!match) {
|
|
22
|
+
if (semverCache.size < 200) semverCache.set(version, null);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const parsed = {
|
|
19
26
|
major: parseInt(match[1], 10),
|
|
20
27
|
minor: parseInt(match[2], 10),
|
|
21
28
|
patch: parseInt(match[3], 10),
|
|
22
29
|
prerelease: match[4] || null
|
|
23
30
|
};
|
|
31
|
+
if (semverCache.size < 200) semverCache.set(version, parsed);
|
|
32
|
+
return parsed;
|
|
24
33
|
},
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -93,21 +102,29 @@ const semver = {
|
|
|
93
102
|
}
|
|
94
103
|
};
|
|
95
104
|
|
|
105
|
+
const patternRegexCache = new Map();
|
|
106
|
+
|
|
96
107
|
/**
|
|
97
108
|
* Simple glob/minimatch pattern matching
|
|
98
109
|
*/
|
|
99
110
|
function matchPattern(path, pattern) {
|
|
100
111
|
if (pattern === '*' || pattern === '**') return true;
|
|
101
112
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
113
|
+
let regex = patternRegexCache.get(pattern);
|
|
114
|
+
if (!regex) {
|
|
115
|
+
// Convert glob to regex
|
|
116
|
+
const regexPattern = pattern
|
|
117
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
118
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
|
|
119
|
+
.replace(/\*/g, '[^/]*') // * matches anything except /
|
|
120
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** matches everything
|
|
121
|
+
.replace(/\?/g, '.'); // ? matches single char
|
|
122
|
+
|
|
123
|
+
regex = new RegExp(`^${regexPattern}$`);
|
|
124
|
+
if (patternRegexCache.size < 500) {
|
|
125
|
+
patternRegexCache.set(pattern, regex);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
111
128
|
return regex.test(path);
|
|
112
129
|
}
|
|
113
130
|
|
|
@@ -206,6 +223,8 @@ class PluginManager {
|
|
|
206
223
|
* Resolve dependency order using topological sort
|
|
207
224
|
*/
|
|
208
225
|
_resolveDependencyOrder(plugins) {
|
|
226
|
+
if (!plugins || plugins.length === 0) return [];
|
|
227
|
+
|
|
209
228
|
const graph = new Map();
|
|
210
229
|
const pluginMap = new Map();
|
|
211
230
|
|
|
@@ -228,6 +247,7 @@ class PluginManager {
|
|
|
228
247
|
|
|
229
248
|
// Topological sort (Kahn's algorithm)
|
|
230
249
|
const sorted = [];
|
|
250
|
+
const sortedNames = new Set();
|
|
231
251
|
const noIncoming = [];
|
|
232
252
|
|
|
233
253
|
// Find nodes with no dependencies
|
|
@@ -243,12 +263,13 @@ class PluginManager {
|
|
|
243
263
|
while (noIncoming.length > 0) {
|
|
244
264
|
const name = noIncoming.shift();
|
|
245
265
|
sorted.push(pluginMap.get(name));
|
|
266
|
+
sortedNames.add(name);
|
|
246
267
|
|
|
247
268
|
// Remove this node from all dependencies
|
|
248
269
|
for (const [n, deps] of graph) {
|
|
249
270
|
if (deps.has(name)) {
|
|
250
271
|
deps.delete(name);
|
|
251
|
-
if (deps.size === 0 && !
|
|
272
|
+
if (deps.size === 0 && !sortedNames.has(n)) {
|
|
252
273
|
noIncoming.push(n);
|
|
253
274
|
}
|
|
254
275
|
}
|
|
@@ -257,7 +278,7 @@ class PluginManager {
|
|
|
257
278
|
|
|
258
279
|
// Check for cycles - warn and exclude circular plugins
|
|
259
280
|
if (sorted.length !== pluginMap.size) {
|
|
260
|
-
const remaining = Array.from(pluginMap.keys()).filter(name => !
|
|
281
|
+
const remaining = Array.from(pluginMap.keys()).filter(name => !sortedNames.has(name));
|
|
261
282
|
console.warn(
|
|
262
283
|
`[plugin-manager] Circular dependency detected. Skipping plugins: ${remaining.join(', ')}`
|
|
263
284
|
);
|
|
@@ -6,19 +6,24 @@
|
|
|
6
6
|
* @module src/routing/route-table
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const routeSortMetaCache = new Map();
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Computes route sorting priority metadata
|
|
11
13
|
* @param {string} routePath
|
|
12
14
|
* @returns {{ tier: number, literalSegCount: number, paramSegCount: number, depth: number, routePath: string }}
|
|
13
15
|
*/
|
|
14
16
|
function getRouteSortMeta(routePath) {
|
|
17
|
+
const cached = routeSortMetaCache.get(routePath);
|
|
18
|
+
if (cached !== undefined) return cached;
|
|
19
|
+
|
|
15
20
|
let hasStar = false;
|
|
16
21
|
let hasColon = false;
|
|
17
22
|
let depth = 0;
|
|
18
23
|
let literalSegCount = 0;
|
|
19
24
|
let paramSegCount = 0;
|
|
20
25
|
|
|
21
|
-
const clean = routePath.split('?')[0];
|
|
26
|
+
const clean = routePath.includes('?') ? routePath.split('?')[0] : routePath;
|
|
22
27
|
const segments = clean.split('/').filter(Boolean);
|
|
23
28
|
|
|
24
29
|
for (const seg of segments) {
|
|
@@ -40,13 +45,18 @@ function getRouteSortMeta(routePath) {
|
|
|
40
45
|
if (hasStar) tier = 2;
|
|
41
46
|
else if (hasColon) tier = 1;
|
|
42
47
|
|
|
43
|
-
|
|
48
|
+
const meta = {
|
|
44
49
|
tier,
|
|
45
50
|
literalSegCount,
|
|
46
51
|
paramSegCount,
|
|
47
52
|
depth,
|
|
48
53
|
routePath,
|
|
49
54
|
};
|
|
55
|
+
|
|
56
|
+
if (routeSortMetaCache.size < 1000) {
|
|
57
|
+
routeSortMetaCache.set(routePath, meta);
|
|
58
|
+
}
|
|
59
|
+
return meta;
|
|
50
60
|
}
|
|
51
61
|
|
|
52
62
|
/**
|
|
@@ -56,8 +66,8 @@ function getRouteSortMeta(routePath) {
|
|
|
56
66
|
* @returns {number}
|
|
57
67
|
*/
|
|
58
68
|
function compareRouteOrder(a, b) {
|
|
59
|
-
const ma = a._sortMeta || getRouteSortMeta(a.path);
|
|
60
|
-
const mb = b._sortMeta || getRouteSortMeta(b.path);
|
|
69
|
+
const ma = a._sortMeta || (a._sortMeta = getRouteSortMeta(a.path));
|
|
70
|
+
const mb = b._sortMeta || (b._sortMeta = getRouteSortMeta(b.path));
|
|
61
71
|
|
|
62
72
|
if (ma.tier !== mb.tier) return ma.tier - mb.tier;
|
|
63
73
|
if (ma.literalSegCount !== mb.literalSegCount) return mb.literalSegCount - ma.literalSegCount;
|
package/src/server.js
CHANGED
|
@@ -54,6 +54,19 @@ const {
|
|
|
54
54
|
// Async storage for tracking template render call stacks (circular extends guard)
|
|
55
55
|
const renderStackStorage = new AsyncLocalStorage();
|
|
56
56
|
|
|
57
|
+
let isWebspressoPackageCache = null;
|
|
58
|
+
function checkIsWebspressoPackage() {
|
|
59
|
+
if (isWebspressoPackageCache === null) {
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
62
|
+
isWebspressoPackageCache = Boolean(pkg && pkg.name === 'webspresso');
|
|
63
|
+
} catch (_) {
|
|
64
|
+
isWebspressoPackageCache = false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return isWebspressoPackageCache;
|
|
68
|
+
}
|
|
69
|
+
|
|
57
70
|
/**
|
|
58
71
|
* Fast hardware-accelerated Weak ETag generator using native zlib.crc32
|
|
59
72
|
* Up to 13x faster than Express default SHA-1, zero external dependencies.
|
|
@@ -91,6 +104,7 @@ function fastETag(body, encoding) {
|
|
|
91
104
|
*/
|
|
92
105
|
function normalizeTemplateName(name) {
|
|
93
106
|
if (typeof name !== 'string') return name;
|
|
107
|
+
if (!name.includes('\\') && !name.includes('views')) return name;
|
|
94
108
|
let normalized = name.replace(/\\/g, '/');
|
|
95
109
|
// Collapse any repeated views/views/... to a single views/ or clean path
|
|
96
110
|
while (/(?:^|\/)views\/(?:\.\/)?views(?:\/|$)/.test(normalized)) {
|
|
@@ -102,6 +116,7 @@ function normalizeTemplateName(name) {
|
|
|
102
116
|
function canonicalTemplateKey(name) {
|
|
103
117
|
const norm = normalizeTemplateName(name);
|
|
104
118
|
if (typeof norm !== 'string') return String(norm);
|
|
119
|
+
if (!norm.startsWith('.') && !norm.startsWith('views/')) return norm;
|
|
105
120
|
// Canonical key strips leading ./ or views/ for cycle detection
|
|
106
121
|
return norm.replace(/^\.\//, '').replace(/^views\//, '');
|
|
107
122
|
}
|
|
@@ -591,10 +606,7 @@ function createApp(options = {}) {
|
|
|
591
606
|
|
|
592
607
|
const rootDir = options.rootDir || (() => {
|
|
593
608
|
if (process.env.NODE_ENV === 'test') {
|
|
594
|
-
|
|
595
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'package.json'), 'utf8'));
|
|
596
|
-
if (pkg.name === 'webspresso') return process.cwd();
|
|
597
|
-
} catch (_) {}
|
|
609
|
+
if (checkIsWebspressoPackage()) return process.cwd();
|
|
598
610
|
return path.resolve(__dirname, '..');
|
|
599
611
|
}
|
|
600
612
|
return process.cwd();
|
|
@@ -630,8 +642,8 @@ function createApp(options = {}) {
|
|
|
630
642
|
|
|
631
643
|
const shutdownConfig = {
|
|
632
644
|
enabled: true,
|
|
633
|
-
mode: 'graceful',
|
|
634
|
-
timeout: 10_000,
|
|
645
|
+
mode: isDev ? 'force' : 'graceful',
|
|
646
|
+
timeout: isDev ? 1000 : 10_000,
|
|
635
647
|
...(options.shutdown || options.server?.shutdown || {}),
|
|
636
648
|
};
|
|
637
649
|
|
|
@@ -875,9 +887,13 @@ function createApp(options = {}) {
|
|
|
875
887
|
for (let i = 0; i < paramNames.length; i++) {
|
|
876
888
|
const val = match[i + 1];
|
|
877
889
|
if (val !== undefined) {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
890
|
+
if (typeof val === 'string' && val.includes('%')) {
|
|
891
|
+
try {
|
|
892
|
+
params[paramNames[i]] = decodeURIComponent(val);
|
|
893
|
+
} catch (_) {
|
|
894
|
+
params[paramNames[i]] = val;
|
|
895
|
+
}
|
|
896
|
+
} else {
|
|
881
897
|
params[paramNames[i]] = val;
|
|
882
898
|
}
|
|
883
899
|
}
|
|
@@ -8,12 +8,17 @@ const { ValidationError } = require('../../core/errors');
|
|
|
8
8
|
const { z: zForServices, sanitizeInput, hasDangerousKeys } = require('../../core/validation');
|
|
9
9
|
const z = zForServices;
|
|
10
10
|
|
|
11
|
+
const descriptorCache = new Map();
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Compile a concise type string descriptor into a Zod schema
|
|
13
15
|
* @param {string} typeName - Type name descriptor (e.g. 'number', 'string', 'email', 'string?')
|
|
14
16
|
* @returns {z.ZodTypeAny}
|
|
15
17
|
*/
|
|
16
18
|
function descriptorToZod(typeName) {
|
|
19
|
+
const cached = descriptorCache.get(typeName);
|
|
20
|
+
if (cached) return cached;
|
|
21
|
+
|
|
17
22
|
const isOptional = typeName.endsWith('?');
|
|
18
23
|
const baseType = isOptional ? typeName.slice(0, -1).toLowerCase() : typeName.toLowerCase();
|
|
19
24
|
|
|
@@ -60,9 +65,15 @@ function descriptorToZod(typeName) {
|
|
|
60
65
|
break;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
const finalSchema = isOptional ? schema.optional() : schema;
|
|
69
|
+
if (descriptorCache.size < 200) {
|
|
70
|
+
descriptorCache.set(typeName, finalSchema);
|
|
71
|
+
}
|
|
72
|
+
return finalSchema;
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
const compiledSchemaCache = new WeakMap();
|
|
76
|
+
|
|
66
77
|
/**
|
|
67
78
|
* Compile service schema definition into an executable validator
|
|
68
79
|
* @param {Function|Object|null} rawSchema - Schema definition
|
|
@@ -71,50 +82,64 @@ function descriptorToZod(typeName) {
|
|
|
71
82
|
function compileServiceSchema(rawSchema) {
|
|
72
83
|
if (!rawSchema) return null;
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
if (typeof rawSchema === 'object' || typeof rawSchema === 'function') {
|
|
86
|
+
const cached = compiledSchemaCache.get(rawSchema);
|
|
87
|
+
if (cached !== undefined) {
|
|
88
|
+
return cached;
|
|
89
|
+
}
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
let compiled = null;
|
|
93
|
+
|
|
94
|
+
// If already a Zod schema
|
|
95
|
+
if (rawSchema instanceof z.ZodType || typeof rawSchema.safeParse === 'function') {
|
|
96
|
+
compiled = rawSchema;
|
|
97
|
+
} else if (typeof rawSchema === 'function') {
|
|
98
|
+
// If function: check if ({ z }) schema compiler or custom validator function
|
|
81
99
|
try {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
100
|
+
const res = rawSchema({ z: zForServices });
|
|
101
|
+
if (res && (res instanceof z.ZodType || typeof res.safeParse === 'function')) {
|
|
102
|
+
compiled = res;
|
|
85
103
|
}
|
|
86
104
|
} catch (e) {
|
|
87
105
|
// If invoking with { z } throws, treat as a direct validator function
|
|
88
106
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
if (!compiled) {
|
|
108
|
+
compiled = rawSchema;
|
|
109
|
+
}
|
|
110
|
+
} else if (typeof rawSchema === 'object' && rawSchema !== null) {
|
|
111
|
+
// If object descriptor: { id: 'number', name: 'string', email: 'email' }
|
|
94
112
|
const keys = Object.keys(rawSchema);
|
|
95
113
|
if (keys.length === 0) {
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
compiled = null;
|
|
115
|
+
} else {
|
|
116
|
+
const shape = {};
|
|
117
|
+
for (const [key, val] of Object.entries(rawSchema)) {
|
|
118
|
+
if (typeof val === 'string') {
|
|
119
|
+
shape[key] = descriptorToZod(val);
|
|
120
|
+
} else if (val instanceof z.ZodType || (val && typeof val.safeParse === 'function')) {
|
|
121
|
+
shape[key] = val;
|
|
122
|
+
} else if (typeof val === 'object' && val !== null) {
|
|
123
|
+
const isOpt = val.optional === true || val.required === false;
|
|
124
|
+
let base = val.type ? descriptorToZod(String(val.type)) : z.any();
|
|
125
|
+
shape[key] = isOpt ? base.optional() : base;
|
|
126
|
+
} else if (typeof val === 'function') {
|
|
127
|
+
shape[key] = z.custom(val);
|
|
128
|
+
} else {
|
|
129
|
+
shape[key] = z.any();
|
|
130
|
+
}
|
|
112
131
|
}
|
|
132
|
+
compiled = z.object(shape);
|
|
113
133
|
}
|
|
114
|
-
|
|
134
|
+
} else {
|
|
135
|
+
compiled = rawSchema;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (typeof rawSchema === 'object' || typeof rawSchema === 'function') {
|
|
139
|
+
compiledSchemaCache.set(rawSchema, compiled);
|
|
115
140
|
}
|
|
116
141
|
|
|
117
|
-
return
|
|
142
|
+
return compiled;
|
|
118
143
|
}
|
|
119
144
|
|
|
120
145
|
/**
|