webspresso 0.0.107 → 0.0.108

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/AGENTS.md +4 -1
  2. package/.agents/CACHE.md +1 -0
  3. package/.agents/CONTEXT.md +152 -0
  4. package/.agents/ORM.md +74 -0
  5. package/.agents/SHUTDOWN.md +8 -1
  6. package/.agents/skills/webspresso-framework/SKILL.md +2 -1
  7. package/.agents/skills/webspresso-orm-and-database/SKILL.md +9 -0
  8. package/.agents/skills/webspresso-routing-and-api/SKILL.md +18 -2
  9. package/.agents/skills/webspresso-services/SKILL.md +36 -3
  10. package/AGENTS.md +6 -3
  11. package/core/app.js +7 -5
  12. package/core/applySchema.js +3 -72
  13. package/core/cache/lru.js +10 -0
  14. package/core/cache/tagged-lru.js +4 -0
  15. package/core/compileSchema.js +3 -68
  16. package/core/compression/compressible.js +53 -6
  17. package/core/compression/negotiate.js +31 -12
  18. package/core/context/app-context.js +201 -0
  19. package/core/context/index.js +55 -0
  20. package/core/context/request.js +241 -0
  21. package/core/events/dispatcher.js +144 -15
  22. package/core/events/signal.js +9 -0
  23. package/core/health/probes.js +12 -2
  24. package/core/http/index.js +10 -1
  25. package/core/http/pipeline.js +519 -368
  26. package/core/index.js +24 -1
  27. package/core/orm/eager-loader.js +90 -55
  28. package/core/orm/index.js +3 -39
  29. package/core/orm/model.js +6 -1
  30. package/core/orm/schema-helpers.js +59 -0
  31. package/core/orm/transaction.js +109 -14
  32. package/core/plugins/index.js +8 -0
  33. package/core/plugins/plugin-manager.js +1098 -0
  34. package/core/queue/manager.js +55 -27
  35. package/core/rate-limit/stores/memory.js +23 -10
  36. package/core/resilience/circuit-breaker.js +34 -14
  37. package/core/schema-cache.js +2 -64
  38. package/core/services/builtins/auth.js +273 -0
  39. package/core/services/builtins/exchange.js +229 -0
  40. package/core/services/builtins/file-manager.js +178 -0
  41. package/core/services/builtins/mail.js +134 -0
  42. package/core/services/builtins/media.js +196 -0
  43. package/core/services/builtins/system.js +219 -0
  44. package/core/services/discovery.js +133 -0
  45. package/core/services/executor.js +629 -0
  46. package/core/services/index.js +110 -5
  47. package/core/services/loader.js +161 -0
  48. package/core/services/memoize.js +408 -0
  49. package/core/services/registry.js +731 -0
  50. package/core/services/validator.js +424 -0
  51. package/core/shutdown/shutdown-manager.js +146 -85
  52. package/core/ssr/stream.js +109 -16
  53. package/core/stream/text-stream.js +45 -4
  54. package/core/utils/index.js +110 -0
  55. package/core/utils/timeout.js +4 -0
  56. package/core/validation/apply.js +74 -0
  57. package/core/validation/cache.js +66 -0
  58. package/core/validation/compiler.js +77 -0
  59. package/core/validation/index.js +22 -1
  60. package/index.d.ts +77 -4
  61. package/index.js +64 -20
  62. package/package.json +1 -1
  63. package/src/app-context.js +4 -196
  64. package/src/dev/typegen.js +1 -1
  65. package/src/plugin-manager.js +5 -1086
  66. package/src/server.js +4 -1
  67. package/src/services/builtins/auth.js +2 -273
  68. package/src/services/builtins/exchange.js +2 -229
  69. package/src/services/builtins/file-manager.js +2 -178
  70. package/src/services/builtins/mail.js +2 -134
  71. package/src/services/builtins/media.js +2 -196
  72. package/src/services/builtins/system.js +2 -219
  73. package/src/services/discovery.js +2 -133
  74. package/src/services/executor.js +2 -575
  75. package/src/services/index.js +4 -109
  76. package/src/services/loader.js +1 -119
  77. package/src/services/memoize.js +2 -408
  78. package/src/services/registry.js +1 -711
  79. package/src/services/validator.js +2 -424
  80. package/core/utils/array.js +0 -12
  81. package/core/utils/base64url.js +0 -8
  82. package/core/utils/ensure-leading-slash.js +0 -6
  83. package/core/utils/escape-html.js +0 -7
  84. package/core/utils/escape-regexp.js +0 -7
  85. package/core/utils/get-client-ip.js +0 -6
  86. package/core/utils/hmac.js +0 -6
  87. package/core/utils/random.js +0 -9
  88. package/core/utils/safe-compare.js +0 -3
  89. package/core/utils/sha256.js +0 -6
  90. package/core/utils/strip-trailing-slash.js +0 -6
  91. package/core/utils/walk.js +0 -7
