webspresso 0.0.104 → 0.0.106

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 (233) hide show
  1. package/.agents/AGENTS.md +21 -2
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +220 -8
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/FLAGS.md +118 -0
  7. package/.agents/HEALTH.md +118 -0
  8. package/.agents/LIMITS.md +109 -0
  9. package/.agents/LOGGING.md +93 -0
  10. package/.agents/METRICS.md +127 -0
  11. package/.agents/MIGRATION_JOBS.md +95 -0
  12. package/.agents/ORM.md +155 -1
  13. package/.agents/PLUGINS.md +38 -3
  14. package/.agents/QUEUE.md +42 -0
  15. package/.agents/RATE_LIMIT.md +147 -0
  16. package/.agents/RESILIENCE.md +235 -0
  17. package/.agents/SCALE_OUT.md +109 -0
  18. package/.agents/SECRETS.md +117 -0
  19. package/AGENTS.md +16 -0
  20. package/README.md +2 -2
  21. package/bin/commands/add-aws.js +396 -0
  22. package/bin/commands/add-ci.js +523 -0
  23. package/bin/commands/add-cloudflare.js +240 -0
  24. package/bin/commands/add-cloudrun.js +381 -0
  25. package/bin/commands/add-docker.js +6 -6
  26. package/bin/commands/add-migration-job.js +396 -0
  27. package/bin/commands/assets-sync.js +209 -0
  28. package/bin/commands/client-generate.js +52 -0
  29. package/bin/commands/console.js +53 -0
  30. package/bin/commands/db-migrate.js +47 -2
  31. package/bin/commands/db-studio.js +84 -0
  32. package/bin/commands/deploy-check.js +242 -13
  33. package/bin/commands/dev.js +1 -1
  34. package/bin/commands/export.js +87 -0
  35. package/bin/commands/new.js +5 -5
  36. package/bin/commands/openapi.js +1 -1
  37. package/bin/commands/secrets-pull.js +208 -0
  38. package/bin/commands/start.js +1 -1
  39. package/bin/utils/db.js +7 -1
  40. package/bin/utils/project.js +2 -2
  41. package/bin/webspresso.js +22 -0
  42. package/core/app.js +421 -0
  43. package/core/auth/authenticate.js +93 -0
  44. package/core/auth/hash.js +5 -4
  45. package/core/auth/index.js +34 -4
  46. package/core/auth/jwt.js +5 -54
  47. package/core/auth/magic-link.js +4 -5
  48. package/core/auth/manager.js +53 -10
  49. package/core/auth/middleware.js +16 -68
  50. package/core/auth/oauth.js +8 -17
  51. package/core/auth/redis-session.js +1008 -0
  52. package/core/auth/sessions.js +156 -0
  53. package/core/auth/totp.js +2 -1
  54. package/core/cache/byte-size.js +123 -0
  55. package/core/cache/cdn.js +296 -0
  56. package/core/cache/index.js +33 -0
  57. package/core/cache/lru.js +477 -0
  58. package/core/cache/tagged-lru.js +354 -0
  59. package/core/commands/registry.js +6 -20
  60. package/core/console/commands.js +148 -0
  61. package/core/console/context.js +260 -0
  62. package/core/console/eval.js +70 -0
  63. package/core/console/index.js +22 -0
  64. package/core/console/repl.js +102 -0
  65. package/core/early-hints/index.js +267 -0
  66. package/core/errors/domain.js +49 -0
  67. package/core/errors/http.js +31 -0
  68. package/core/errors/index.js +10 -0
  69. package/core/errors/normalize.js +88 -2
  70. package/core/events/discovery.js +6 -24
  71. package/core/events/dispatcher.js +9 -7
  72. package/core/events/signal.js +7 -0
  73. package/core/export/discovery.js +191 -0
  74. package/core/export/exporter.js +277 -0
  75. package/core/export/index.js +24 -0
  76. package/core/flags/evaluator.js +252 -0
  77. package/core/flags/hasher.js +37 -0
  78. package/core/flags/index.js +21 -0
  79. package/core/flags/manager.js +263 -0
  80. package/core/flags/store.js +126 -0
  81. package/core/health/index.js +41 -0
  82. package/core/health/middleware.js +213 -0
  83. package/core/health/normalize.js +140 -0
  84. package/core/health/probes.js +203 -0
  85. package/core/http/adapter.js +74 -0
  86. package/core/http/express-adapter.js +46 -0
  87. package/core/http/hono-adapter.js +531 -0
  88. package/core/http/index.js +41 -0
  89. package/core/http/pipeline.js +485 -0
  90. package/core/index.js +336 -0
  91. package/core/limits/index.js +34 -0
  92. package/core/limits/middleware.js +325 -0
  93. package/core/limits/normalize.js +222 -0
  94. package/core/logger/formatters.js +264 -0
  95. package/core/logger/index.js +260 -0
  96. package/core/logger/trace.js +137 -0
  97. package/core/metrics/collectors.js +125 -0
  98. package/core/metrics/index.js +45 -0
  99. package/core/metrics/middleware.js +230 -0
  100. package/core/metrics/registry.js +366 -0
  101. package/core/orm/batch.js +70 -0
  102. package/core/orm/cache/fingerprint.js +2 -2
  103. package/core/orm/cache/memory-provider.js +27 -110
  104. package/core/orm/d1.js +265 -0
  105. package/core/orm/index.js +176 -13
  106. package/core/orm/json-fields.js +22 -8
  107. package/core/orm/migrate-cloud.js +194 -0
  108. package/core/orm/model.js +10 -0
  109. package/core/orm/pool.js +97 -0
  110. package/core/orm/query-builder.js +72 -9
  111. package/core/orm/repository.js +318 -19
  112. package/core/orm/schema-helpers.js +8 -10
  113. package/core/orm/scopes.js +321 -1
  114. package/core/orm/transaction.js +10 -2
  115. package/core/orm/utils.js +4 -86
  116. package/core/queue/adapters/cloudflare.js +131 -0
  117. package/core/queue/adapters/database.js +4 -8
  118. package/core/queue/adapters/pubsub.js +400 -0
  119. package/core/queue/adapters/sqs.js +419 -0
  120. package/core/queue/adapters/wait-until.js +77 -0
  121. package/core/queue/index.js +8 -0
  122. package/core/queue/manager.js +66 -41
  123. package/core/rate-limit/index.js +22 -0
  124. package/core/rate-limit/limiter.js +219 -0
  125. package/core/rate-limit/stores/memory.js +129 -0
  126. package/core/rate-limit/stores/redis.js +151 -0
  127. package/core/rate-limit/utils.js +112 -0
  128. package/core/resilience/circuit-breaker.js +422 -0
  129. package/core/resilience/errors.js +63 -0
  130. package/core/resilience/index.js +53 -0
  131. package/core/resilience/registry.js +97 -0
  132. package/core/resilience/retry.js +203 -0
  133. package/core/schema-cache.js +9 -2
  134. package/core/secrets/index.js +113 -0
  135. package/core/secrets/manager.js +308 -0
  136. package/core/secrets/providers/aws.js +288 -0
  137. package/core/secrets/providers/env.js +44 -0
  138. package/core/secrets/providers/gcp.js +307 -0
  139. package/core/secrets/providers/memory.js +76 -0
  140. package/core/shutdown/http-adapter.js +4 -1
  141. package/core/shutdown/shutdown-manager.js +129 -19
  142. package/core/stream/index.js +19 -0
  143. package/core/stream/text-stream.js +326 -0
  144. package/core/studio/index.js +43 -0
  145. package/core/studio/inspector.js +258 -0
  146. package/core/studio/server.js +325 -0
  147. package/core/studio/ui.js +681 -0
  148. package/core/utils/array.js +12 -0
  149. package/core/utils/base64url.js +8 -0
  150. package/core/utils/crypto.js +118 -0
  151. package/core/utils/ensure-leading-slash.js +6 -0
  152. package/core/utils/escape-regexp.js +7 -0
  153. package/core/utils/fetch.js +6 -5
  154. package/core/utils/fs.js +144 -0
  155. package/core/utils/hmac.js +6 -0
  156. package/core/utils/json.js +98 -0
  157. package/core/utils/object.js +106 -0
  158. package/core/utils/random.js +9 -0
  159. package/core/utils/regex.js +77 -0
  160. package/core/utils/sha256.js +6 -0
  161. package/core/utils/string.js +87 -0
  162. package/core/utils/strip-trailing-slash.js +6 -0
  163. package/core/utils/time.js +96 -0
  164. package/core/utils/timeout.js +348 -0
  165. package/core/utils/url.js +106 -0
  166. package/core/utils/walk.js +7 -0
  167. package/index.d.ts +1549 -2
  168. package/index.js +272 -2
  169. package/package.json +3 -2
  170. package/plugins/admin-panel/core/api-extensions.js +2 -5
  171. package/plugins/admin-panel/index.js +13 -6
  172. package/plugins/admin-panel/modules/menu.js +1 -1
  173. package/plugins/api-key/crypto.js +3 -3
  174. package/plugins/api-key/index.js +3 -2
  175. package/plugins/api-key/scopes.js +5 -7
  176. package/plugins/audit-log/api-handlers.js +6 -15
  177. package/plugins/audit-log/index.js +3 -1
  178. package/plugins/audit-log/parse.js +3 -3
  179. package/plugins/basic-auth/index.js +3 -2
  180. package/plugins/cors.js +3 -2
  181. package/plugins/csrf/index.js +9 -12
  182. package/plugins/csv/index.js +8 -5
  183. package/plugins/dashboard/app.js +2 -2
  184. package/plugins/dashboard/index.js +5 -5
  185. package/plugins/data-exchange/record-selection.js +2 -5
  186. package/plugins/email/api-handlers.js +5 -18
  187. package/plugins/email/auth-bridge.js +3 -2
  188. package/plugins/email/index.js +11 -9
  189. package/plugins/health-check.js +2 -1
  190. package/plugins/index.js +5 -0
  191. package/plugins/lazy.js +5 -6
  192. package/plugins/mcp/discovery.js +18 -17
  193. package/plugins/polar/src/api.js +2 -1
  194. package/plugins/polar/src/webhooks.js +5 -6
  195. package/plugins/queue/index.js +18 -2
  196. package/plugins/rate-limit/index.js +3 -1
  197. package/plugins/redirect/index.js +16 -9
  198. package/plugins/rls/index.js +170 -0
  199. package/plugins/seo-checker/index.js +2 -2
  200. package/plugins/site-analytics/index.js +3 -1
  201. package/plugins/site-analytics/tracking.js +2 -2
  202. package/plugins/sitemap.js +3 -2
  203. package/plugins/swagger.js +12 -5
  204. package/plugins/upload/gcs-provider.js +148 -0
  205. package/plugins/upload/index.js +136 -77
  206. package/plugins/upload/orphan-pruner.js +4 -3
  207. package/plugins/upload/s3-provider.js +257 -0
  208. package/plugins/webhooks/crypto.js +6 -11
  209. package/plugins/webhooks/dispatcher.js +8 -8
  210. package/plugins/webhooks/index.js +33 -23
  211. package/plugins/webhooks/matcher.js +7 -11
  212. package/plugins/webhooks/sender.js +8 -11
  213. package/plugins/xlsx/index.js +7 -5
  214. package/src/api/api-loader.js +12 -95
  215. package/src/api/define-api.js +1 -0
  216. package/src/app-context.js +41 -5
  217. package/src/client-runtime/constants.js +10 -0
  218. package/src/client-runtime/mount.js +2 -3
  219. package/src/dev/banner.js +6 -2
  220. package/src/dev/client-gen.js +458 -0
  221. package/src/discovery/scan-routes.js +5 -2
  222. package/src/file-router.js +3 -3
  223. package/src/helpers.js +212 -24
  224. package/src/modules/module-discovery.js +1 -9
  225. package/src/pages/page-loader.js +13 -154
  226. package/src/plugin-manager.js +99 -18
  227. package/src/server.js +335 -133
  228. package/src/services/discovery.js +19 -40
  229. package/src/services/executor.js +95 -25
  230. package/src/services/memoize.js +3 -25
  231. package/src/services/registry.js +15 -1
  232. package/src/services/validator.js +38 -20
  233. package/src/template/precompile.js +8 -19
