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.
Files changed (91) hide show
  1. package/.agents/ORM.md +13 -4
  2. package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
  3. package/README.md +2 -2
  4. package/bin/commands/dev.js +3 -2
  5. package/bin/utils/orm-map-html.js +2 -8
  6. package/core/auth/jwt.js +12 -21
  7. package/core/auth/oauth.js +38 -14
  8. package/core/auth/totp.js +3 -15
  9. package/core/auth/user-agent.js +59 -60
  10. package/core/compression/compression-stream.js +2 -2
  11. package/core/compression/middleware.js +1 -1
  12. package/core/compression/negotiate.js +11 -4
  13. package/core/content/renderer.js +1 -13
  14. package/core/errors/middleware.js +2 -8
  15. package/core/events/dispatcher.js +56 -33
  16. package/core/forms/form-builder.js +1 -9
  17. package/core/openapi/orm-components.js +2 -3
  18. package/core/orm/cache/listeners.js +12 -2
  19. package/core/orm/index.js +5 -0
  20. package/core/orm/json-fields.js +7 -1
  21. package/core/orm/model.js +99 -8
  22. package/core/orm/query-builder.js +71 -25
  23. package/core/orm/query-filters.js +393 -0
  24. package/core/orm/repository.js +42 -17
  25. package/core/orm/schema-helpers.js +49 -0
  26. package/core/orm/scopes.js +40 -9
  27. package/core/orm/types.js +12 -1
  28. package/core/queue/manager.js +5 -0
  29. package/core/shutdown/shutdown-manager.js +16 -16
  30. package/core/utils/crypto.js +73 -0
  31. package/core/utils/escape-html.js +7 -0
  32. package/core/utils/fetch.js +120 -0
  33. package/core/utils/get-client-ip.js +6 -0
  34. package/core/utils/html.js +72 -0
  35. package/core/utils/ip.js +86 -0
  36. package/core/utils/safe-compare.js +3 -0
  37. package/core/validation/index.js +9 -3
  38. package/index.d.ts +33 -6
  39. package/index.js +10 -0
  40. package/package.json +2 -1
  41. package/plugins/admin-panel/auth.js +15 -7
  42. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
  43. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
  44. package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
  45. package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
  46. package/plugins/admin-panel/core/api-extensions.js +1 -75
  47. package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
  48. package/plugins/api-key/crypto.js +1 -24
  49. package/plugins/audit-log/api-handlers.js +11 -1
  50. package/plugins/audit-log/middleware.js +50 -6
  51. package/plugins/basic-auth/index.js +2 -26
  52. package/plugins/cors.js +2 -2
  53. package/plugins/csrf/index.js +3 -15
  54. package/plugins/email/service.js +32 -1
  55. package/plugins/file-manager/admin-component.js +3 -1
  56. package/plugins/file-manager/api-handlers.js +10 -17
  57. package/plugins/file-manager/core.js +34 -4
  58. package/plugins/file-manager/index.js +11 -9
  59. package/plugins/file-manager/security.js +18 -6
  60. package/plugins/mcp/auth.js +1 -13
  61. package/plugins/mcp/index.js +23 -1
  62. package/plugins/mcp/transports/sse.js +10 -1
  63. package/plugins/orm-cache-admin/admin-component.js +3 -2
  64. package/plugins/polar/src/api.js +9 -2
  65. package/plugins/polar/src/webhooks.js +3 -8
  66. package/plugins/recaptcha/helpers.js +1 -7
  67. package/plugins/recaptcha/verify.js +37 -23
  68. package/plugins/redirect/index.js +51 -3
  69. package/plugins/rest-resources/index.js +1 -207
  70. package/plugins/schema-explorer/admin-component.js +4 -2
  71. package/plugins/schema-explorer.js +40 -29
  72. package/plugins/seo-checker/index.js +5 -2
  73. package/plugins/site-analytics/bot-patterns.js +3 -0
  74. package/plugins/site-analytics/client-error-handler.js +3 -1
  75. package/plugins/site-analytics/index.js +10 -1
  76. package/plugins/site-analytics/tracking.js +265 -63
  77. package/plugins/sitemap.js +18 -15
  78. package/plugins/swagger.js +9 -14
  79. package/plugins/webhooks/crypto.js +1 -18
  80. package/plugins/webhooks/dispatcher.js +6 -1
  81. package/plugins/webhooks/matcher.js +25 -10
  82. package/plugins/webhooks/sender.js +32 -2
  83. package/src/api/api-loader.js +1 -1
  84. package/src/dev-reload/index.js +47 -6
  85. package/src/discovery/scan-routes.js +14 -4
  86. package/src/file-router.js +62 -6
  87. package/src/helpers.js +157 -32
  88. package/src/plugin-manager.js +34 -13
  89. package/src/routing/route-table.js +14 -4
  90. package/src/server.js +25 -9
  91. package/src/services/validator.js +57 -32
@@ -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) return null;
18
- return {
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
- // Convert glob to regex
103
- const regexPattern = pattern
104
- .replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
105
- .replace(/\*\*/g, '{{GLOBSTAR}}') // Temp placeholder for **
106
- .replace(/\*/g, '[^/]*') // * matches anything except /
107
- .replace(/\{\{GLOBSTAR\}\}/g, '.*') // ** matches everything
108
- .replace(/\?/g, '.'); // ? matches single char
109
-
110
- const regex = new RegExp(`^${regexPattern}$`);
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 && !sorted.find(p => p.name === n)) {
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 => !sorted.find(p => p.name === 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
- return {
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
- try {
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
- try {
879
- params[paramNames[i]] = decodeURIComponent(val);
880
- } catch (_) {
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
- return isOptional ? schema.optional() : schema;
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
- // If already a Zod schema
75
- if (rawSchema instanceof z.ZodType || typeof rawSchema.safeParse === 'function') {
76
- return rawSchema;
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
- // If function: check if ({ z }) schema compiler or custom validator function
80
- if (typeof rawSchema === 'function') {
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 compiled = rawSchema({ z: zForServices });
83
- if (compiled && (compiled instanceof z.ZodType || typeof compiled.safeParse === 'function')) {
84
- return compiled;
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
- return rawSchema;
90
- }
91
-
92
- // If object descriptor: { id: 'number', name: 'string', email: 'email' }
93
- if (typeof rawSchema === 'object' && rawSchema !== null) {
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
- return null;
97
- }
98
- const shape = {};
99
- for (const [key, val] of Object.entries(rawSchema)) {
100
- if (typeof val === 'string') {
101
- shape[key] = descriptorToZod(val);
102
- } else if (val instanceof z.ZodType || (val && typeof val.safeParse === 'function')) {
103
- shape[key] = val;
104
- } else if (typeof val === 'object' && val !== null) {
105
- const isOpt = val.optional === true || val.required === false;
106
- let base = val.type ? descriptorToZod(String(val.type)) : z.any();
107
- shape[key] = isOpt ? base.optional() : base;
108
- } else if (typeof val === 'function') {
109
- shape[key] = z.custom(val);
110
- } else {
111
- shape[key] = z.any();
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
- return z.object(shape);
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 rawSchema;
142
+ return compiled;
118
143
  }
119
144
 
120
145
  /**