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
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency String Utilities
5
+ * Fast, standard case conversion and string manipulation helpers.
6
+ * @module core/utils/string
7
+ */
8
+
9
+ /**
10
+ * Convert a string (kebab-case, snake_case, spaces) to camelCase.
11
+ *
12
+ * @param {string|unknown} str - Input string
13
+ * @returns {string} camelCase formatted string
14
+ * @example
15
+ * toCamelCase('order-items') // -> 'orderItems'
16
+ * toCamelCase('get_by_id') // -> 'getById'
17
+ * toCamelCase('user_profile_data') // -> 'userProfileData'
18
+ */
19
+ function toCamelCase(str) {
20
+ if (!str || typeof str !== 'string') return '';
21
+ const trimmed = str.replace(/^[-_\s]+|[-_\s]+$/g, '');
22
+ if (!trimmed) return '';
23
+ const res = trimmed.replace(/[-_\s]+([a-zA-Z0-9])/g, (_, letter) => letter.toUpperCase());
24
+ return res.charAt(0).toLowerCase() + res.slice(1);
25
+ }
26
+
27
+ /**
28
+ * Convert a string (camelCase, kebab-case, spaces) to snake_case.
29
+ *
30
+ * @param {string|unknown} str - Input string
31
+ * @returns {string} snake_case formatted string
32
+ * @example
33
+ * toSnakeCase('fooBarBaz') // -> 'foo_bar_baz'
34
+ * toSnakeCase('order-items') // -> 'order_items'
35
+ */
36
+ function toSnakeCase(str) {
37
+ if (!str || typeof str !== 'string') return '';
38
+ return str
39
+ .trim()
40
+ .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
41
+ .replace(/[-_\s]+/g, '_')
42
+ .replace(/^_+|_+$/g, '')
43
+ .toLowerCase();
44
+ }
45
+
46
+ /**
47
+ * Convert a string (camelCase, snake_case, spaces) to kebab-case.
48
+ *
49
+ * @param {string|unknown} str - Input string
50
+ * @returns {string} kebab-case formatted string
51
+ * @example
52
+ * toKebabCase('orderItems') // -> 'order-items'
53
+ * toKebabCase('foo_bar_baz') // -> 'foo-bar-baz'
54
+ */
55
+ function toKebabCase(str) {
56
+ if (!str || typeof str !== 'string') return '';
57
+ return str
58
+ .trim()
59
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
60
+ .replace(/[-_\s]+/g, '-')
61
+ .replace(/^-+|-+$/g, '')
62
+ .toLowerCase();
63
+ }
64
+
65
+ /**
66
+ * Convert a string to PascalCase.
67
+ *
68
+ * @param {string|unknown} str - Input string
69
+ * @returns {string} PascalCase formatted string
70
+ * @example
71
+ * toPascalCase('order-items') // -> 'OrderItems'
72
+ * toPascalCase('user_id') // -> 'UserId'
73
+ */
74
+ function toPascalCase(str) {
75
+ const camel = toCamelCase(str);
76
+ if (!camel) return '';
77
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
78
+ }
79
+
80
+ module.exports = {
81
+ toCamelCase,
82
+ snakeToCamel: toCamelCase,
83
+ toSnakeCase,
84
+ camelToSnake: toSnakeCase,
85
+ toKebabCase,
86
+ toPascalCase,
87
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { stripTrailingSlash } = require('./url');
4
+
5
+ module.exports = stripTrailingSlash;
6
+ module.exports.stripTrailingSlash = stripTrailingSlash;
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency Time & Duration Utilities
5
+ * Fast, robust duration parsing and formatting helpers for cache TTL, JWT expiration, timeouts, and delays.
6
+ * @module core/utils/time
7
+ */
8
+
9
+ /**
10
+ * Parses duration / TTL input into seconds.
11
+ * Compatible with core/auth/jwt parseDuration contract.
12
+ *
13
+ * @param {string|number|unknown} duration - Duration string (e.g. '10s', '15m', '2h', '7d', '500') or seconds number
14
+ * @param {number} [defaultSec=3600] - Default seconds if input is invalid or missing
15
+ * @returns {number} Seconds
16
+ */
17
+ function parseDurationSec(duration, defaultSec = 3600) {
18
+ if (typeof duration === 'number') {
19
+ return duration;
20
+ }
21
+ if (typeof duration !== 'string') {
22
+ return defaultSec;
23
+ }
24
+
25
+ const match = duration.trim().match(/^(\d+)\s*([smhdw])?$/i);
26
+ if (!match) {
27
+ const parsed = parseInt(duration, 10);
28
+ return isNaN(parsed) ? defaultSec : parsed;
29
+ }
30
+
31
+ const value = parseInt(match[1], 10);
32
+ const unit = (match[2] || 's').toLowerCase();
33
+
34
+ switch (unit) {
35
+ case 's': return value;
36
+ case 'm': return value * 60;
37
+ case 'h': return value * 3600;
38
+ case 'd': return value * 86400;
39
+ case 'w': return value * 604800;
40
+ default: return value;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Parses TTL / duration input into milliseconds.
46
+ * Compatible with src/services/memoize parseTtlMs contract.
47
+ *
48
+ * @param {string|number|boolean|unknown} ttl - Duration string ('5s', '1m', '2h', '1d', '100'), ms number, or boolean
49
+ * @param {number} [defaultMs=60000] - Default milliseconds if input is invalid or missing
50
+ * @returns {number} Milliseconds
51
+ */
52
+ function parseDurationMs(ttl, defaultMs = 60000) {
53
+ if (typeof ttl === 'number') {
54
+ return ttl > 0 ? ttl : defaultMs;
55
+ }
56
+ if (ttl === true || !ttl) {
57
+ return defaultMs;
58
+ }
59
+ if (typeof ttl === 'string') {
60
+ const match = ttl.trim().match(/^(\d+)\s*([smhdw]|ms)?$/i);
61
+ if (match) {
62
+ const val = parseInt(match[1], 10);
63
+ const unit = (match[2] || 's').toLowerCase();
64
+ switch (unit) {
65
+ case 'ms': return val;
66
+ case 's': return val * 1_000;
67
+ case 'm': return val * 60_000;
68
+ case 'h': return val * 3_600_000;
69
+ case 'd': return val * 86_400_000;
70
+ case 'w': return val * 604_800_000;
71
+ default: return val * 1_000;
72
+ }
73
+ }
74
+ const num = parseInt(ttl, 10);
75
+ return !Number.isNaN(num) && num > 0 ? num : defaultMs;
76
+ }
77
+ return defaultMs;
78
+ }
79
+
80
+ /**
81
+ * Returns a Promise that resolves after the specified milliseconds.
82
+ *
83
+ * @param {number} ms - Milliseconds to sleep
84
+ * @returns {Promise<void>}
85
+ */
86
+ function sleep(ms) {
87
+ return new Promise(resolve => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
88
+ }
89
+
90
+ module.exports = {
91
+ parseDurationSec,
92
+ parseDurationMs,
93
+ parseDuration: parseDurationSec,
94
+ parseTtlMs: parseDurationMs,
95
+ sleep,
96
+ };
@@ -0,0 +1,348 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency Centralized Timeout Engine & Middleware
5
+ *
6
+ * Provides:
7
+ * - Centralized global timeouts configuration hierarchy (`normalizeTimeouts`, `DEFAULT_TIMEOUTS`)
8
+ * - Universal async wrapper with timer unref and AbortSignal support (`withTimeout`)
9
+ * - Native, zero-dependency drop-in replacement for connect-timeout (`createTimeoutMiddleware`)
10
+ * - Semantic `TimeoutError`
11
+ *
12
+ * @module core/utils/timeout
13
+ */
14
+
15
+ const { parseDurationMs } = require('./time');
16
+ const { WebspressoError } = require('../errors/base');
17
+
18
+ /**
19
+ * Default framework timeout thresholds (in milliseconds)
20
+ */
21
+ const DEFAULT_TIMEOUTS = Object.freeze({
22
+ request: 30_000, // HTTP request processing timeout (30s)
23
+ service: 15_000, // Services layer execution timeout (15s)
24
+ queue: 30_000, // Background job queue worker timeout (30s)
25
+ fetch: 10_000, // Outgoing external fetchWithTimeout (10s)
26
+ webhook: 10_000, // Outgoing webhook dispatch timeout (10s)
27
+ shutdown: 10_000, // Graceful shutdown server draining timeout (10s)
28
+ loader: 10_000, // SSR page data loader load() execution timeout (10s)
29
+ });
30
+
31
+ /**
32
+ * Normalizes application timeout settings from createApp options into a single,
33
+ * cohesive configuration object with sensible defaults and backward compatibility.
34
+ *
35
+ * @param {Object} [options={}] - Options passed to createApp() or framework components
36
+ * @returns {Readonly<{
37
+ * request: number | false,
38
+ * service: number | false,
39
+ * queue: number | false,
40
+ * fetch: number | false,
41
+ * webhook: number | false,
42
+ * shutdown: number | false,
43
+ * loader: number | false,
44
+ * }>}
45
+ */
46
+ function normalizeTimeouts(options = {}) {
47
+ const rootTimeouts = (options && typeof options.timeouts === 'object' && options.timeouts !== null)
48
+ ? options.timeouts
49
+ : {};
50
+
51
+ function parseVal(val, defaultVal) {
52
+ if (val === false || val === 'false' || val === 0 || val === '0') {
53
+ return false;
54
+ }
55
+ if (val === null || val === undefined) {
56
+ return defaultVal;
57
+ }
58
+ return parseDurationMs(val, defaultVal);
59
+ }
60
+
61
+ // Request timeout: timeouts.request -> options.timeout -> options.server.timeout -> default (30s)
62
+ const requestRaw = rootTimeouts.request !== undefined
63
+ ? rootTimeouts.request
64
+ : (options.timeout !== undefined
65
+ ? options.timeout
66
+ : (options.server?.timeout !== undefined ? options.server.timeout : DEFAULT_TIMEOUTS.request));
67
+
68
+ // Service timeout: timeouts.service -> options.services.timeout -> options.serviceTimeout -> default (15s)
69
+ const serviceRaw = rootTimeouts.service !== undefined
70
+ ? rootTimeouts.service
71
+ : (options.services?.timeout !== undefined
72
+ ? options.services.timeout
73
+ : (options.serviceTimeout !== undefined ? options.serviceTimeout : DEFAULT_TIMEOUTS.service));
74
+
75
+ // Queue timeout: timeouts.queue -> options.queue.timeout -> options.queueTimeout -> default (30s)
76
+ const queueRaw = rootTimeouts.queue !== undefined
77
+ ? rootTimeouts.queue
78
+ : (options.queue?.timeout !== undefined
79
+ ? options.queue.timeout
80
+ : (options.queueTimeout !== undefined ? options.queueTimeout : DEFAULT_TIMEOUTS.queue));
81
+
82
+ // Fetch timeout: timeouts.fetch -> options.fetch.timeout -> options.fetchTimeout -> default (10s)
83
+ const fetchRaw = rootTimeouts.fetch !== undefined
84
+ ? rootTimeouts.fetch
85
+ : (options.fetch?.timeout !== undefined
86
+ ? options.fetch.timeout
87
+ : (options.fetchTimeout !== undefined ? options.fetchTimeout : DEFAULT_TIMEOUTS.fetch));
88
+
89
+ // Webhook timeout: timeouts.webhook -> options.webhooks.timeout -> options.webhookTimeout -> default (10s)
90
+ const webhookRaw = rootTimeouts.webhook !== undefined
91
+ ? rootTimeouts.webhook
92
+ : (options.webhooks?.timeout !== undefined
93
+ ? options.webhooks.timeout
94
+ : (options.webhookTimeout !== undefined ? options.webhookTimeout : DEFAULT_TIMEOUTS.webhook));
95
+
96
+ // Shutdown timeout: timeouts.shutdown -> options.shutdown.timeout -> options.server.shutdown.timeout -> default (10s)
97
+ const shutdownRaw = rootTimeouts.shutdown !== undefined
98
+ ? rootTimeouts.shutdown
99
+ : (options.shutdown?.timeout !== undefined
100
+ ? options.shutdown.timeout
101
+ : (options.server?.shutdown?.timeout !== undefined ? options.server.shutdown.timeout : DEFAULT_TIMEOUTS.shutdown));
102
+
103
+ // Page loader timeout: timeouts.loader -> options.loader.timeout -> options.loaderTimeout -> default (10s)
104
+ const loaderRaw = rootTimeouts.loader !== undefined
105
+ ? rootTimeouts.loader
106
+ : (options.loader?.timeout !== undefined
107
+ ? options.loader.timeout
108
+ : (options.loaderTimeout !== undefined ? options.loaderTimeout : DEFAULT_TIMEOUTS.loader));
109
+
110
+ return Object.freeze({
111
+ request: parseVal(requestRaw, DEFAULT_TIMEOUTS.request),
112
+ service: parseVal(serviceRaw, DEFAULT_TIMEOUTS.service),
113
+ queue: parseVal(queueRaw, DEFAULT_TIMEOUTS.queue),
114
+ fetch: parseVal(fetchRaw, DEFAULT_TIMEOUTS.fetch),
115
+ webhook: parseVal(webhookRaw, DEFAULT_TIMEOUTS.webhook),
116
+ shutdown: parseVal(shutdownRaw, DEFAULT_TIMEOUTS.shutdown),
117
+ loader: parseVal(loaderRaw, DEFAULT_TIMEOUTS.loader),
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Standard framework Timeout Error
123
+ */
124
+ class TimeoutError extends WebspressoError {
125
+ /**
126
+ * @param {string} [message='Operation timed out']
127
+ * @param {Object} [options]
128
+ * @param {number} [options.timeout]
129
+ * @param {string} [options.code='ETIMEDOUT']
130
+ * @param {number} [options.status=504]
131
+ * @param {number} [options.statusCode=504]
132
+ * @param {string} [options.scope='timeout']
133
+ */
134
+ constructor(message = 'Operation timed out', options = {}) {
135
+ super(message, { code: options.code || 'ETIMEDOUT', ...options });
136
+ this.name = 'TimeoutError';
137
+ this.status = typeof options.status === 'number' ? options.status : (options.statusCode || 504);
138
+ this.statusCode = this.status;
139
+ this.timeout = options.timeout !== undefined ? options.timeout : null;
140
+ this.scope = options.scope || 'timeout';
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Universal async execution wrapper with timeout, timer unreferencing,
146
+ * and AbortController / AbortSignal cancellation support.
147
+ *
148
+ * @template T
149
+ * @param {Promise<T> | ((signal?: AbortSignal) => Promise<T> | T)} promiseOrFn
150
+ * @param {number | string | boolean} timeout - Timeout duration (ms number, '5s', or false)
151
+ * @param {Object} [options={}]
152
+ * @param {AbortController} [options.controller] - Optional AbortController to abort on timeout
153
+ * @param {AbortSignal} [options.signal] - Optional external AbortSignal to monitor
154
+ * @param {string} [options.errorMessage] - Custom error message
155
+ * @param {string} [options.errorCode='ETIMEDOUT'] - Custom error code
156
+ * @param {number} [options.statusCode=504] - HTTP status code
157
+ * @param {string} [options.scope='timeout'] - Scope identifier ('service', 'queue', 'fetch', etc.)
158
+ * @param {boolean} [options.unref=true] - Whether to unref the timer
159
+ * @param {((timeoutMs: number) => Error)} [options.createError] - Custom error factory
160
+ * @returns {Promise<T>}
161
+ */
162
+ async function withTimeout(promiseOrFn, timeout, options = {}) {
163
+ const timeoutMs = (timeout === false || timeout === 0 || timeout === null || timeout === undefined)
164
+ ? 0
165
+ : parseDurationMs(timeout, 0);
166
+
167
+ // If timeout is disabled or <= 0, run directly
168
+ if (timeoutMs <= 0) {
169
+ if (typeof promiseOrFn === 'function') {
170
+ return await promiseOrFn(options.controller?.signal || options.signal);
171
+ }
172
+ return await promiseOrFn;
173
+ }
174
+
175
+ // If external signal is already aborted, reject immediately
176
+ if (options.signal && options.signal.aborted) {
177
+ const reason = options.signal.reason || new Error('This operation was aborted');
178
+ throw reason;
179
+ }
180
+
181
+ let timer = null;
182
+ let callerSignalListener = null;
183
+
184
+ try {
185
+ const timeoutPromise = new Promise((_, reject) => {
186
+ timer = setTimeout(() => {
187
+ timer = null;
188
+ let err;
189
+ if (typeof options.createError === 'function') {
190
+ err = options.createError(timeoutMs);
191
+ } else {
192
+ const msg = options.errorMessage || `Operation timed out after ${timeoutMs}ms`;
193
+ err = new TimeoutError(msg, {
194
+ timeout: timeoutMs,
195
+ code: options.errorCode || 'ETIMEDOUT',
196
+ statusCode: options.statusCode || 504,
197
+ scope: options.scope || 'timeout',
198
+ });
199
+ }
200
+
201
+ if (options.controller && typeof options.controller.abort === 'function') {
202
+ try {
203
+ options.controller.abort(err);
204
+ } catch (_) {}
205
+ }
206
+
207
+ reject(err);
208
+ }, timeoutMs);
209
+
210
+ if (options.unref !== false && timer && typeof timer.unref === 'function') {
211
+ timer.unref();
212
+ }
213
+ });
214
+
215
+ const executionSignal = options.controller ? options.controller.signal : options.signal;
216
+ const executionPromise = typeof promiseOrFn === 'function'
217
+ ? promiseOrFn(executionSignal)
218
+ : promiseOrFn;
219
+
220
+ if (options.signal) {
221
+ const abortPromise = new Promise((_, reject) => {
222
+ callerSignalListener = () => {
223
+ const reason = options.signal.reason || new Error('This operation was aborted');
224
+ reject(reason);
225
+ };
226
+ options.signal.addEventListener('abort', callerSignalListener, { once: true });
227
+ });
228
+
229
+ return await Promise.race([executionPromise, timeoutPromise, abortPromise]);
230
+ }
231
+
232
+ return await Promise.race([executionPromise, timeoutPromise]);
233
+ } finally {
234
+ if (timer) {
235
+ clearTimeout(timer);
236
+ timer = null;
237
+ }
238
+ if (options.signal && callerSignalListener) {
239
+ options.signal.removeEventListener('abort', callerSignalListener);
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Creates an Express request timeout middleware.
246
+ * Zero-dependency replacement for connect-timeout with native AbortController,
247
+ * timer unreferencing, and automatic request cleanup.
248
+ *
249
+ * @param {string | number | boolean} timeoutConfig - Timeout duration (e.g. '30s', 30000, or false)
250
+ * @param {Object} [options={}]
251
+ * @param {boolean} [options.respond=true] - Whether to pass 503 error to next() when timed out
252
+ * @returns {Function} Express middleware (req, res, next)
253
+ */
254
+ function createTimeoutMiddleware(timeoutConfig, options = {}) {
255
+ if (timeoutConfig === false || timeoutConfig === 'false' || timeoutConfig === 0) {
256
+ return function disabledTimeoutMiddleware(req, res, next) {
257
+ req.timedout = false;
258
+ req.clearTimeout = () => {};
259
+ next();
260
+ };
261
+ }
262
+
263
+ const timeoutMs = parseDurationMs(timeoutConfig, DEFAULT_TIMEOUTS.request);
264
+ if (timeoutMs <= 0) {
265
+ return function zeroTimeoutMiddleware(req, res, next) {
266
+ req.timedout = false;
267
+ req.clearTimeout = () => {};
268
+ next();
269
+ };
270
+ }
271
+
272
+ return function requestTimeoutMiddleware(req, res, next) {
273
+ req.timedout = false;
274
+
275
+ // Attach native AbortController and AbortSignal to req
276
+ let controller = null;
277
+ if (typeof AbortController !== 'undefined') {
278
+ controller = new AbortController();
279
+ req.abortController = controller;
280
+ req.signal = controller.signal;
281
+ }
282
+
283
+ let timer = setTimeout(() => {
284
+ timer = null;
285
+ req.timedout = true;
286
+
287
+ const err = new Error(`Response timeout after ${timeoutMs}ms`);
288
+ err.timeout = timeoutMs;
289
+ err.status = 503;
290
+ err.statusCode = 503;
291
+ err.code = 'ETIMEDOUT';
292
+
293
+ if (controller) {
294
+ try {
295
+ controller.abort(err);
296
+ } catch (_) {}
297
+ }
298
+
299
+ try {
300
+ req.emit('timeout', timeoutMs);
301
+ } catch (_) {}
302
+
303
+ if (!res.headersSent && !res.writableEnded) {
304
+ if (options.respond !== false) {
305
+ next(err);
306
+ }
307
+ }
308
+ }, timeoutMs);
309
+
310
+ if (timer && typeof timer.unref === 'function') {
311
+ timer.unref();
312
+ }
313
+
314
+ req.clearTimeout = function clearTimeoutHandler() {
315
+ if (timer) {
316
+ clearTimeout(timer);
317
+ timer = null;
318
+ }
319
+ };
320
+
321
+ const cleanup = () => {
322
+ if (timer) {
323
+ clearTimeout(timer);
324
+ timer = null;
325
+ }
326
+ };
327
+
328
+ res.once('finish', cleanup);
329
+ res.once('close', () => {
330
+ cleanup();
331
+ if (!res.writableEnded && !req.timedout && controller) {
332
+ try {
333
+ controller.abort(new Error('Client connection closed'));
334
+ } catch (_) {}
335
+ }
336
+ });
337
+
338
+ next();
339
+ };
340
+ }
341
+
342
+ module.exports = {
343
+ DEFAULT_TIMEOUTS,
344
+ normalizeTimeouts,
345
+ TimeoutError,
346
+ withTimeout,
347
+ createTimeoutMiddleware,
348
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency URL & Path Normalization Utilities
5
+ * High-performance, linear-time URL and path formatting helpers.
6
+ * @module core/utils/url
7
+ */
8
+
9
+ const { trimUrlPathSlashes } = require('../url-path-normalize');
10
+
11
+ /**
12
+ * Safely removes trailing slash(es) from a URL or path.
13
+ *
14
+ * @param {string|unknown} input - URL, pathname, or path fragment
15
+ * @param {Object} [options]
16
+ * @param {boolean} [options.preserveRoot=false] - If true and input resolves to '/', keeps single '/'
17
+ * @returns {string} Truncated string without trailing slash(es)
18
+ */
19
+ function stripTrailingSlash(input, options = {}) {
20
+ if (input == null) return '';
21
+ let s = String(input);
22
+ if (s.length === 0) return '';
23
+
24
+ const preserveRoot = options.preserveRoot === true;
25
+
26
+ let end = s.length;
27
+ while (end > 0 && s.charCodeAt(end - 1) === 47 /* / */) {
28
+ end--;
29
+ }
30
+
31
+ if (end === 0) {
32
+ return preserveRoot ? '/' : '';
33
+ }
34
+
35
+ return s.slice(0, end);
36
+ }
37
+
38
+ /**
39
+ * Ensures a path starts with a leading slash ('/').
40
+ *
41
+ * @param {string|unknown} input - Path fragment
42
+ * @returns {string} Path with leading slash
43
+ */
44
+ function ensureLeadingSlash(input) {
45
+ if (input == null) return '/';
46
+ const s = String(input).trim();
47
+ if (s.length === 0) return '/';
48
+ return s.charCodeAt(0) === 47 /* / */ ? s : `/${s}`;
49
+ }
50
+
51
+ /**
52
+ * Ensures a URL or path ends with a trailing slash ('/').
53
+ *
54
+ * @param {string|unknown} input - Path or URL
55
+ * @returns {string} String with trailing slash
56
+ */
57
+ function ensureTrailingSlash(input) {
58
+ if (input == null) return '/';
59
+ const s = String(input);
60
+ if (s.length === 0) return '/';
61
+ return s.charCodeAt(s.length - 1) === 47 /* / */ ? s : `${s}/`;
62
+ }
63
+
64
+ /**
65
+ * Safely removes leading slash(es) from a path fragment.
66
+ *
67
+ * @param {string|unknown} input - Path fragment
68
+ * @returns {string} Path without leading slash(es)
69
+ */
70
+ function stripLeadingSlash(input) {
71
+ if (input == null) return '';
72
+ let s = String(input);
73
+ let start = 0;
74
+ while (start < s.length && s.charCodeAt(start) === 47 /* / */) {
75
+ start++;
76
+ }
77
+ return s.slice(start);
78
+ }
79
+
80
+ /**
81
+ * Joins a base URL with one or more path fragments without double slashes.
82
+ *
83
+ * @param {string} base - Base URL or path prefix
84
+ * @param {...string} paths - Path fragments to append
85
+ * @returns {string} Combined normalized URL or path
86
+ */
87
+ function joinUrl(base, ...paths) {
88
+ let result = stripTrailingSlash(base || '');
89
+ for (const part of paths) {
90
+ if (part == null) continue;
91
+ const clean = stripLeadingSlash(stripTrailingSlash(part));
92
+ if (clean.length > 0) {
93
+ result = result ? `${result}/${clean}` : clean;
94
+ }
95
+ }
96
+ return result || '/';
97
+ }
98
+
99
+ module.exports = {
100
+ stripTrailingSlash,
101
+ ensureLeadingSlash,
102
+ ensureTrailingSlash,
103
+ stripLeadingSlash,
104
+ joinUrl,
105
+ trimUrlPathSlashes,
106
+ };
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const { walkDirectory, walkDirectoryDetailed } = require('./fs');
4
+
5
+ module.exports = walkDirectory;
6
+ module.exports.walkDirectory = walkDirectory;
7
+ module.exports.walkDirectoryDetailed = walkDirectoryDetailed;