@@ -8,21 +8,10 @@ const fs = require('fs');
8
8
  const path = require('path');
9
9
  const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
10
10
 
11
- const VALID_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
11
+ const { walkDirectoryDetailed } = require('../../core/utils/fs');
12
+ const { toCamelCase } = require('../../core/utils/string');
12
13
 
13
- /**
14
- * Convert kebab-case or snake_case segment to camelCase
15
- * @param {string} str
16
- * @returns {string}
17
- */
18
- function toCamelCase(str) {
19
- if (!str) return '';
20
- const parts = str.split(/[-_]+/);
21
- return parts.map((part, index) => {
22
- if (!part) return '';
23
- return index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1);
24
- }).join('');
25
- }
14
+ const VALID_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
26
15
 
27
16
  /**
28
17
  * Check if a file should be included as a service
@@ -105,36 +94,26 @@ function discoverServices(servicesDir) {
105
94
  return [];
106
95
  }
107
96
 
108
- const results = [];
97
+ const items = walkDirectoryDetailed(servicesDir, {
98
+ extensions: VALID_EXTENSIONS,
99
+ ignorePrefixes: ['.', '_'],
100
+ ignoreNames: ['node_modules'],
101
+ ignoreTests: true,
102
+ });
109
103
 
110
- function scan(currentDir, relativePrefix = '') {
111
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
112
-
113
- for (const entry of entries) {
114
- if (entry.name.startsWith('.') || entry.name.startsWith('_') || entry.name === 'node_modules') {
115
- continue;
116
- }
117
-
118
- const fullPath = path.join(currentDir, entry.name);
119
- const relPath = relativePrefix ? path.join(relativePrefix, entry.name) : entry.name;
120
-
121
- if (entry.isDirectory()) {
122
- scan(fullPath, relPath);
123
- } else if (entry.isFile() && isValidServiceFile(entry.name)) {
124
- const { name, aliases } = filePathToServiceName(relPath);
125
- if (name) {
126
- results.push({
127
- name,
128
- aliases,
129
- filePath: fullPath,
130
- relativePath: relPath.replace(/\\/g, '/'),
131
- });
132
- }
133
- }
104
+ const results = [];
105
+ for (const item of items) {
106
+ const { name, aliases } = filePathToServiceName(item.relativePath);
107
+ if (name) {
108
+ results.push({
109
+ name,
110
+ aliases,
111
+ filePath: item.absolutePath,
112
+ relativePath: item.relativePath,
113
+ });
134
114
  }
135
115
  }
136
116
 
137
- scan(path.resolve(servicesDir));
138
117
  return results;
139
118
  }
140
119
 
@@ -17,6 +17,12 @@ const {
17
17
  runWithAmbientTransaction,
18
18
  hasAmbientTransaction,
19
19
  } = require('../../core/orm/transaction');
20
+ const { ensureArray } = require('../../core/utils/object');
21
+ const {
22
+ CircuitBreaker,
23
+ circuitBreakerRegistry,
24
+ retry,
25
+ } = require('../../core/resilience');
20
26
 
21
27
  const SERVICE_CALL_STACK_SYMBOL = Symbol.for('webspresso.service.call_stack');
22
28
 
@@ -93,7 +99,7 @@ async function checkServiceAuth(serviceDef, ctx, options = {}) {
93
99
  if (!user) {
94
100
  throw new UnauthorizedError(`Authentication required to execute service '${serviceDef.name}'`);
95
101
  }
96
- const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : []);
102
+ const roles = ensureArray(user.roles || user.role);
97
103
  if (!roles.includes(authRequirement)) {
98
104
  throw new ForbiddenError(
99
105
  `Insufficient permissions to execute service '${serviceDef.name}' (required role: '${authRequirement}')`
@@ -107,7 +113,7 @@ async function checkServiceAuth(serviceDef, ctx, options = {}) {
107
113
  if (!user) {
108
114
  throw new UnauthorizedError(`Authentication required to execute service '${serviceDef.name}'`);
109
115
  }
110
- const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : []);
116
+ const roles = ensureArray(user.roles || user.role);
111
117
  const hasAny = authRequirement.some(r => roles.includes(r));
112
118
  if (!hasAny) {
113
119
  throw new ForbiddenError(
@@ -185,15 +191,71 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
185
191
  registry.invalidate(subName, subInput, subCtx || executionContext);
186
192
  executionContext.service.clearCache = (subName) =>
187
193
  registry.clearCache(subName);
194
+ executionContext.breaker = (bName, bOpts) =>
195
+ circuitBreakerRegistry.getBreaker(bName, bOpts);
196
+ executionContext.retry = retry;
188
197
 
189
198
  // Validate input
190
199
  const validatedInput = await validateServiceInput(serviceDef.schema, input, name);
200
+ if (validatedInput && typeof validatedInput === 'object' && !('input' in validatedInput)) {
201
+ try {
202
+ Object.defineProperty(validatedInput, 'input', {
203
+ value: validatedInput,
204
+ enumerable: false,
205
+ configurable: true,
206
+ });
207
+ } catch (_) {}
208
+ }
191
209
 
192
210
  const startTime = Date.now();
193
211
  const logger = executionContext.logger || registry.logger || null;
194
212
 
195
- const timeoutConfig = options.timeout ?? serviceDef.timeout;
196
- const timeoutMs = timeoutConfig ? parseTtlMs(timeoutConfig) : null;
213
+ const timeoutConfig = options.timeout ?? serviceDef.timeout ?? executionContext?.timeouts?.service ?? registry?.timeouts?.service ?? registry?.timeout;
214
+ const timeoutMs = (timeoutConfig !== undefined && timeoutConfig !== null && timeoutConfig !== false)
215
+ ? parseTtlMs(timeoutConfig)
216
+ : null;
217
+
218
+ // Resolve circuit breaker configuration
219
+ let breaker = null;
220
+ const cbConfig = options.skipCircuitBreaker === true
221
+ ? null
222
+ : (options.circuitBreaker !== undefined ? options.circuitBreaker : serviceDef.circuitBreaker);
223
+
224
+ if (cbConfig) {
225
+ if (cbConfig instanceof CircuitBreaker) {
226
+ breaker = cbConfig;
227
+ } else if (typeof cbConfig === 'string') {
228
+ breaker = circuitBreakerRegistry.getBreaker(cbConfig);
229
+ } else if (typeof cbConfig === 'object' && cbConfig !== null) {
230
+ const breakerName = cbConfig.name || name;
231
+ breaker = circuitBreakerRegistry.getBreaker(breakerName, cbConfig);
232
+ } else if (cbConfig === true) {
233
+ breaker = circuitBreakerRegistry.getBreaker(name);
234
+ }
235
+ }
236
+
237
+ // Resolve retry configuration
238
+ const retryConfig = options.skipRetry === true
239
+ ? null
240
+ : (options.retry !== undefined ? options.retry : serviceDef.retry);
241
+
242
+ const runWithRetry = async (fn) => {
243
+ if (!retryConfig) {
244
+ return await fn();
245
+ }
246
+ const retryOpts = typeof retryConfig === 'number'
247
+ ? { retries: retryConfig }
248
+ : (typeof retryConfig === 'object' && retryConfig !== null ? retryConfig : { retries: 3 });
249
+
250
+ return await retry((attempt) => fn(attempt), retryOpts);
251
+ };
252
+
253
+ const runWithBreaker = async (fn) => {
254
+ if (!breaker) {
255
+ return await fn(validatedInput, executionContext);
256
+ }
257
+ return await breaker.execute(fn, validatedInput, executionContext);
258
+ };
197
259
 
198
260
  // Helper to execute handler with optional database transaction
199
261
  const executeHandler = async (activeCtx) => {
@@ -227,6 +289,9 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
227
289
  registry.invalidate(subName, subInput, subCtx || txCtx);
228
290
  txCtx.service.clearCache = (subName) =>
229
291
  registry.clearCache(subName);
292
+ txCtx.breaker = (bName, bOpts) =>
293
+ circuitBreakerRegistry.getBreaker(bName, bOpts);
294
+ txCtx.retry = retry;
230
295
  return await serviceDef.handler(validatedInput, txCtx);
231
296
  });
232
297
  });
@@ -236,31 +301,36 @@ async function executeService(registry, name, input = {}, ctx = {}, options = {}
236
301
  };
237
302
 
238
303
  try {
239
- let result;
240
-
241
- if (timeoutMs && timeoutMs > 0) {
242
- let timerId;
243
- const timeoutPromise = new Promise((_, reject) => {
244
- timerId = setTimeout(() => {
245
- const timeoutErr = new WebspressoError(`Service '${name}' timed out after ${timeoutMs}ms`);
246
- timeoutErr.code = 'SERVICE_TIMEOUT';
247
- timeoutErr.statusCode = 504;
248
- timeoutErr.service = name;
249
- reject(timeoutErr);
250
- }, timeoutMs);
251
- });
304
+ const invokeOperation = () => {
305
+ if (timeoutMs && timeoutMs > 0) {
306
+ let timerId;
307
+ const timeoutPromise = new Promise((_, reject) => {
308
+ timerId = setTimeout(() => {
309
+ const timeoutErr = new WebspressoError(`Service '${name}' timed out after ${timeoutMs}ms`);
310
+ timeoutErr.code = 'SERVICE_TIMEOUT';
311
+ timeoutErr.statusCode = 504;
312
+ timeoutErr.service = name;
313
+ reject(timeoutErr);
314
+ }, timeoutMs);
315
+ if (timerId && typeof timerId.unref === 'function') {
316
+ timerId.unref();
317
+ }
318
+ });
252
319
 
253
- try {
254
- result = await Promise.race([
320
+ return Promise.race([
255
321
  executeHandler(executionContext),
256
322
  timeoutPromise,
257
- ]);
258
- } finally {
259
- clearTimeout(timerId);
323
+ ]).finally(() => {
324
+ if (timerId) {
325
+ clearTimeout(timerId);
326
+ }
327
+ });
260
328
  }
261
- } else {
262
- result = await executeHandler(executionContext);
263
- }
329
+
330
+ return executeHandler(executionContext);
331
+ };
332
+
333
+ const result = await runWithBreaker(() => runWithRetry(invokeOperation));
264
334
 
265
335
  const duration = Date.now() - startTime;
266
336
  if (logger && typeof logger.debug === 'function') {
@@ -4,36 +4,14 @@
4
4
  * @module src/services/memoize
5
5
  */