@@ -1,69 +1,4 @@
1
- /**
2
- * Schema Compiler
3
- * Compiles schema definitions from API files
4
- */
5
-
6
- const { z: zForApi } = require('./validation');
7
- const schemaCache = require('./schema-cache');
8
-
9
- /**
10
- * Compile schema from an API module
11
- * @param {string} filePath - Absolute file path to API module
12
- * @param {Object} apiModule - The loaded API module
13
- * @returns {Object|null} Compiled schema object or null if no schema
14
- */
15
- function compileSchema(filePath, apiModule) {
16
- // Return cached schema if exists
17
- if (schemaCache.has(filePath)) {
18
- return schemaCache.get(filePath);
19
- }
20
-
21
- // Check if module exports schema
22
- const schemaFn = apiModule.schema;
23
-
24
- // Schema is optional
25
- if (schemaFn === undefined) {
26
- schemaCache.set(filePath, null);
27
- return null;
28
- }
29
-
30
- // Schema must be a function
31
- if (typeof schemaFn !== 'function') {
32
- throw new Error(`Schema in ${filePath} must be a function`);
33
- }
34
-
35
- // Call schema function with { z } (extended with z.nanoid, same as generateNanoid / zdb.nanoid)
36
- const compiled = schemaFn({ z: zForApi });
37
-
38
- // Validate compiled schema structure
39
- if (compiled !== null && typeof compiled !== 'object') {
40
- throw new Error(`Schema function in ${filePath} must return an object or null`);
41
- }
42
-
43
- // Cache and return
44
- schemaCache.set(filePath, compiled);
45
- return compiled;
46
- }
47
-
48
- /**
49
- * Clear schema cache for a file (for hot-reload)
50
- * @param {string} filePath - Absolute file path
51
- */
52
- function invalidateSchema(filePath) {
53
- schemaCache.del(filePath);
54
- }
55
-
56
- /**
57
- * Clear all cached schemas
58
- */
59
- function clearAllSchemas() {
60
- schemaCache.clear();
61
- }
62
-
63
- module.exports = {
64
- compileSchema,
65
- invalidateSchema,
66
- clearAllSchemas
67
- };
68
-
1
+ 'use strict';
69
2
 
