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,170 @@
1
+ /**
2
+ * Webspresso RLS Plugin — Dynamic Model Row-Level Security
3
+ * Automatically bridges request authentication (req.user, req.auth, req.tenantId)
4
+ * to ORM repositories with role-based bypass and route-level overrides.
5
+ * Zero external dependencies.
6
+ * @module plugins/rls
7
+ */
8
+
9
+ /**
10
+ * Default user extractor from HTTP request
11
+ * @param {import('express').Request} req
12
+ * @returns {Object|null}
13
+ */
14
+ function defaultGetUser(req) {
15
+ if (!req) return null;
16
+ return req.user || req.adminUser || (req.auth && req.auth.user) || null;
17
+ }
18
+
19
+ /**
20
+ * Default tenant extractor from HTTP request
21
+ * @param {import('express').Request} req
22
+ * @param {Object|null} [user]
23
+ * @returns {unknown|null}
24
+ */
25
+ function defaultGetTenantId(req, user) {
26
+ if (!req) return null;
27
+ if (req.tenantId !== undefined && req.tenantId !== null) return req.tenantId;
28
+ if (req.tenant !== undefined && req.tenant !== null) return req.tenant;
29
+ if (user && typeof user === 'object') {
30
+ if (user.tenant_id !== undefined && user.tenant_id !== null) return user.tenant_id;
31
+ if (user.tenantId !== undefined && user.tenantId !== null) return user.tenantId;
32
+ if (user.organization_id !== undefined && user.organization_id !== null) return user.organization_id;
33
+ if (user.organizationId !== undefined && user.organizationId !== null) return user.organizationId;
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Create Express middleware for Row Level Security
40
+ * Wraps req.db methods to automatically inject authenticated user and tenant context
41
+ *
42
+ * @param {Object} [options]
43
+ * @param {string[]|Function|false} [options.bypassRoles=['admin', 'superadmin']] - Roles that bypass RLS
44
+ * @param {Function} [options.getUser] - Custom user extractor (req) => user
45
+ * @param {Function} [options.getTenantId] - Custom tenant extractor (req, user) => tenantId
46
+ * @param {boolean} [options.enabled=true] - Whether RLS middleware is active
47
+ * @returns {Function} Express middleware handler
48
+ */
49
+ function createRlsMiddleware(options = {}) {
50
+ const {
51
+ bypassRoles = ['admin', 'superadmin'],
52
+ getUser = defaultGetUser,
53
+ getTenantId = defaultGetTenantId,
54
+ enabled = true,
55
+ } = options;
56
+
57
+ return (req, res, next) => {
58
+ if (!enabled) return next();
59
+
60
+ const user = typeof getUser === 'function' ? getUser(req) : null;
61
+ const tenantId = typeof getTenantId === 'function' ? getTenantId(req, user) : null;
62
+
63
+ // Check if user has global bypass role
64
+ let isBypassed = false;
65
+ if (user && bypassRoles) {
66
+ if (typeof bypassRoles === 'function') {
67
+ isBypassed = Boolean(bypassRoles(user, req));
68
+ } else if (Array.isArray(bypassRoles)) {
69
+ const uRole = user.role || user.roleName || user.type;
70
+ const uRoles = Array.isArray(user.roles) ? user.roles : (uRole ? [uRole] : []);
71
+ isBypassed = bypassRoles.some((r) => uRoles.includes(r));
72
+ }
73
+ }
74
+
75
+ // Wrap req.db if it exists on the request
76
+ if (req.db && typeof req.db.getRepository === 'function' && !req.db._rlsWrapped) {
77
+ const origGetRepo = req.db.getRepository.bind(req.db);
78
+ const origQuery = req.db.query ? req.db.query.bind(req.db) : null;
79
+ const origCreateRepo = req.db.createRepository ? req.db.createRepository.bind(req.db) : null;
80
+
81
+ const reqDb = Object.create(req.db);
82
+ reqDb._rlsWrapped = true;
83
+
84
+ reqDb.getRepository = (modelName, customContext = {}) => {
85
+ const ctx = {
86
+ user,
87
+ tenantId: customContext.tenantId !== undefined ? customContext.tenantId : tenantId,
88
+ rlsDisabled: isBypassed || customContext.rlsDisabled === true,
89
+ ...customContext,
90
+ };
91
+ return origGetRepo(modelName, ctx);
92
+ };
93
+
94
+ if (origCreateRepo) {
95
+ reqDb.createRepository = (model, customContext = {}) => {
96
+ const ctx = {
97
+ user,
98
+ tenantId: customContext.tenantId !== undefined ? customContext.tenantId : tenantId,
99
+ rlsDisabled: isBypassed || customContext.rlsDisabled === true,
100
+ ...customContext,
101
+ };
102
+ return origCreateRepo(model, ctx);
103
+ };
104
+ }
105
+
106
+ if (origQuery) {
107
+ reqDb.query = (modelName, customContext = {}) => {
108
+ const ctx = {
109
+ user,
110
+ tenantId: customContext.tenantId !== undefined ? customContext.tenantId : tenantId,
111
+ rlsDisabled: isBypassed || customContext.rlsDisabled === true,
112
+ ...customContext,
113
+ };
114
+ return origQuery(modelName, ctx);
115
+ };
116
+ }
117
+
118
+ req.db = reqDb;
119
+ }
120
+
121
+ next();
122
+ };
123
+ }
124
+
125
+ /**
126
+ * Webspresso RLS Plugin Factory
127
+ *
128
+ * @param {Object} [options] - Plugin configuration options
129
+ * @param {boolean} [options.global=true] - Mount global middleware on app
130
+ * @param {string[]|Function|false} [options.bypassRoles=['admin', 'superadmin']] - Global roles bypassing RLS
131
+ * @param {Function} [options.getUser] - Custom user extractor
132
+ * @param {Function} [options.getTenantId] - Custom tenant extractor
133
+ * @param {boolean} [options.enabled=true] - Enabled flag
134
+ * @returns {Object} Webspresso Plugin Definition
135
+ */
136
+ function rlsPlugin(options = {}) {
137
+ const { global: applyGlobal = true, ...pluginDefaults } = options;
138
+
139
+ return {
140
+ name: 'rls',
141
+ version: '1.0.0',
142
+ description: 'Dynamic Model Row-Level Security (RLS) with automatic request context binding',
143
+ options,
144
+
145
+ api: {
146
+ createMiddleware: (routeOpts = {}) =>
147
+ createRlsMiddleware({ ...pluginDefaults, ...routeOpts }),
148
+ },
149
+
150
+ register(ctx) {
151
+ // Named middleware for route definitions: middleware: ['rls']
152
+ if (ctx.middlewares) {
153
+ ctx.middlewares.rls = (routeOpts = {}) =>
154
+ createRlsMiddleware({ ...pluginDefaults, ...routeOpts });
155
+ }
156
+
157
+ // Global middleware (mounted by default unless global: false)
158
+ if (applyGlobal && ctx.app) {
159
+ ctx.app.use(createRlsMiddleware(options));
160
+ }
161
+ },
162
+ };
163
+ }
164
+
165
+ rlsPlugin.rlsPlugin = rlsPlugin;
166
+ rlsPlugin.createRlsMiddleware = createRlsMiddleware;
167
+
168
+ module.exports = rlsPlugin;
169
+ module.exports.rlsPlugin = rlsPlugin;
170
+ module.exports.createRlsMiddleware = createRlsMiddleware;
@@ -48,7 +48,7 @@ function seoCheckerPlugin(options = {}) {
48
48
  });
49
49
 
50
50
  // Inject panel styles into head
51
- ctx.injectHead(generatePanelStyles(), { priority: 100 });
51
+ ctx.injectHead(generatePanelStyles(), { priority: 100, id: 'seo-checker-styles' });
52
52
 
53
53
  const safeSettings = JSON.stringify(mergedSettings).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
54
54
  const safeChecks = JSON.stringify(checkDefinitions).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
@@ -61,7 +61,7 @@ function seoCheckerPlugin(options = {}) {
61
61
  window.__SEO_CHECKER_CHECKS__ = ${safeChecks};
62
62
  </script>
63
63
  <script>${analyzerScript}</script>
64
- `);
64
+ `, { id: 'seo-checker-panel' });
65
65
  },
