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,12 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency Array Utilities
5
+ * @module core/utils/array
6
+ */
7
+
8
+ const { ensureArray } = require('./object');
9
+
10
+ module.exports = {
11
+ ensureArray,
12
+ };
@@ -0,0 +1,8 @@
1
+ 'use strict';
2
+
3
+ const { base64UrlEncode, base64UrlDecode } = require('./crypto');
4
+
5
+ module.exports = {
6
+ base64UrlEncode,
7
+ base64UrlDecode,
8
+ };
@@ -67,7 +67,125 @@ function safeCompare(a, b) {
67
67
  }
68
68
  }
69
69
 
70
+ /**
71
+ * Compute SHA-256 hash of a string, Buffer, or value.
72
+ *
73
+ * Zero-dependency wrapper around native `crypto.createHash('sha256')`.
74
+ * Safely coerces non-Buffer/non-string values to strings (or empty string for null/undefined).
75
+ *
76
+ * @param {string|Buffer|any} data - Data to hash
77
+ * @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
78
+ * @returns {string|Buffer} Digest in specified encoding, or Buffer if encoding is 'buffer'
79
+ */
80
+ function sha256(data, encoding = 'hex') {
81
+ if (data === null || data === undefined) {
82
+ data = '';
83
+ }
84
+ const input = Buffer.isBuffer(data) || typeof data === 'string' ? data : String(data);
85
+ const hash = crypto.createHash('sha256').update(input);
86
+ return encoding === 'buffer' ? hash.digest() : hash.digest(encoding || 'hex');
87
+ }
88
+
89
+ /**
90
+ * Compute HMAC-SHA256 signature of data using a secret key.
91
+ *
92
+ * Zero-dependency wrapper around native `crypto.createHmac('sha256', secret)`.
93
+ *
94
+ * @param {string|Buffer|any} data - Data to sign
95
+ * @param {string|Buffer} secret - Secret key
96
+ * @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
97
+ * @returns {string|Buffer} Signature in specified encoding, or Buffer if encoding is 'buffer'
98
+ */
99
+ function hmacSha256(data, secret, encoding = 'hex') {
100
+ if (secret === null || secret === undefined) {
101
+ throw new TypeError('A secret key is required for HMAC computation');
102
+ }
103
+ if (data === null || data === undefined) {
104
+ data = '';
105
+ }
106
+ const input = Buffer.isBuffer(data) || typeof data === 'string' ? data : String(data);
107
+ const hmac = crypto.createHmac('sha256', secret).update(input);
108
+ return encoding === 'buffer' ? hmac.digest() : hmac.digest(encoding || 'hex');
109
+ }
110
+
111
+ /**
112
+ * Generate a cryptographically secure random token string or buffer.
113
+ *
114
+ * @param {number} [byteLength=32] - Number of random bytes
115
+ * @param {import('crypto').BinaryToTextEncoding|'buffer'} [encoding='hex'] - Output encoding ('hex', 'base64', 'base64url', or 'buffer')
116
+ * @returns {string|Buffer}
117
+ */
118
+ function randomToken(byteLength = 32, encoding = 'hex') {
119
+ const bytes = crypto.randomBytes(byteLength);
120
+ return encoding === 'buffer' ? bytes : bytes.toString(encoding || 'hex');
121
+ }
122
+
123
+ /**
124
+ * Generate a cryptographically secure random string with an optional prefix.
125
+ * E.g. randomId('whsec', 24) -> 'whsec_a1b2c3...'
126
+ *
127
+ * @param {string} [prefix=''] - Identifier prefix
128
+ * @param {number} [byteLength=16] - Random byte length
129
+ * @param {import('crypto').BinaryToTextEncoding} [encoding='hex'] - Output encoding
130
+ * @returns {string}
131
+ */
132
+ function randomId(prefix = '', byteLength = 16, encoding = 'hex') {
133
+ const token = randomToken(byteLength, encoding);
134
+ if (!prefix) return token;
135
+ const p = prefix.endsWith('_') ? prefix : `${prefix}_`;
136
+ return `${p}${token}`;
137
+ }
138
+
139
+ /**
140
+ * Generate a timestamped unique ID with optional prefix.
141
+ * E.g. randomTimestampedId('dlv', 8) -> 'dlv_1729221234567_abcdef1234567890'
142
+ *
143
+ * @param {string} [prefix=''] - Identifier prefix
144
+ * @param {number} [byteLength=8] - Random byte length
145
+ * @param {import('crypto').BinaryToTextEncoding} [encoding='hex'] - Output encoding
146
+ * @returns {string}
147
+ */
148
+ function randomTimestampedId(prefix = '', byteLength = 8, encoding = 'hex') {
149
+ const token = randomToken(byteLength, encoding);
150
+ const p = prefix ? (prefix.endsWith('_') ? prefix : `${prefix}_`) : '';
151
+ return `${p}${Date.now()}_${token}`;
152
+ }
153
+
154
+ /**
155
+ * Encode string or Buffer to Base64URL string (RFC 4648 §5).
156
+ *
157
+ * @param {string|Buffer|any} input
158
+ * @returns {string}
159
+ */
160
+ function base64UrlEncode(input) {
161
+ if (input === null || input === undefined) return '';
162
+ return Buffer.isBuffer(input)
163
+ ? input.toString('base64url')
164
+ : Buffer.from(String(input), 'utf8').toString('base64url');
165
+ }
166
+
167
+ /**
168
+ * Decode Base64URL string to UTF-8 string.
169
+ *
170
+ * @param {string} input
171
+ * @returns {string}
172
+ */
173
+ function base64UrlDecode(input) {
174
+ if (!input) return '';
175
+ return Buffer.from(String(input), 'base64url').toString('utf8');
176
+ }
177
+
70
178
  module.exports = {
71
179
  safeCompare,
72
180
  timingSafeEqual: safeCompare,
181
+ sha256,
182
+ hashSha256: sha256,
183
+ hmacSha256,
184
+ createHmacSha256: hmacSha256,
185
+ randomToken,
186
+ randomId,
187
+ randomTimestampedId,
188
+ base64UrlEncode,
189
+ base64UrlDecode,
73
190
  };