6
6
 
7
+ const { parseDurationMs } = require('../../core/utils/time');
8
+
7
9
  /**
8
10
  * Parse TTL duration into milliseconds
9
11
  * @param {string|number|boolean} ttl - Duration string ('5s', '1m', '5m', '1h', '1d'), ms number, or boolean
10
12
  * @returns {number} Milliseconds
11
13
  */
12
- function parseTtlMs(ttl) {
13
- if (typeof ttl === 'number') {
14
- return ttl > 0 ? ttl : 60_000;
15
- }
16
- if (ttl === true || !ttl) {
17
- return 60_000; // Default 1 minute
18
- }
19
- if (typeof ttl === 'string') {
20
- const match = ttl.trim().match(/^(\d+)\s*([smhd])?$/i);
21
- if (match) {
22
- const val = parseInt(match[1], 10);
23
- const unit = (match[2] || 's').toLowerCase();
24
- switch (unit) {
25
- case 's': return val * 1_000;
26
- case 'm': return val * 60_000;
27
- case 'h': return val * 3_600_000;
28
- case 'd': return val * 86_400_000;
29
- default: return val * 1_000;
30
- }
31
- }
32
- const num = parseInt(ttl, 10);
33
- return !Number.isNaN(num) && num > 0 ? num : 60_000;
34
- }
35
- return 60_000;
36
- }
14
+ const parseTtlMs = parseDurationMs;
37
15
 