66
66
 
67
67
  /**
@@ -192,7 +192,9 @@ function siteAnalyticsPlugin(options = {}) {
192
192
  errorLogIntervalMs: options.errorLogIntervalMs ?? 60000,
193
193
  });
194
194
 
195
- ctx.app.use(trackingMiddleware);
195
+ if (typeof ctx.app?.use === 'function') {
196
+ ctx.app.use(trackingMiddleware);
197
+ }
196
198
 
197
199
  if (trackClientErrors) {
198
200
  const script = generateErrorTrackerScript({ endpoint: '/_analytics/report-error' });
@@ -4,7 +4,7 @@
4
4
  * @module plugins/site-analytics/tracking
5
5
  */
6
6
 
7
- const crypto = require('crypto');
7
+ const { sha256 } = require('../../core/utils/crypto');
8
8
  const { getClientIp: resolveClientIp } = require('../../core/utils/ip');
9
9
  const { detectBot } = require('./bot-patterns');
10
10
  const { detectCountry } = require('./geo');
@@ -76,7 +76,7 @@ function isPermanentDbError(e) {
76
76
  * Hash a string with SHA-256 (truncated to 16 hex chars for compactness)
77
77
  */
78
78
  function quickHash(str) {
79
- return crypto.createHash('sha256').update(str).digest('hex').slice(0, 16);
79
+ return sha256(str).slice(0, 16);
80
80
  }
81
81
 
82
82
  /**
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const { matchPattern } = require('../src/plugin-manager');
7
+ const { stripTrailingSlash } = require('../core/utils/url');
7
8
 
8
9
  /**
9
10
  * Escape XML special characters
@@ -140,7 +141,7 @@ function generateRobotsTxt(hostname, options = {}) {
140
141
  */
141
142
  function sitemapPlugin(options = {}) {
142
143
  const {
143
- hostname = process.env.BASE_URL || 'http://localhost:3000',
144
+ hostname = process.env.BASE_URL || 'http://localhost:7331',
144
145
  exclude = ['/api/*'],
145
146
  changefreq = 'weekly',
146
147
  priority = 0.8,
@@ -229,7 +230,7 @@ function sitemapPlugin(options = {}) {
229
230
  */
230
231
  async function buildAllUrls(routes, dbInstance) {
231
232
  const urls = [];
232
- const hostnameNormalized = hostname.replace(/\/$/, '');
233
+ const hostnameNormalized = stripTrailingSlash(hostname);
233
234
 
234
235
  // Filter static routes for sitemap
235
236
  const sitemapRoutes = routes
@@ -12,6 +12,7 @@ const { buildOpenApiDocument } = require('../core/openapi/build-from-api-routes'
12
12
  const PKG = require(path.join(__dirname, '..', 'package.json'));
13
13
 
14
14
  const { escapeHtml } = require('../core/utils/html');
15
+ const { stripTrailingSlash } = require('../core/utils/url');
15
16
 
16
17
  /**
17
18
  * Generates Swagger UI HTML page
@@ -180,7 +181,7 @@ function generateApiDocsAdminComponent({ uiPath, jsonPath, title }) {
180
181
  * @param {string} [options.title] - OpenAPI info.title
181
182
  * @param {string} [options.version] - OpenAPI info.version
182
183
  * @param {string} [options.description] - OpenAPI info.description
183
- * @param {string} [options.serverUrl] - Override servers[0].url (default: BASE_URL or http://localhost:3000)
184
+ * @param {string} [options.serverUrl] - Override servers[0].url (default: BASE_URL or http://localhost:7331)
184
185
  * @param {string} [options.theme='default'] - Theme for Scalar UI
185
186
  * @param {boolean} [options.darkMode=true] - Dark mode default for Scalar UI
186
187
  * @param {boolean} [options.admin=true] - Auto-register in Admin Panel if admin-panel plugin is present
@@ -197,6 +198,7 @@ function swaggerPlugin(options = {}) {
197
198
  authorize,
198
199
  includeOrmSchemas = false,
199
200
  ormExclude = [],
201
+ jsonPath: customJsonPath,
200
202
  title,
201
203
  version,
202
204
  description,
@@ -211,7 +213,7 @@ function swaggerPlugin(options = {}) {
211
213
  } = options;
212
214
 
213
215
  const normalizedBase = `/${trimUrlPathSlashes(basePath)}`;
214
- const jsonPath = `${normalizedBase}/openapi.json`;
216
+ const jsonPath = customJsonPath ? `/${trimUrlPathSlashes(customJsonPath)}` : `${normalizedBase}/openapi.json`;
215
217
  const uiPath = normalizedBase;
216
218
  const swaggerSubPath = `${normalizedBase}/swagger`;
217
219
  const scalarSubPath = `${normalizedBase}/scalar`;
@@ -243,7 +245,7 @@ function swaggerPlugin(options = {}) {
243
245
  return false;
244
246
  };
245
247
 
246
- const server = serverUrl || process.env.BASE_URL || 'http://localhost:3000';
248
+ const server = serverUrl || process.env.BASE_URL || 'http://localhost:7331';
247
249
 
248
250
  let cachedDoc = null;
249
251
 
@@ -254,9 +256,14 @@ function swaggerPlugin(options = {}) {
254
256
  if (cachedDoc && process.env.NODE_ENV === 'production') {
255
257
  return res.json(cachedDoc);
256
258
  }
259
+ const resolvedPagesDir = ctx.pagesDir
260
+ || ctx.options?.pagesDir
261
+ || (typeof ctx.options?.pages === 'string' ? ctx.options.pages : null)
262
+ || (ctx.options?.rootDir ? path.join(ctx.options.rootDir, 'pages') : path.join(process.cwd(), 'pages'));
263
+
257
264
  const doc = buildOpenApiDocument({
258
265
  routes: ctx.routes || [],
259
- pagesDir: ctx.options.pagesDir,
266
+ pagesDir: resolvedPagesDir,
260
267
  includeOrmSchemas,
261
268
  ormExclude,
262
269
  info: {
@@ -264,7 +271,7 @@ function swaggerPlugin(options = {}) {
264
271
  version: docVersion,
265
272
  description: docDescription,
266
273
  },
267
- servers: [{ url: server.replace(/\/$/, '') }],
274
+ servers: [{ url: stripTrailingSlash(server) }],
268
275
  });
269
276
  if (process.env.NODE_ENV === 'production') {
270
277
  cachedDoc = doc;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Google Cloud Storage (GCS) File Provider for Webspresso
3
+ * Enables direct file storage, streaming, and signed upload URLs with Google Cloud Storage
4
+ * Zero-dependency compatible: supports official @google-cloud/storage or standard duck-typed client interfaces
5
+ * @module plugins/upload/gcs-provider
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { generateStorageKey } = require('./s3-provider');
11
+
12
+ /**
13
+ * Google Cloud Storage Provider
14
+ * @param {Object} options
15
+ * @param {Object|string} options.bucket - GCS Bucket name (string) or Bucket instance (e.g. storage.bucket('my-bucket'))
16
+ * @param {Object} [options.storageClient] - Optional Storage instance from @google-cloud/storage
17
+ * @param {string} [options.publicBaseUrl] - Custom public domain or CDN URL (e.g. 'https://cdn.example.com')
18
+ * @param {string} [options.prefix='uploads'] - Subdirectory prefix inside the GCS bucket
19
+ * @returns {Object} Upload storage provider implementing put, delete, and createSignedUploadUrl
20
+ */
21
+ function createGcsFileProvider(options = {}) {
22
+ const { storageClient, publicBaseUrl, prefix = 'uploads' } = options;
23
+ let bucket = options.bucket;
24
+
25
+ if (!bucket) {
26
+ throw new Error('[createGcsFileProvider] GCS Bucket (name string or Bucket object) is required.');
27
+ }
28
+
29
+ const bucketName = typeof bucket === 'string' ? bucket : (bucket.name || 'bucket');
30
+ let bucketInstance = typeof bucket === 'object' && bucket !== null ? bucket : null;
31
+
32
+ if (typeof bucket === 'string' && storageClient && typeof storageClient.bucket === 'function') {
33
+ bucketInstance = storageClient.bucket(bucket);
34
+ }
35
+
36
+ const defaultBaseUrl = `https://storage.googleapis.com/${bucketName}`;
37
+ const normalizedBaseUrl = (publicBaseUrl || defaultBaseUrl).replace(/\/+$/, '');
38
+
39
+ return {
40
+ bucketName,
41
+ publicBaseUrl: normalizedBaseUrl,
42
+
43
+ /**
44
+ * Store file in Google Cloud Storage
45
+ * @param {Object} args
46
+ * @param {Buffer} args.buffer
47
+ * @param {string} args.originalName
48
+ * @param {string} args.mimeType
49
+ * @returns {Promise<{ publicUrl: string, key: string }>}
50
+ */
51
+ async put({ buffer, originalName, mimeType }) {
52
+ const key = generateStorageKey(originalName, mimeType, prefix);
53
+
54
+ if (bucketInstance && typeof bucketInstance.file === 'function') {
55
+ const file = bucketInstance.file(key);
56
+ await file.save(buffer, {
57
+ metadata: {
58
+ contentType: mimeType || 'application/octet-stream',
59
+ metadata: {
60
+ originalName: originalName || '',
61
+ uploadedAt: new Date().toISOString(),
62
+ },
63
+ },
64
+ resumable: false,
65
+ });
66
+ } else {
67
+ throw new Error(
68
+ '[createGcsFileProvider] A valid GCS bucket instance with .file() is required to perform .put().'
69
+ );
70
+ }
71
+
72
+ const publicUrl = `${normalizedBaseUrl}/${key}`;
73
+ return { publicUrl, key };
74
+ },
75
+
76
+ /**
77
+ * Delete file from Google Cloud Storage
78
+ * @param {string} key
79
+ * @returns {Promise<boolean>}
80
+ */
81
+ async delete(key) {
82
+ if (bucketInstance && typeof bucketInstance.file === 'function') {
83
+ const file = bucketInstance.file(key);
84
+ try {
85
+ if (typeof file.delete === 'function') {
86
+ await file.delete({ ignoreNotFound: true });
87
+ return true;
88
+ }
89
+ } catch (_) {
90
+ return false;
91
+ }
92
+ }
93
+ return false;
94
+ },
95
+
96
+ /**
97
+ * Generate a V4 signed URL for direct browser-to-GCS upload
98
+ * @param {Object} [params]
99
+ * @param {string} [params.originalName]
100
+ * @param {string} [params.mimeType]
101
+ * @param {number} [params.expiresInMs=900000] - 15 minutes default
102
+ * @param {string} [params.key] - Custom key override
103
+ * @returns {Promise<{ uploadUrl: string, publicUrl: string, key: string, method: string, headers: Record<string, string> }>}
104
+ */
105
+ async createSignedUploadUrl(params = {}) {
106
+ const {
107
+ originalName = 'upload',
108
+ mimeType = 'application/octet-stream',
109
+ expiresInMs = 15 * 60 * 1000,
110
+ key: customKey,
111
+ } = params;
112
+
113
+ const key = customKey || generateStorageKey(originalName, mimeType, prefix);
114
+
115
+ if (!bucketInstance || typeof bucketInstance.file !== 'function') {
116
+ throw new Error('[createGcsFileProvider] GCS bucket instance required for generating signed upload URLs.');
117
+ }
118
+
119
+ const file = bucketInstance.file(key);
120
+ if (typeof file.getSignedUrl !== 'function') {
121
+ throw new Error('[createGcsFileProvider] file.getSignedUrl() method is not implemented on GCS File instance.');
122
+ }
123
+
124
+ const [signedUrl] = await file.getSignedUrl({
125
+ version: 'v4',
126
+ action: 'write',
127
+ expires: Date.now() + expiresInMs,
128
+ contentType: mimeType,
129
+ });
130
+
131
+ const publicUrl = `${normalizedBaseUrl}/${key}`;
132
+
133
+ return {
134
+ uploadUrl: signedUrl,
135
+ publicUrl,
136
+ key,
137
+ method: 'PUT',
138
+ headers: {
139
+ 'Content-Type': mimeType,
140
+ },
141
+ };
142
+ },
143
+ };
144
+ }
145
+
146
+ module.exports = {
147
+ createGcsFileProvider,
148
+ };