191
+
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { ensureLeadingSlash } = require('./url');
4
+
5
+ module.exports = ensureLeadingSlash;
6
+ module.exports.ensureLeadingSlash = ensureLeadingSlash;
@@ -0,0 +1,7 @@
1
+ 'use strict';
2
+
3
+ const { escapeRegExp } = require('./regex');
4
+
5
+ module.exports = escapeRegExp;
6
+ module.exports.escapeRegExp = escapeRegExp;
7
+ module.exports.escapeRegex = escapeRegExp;
@@ -11,13 +11,13 @@
11
11
  * @module core/utils/fetch
12
12
  */
13
13
 
14
- 'use strict';
14
+ const { parseDurationMs } = require('./time');
15
15
 
16
16
  /**
17
17
  * Executes a fetch request with timeout and abort controller support.
18
18
  *
19
19
  * @param {string | URL | Request} url - Target URL or Request instance
20
- * @param {RequestInit & { timeout?: number }} [options] - Standard RequestInit plus optional timeout (ms)
20
+ * @param {RequestInit & { timeout?: number | string }} [options] - Standard RequestInit plus optional timeout (ms or '5s')
21
21
  * @returns {Promise<Response>}
22
22
  */
23
23
  async function fetchWithTimeout(url, options = {}) {
@@ -38,13 +38,14 @@ async function fetchWithTimeout(url, options = {}) {
38
38
  let timer = null;
39
39
  let callerSignalListener = null;
40
40
 
41
- if (typeof timeout === 'number' && timeout > 0) {
41
+ const timeoutMs = timeout !== undefined ? parseDurationMs(timeout, 10000) : 10000;
42
+ if (typeof timeoutMs === 'number' && timeoutMs > 0) {
42
43
  timer = setTimeout(() => {
43
- const err = new Error(`Request timed out after ${timeout}ms`);
44
+ const err = new Error(`Request timed out after ${timeoutMs}ms`);
44
45
  err.name = 'TimeoutError';
45
46
  err.code = 'ETIMEDOUT';
46
47
  controller.abort(err);
47
- }, timeout);
48
+ }, timeoutMs);
48
49
  if (timer && typeof timer.unref === 'function') {
49
50
  timer.unref();
50
51
  }
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Native Filesystem & Directory Walking Utilities
5
+ * Zero-dependency recursive and flat directory scanner with configurable filters.
6
+ * @module core/utils/fs
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ /**
13
+ * Standard test file extensions and type declarations to ignore by default
14
+ */
15
+ const DEFAULT_TEST_SUFFIXES = [
16
+ '.test.js',
17
+ '.test.cjs',
18
+ '.test.mjs',
19
+ '.test.ts',
20
+ '.spec.js',
21
+ '.spec.cjs',
22
+ '.spec.mjs',
23
+ '.spec.ts',
24
+ '.d.ts',
25
+ ];
26
+
27
+ /**
28
+ * Recursively (or flatly) scans a directory for files matching specified criteria.
29
+ *
30
+ * @param {string} dir - Directory path to scan
31
+ * @param {Object} [options]
32
+ * @param {string[]|Set<string>|null} [options.extensions] - Allowed extensions (e.g. ['.js', '.cjs']). Null allows any extension.
33
+ * @param {boolean} [options.recursive=true] - Whether to scan subdirectories recursively
34
+ * @param {boolean} [options.ignoreTests=true] - Whether to ignore test files (.test.js, .spec.js, .d.ts)
35
+ * @param {string[]} [options.ignorePrefixes=['.', '_']] - Prefixes of files/directories to ignore
36
+ * @param {string[]} [options.ignoreNames=['node_modules']] - Exact file or directory names to ignore
37
+ * @param {Function} [options.filter] - Custom filter function (fullPath, dirent) => boolean
38
+ * @param {string} [options.baseDir] - Base directory for computing relative paths (defaults to dir)
39
+ * @param {boolean} [options.detailed=false] - If true, returns array of objects with { absolutePath, relativePath, name, ext }
40
+ * @returns {string[]|Array<{ absolutePath: string, relativePath: string, name: string, ext: string }>}
41
+ */
42
+ function walkDirectory(dir, options = {}) {
43
+ const {
44
+ extensions = null,
45
+ recursive = true,
46
+ ignoreTests = true,
47
+ ignorePrefixes = ['.', '_'],
48
+ ignoreNames = ['node_modules'],
49
+ filter = null,
50
+ baseDir = dir,
51
+ detailed = false,
52
+ } = options;
53
+
54
+ if (!dir || typeof dir !== 'string') return [];
55
+
56
+ const resolvedDir = path.resolve(dir);
57
+ if (!fs.existsSync(resolvedDir)) return [];
58
+
59
+ const extSet = extensions
60
+ ? new Set(Array.from(extensions).map(e => (e.startsWith('.') ? e.toLowerCase() : `.${e.toLowerCase()}`)))
61
+ : null;
62
+
63
+ const results = [];
64
+ const resolvedBase = path.resolve(baseDir);
65
+
66
+ function walk(currentDir) {
67
+ let entries;
68
+ try {
69
+ entries = fs.readdirSync(currentDir, { withFileTypes: true });
70
+ } catch (_) {
71
+ return;
72
+ }
73
+
74
+ for (const entry of entries) {
75
+ const name = entry.name;
76
+
77
+ // Ignore specified exact names (e.g. 'node_modules')
78
+ if (ignoreNames && ignoreNames.includes(name)) {
79
+ continue;
80
+ }
81
+
82
+ // Ignore specified prefixes (e.g. '.' or '_')
83
+ if (ignorePrefixes && ignorePrefixes.some(prefix => name.startsWith(prefix))) {
84
+ continue;
85
+ }
86
+
87
+ const fullPath = path.join(currentDir, name);
88
+
89
+ if (entry.isDirectory()) {
90
+ if (recursive) {
91
+ walk(fullPath);
92
+ }
93
+ } else if (entry.isFile()) {
94
+ const ext = path.extname(name).toLowerCase();
95
+
96
+ // Check extensions if specified
97
+ if (extSet && !extSet.has(ext)) {
98
+ continue;
99
+ }
100
+
101
+ // Check test file suffixes
102
+ if (ignoreTests && DEFAULT_TEST_SUFFIXES.some(suffix => name.toLowerCase().endsWith(suffix))) {
103
+ continue;
104
+ }
105
+
106
+ // Custom filter function
107
+ if (typeof filter === 'function' && !filter(fullPath, entry)) {
108
+ continue;
109
+ }
110
+
111
+ if (detailed) {
112
+ const relPath = path.relative(resolvedBase, fullPath).split(path.sep).join('/');
113
+ results.push({
114
+ absolutePath: fullPath,
115
+ relativePath: relPath,
116
+ name,
117
+ ext,
118
+ });
119
+ } else {
120
+ results.push(fullPath);
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ walk(resolvedDir);
127
+ return results;
128
+ }
129
+
130
+ /**
131
+ * Convenient wrapper for walkDirectory with detailed: true
132
+ * @param {string} dir
133
+ * @param {Object} [options]
134
+ * @returns {Array<{ absolutePath: string, relativePath: string, name: string, ext: string }>}
135
+ */
136
+ function walkDirectoryDetailed(dir, options = {}) {
137
+ return walkDirectory(dir, { ...options, detailed: true });
138
+ }
139
+
140
+ module.exports = {
141
+ walkDirectory,
142
+ walkDirectoryDetailed,
143
+ DEFAULT_TEST_SUFFIXES,
144
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { hmacSha256 } = require('./crypto');
4
+
5
+ module.exports = hmacSha256;
6
+ module.exports.hmacSha256 = hmacSha256;
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency JSON Utilities
5
+ * Safe, fault-tolerant JSON parse and stringify helpers that never throw unhandled exceptions.
6
+ * @module core/utils/json
7
+ */
8
+
9
+ /**
10
+ * Safely parses a JSON string without throwing exceptions.
11
+ * Returns the provided fallback value if parsing fails or input is missing.
12
+ * If input is already an object or array (and not a string), returns input directly.
13
+ *
14
+ * @template T
15
+ * @param {string|unknown} input - Value or JSON string to parse
16
+ * @param {T} [fallback=null] - Value to return if parsing fails
17
+ * @param {Function} [reviver] - Optional JSON.parse reviver function
18
+ * @returns {any|T}
19
+ */
20
+ function safeJsonParse(input, fallback = null, reviver) {
21
+ if (input === null || input === undefined || input === '') {
22
+ return fallback;
23
+ }
24
+ if (typeof input !== 'string') {
25
+ return input;
26
+ }
27
+
28
+ try {
29
+ return JSON.parse(input, reviver);
30
+ } catch (_) {
31
+ return fallback;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Safely serializes a value to a JSON string without throwing exceptions.
37
+ * Handles circular references and BigInt values gracefully.
38
+ *
39
+ * @param {unknown} value - Value to serialize
40
+ * @param {string|number|null} [space=null] - Indentation spaces
41
+ * @param {string} [fallback=''] - Fallback value if serialization fails
42
+ * @returns {string}
43
+ */
44
+ function safeJsonStringify(value, space = null, fallback = '') {
45
+ if (value === undefined) {
46
+ return fallback;
47
+ }
48
+ try {
49
+ return JSON.stringify(value, null, space ?? undefined);
50
+ } catch (_) {
51
+ // Attempt circular-safe serialization
52
+ try {
53
+ const seen = new WeakSet();
54
+ return JSON.stringify(
55
+ value,
56
+ (key, val) => {
57
+ if (typeof val === 'bigint') {
58
+ return val.toString();
59
+ }
60
+ if (typeof val === 'object' && val !== null) {
61
+ if (seen.has(val)) {
62
+ return '[Circular]';
63
+ }
64
+ seen.add(val);
65
+ }
66
+ return val;
67
+ },
68
+ space ?? undefined
69
+ );
70
+ } catch {
71
+ return fallback;
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Checks whether a given string is valid JSON without throwing.
78
+ *
79
+ * @param {string|unknown} str - String to test
80
+ * @returns {boolean}
81
+ */
82
+ function isValidJson(str) {
83
+ if (typeof str !== 'string' || !str.trim()) {
84
+ return false;
85
+ }
86
+ try {
87
+ JSON.parse(str);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ safeJsonParse,
96
+ safeJsonStringify,
97
+ isValidJson,
98
+ };
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency Object & Array Utilities
5
+ * Safe property picking, omission, array wrapping, and deep cloning.
6
+ * @module core/utils/object
7
+ */
8
+
9
+ /**
10
+ * Ensure a value is wrapped in an array.
11
+ * Returns empty array for null or undefined.
12
+ * Returns array as-is if already an array.
13
+ * Wraps any other value in a single-element array.
14
+ *
15
+ * @template T
16
+ * @param {T|T[]|null|undefined} value - Value to ensure as array
17
+ * @returns {T[]}
18
+ * @example
19
+ * ensureArray(null) // -> []
20
+ * ensureArray(undefined) // -> []
21
+ * ensureArray('foo') // -> ['foo']
22
+ * ensureArray(['bar']) // -> ['bar']
23
+ */
24
+ function ensureArray(value) {
25
+ if (value === undefined || value === null) {
26
+ return [];
27
+ }
28
+ return Array.isArray(value) ? value : [value];
29
+ }
30
+
31
+ /**
32
+ * Pick specific keys from an object.
33
+ *
34
+ * @param {Record<string, any>|null|undefined} obj - Source object
35
+ * @param {string[]|string} keys - Keys to pick
36
+ * @returns {Record<string, any>} Object with picked keys
37
+ */
38
+ function pick(obj, keys) {
39
+ if (!obj || typeof obj !== 'object') {
40
+ return {};
41
+ }
42
+ const result = {};
43
+ const keyList = Array.isArray(keys) ? keys : [keys];
44
+ for (const key of keyList) {
45
+ if (key in obj) {
46
+ result[key] = obj[key];
47
+ }
48
+ }
49
+ return result;
50
+ }
51
+
52
+ /**
53
+ * Omit specific keys from an object.
54
+ *
55
+ * @param {Record<string, any>|null|undefined} obj - Source object
56
+ * @param {string[]|string} keys - Keys to omit
57
+ * @returns {Record<string, any>} Object without omitted keys
58
+ */
59
+ function omit(obj, keys) {
60
+ if (!obj || typeof obj !== 'object') {
61
+ return {};
62
+ }
63
+ const result = {};
64
+ const omitSet = new Set(Array.isArray(keys) ? keys : [keys]);
65
+ for (const key in obj) {
66
+ if (!omitSet.has(key)) {
67
+ result[key] = obj[key];
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Deep clone an object, array, or Date instance safely.
75
+ * Strips prototype pollution attempts (__proto__, constructor, prototype).
76
+ *
77
+ * @template T
78
+ * @param {T} obj - Target object to clone
79
+ * @returns {T} Cloned copy
80
+ */
81
+ function deepClone(obj) {
82
+ if (obj === null || typeof obj !== 'object') {
83
+ return obj;
84
+ }
85
+ if (obj instanceof Date) {
86
+ return new Date(obj.getTime());
87
+ }
88
+ if (Array.isArray(obj)) {
89
+ return obj.map(deepClone);
90
+ }
91
+ const cloned = Object.create(null);
92
+ for (const key of Object.keys(obj)) {
93
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
94
+ continue;
95
+ }
96
+ cloned[key] = deepClone(obj[key]);
97
+ }
98
+ return { ...cloned };
99
+ }
100
+
101
+ module.exports = {
102
+ ensureArray,
103
+ pick,
104
+ omit,
105
+ deepClone,
106
+ };
@@ -0,0 +1,9 @@
1
+ 'use strict';
2
+
3
+ const { randomToken, randomId, randomTimestampedId } = require('./crypto');
4
+
5
+ module.exports = {
6
+ randomToken,
7
+ randomId,
8
+ randomTimestampedId,
9
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso - Zero-Dependency RegExp Utilities
5
+ * Fast, secure regular expression escaping and glob-to-regex conversion helpers.
6
+ * @module core/utils/regex
7
+ */
8
+
9
+ /**
10
+ * Escapes all regular expression special characters in a string.
11
+ * Safe against ReDoS and regex injection when inserting dynamic substrings into RegExp.
12
+ *
13
+ * @param {string|unknown} str - String to escape
14
+ * @returns {string} Escaped string safe for new RegExp()
15
+ */
16
+ function escapeRegExp(str) {
17
+ if (str == null) return '';
18
+ return String(str).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
19
+ }
20
+
21
+ /**
22
+ * Converts a glob pattern string (e.g. 'users/*', 'api/**', 'order.*') into a RegExp.
23
+ *
24
+ * @param {string} pattern - Glob pattern string
25
+ * @param {Object} [options]
26
+ * @param {'/'|'.'|null} [options.separator='/'] - Segment separator ('/' for URLs/paths, '.' for events/dots, null for flat)
27
+ * @param {boolean} [options.exact=true] - Whether to match entire string (^...$)
28
+ * @param {string} [options.flags=''] - RegExp flags (e.g. 'i')
29
+ * @returns {RegExp}
30
+ */
31
+ function globToRegExp(pattern, options = {}) {
32
+ const {
33
+ separator = '/',
34
+ exact = true,
35
+ flags = '',
36
+ } = options;
37
+
38
+ if (typeof pattern !== 'string') {
39
+ return exact ? /(?!)/ : /(?:)/;
40
+ }
41
+
42
+ if (pattern === '*' || pattern === '**') {
43
+ return new RegExp(exact ? '^.*$' : '.*', flags);
44
+ }
45
+
46
+ // 1. Fully escape all regex characters first
47
+ let escaped = escapeRegExp(pattern);
48
+
49
+ // 2. Replace escaped wildcard sequences
50
+ if (separator === '/') {
51
+ // URL or path globbing
52
+ escaped = escaped
53
+ .replace(/\\\*\\\*/g, '.*')
54
+ .replace(/\\\*/g, '[^/]*')
55
+ .replace(/\\\?/g, '.');
56
+ } else if (separator === '.') {
57
+ // Dot-notation event globbing (e.g. order.* or order.**)
58
+ escaped = escaped
59
+ .replace(/\\\*\\\*/g, '.*')
60
+ .replace(/\\\*/g, '[^.]+')
61
+ .replace(/\\\?/g, '.');
62
+ } else {
63
+ // Flat wildcard matching
64
+ escaped = escaped
65
+ .replace(/\\\*+/g, '.*')
66
+ .replace(/\\\?/g, '.');
67
+ }
68
+
69
+ const regexStr = exact ? `^${escaped}$` : escaped;
70
+ return new RegExp(regexStr, flags);
71
+ }
72
+
73
+ module.exports = {
74
+ escapeRegExp,
75
+ escapeRegex: escapeRegExp,
76
+ globToRegExp,
77
+ };
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const { sha256 } = require('./crypto');
4
+
5
+ module.exports = sha256;
6
+ module.exports.sha256 = sha256;