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
@@ -249,9 +249,10 @@ function basicAuthPlugin(options = {}) {
249
249
  }
250
250
 
251
251
  // Mount global middleware if global: true
252
- if (applyGlobal && ctx.app) {
252
+ const target = ctx.http || ctx.app;
253
+ if (applyGlobal && target) {
253
254
  const globalMw = createBasicAuthMiddleware(options);
254
- ctx.app.use(globalMw);
255
+ target.use(globalMw);
255
256
  }
256
257
  },
257
258
  };
package/plugins/cors.js CHANGED
@@ -173,8 +173,9 @@ function corsPlugin(options = {}) {
173
173
  ctx.middlewares.cors = corsMiddleware;
174
174
  }
175
175
 
176
- if (ctx.app && options.global !== false) {
177
- ctx.app.use(corsMiddleware);
176
+ const target = ctx.http || ctx.app;
177
+ if (target && options.global !== false && typeof target.use === 'function') {
178
+ target.use(corsMiddleware);
178
179
  }
179
180
  },
180
181
  };
@@ -15,7 +15,8 @@ const NOOP = () => {};
15
15
  const EMPTY_OBJECT = Object.freeze({});
16
16
  const ignoreRegexCache = new Map();
17
17
 
18
- const { safeCompare } = require('../../core/utils/crypto');
18
+ const { safeCompare, randomToken } = require('../../core/utils/crypto');
19
+ const { globToRegExp } = require('../../core/utils/regex');
19
20
 
20
21
  /**
21
22
  * Check if the request path matches any ignore patterns
@@ -35,12 +36,7 @@ function isIgnored(req, ignorePaths) {
35
36
  if (pattern.includes('*')) {
36
37
  let regex = ignoreRegexCache.get(pattern);
37
38
  if (regex === undefined) {
38
- const regexPattern = pattern
39
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
40
- .replace(/\*\*/g, '___GLOB_STAR_STAR___')
41
- .replace(/\*/g, '[^/]*')
42
- .replace(/___GLOB_STAR_STAR___/g, '.*');
43
- regex = new RegExp(`^${regexPattern}$`);
39
+ regex = globToRegExp(pattern, { separator: '/' });
44
40
  if (ignoreRegexCache.size < 500) {
45
41
  ignoreRegexCache.set(pattern, regex);
46
42
  }