38
16
  /**
39
17
  * Safely deep clone an object or array to prevent cache mutation
@@ -21,6 +21,8 @@ class ServiceRegistry {
21
21
  this.servicesDir = options.servicesDir ? path.resolve(options.servicesDir) : null;
22
22
  this.isDev = Boolean(options.isDev);
23
23
  this.logger = options.logger || null;
24
+ this.timeouts = options.timeouts || null;
25
+ this.timeout = options.timeout !== undefined ? options.timeout : null;
24
26
 
25
27
  /** @type {Map<string, Object>} */
26
28
  this.services = new Map();
@@ -107,6 +109,8 @@ class ServiceRegistry {
107
109
  cache: definition.cache,
108
110
  timeout: definition.timeout,
109
111
  transaction: definition.transaction,
112
+ circuitBreaker: definition.circuitBreaker,
113
+ retry: definition.retry,
110
114
  };
111
115
 
112
116
  this.services.set(name, serviceDef);
@@ -125,7 +129,15 @@ class ServiceRegistry {
125
129
  * @param {Object|Function} definition - Service definition or handler function
126
130
  * @returns {ServiceRegistry}
127
131
  */
128
- register(name, definition, options = {}) {
132
+ register(nameOrDef, maybeDef, options = {}) {
133
+ let name = nameOrDef;
134
+ let definition = maybeDef;
135
+
136
+ if (nameOrDef && typeof nameOrDef === 'object' && nameOrDef.name && maybeDef === undefined) {
137
+ name = nameOrDef.name;
138
+ definition = nameOrDef;
139
+ }
140
+
129
141
  if (!name || typeof name !== 'string') {
130
142
  throw new WebspressoError('Service name must be a non-empty string');
131
143
  }
@@ -164,6 +176,8 @@ class ServiceRegistry {
164
176
  cache: def.cache,
165
177
  timeout: def.timeout,
166
178
  transaction: def.transaction,
179
+ circuitBreaker: def.circuitBreaker,
180
+ retry: def.retry,
167
181
  };
168
182
 
169
183
  this.services.set(name, serviceDef);
@@ -72,6 +72,26 @@ function descriptorToZod(typeName) {
72
72
  return finalSchema;
73
73
  }
74
74
 
75
+ function shapeFromDescriptor(descriptorObj) {
76
+ const shape = {};
77
+ for (const [key, val] of Object.entries(descriptorObj)) {
78
+ if (typeof val === 'string') {
79
+ shape[key] = descriptorToZod(val);
80
+ } else if (val instanceof z.ZodType || (val && typeof val.safeParse === 'function')) {
81
+ shape[key] = val;
82
+ } else if (typeof val === 'object' && val !== null) {
83
+ const isOpt = val.optional === true || val.required === false;
84
+ let base = val.type ? descriptorToZod(String(val.type)) : z.any();
85
+ shape[key] = isOpt ? base.optional() : base;
86
+ } else if (typeof val === 'function') {
87
+ shape[key] = z.custom(val);
88
+ } else {
89
+ shape[key] = z.any();
90
+ }
91
+ }
92
+ return z.object(shape);
93
+ }
94
+
75
95
  const compiledSchemaCache = new WeakMap();
76
96
 
77
97
  /**
@@ -100,6 +120,14 @@ function compileServiceSchema(rawSchema) {
100
120
  const res = rawSchema({ z: zForServices });
101
121
  if (res && (res instanceof z.ZodType || typeof res.safeParse === 'function')) {
102
122
  compiled = res;
123
+ } else if (res && typeof res === 'object' && !(res instanceof Promise) && typeof res.then !== 'function' && (res.constructor === Object || !res.constructor)) {
124
+ if (res.input && (res.input instanceof z.ZodType || typeof res.input.safeParse === 'function' || (typeof res.input === 'object' && res.input !== null))) {
125
+ compiled = (res.input instanceof z.ZodType || typeof res.input.safeParse === 'function')
126
+ ? res.input
127
+ : shapeFromDescriptor(res.input);
128
+ } else {
129
+ compiled = shapeFromDescriptor(res);
130
+ }
103
131
  }
104
132
  } catch (e) {
105
133
  // If invoking with { z } throws, treat as a direct validator function
@@ -108,28 +136,18 @@ function compileServiceSchema(rawSchema) {
108
136
  compiled = rawSchema;
109
137
  }
110
138
  } else if (typeof rawSchema === 'object' && rawSchema !== null) {
111
- // If object descriptor: { id: 'number', name: 'string', email: 'email' }
112
- const keys = Object.keys(rawSchema);
113
- if (keys.length === 0) {
114
- compiled = null;
139
+ if (rawSchema.input && (rawSchema.input instanceof z.ZodType || typeof rawSchema.input.safeParse === 'function' || (typeof rawSchema.input === 'object' && rawSchema.input !== null))) {
140
+ compiled = (rawSchema.input instanceof z.ZodType || typeof rawSchema.input.safeParse === 'function')
141
+ ? rawSchema.input
142
+ : shapeFromDescriptor(rawSchema.input);
115
143
  } 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
- }
144
+ // If object descriptor: { id: 'number', name: 'string', email: 'email' }
145
+ const keys = Object.keys(rawSchema);
146
+ if (keys.length === 0) {
147
+ compiled = null;
148
+ } else {
149
+ compiled = shapeFromDescriptor(rawSchema);
131
150
  }
132
- compiled = z.object(shape);
133
151
  }
134
152
  } else {
135
153
  compiled = rawSchema;
@@ -11,6 +11,7 @@ const fs = require('fs');
11
11
  const path = require('path');
12
12
  const nunjucks = require('nunjucks');
13
13
  const { parseNjkFrontmatter, frontmatterToPatches, seedProdRouteCache } = require('../njk-frontmatter');
14
+ const { walkDirectoryDetailed } = require('../../core/utils/fs');
14
15
 
15
16
  const DEFAULT_PRECOMPILED_REL_PATH = path.join('.webspresso', 'templates.compiled.js');
16
17
 
@@ -21,25 +22,13 @@ const DEFAULT_PRECOMPILED_REL_PATH = path.join('.webspresso', 'templates.compile
21
22
  * @returns {Array<{ absolutePath: string, relativePath: string }>}
22
23
  */
23
24
  function scanTemplateDir(dir, baseDir = dir) {
24
- if (!fs.existsSync(dir)) return [];
25
- const entries = fs.readdirSync(dir, { withFileTypes: true });
26
- const results = [];
27
-
28
- for (const entry of entries) {
29
- const full = path.join(dir, entry.name);
30
- if (entry.isDirectory()) {
31
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
32
- results.push(...scanTemplateDir(full, baseDir));
33
- } else if (entry.isFile()) {
34
- const ext = path.extname(entry.name);
35
- if (ext === '.njk' || ext === '.html') {
36
- const rel = path.relative(baseDir, full).split(path.sep).join('/');
37
- results.push({ absolutePath: full, relativePath: rel });
38
- }
39
- }
40
- }
41
-
42
- return results;
25
+ return walkDirectoryDetailed(dir, {
26
+ extensions: ['.njk', '.html'],
27
+ ignorePrefixes: ['.'],
28
+ ignoreNames: ['node_modules'],
29
+ ignoreTests: false,
30
+ baseDir,
31
+ });
43
32
  }
44
33
 
45
34
  /**