3
+ // Backward-compatibility shim
4
+ module.exports = require('./validation/compiler');
@@ -11,7 +11,56 @@ const COMPRESSIBLE_TYPE_REGEX = /^(?:text\/[a-z0-9.+_-]+|application\/(?:javascr
11
11
  const INCOMPRESSIBLE_TYPE_REGEX = /^(?:text\/event-stream|image\/(?:jpeg|png|gif|webp|avif|heic|heif)|video\/[a-z0-9.+_-]+|audio\/[a-z0-9.+_-]+|application\/(?:zip|gzip|x-gzip|x-bzip2|x-compress|x-7z-compressed|x-rar-compressed|pdf|wasm|octet-stream))$/i;
12
12
 
13
13
  const compressibleCache = new Map();
14
- const MAX_MIME_CACHE_SIZE = 256;
14
+ const MAX_MIME_CACHE_SIZE = 512;
15
+
16
+ // Pre-fill standard common web & API MIME types for zero-lookup warmup
17
+ const PREFILLED_MIMES = [
18
+ ['text/html', true],
19
+ ['text/html; charset=utf-8', true],
20
+ ['text/plain', true],
21
+ ['text/plain; charset=utf-8', true],
22
+ ['text/css', true],
23
+ ['text/css; charset=utf-8', true],
24
+ ['text/javascript', true],
25
+ ['text/javascript; charset=utf-8', true],
26
+ ['application/javascript', true],
27
+ ['application/javascript; charset=utf-8', true],
28
+ ['application/json', true],
29
+ ['application/json; charset=utf-8', true],
30
+ ['application/ld+json', true],
31
+ ['application/manifest+json', true],
32
+ ['application/graphql+json', true],
33
+ ['application/geo+json', true],
34
+ ['application/xml', true],
35
+ ['application/atom+xml', true],
36
+ ['application/rss+xml', true],
37
+ ['application/xhtml+xml', true],
38
+ ['image/svg+xml', true],
39
+ ['text/event-stream', false],
40
+ ['image/png', false],
41
+ ['image/jpeg', false],
42
+ ['image/webp', false],
43
+ ['image/gif', false],
44
+ ['image/avif', false],
45
+ ['application/pdf', false],
46
+ ['application/zip', false],
47
+ ['application/gzip', false],
48
+ ['application/octet-stream', false],
49
+ ['video/mp4', false],
50
+ ['audio/mpeg', false],
51
+ ];
52
+
53
+ for (const [mime, val] of PREFILLED_MIMES) {
54
+ compressibleCache.set(mime, val);
55
+ }
56
+
57
+ function setCompressibleCache(key, value) {
58
+ if (compressibleCache.size >= MAX_MIME_CACHE_SIZE) {
59
+ const firstKey = compressibleCache.keys().next().value;
60
+ compressibleCache.delete(firstKey);
61
+ }
62
+ compressibleCache.set(key, value);
63
+ }
15
64
 
16
65
  /**
17
66
  * Determine if a Content-Type is compressible
@@ -33,13 +82,13 @@ function isCompressible(contentType) {
33
82
  const mime = (semiIndex !== -1 ? contentType.slice(0, semiIndex) : contentType).trim().toLowerCase();
34
83
 
35
84
  if (!mime) {
36
- if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
85
+ setCompressibleCache(contentType, false);
37
86
  return false;
38
87
  }
39
88
 
40
89
  // Fast negative check
41
90
  if (INCOMPRESSIBLE_TYPE_REGEX.test(mime)) {
42
- if (compressibleCache.size < MAX_MIME_CACHE_SIZE) compressibleCache.set(contentType, false);
91
+ setCompressibleCache(contentType, false);
43
92
  return false;
44
93
  }
45
94
 
@@ -51,9 +100,7 @@ function isCompressible(contentType) {
51
100
  result = true;
52
101
  }
53
102
 
54
- if (compressibleCache.size < MAX_MIME_CACHE_SIZE) {
55
- compressibleCache.set(contentType, result);
56
- }
103
+ setCompressibleCache(contentType, result);
57
104
 
58
105
  return result;
59
106
  }
@@ -71,9 +71,29 @@ function parseAcceptEncoding(header) {
71
71
 
72
72
  const DEFAULT_SUPPORTED_ENCODINGS = Object.freeze(getDefaultSupportedEncodings());
73
73
  const DEFAULT_SUPPORTED_STRING = DEFAULT_SUPPORTED_ENCODINGS.join(',');
74
+ const DEFAULT_PREFERRED_ENCODING = DEFAULT_SUPPORTED_ENCODINGS[0] || 'identity';
74
75
 
75
76
  const selectEncodingCache = new Map();
76
- const MAX_ENCODING_CACHE_SIZE = 128;
77
+ const MAX_ENCODING_CACHE_SIZE = 256;
78
+
79
+ function setSelectEncodingCache(key, value) {
80
+ if (selectEncodingCache.size >= MAX_ENCODING_CACHE_SIZE) {
81
+ const firstKey = selectEncodingCache.keys().next().value;
82
+ selectEncodingCache.delete(firstKey);
83
+ }
84
+ selectEncodingCache.set(key, value);
85
+ }
86
+
87
+ // Pre-fill common browser patterns for default configuration
88
+ if (DEFAULT_SUPPORTED_ENCODINGS.includes('br')) {
89
+ selectEncodingCache.set('gzip, deflate, br', 'br');
90
+ selectEncodingCache.set('gzip, deflate, br, zstd', 'br');
91
+ selectEncodingCache.set('br;q=1.0, gzip;q=0.8, *;q=0.1', 'br');
92
+ }
93
+ selectEncodingCache.set('gzip, deflate', 'gzip');
94
+ selectEncodingCache.set('gzip', 'gzip');
95
+ selectEncodingCache.set('*', DEFAULT_PREFERRED_ENCODING);
96
+ selectEncodingCache.set('identity', 'identity');
77
97
 
78
98
  /**
79
99
  * Select the best compression encoding given client header and server supported list
@@ -82,14 +102,17 @@ const MAX_ENCODING_CACHE_SIZE = 128;
82
102
  * @returns {string|null} The selected encoding ('br', 'gzip', 'deflate', 'identity', or null)
83
103
  */
84
104
  function selectEncoding(acceptEncoding, supportedEncodings) {
85
- if (!acceptEncoding) {
105
+ if (!acceptEncoding || typeof acceptEncoding !== 'string') {
106
+ return 'identity';
107
+ }
108
+
109
+ const raw = acceptEncoding.trim();
110
+ if (!raw) {
86
111
  return 'identity';
87
112
  }
88
113
 
89
114
  const isDefault = !supportedEncodings || !Array.isArray(supportedEncodings) || supportedEncodings.length === 0;
90
- const cacheKey = isDefault
91
- ? `${acceptEncoding}|${DEFAULT_SUPPORTED_STRING}`
92
- : `${acceptEncoding}|${supportedEncodings.join(',')}`;
115
+ const cacheKey = isDefault ? raw : `${raw}|${supportedEncodings.join(',')}`;
93
116
 
94
117
  const cached = selectEncodingCache.get(cacheKey);
95
118
  if (cached !== undefined) {
@@ -100,11 +123,9 @@ function selectEncoding(acceptEncoding, supportedEncodings) {
100
123
  ? DEFAULT_SUPPORTED_ENCODINGS
101
124
  : supportedEncodings.map(e => e.toLowerCase()).filter(e => e !== 'br' || supportsBrotli());
102
125
 
103
- const clientPreferences = parseAcceptEncoding(acceptEncoding);
126
+ const clientPreferences = parseAcceptEncoding(raw);
104
127
  if (clientPreferences.length === 0) {
105
- if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
106
- selectEncodingCache.set(cacheKey, 'identity');
107
- }
128
+ setSelectEncodingCache(cacheKey, 'identity');
108
129
  return 'identity';
109
130
  }
110
131
 
@@ -157,9 +178,7 @@ function selectEncoding(acceptEncoding, supportedEncodings) {
157
178
  result = wildcardQ > 0 ? 'identity' : null;
158
179
  }
159
180
 
160
- if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
161
- selectEncodingCache.set(cacheKey, result);
162
- }
181
+ setSelectEncodingCache(cacheKey, result);
163
182
 
164
183
  return result;
165
184
  }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Process-wide app context set by createApp() — use from API handlers, jobs, etc.
3
+ * @module src/app-context
4
+ */
5
+
6
+ const { circuitBreakerRegistry, retry } = require('../resilience');
7
+
8
+ /** @type {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null, events: object|null, mailer: object|null, cache: object|null, breakers: object|null }} */
9
+ let context = {
10
+ db: null,
11
+ shutdownManager: null,
12
+ serviceRegistry: null,
13
+ events: null,
14
+ mailer: null,
15
+ cache: null,
16
+ breakers: circuitBreakerRegistry,
17
+ };
18
+
19
+ /**
20
+ * Merge into the current context (typically called once from createApp).
21
+ * @param {{ db?: object|null, shutdownManager?: object|null, serviceRegistry?: object|null, events?: object|null, mailer?: object|null, cache?: object|null }} partial
22
+ */
23
+ function setAppContext(partial) {
24
+ context = { ...context, ...partial };
25
+ }
26
+
27
+ /**
28
+ * @returns {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null, events: object|null, mailer: object|null }}
29
+ */
30
+ function getAppContext() {
31
+ return context;
32
+ }
33
+
34
+ /**
35
+ * Database instance passed to createApp({ db }) (Knex + ORM helpers).
36
+ * @returns {object}
37
+ * @throws {Error} If createApp was not given a db instance
38
+ */
39
+ function getDb() {
40
+ if (!context.db) {
41
+ throw new Error(
42
+ 'No database registered. Create a DB with createDatabase(), then pass it to createApp({ db }). ' +
43
+ 'Or use hasDb() before calling getDb().'
44
+ );
45
+ }
46
+ return context.db;
47
+ }
48
+
49
+ /**
50
+ * @returns {boolean}
51
+ */
52
+ function hasDb() {
53
+ return context.db != null;
54
+ }
55
+
56
+ /**
57
+ * @returns {object|null}
58
+ */
59
+ function getShutdownManager() {
60
+ return context.shutdownManager;
61
+ }
62
+
63
+ /**
64
+ * @returns {boolean}
65
+ */
66
+ function hasShutdownManager() {
67
+ return context.shutdownManager != null;
68
+ }
69
+
70
+ /**
71
+ * Get the registered ServiceRegistry instance
72
+ * @returns {object|null}
73
+ */
74
+ function getServiceRegistry() {
75
+ return context.serviceRegistry;
76
+ }
77
+
78
+ /**
79
+ * Check if a ServiceRegistry is registered
80
+ * @returns {boolean}
81
+ */
82
+ function hasServiceRegistry() {
83
+ return context.serviceRegistry != null;
84
+ }
85
+
86
+ /**
87
+ * Get the registered EventDispatcher instance
88
+ * @returns {object|null}
89
+ */
90
+ function getEvents() {
91
+ return context.events;
92
+ }
93
+
94
+ /**
95
+ * Check if an EventDispatcher is registered
96
+ * @returns {boolean}
97
+ */
98
+ function hasEvents() {
99
+ return context.events != null;
100
+ }
101
+
102
+ /**
103
+ * Clear context (e.g. between tests).
104
+ */
105
+ function resetAppContext() {
106
+ context = {
107
+ db: null,
108
+ shutdownManager: null,
109
+ serviceRegistry: null,
110
+ events: null,
111
+ mailer: null,
112
+ cache: null,
113
+ breakers: circuitBreakerRegistry,
114
+ };
115
+ }
116
+
117
+ /**
118
+ * @returns {object|null}
119
+ */
120
+ function getMailer() {
121
+ return context.mailer;
122
+ }
123
+
124
+ /**
125
+ * @returns {boolean}
126
+ */
127
+ function hasMailer() {
128
+ return context.mailer != null;
129
+ }
130
+
131
+ /**
132
+ * @returns {object|null}
133
+ */
134
+ function getCache() {
135
+ return context.cache;
136
+ }
137
+
138
+ /**
139
+ * @returns {boolean}
140
+ */
141
+ function hasCache() {
142
+ return context.cache != null;
143
+ }
144
+
145
+ /**
146
+ * Express middleware: sets `req.db`, `req.service`, `req.events`, `req.mailer`, and `req.cache` from registered app context.
147
+ * File-based `pages/api/*` routes attach this automatically; use in `setupRoutes`
148
+ * for manually registered handlers that need `req.db` and `req.service`.
149
+ * @type {import('express').RequestHandler}
150
+ */
151
+ function attachDbMiddleware(req, res, next) {
152
+ if (context.db != null) {
153
+ req.db = context.db;
154
+ }
155
+ if (context.events != null) {
156
+ req.events = context.events;
157
+ }
158
+ if (context.mailer != null) {
159
+ req.mailer = context.mailer;
160
+ }
161
+ if (context.cache != null) {
162
+ req.cache = context.cache;
163
+ }
164
+ req.breakers = context.breakers || circuitBreakerRegistry;
165
+ req.breaker = (name, opts) => (context.breakers || circuitBreakerRegistry).getBreaker(name, opts);
166
+ req.retry = retry;
167
+ if (context.serviceRegistry != null) {
168
+ req.service = (name, input, opts) =>
169
+ context.serviceRegistry.call(name, input, req.context || {
170
+ req,
171
+ res,
172
+ db: context.db,
173
+ events: context.events,
174
+ mailer: context.mailer,
175
+ cache: context.cache,
176
+ breakers: req.breakers,
177
+ breaker: req.breaker,
178
+ retry,
179
+ }, opts);
180
+ }
181
+ next();
182
+ }
183
+
184
+ module.exports = {
185
+ setAppContext,
186
+ getAppContext,
187
+ getDb,
188
+ hasDb,
189
+ getShutdownManager,
190
+ hasShutdownManager,
191
+ getServiceRegistry,
192
+ hasServiceRegistry,
193
+ getEvents,
194
+ hasEvents,
195
+ getMailer,
196
+ hasMailer,
197
+ getCache,
198
+ hasCache,
199
+ resetAppContext,
200
+ attachDbMiddleware,
201
+ };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso Context Management Layer
5
+ * Process-wide application context and request-scoped AsyncLocalStorage ambient context.
6
+ *
7
+ * @module core/context
8
+ */
9
+
10
+ const appContext = require('./app-context');
11
+ const requestContext = require('./request');
12
+
13
+ module.exports = {
14
+ // App context (process-wide)
15
+ setAppContext: appContext.setAppContext,
16
+ getAppContext: appContext.getAppContext,
17
+ getDb: appContext.getDb,
18
+ hasDb: appContext.hasDb,
19
+ getShutdownManager: appContext.getShutdownManager,
20
+ hasShutdownManager: appContext.hasShutdownManager,
21
+ getServiceRegistry: appContext.getServiceRegistry,
22
+ hasServiceRegistry: appContext.hasServiceRegistry,
23
+ getEvents: appContext.getEvents,
24
+ hasEvents: appContext.hasEvents,
25
+ getMailer: appContext.getMailer,
26
+ hasMailer: appContext.hasMailer,
27
+ getCache: appContext.getCache,
28
+ hasCache: appContext.hasCache,
29
+ resetAppContext: appContext.resetAppContext,
30
+ attachDbMiddleware: appContext.attachDbMiddleware,
31
+
32
+ // Request & ambient execution context (AsyncLocalStorage)
33
+ requestContextStorage: requestContext.requestContextStorage,
34
+ runWithContext: requestContext.runWithContext,
35
+ withContext: requestContext.withContext,
36
+ getContext: requestContext.getContext,
37
+ useContext: requestContext.useContext,
38
+ useDb: requestContext.useDb,
39
+ useAuth: requestContext.useAuth,
40
+ useUser: requestContext.useUser,
41
+ useLogger: requestContext.useLogger,
42
+ useReq: requestContext.useReq,
43
+ useRes: requestContext.useRes,
44
+ useParams: requestContext.useParams,
45
+ useQuery: requestContext.useQuery,
46
+ useService: requestContext.useService,
47
+ useQueue: requestContext.useQueue,
48
+ useCache: requestContext.useCache,
49
+ useBreaker: requestContext.useBreaker,
50
+ useTransaction: requestContext.useTransaction,
51
+
52
+ // Namespace accessors
53
+ appContext,
54
+ requestContext,
55
+ };