@@ -149,7 +145,7 @@ function csrfPlugin(options = {}) {
149
145
  if (useCookie) {
150
146
  csrfTokenValue = cookieOptions.signed ? req.signedCookies[cookieKey] : req.cookies[cookieKey];
151
147
  if (!csrfTokenValue) {
152
- csrfTokenValue = crypto.randomBytes(32).toString('hex');
148
+ csrfTokenValue = randomToken(32, 'hex');
153
149
  const isHttps = req.secure || req.headers?.['x-forwarded-proto'] === 'https' || process.env.NODE_ENV === 'production' || process.env.COOKIE_SECURE === 'true';
154
150
  const effectiveCookieOptions = {
155
151
  httpOnly: true,
@@ -162,7 +158,7 @@ function csrfPlugin(options = {}) {
162
158
  } else {
163
159
  // Session based
164
160
  if (!req.session[sessionKey]) {
165
- req.session[sessionKey] = crypto.randomBytes(32).toString('hex');
161
+ req.session[sessionKey] = randomToken(32, 'hex');
166
162
  }
167
163
  csrfTokenValue = req.session[sessionKey];
168
164
  }
@@ -217,9 +213,10 @@ function csrfPlugin(options = {}) {
217
213
  ctx.addHelper('csrfToken', csrfToken);
218
214
  ctx.addHelper('csrfInput', csrfInput);
219
215
 
220
- // If global validation is enabled, mount the middleware globally on the Express app
221
- if (applyGlobal) {
222
- ctx.app.use(createMiddleware());
216
+ // If global validation is enabled, mount the middleware globally
217
+ const target = ctx.http || ctx.app;
218
+ if (applyGlobal && target && typeof target.use === 'function') {
219
+ target.use(createMiddleware());
223
220
  }
224
221
  },
225
222
  };
@@ -55,12 +55,14 @@ function csvPlugin(options = {}) {
55
55
  }
56
56
 
57
57
  // 2. Register built-in services (csv.generate, csv.parse, csv.exportModel)
58
- if (ctx.app && ctx.app.serviceRegistry) {
58
+ // 2. Register built-in services (csv.generate, csv.parse, csv.exportModel)
59
+ const serviceRegistry = ctx.serviceRegistry || ctx.services || ctx.app?.serviceRegistry;
60
+ if (serviceRegistry) {
59
61
  const db = dbOption ?? ctx.db ?? ctx.app?.get?.('db') ?? null;
60
62
  const services = createCsvServices({ db, ...options });
61
63
  for (const [name, def] of Object.entries(services)) {
62
- if (!ctx.app.serviceRegistry.has(name)) {
63
- ctx.app.serviceRegistry.register(name, def);
64
+ if (!serviceRegistry.has(name)) {
65
+ serviceRegistry.register(name, def);
64
66
  }
65
67
  }
66
68
  }
@@ -77,8 +79,9 @@ function csvPlugin(options = {}) {
77
79
  ctx.middlewares.csv = middleware;
78
80
  }
79
81
 
80
- if (ctx.app && typeof ctx.app.use === 'function') {
81
- ctx.app.use(middleware);
82
+ const target = ctx.http || ctx.app;
83
+ if (target && typeof target.use === 'function') {
84
+ target.use(middleware);
82
85
  }
83
86
  },
84
87
  };
@@ -363,11 +363,11 @@ const ConfigView = {
363
363
  m('.table-container', [
364
364
  m('.config-item', [
365
365
  m('.config-key', 'Port'),
366
- m('.config-value', config.server?.port || '3000')
366
+ m('.config-value', config.server?.port || '7331')
367
367
  ]),
368
368
  m('.config-item', [
369
369
  m('.config-key', 'Base URL'),
370
- m('.config-value', config.server?.baseUrl || 'http://localhost:3000')
370
+ m('.config-value', config.server?.baseUrl || 'http://localhost:7331')
371
371
  ])
372
372
  ])
373
373
  ])
@@ -164,16 +164,16 @@ function dashboardPlugin(options = {}) {
164
164
  const config = {
165
165
  env: filterSensitiveEnv({
166
166
  NODE_ENV: process.env.NODE_ENV || 'development',
167
- PORT: process.env.PORT || '3000',
168
- BASE_URL: process.env.BASE_URL || 'http://localhost:3000',
167
+ PORT: process.env.PORT || '7331',
168
+ BASE_URL: process.env.BASE_URL || 'http://localhost:7331',
169
169
  }),
170
170
  i18n: {
171
171
  defaultLocale: process.env.DEFAULT_LOCALE || 'en',
172
172
  supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(','),
173
173
  },
174
174
  server: {
175
- port: process.env.PORT || '3000',
176
- baseUrl: process.env.BASE_URL || 'http://localhost:3000',
175
+ port: process.env.PORT || '7331',
176
+ baseUrl: process.env.BASE_URL || 'http://localhost:7331',
177
177
  },
178
178
  database: {
179
179
  connected: hasDb(),
@@ -232,7 +232,7 @@ function dashboardPlugin(options = {}) {
232
232
  });
233
233
 
234
234
  // Log dashboard URL
235
- console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 3000}${dashboardPath}\n`);
235
+ console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 7331}${dashboardPath}\n`);
236
236
  },
237
237
  };
238
238
  }
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { buildFilteredQuery } = require('../admin-panel/core/api-extensions');
7
+ const { safeJsonParse } = require('../../core/utils/json');
7
8
 
8
9
  /**
9
10
  * @param {import('../../core/orm/database').Database} db
@@ -17,11 +18,7 @@ async function resolveExportRecords(db, modelName, req) {
17
18
  let filters = body.filters ?? req.query.filters;
18
19
  if (typeof selectAll === 'string') selectAll = selectAll === 'true';
19
20
  if (typeof filters === 'string') {
20
- try {
21
- filters = JSON.parse(filters);
22
- } catch {
23
- filters = undefined;
24
- }
21
+ filters = safeJsonParse(filters, undefined);
25
22
  }
26
23
 
27
24
  const trashedOnly =
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { purgeEmailLogs } = require('./purge');
7
+ const { safeJsonParse } = require('../../core/utils/json');
7
8
 
8
9
  /**
9
10
  * @param {Object} options
@@ -44,14 +45,7 @@ function createEmailApiHandlers(options) {
44
45
  return res.status(400).json({ error: 'to and subject are required' });
45
46
  }
46
47
 
47
- let parsedData = data;
48
- if (typeof data === 'string') {
49
- try {
50
- parsedData = JSON.parse(data);
51
- } catch {
52
- parsedData = {};
53
- }
54
- }
48
+ const parsedData = safeJsonParse(data, {}) || {};
55
49
 
56
50
  const result = await emailService.send({
57
51
  to,
@@ -59,7 +53,7 @@ function createEmailApiHandlers(options) {
59
53
  template,
60
54
  mjml,
61
55
  html,
62
- data: parsedData || {},
56
+ data: parsedData,
63
57
  });
64
58
 
65
59
  res.json({ ok: true, ...result });
@@ -73,17 +67,10 @@ function createEmailApiHandlers(options) {
73
67
  const body = req.body || {};
74
68
  const { template, mjml, html, data } = body;
75
69
 
76
- let parsedData = data;
77
- if (typeof data === 'string') {
78
- try {
79
- parsedData = JSON.parse(data);
80
- } catch {
81
- parsedData = {};
82
- }
83
- }
70
+ const parsedData = safeJsonParse(data, {}) || {};
84
71
 
85
72
  const result = await emailService.preview(
86
- template ? { template, data: parsedData || {} } : { mjml, html, data: parsedData || {} }
73
+ template ? { template, data: parsedData } : { mjml, html, data: parsedData }
87
74
  );
88
75
 
89
76
  res.json(result);
@@ -5,6 +5,7 @@
5
5
 
6
6
  const { createKnexAuthTokensAdapter } = require('../../core/auth/tokens');
7
7
  const { registerBundledAuthTemplates } = require('./bundled-templates');
8
+ const { stripTrailingSlash } = require('../../core/utils/url');
8
9
 
9
10
  const DEFAULT_AUTH_EMAILS = {
10
11
  passwordReset: {
@@ -62,7 +63,7 @@ function wireAuthEmails(authManager, emailService, registry, config, db) {
62
63
  authManager.authTokens = createKnexAuthTokensAdapter(db);
63
64
  }
64
65
 
65
- const baseUrl = (merged.baseUrl || process.env.BASE_URL || 'http://localhost:3000').replace(/\/$/, '');
66
+ const baseUrl = stripTrailingSlash(merged.baseUrl || process.env.BASE_URL || 'http://localhost:7331');
66
67
  const identifierField = authManager.identifierField || 'email';
67
68
 
68
69
  function userDisplayName(user) {
@@ -153,7 +154,7 @@ function registerAuthEmailRoutes(options) {
153
154
  const guest = ctx.middlewares?.guest;
154
155
  const authMw = ctx.middlewares?.auth;
155
156
 
156
- const base = (authEmailsConfig.baseUrl || process.env.BASE_URL || 'http://localhost:3000').replace(/\/$/, '');
157
+ const base = stripTrailingSlash(authEmailsConfig.baseUrl || process.env.BASE_URL || 'http://localhost:7331');
157
158
 
158
159
  async function forgotPasswordHandler(req, res) {
159
160
  try {
@@ -241,16 +241,18 @@ function emailPlugin(options = {}) {
241
241
  setAppContext({ mailer });
242
242
  if (ctx.app) {
243
243
  ctx.app.mailer = mailer;
244
- if (typeof ctx.app.use === 'function') {
245
- ctx.app.use((req, res, next) => {
246
- req.mailer = mailer;
247
- next();
248
- });
249
- }
244
+ }
245
+ const target = ctx.http || ctx.app;
246
+ if (target && typeof target.use === 'function') {
247
+ target.use((req, res, next) => {
248
+ req.mailer = mailer;
249
+ next();
250
+ });
250
251
  }
251
252
  ctx.mailer = mailer;
252
253
 
253
- if (ctx.app && ctx.app.serviceRegistry) {
254
+ const registry = ctx.serviceRegistry || ctx.services || ctx.app?.serviceRegistry;
255
+ if (registry) {
254
256
  const { createMailServices } = require('../../src/services/builtins/mail');
255
257
  const mailServices = createMailServices({
256
258
  emailService,
@@ -259,8 +261,8 @@ function emailPlugin(options = {}) {
259
261
  tableName,
260
262
  });
261
263
  for (const [name, def] of Object.entries(mailServices)) {
262
- if (!ctx.app.serviceRegistry.has(name)) {
263
- ctx.app.serviceRegistry.register(name, def);
264
+ if (!registry.has(name)) {
265
+ registry.register(name, def);
264
266
  }
265
267
  }
266
268
  }
@@ -4,6 +4,7 @@
4
4
 
5
5
  const pathMod = require('path');
6
6
  const PKG = require(pathMod.join(__dirname, '..', 'package.json'));
7
+ const { ensureLeadingSlash } = require('../core/utils/url');
7
8
 
8
9
  /**
9
10
  * @param {Object} [options]
@@ -23,7 +24,7 @@ function healthCheckPlugin(options = {}) {
23
24
  } = options;
24
25
 
25
26
  const isEnabled = enabled !== undefined ? enabled : true;
26
- const normalizedPath = routePath.startsWith('/') ? routePath : `/${routePath}`;
27
+ const normalizedPath = ensureLeadingSlash(routePath);
27
28
 
28
29
  return {
29
30
  name: 'health-check',
package/plugins/index.js CHANGED
@@ -23,6 +23,9 @@ module.exports = {
23
23
  get ormCacheAdminPlugin() { return require('./orm-cache-admin'); },
24
24
  get uploadPlugin() { return require('./upload').uploadPlugin; },
25
25
  get createLocalFileProvider() { return require('./upload').createLocalFileProvider; },
26
+ get createR2FileProvider() { return require('./upload').createR2FileProvider; },
27
+ get createS3FileProvider() { return require('./upload').createS3FileProvider; },
28
+ get createGcsFileProvider() { return require('./upload').createGcsFileProvider; },
26
29
  get dataExchangePlugin() { return require('./data-exchange').dataExchangePlugin; },
27
30
  get redirectPlugin() { return require('./redirect').redirectPlugin; },
28
31
  get rateLimitPlugin() { return require('./rate-limit').rateLimitPlugin; },
@@ -63,6 +66,8 @@ module.exports = {
63
66
  get WebhookDispatcher() { return require('./webhooks').WebhookDispatcher; },
64
67
  get lazyPlugin() { return require('./lazy').lazyPlugin; },
65
68
  get defineLazyPlugin() { return require('./lazy').defineLazyPlugin; },
69
+ get rlsPlugin() { return require('./rls').rlsPlugin; },
70
+ get createRlsMiddleware() { return require('./rls').createRlsMiddleware; },
66
71
  };
67
72
 
68
73
 
package/plugins/lazy.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  const express = require('express');
16
+ const { ensureArray } = require('../core/utils/object');
16
17
 
17
18
  /**
18
19
  * Wraps a plugin definition or factory function into an on-demand lazy plugin.
@@ -43,9 +44,7 @@ function lazyPlugin(pluginFactory, lazyOptions = {}) {
43
44
  (typeof pluginFactory === 'object' && pluginFactory ? pluginFactory.name : null) ||
44
45
  'anonymous-lazy-plugin';
45
46
 
46
- const rawPrefixes = lazyOptions.prefix
47
- ? (Array.isArray(lazyOptions.prefix) ? lazyOptions.prefix : [lazyOptions.prefix])
48
- : [];
47
+ const rawPrefixes = ensureArray(lazyOptions.prefix);
49
48
  // Normalize prefixes: ensure leading slash, remove trailing slash (except root '/')
50
49
  const prefixes = rawPrefixes.map(p => {
51
50
  let norm = String(p).trim();
@@ -54,9 +53,9 @@ function lazyPlugin(pluginFactory, lazyOptions = {}) {
54
53
  return norm;
55
54
  });
56
55
 
57
- const lazyServices = Array.isArray(lazyOptions.services) ? lazyOptions.services : [];
58
- const lazyHelpers = Array.isArray(lazyOptions.helpers) ? lazyOptions.helpers : [];
59
- const lazyEvents = Array.isArray(lazyOptions.events) ? lazyOptions.events : [];
56
+ const lazyServices = ensureArray(lazyOptions.services);
57
+ const lazyHelpers = ensureArray(lazyOptions.helpers);
58
+ const lazyEvents = ensureArray(lazyOptions.events);
60
59
 
61
60
  function hydrate() {
62
61
  if (isHydrated) return realPlugin;
@@ -6,6 +6,7 @@
6
6
 
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
+ const { walkDirectory } = require('../../core/utils/fs');
9
10
 
10
11
  /**
11
12
  * Scan a directory for JS files and require them
@@ -13,27 +14,27 @@ const path = require('path');
13
14
  * @returns {Array<Object>}
14
15
  */
15
16
  function scanModuleDir(dirPath) {
16
- if (!fs.existsSync(dirPath)) return [];
17
- const entries = fs.readdirSync(dirPath, { withFileTypes: true });
17
+ const files = walkDirectory(dirPath, {
18
+ recursive: false,
19
+ extensions: ['.js', '.cjs'],
20
+ ignorePrefixes: ['_', '.'],
21
+ ignoreTests: true,
22
+ });
18
23
  const items = [];
19
24
 
20
- for (const entry of entries) {
21
- if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
22
- if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
23
- const fullPath = path.join(dirPath, entry.name);
24
- try {
25
- const mod = require(fullPath);
26
- if (mod && typeof mod === 'object') {
27
- // If mod doesn't have a name, default to filename without extension
28
- if (!mod.name && !mod.uri) {
29
- const baseName = path.basename(entry.name, path.extname(entry.name));
30
- mod.name = baseName;
31
- }
32
- items.push(mod);
25
+ for (const fullPath of files) {
26
+ try {
27
+ const mod = require(fullPath);
28
+ if (mod && typeof mod === 'object') {
29
+ // If mod doesn't have a name, default to filename without extension
30
+ if (!mod.name && !mod.uri) {
31
+ const baseName = path.basename(fullPath, path.extname(fullPath));
32
+ mod.name = baseName;
33
33
  }
34
- } catch (err) {
35
- console.warn(`[mcp-discovery] Failed to load module at ${fullPath}:`, err.message);
34
+ items.push(mod);
36
35
  }
36
+ } catch (err) {
37
+ console.warn(`[mcp-discovery] Failed to load module at ${fullPath}:`, err.message);
37
38
  }
38
39
  }
39
40
 
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { fetchWithTimeout } = require('../../../core/utils/fetch');
4
+ const { ensureLeadingSlash } = require('../../../core/utils/url');
4
5
 
5
6
  function getPolarApiBase(sandbox) {
6
7
  return sandbox ? 'https://sandbox-api.polar.sh/v1' : 'https://api.polar.sh/v1';
@@ -21,7 +22,7 @@ async function polarApiRequest(path, { method = 'GET', body, accessToken, sandbo
21
22
  throw new Error('POLAR_ACCESS_TOKEN is not configured');
22
23
  }
23
24
 
24
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
25
+ const normalizedPath = ensureLeadingSlash(path);
25
26
 
26
27
  const res = await fetchWithTimeout(`${getPolarApiBase(sandbox)}${normalizedPath}`, {
27
28
  method,
@@ -1,7 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const crypto = require('crypto');
4
- const { safeCompare } = require('../../../core/utils/crypto');
3
+ const { safeCompare, hmacSha256 } = require('../../../core/utils/crypto');
5
4
  const { syncPolarBillingForUser, applyPolarProSubscription } = require('./sync');
6
5
  const {
7
6
  collectPolarMetadata,
@@ -38,10 +37,10 @@ function verifyPolarWebhook(rawPayload, headers, secret) {
38
37
  ? Buffer.from(secret.slice(6), 'base64')
39
38
  : secret;
40
39
 
41
- const expectedBase64 = crypto.createHmac('sha256', cleanSecret).update(standardToSign).digest('base64');
42
- const expectedHex = crypto.createHmac('sha256', cleanSecret).update(standardToSign).digest('hex');
43
- const rawExpectedBase64 = crypto.createHmac('sha256', cleanSecret).update(payloadStr).digest('base64');
44
- const rawExpectedHex = crypto.createHmac('sha256', cleanSecret).update(payloadStr).digest('hex');
40
+ const expectedBase64 = hmacSha256(standardToSign, cleanSecret, 'base64');
41
+ const expectedHex = hmacSha256(standardToSign, cleanSecret, 'hex');
42
+ const rawExpectedBase64 = hmacSha256(payloadStr, cleanSecret, 'base64');
43
+ const rawExpectedHex = hmacSha256(payloadStr, cleanSecret, 'hex');
45
44
 
46
45
  const expectedList = [expectedBase64, expectedHex, rawExpectedBase64, rawExpectedHex];
47
46
 
@@ -12,13 +12,17 @@ const {
12
12
  MemoryQueueAdapter,
13
13
  DatabaseQueueAdapter,
14
14
  RedisQueueAdapter,
15
+ WaitUntilQueueAdapter,
16
+ CloudflareQueueAdapter,
15
17
  } = require('../../core/queue');
16
18
 
17
19
  /**
18
20
  * @param {Object} [options]
19
- * @param {'memory'|'database'|'redis'|Object} [options.adapter='memory'] - Queue backend adapter or name
21
+ * @param {'memory'|'database'|'redis'|'wait-until'|'cloudflare'|Object} [options.adapter='memory'] - Queue backend adapter or name
20
22
  * @param {Object} [options.db] - Knex/ORM database instance for database adapter
21
23
  * @param {Object} [options.redisClient] - Redis client instance for redis adapter
24
+ * @param {Object} [options.queue] - Cloudflare Queue binding for cloudflare adapter
25
+ * @param {Function} [options.waitUntil] - Optional global waitUntil handler for wait-until adapter
22
26
  * @param {string} [options.tableName='webspresso_jobs'] - Database queue table name
23
27
  * @param {number} [options.concurrency=2] - Worker concurrency
24
28
  * @param {number} [options.pollInterval=500] - Idle poll interval in ms
@@ -37,7 +41,7 @@ function queuePlugin(options = {}) {
37
41
  return {
38
42
  name: 'queue',
39
43
  version: '1.0.0',
40
- description: 'Background Job Queue Engine with Memory, DB, and Redis adapters',
44
+ description: 'Background Job Queue Engine with Memory, DB, Redis, WaitUntil, and Cloudflare adapters',
41
45
 
42
46
  register(ctx) {
43
47
  const { app, db } = ctx;
@@ -46,6 +50,18 @@ function queuePlugin(options = {}) {
46
50
 
47
51
  if (!adapter || adapter === 'memory') {
48
52
  adapter = new MemoryQueueAdapter();
53
+ } else if (adapter === 'wait-until' || adapter === 'edge') {
54
+ adapter = new WaitUntilQueueAdapter({
55
+ waitUntil: options.waitUntil,
56
+ });
57
+ } else if (adapter === 'cloudflare' || adapter === 'cf') {
58
+ const cfQueue = options.queue || options.cfQueue;
59
+ if (!cfQueue) {
60
+ throw new Error('[queuePlugin] Cloudflare queue adapter requires a valid queue binding in options.queue.');
61
+ }
62
+ adapter = new CloudflareQueueAdapter({
63
+ queue: cfQueue,
64
+ });
49
65
  } else if (adapter === 'database' || adapter === 'db') {
50
66
  const knexInstance = options.db?.knex || db?.knex || options.knex;
51
67
  if (!knexInstance) {
@@ -3,6 +3,8 @@
3
3
  * @module plugins/rate-limit
4
4
  */
5
5
 
6
+ const { ensureArray } = require('../../core/utils/object');
7
+
6
8
  const PLUGIN_ONLY_KEYS = new Set(['global', 'globalOverrides', 'globalSkipPaths']);
7
9
 
8
10
  const DEFAULT_BASE = {
@@ -95,7 +97,7 @@ function combineSkip(userSkip, builtin) {
95
97
  * @returns {import('express').RequestHandler}
96
98
  */
97
99
  function createDefaultGlobalSkip(extraPathPrefixes = []) {
98
- const extras = (Array.isArray(extraPathPrefixes) ? extraPathPrefixes : []).filter(Boolean);
100
+ const extras = ensureArray(extraPathPrefixes).filter(Boolean);
99
101
  /** @type {string[]} */
100
102
  const prefixes = [
101
103
  '/__webspresso/client-runtime',
@@ -4,6 +4,8 @@
4
4
  */
5
5
 
6
6
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
7
+ const { stripTrailingSlash, ensureTrailingSlash } = require('../../core/utils/url');
8
+ const { ensureArray } = require('../../core/utils/object');
7
9
 
8
10
  /**
9
11
  * @param {string} to
@@ -32,10 +34,10 @@ function normalizePathForTrailingSlash(mode, path) {
32
34
  if (!path || mode === false || mode == null) return path;
33
35
  if (path === '/') return path;
34
36
  if (mode === 'strip') {
35
- return path.endsWith('/') ? path.slice(0, -1) || '/' : path;
37
+ return stripTrailingSlash(path, { preserveRoot: true });
36
38
  }
37
39
  if (mode === 'add') {
38
- return path.endsWith('/') ? path : `${path}/`;
40
+ return ensureTrailingSlash(path);
39
41
  }
40
42
  return path;
41
43
  }
@@ -119,8 +121,8 @@ function pathMatches(compiled, pathForMatch, rawPath) {
119
121
  if (compiled.kind === 'string') {
120
122
  if (pathForMatch === compiled.fromNorm) return true;
121
123
  if (!compiled.trailing) {
122
- const a = rawPath.replace(/\/$/, '') || '/';
123
- const b = compiled.fromRaw.replace(/\/$/, '') || '/';
124
+ const a = stripTrailingSlash(rawPath, { preserveRoot: true });
125
+ const b = stripTrailingSlash(compiled.fromRaw, { preserveRoot: true });
124
126
  if (a === b) return true;
125
127
  }
126
128
  return false;
@@ -173,7 +175,7 @@ function redirectPlugin(options = {}) {
173
175
  const stringRulesMap = new Map();
174
176
  let hasRegexRules = false;
175
177
 
176
- const ruleList = Array.isArray(rules) ? rules : [];
178
+ const ruleList = ensureArray(rules);
177
179
  for (const rule of ruleList) {
178
180
  const c = compileRule(rule, pluginOpts);
179
181
  if (!c) continue;
@@ -181,7 +183,7 @@ function redirectPlugin(options = {}) {
181
183
  if (c.kind === 'string') {
182
184
  const keys = [c.fromNorm];
183
185
  if (!c.trailing) {
184
- const alt = c.fromRaw.replace(/\/$/, '') || '/';
186
+ const alt = stripTrailingSlash(c.fromRaw, { preserveRoot: true });
185
187
  if (!keys.includes(alt)) keys.push(alt);
186
188
  }
187
189
  for (const k of keys) {
@@ -205,7 +207,7 @@ function redirectPlugin(options = {}) {
205
207
 
206
208
  // Fast O(1) path when all configured rules are string based
207
209
  if (!hasRegexRules) {
208
- const candidates = stringRulesMap.get(pathForMatch) || (!pluginOpts.trailingSlash ? stringRulesMap.get(rawPath.replace(/\/$/, '') || '/') : null);
210
+ const candidates = stringRulesMap.get(pathForMatch) || (!pluginOpts.trailingSlash ? stringRulesMap.get(stripTrailingSlash(rawPath, { preserveRoot: true })) : null);
209
211
  if (candidates) {
210
212
  for (const c of candidates) {
211
213
  if (!methodMatches(c, method)) continue;
@@ -251,8 +253,13 @@ function redirectPlugin(options = {}) {
251
253
  description: 'Configurable HTTP redirects before file-based routes',
252
254
 
253
255
  register(ctx) {
254
- if (compiled.length === 0) return;
255
- ctx.app.use(redirectMiddleware);
256
+ if (compiled.length === 0) {
257
+ return;
258
+ }
259
+ const target = ctx.http || ctx.app;
260
+ if (target && typeof target.use === 'function') {
261
+ target.use(redirectMiddleware);
262
+ }
256
263
  },
257
264
  };
258
265
  }