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.
- package/.agents/AGENTS.md +21 -2
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +220 -8
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/FLAGS.md +118 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/LOGGING.md +93 -0
- package/.agents/METRICS.md +127 -0
- package/.agents/MIGRATION_JOBS.md +95 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +38 -3
- package/.agents/QUEUE.md +42 -0
- package/.agents/RATE_LIMIT.md +147 -0
- package/.agents/RESILIENCE.md +235 -0
- package/.agents/SCALE_OUT.md +109 -0
- package/.agents/SECRETS.md +117 -0
- package/AGENTS.md +16 -0
- package/README.md +2 -2
- package/bin/commands/add-aws.js +396 -0
- package/bin/commands/add-ci.js +523 -0
- package/bin/commands/add-cloudflare.js +240 -0
- package/bin/commands/add-cloudrun.js +381 -0
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/add-migration-job.js +396 -0
- package/bin/commands/assets-sync.js +209 -0
- package/bin/commands/client-generate.js +52 -0
- package/bin/commands/console.js +53 -0
- package/bin/commands/db-migrate.js +47 -2
- package/bin/commands/db-studio.js +84 -0
- package/bin/commands/deploy-check.js +242 -13
- package/bin/commands/dev.js +1 -1
- package/bin/commands/export.js +87 -0
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/secrets-pull.js +208 -0
- package/bin/commands/start.js +1 -1
- package/bin/utils/db.js +7 -1
- package/bin/utils/project.js +2 -2
- package/bin/webspresso.js +22 -0
- package/core/app.js +421 -0
- package/core/auth/authenticate.js +93 -0
- package/core/auth/hash.js +5 -4
- package/core/auth/index.js +34 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +53 -10
- package/core/auth/middleware.js +16 -68
- package/core/auth/oauth.js +8 -17
- package/core/auth/redis-session.js +1008 -0
- package/core/auth/sessions.js +156 -0
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/cdn.js +296 -0
- package/core/cache/index.js +33 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +354 -0
- package/core/commands/registry.js +6 -20
- package/core/console/commands.js +148 -0
- package/core/console/context.js +260 -0
- package/core/console/eval.js +70 -0
- package/core/console/index.js +22 -0
- package/core/console/repl.js +102 -0
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +49 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +9 -7
- package/core/events/signal.js +7 -0
- package/core/export/discovery.js +191 -0
- package/core/export/exporter.js +277 -0
- package/core/export/index.js +24 -0
- package/core/flags/evaluator.js +252 -0
- package/core/flags/hasher.js +37 -0
- package/core/flags/index.js +21 -0
- package/core/flags/manager.js +263 -0
- package/core/flags/store.js +126 -0
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/http/adapter.js +74 -0
- package/core/http/express-adapter.js +46 -0
- package/core/http/hono-adapter.js +531 -0
- package/core/http/index.js +41 -0
- package/core/http/pipeline.js +485 -0
- package/core/index.js +336 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +325 -0
- package/core/limits/normalize.js +222 -0
- package/core/logger/formatters.js +264 -0
- package/core/logger/index.js +260 -0
- package/core/logger/trace.js +137 -0
- package/core/metrics/collectors.js +125 -0
- package/core/metrics/index.js +45 -0
- package/core/metrics/middleware.js +230 -0
- package/core/metrics/registry.js +366 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/d1.js +265 -0
- package/core/orm/index.js +176 -13
- package/core/orm/json-fields.js +22 -8
- package/core/orm/migrate-cloud.js +194 -0
- package/core/orm/model.js +10 -0
- package/core/orm/pool.js +97 -0
- package/core/orm/query-builder.js +72 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/cloudflare.js +131 -0
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/adapters/pubsub.js +400 -0
- package/core/queue/adapters/sqs.js +419 -0
- package/core/queue/adapters/wait-until.js +77 -0
- package/core/queue/index.js +8 -0
- package/core/queue/manager.js +66 -41
- package/core/rate-limit/index.js +22 -0
- package/core/rate-limit/limiter.js +219 -0
- package/core/rate-limit/stores/memory.js +129 -0
- package/core/rate-limit/stores/redis.js +151 -0
- package/core/rate-limit/utils.js +112 -0
- package/core/resilience/circuit-breaker.js +422 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/secrets/index.js +113 -0
- package/core/secrets/manager.js +308 -0
- package/core/secrets/providers/aws.js +288 -0
- package/core/secrets/providers/env.js +44 -0
- package/core/secrets/providers/gcp.js +307 -0
- package/core/secrets/providers/memory.js +76 -0
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +129 -19
- package/core/stream/index.js +19 -0
- package/core/stream/text-stream.js +326 -0
- package/core/studio/index.js +43 -0
- package/core/studio/inspector.js +258 -0
- package/core/studio/server.js +325 -0
- package/core/studio/ui.js +681 -0
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +1549 -2
- package/index.js +272 -2
- package/package.json +3 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +13 -6
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/index.js +3 -2
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/index.js +3 -1
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +3 -2
- package/plugins/cors.js +3 -2
- package/plugins/csrf/index.js +9 -12
- package/plugins/csv/index.js +8 -5
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/email/index.js +11 -9
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +5 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/queue/index.js +18 -2
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +16 -9
- package/plugins/rls/index.js +170 -0
- package/plugins/seo-checker/index.js +2 -2
- package/plugins/site-analytics/index.js +3 -1
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +12 -5
- package/plugins/upload/gcs-provider.js +148 -0
- package/plugins/upload/index.js +136 -77
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/upload/s3-provider.js +257 -0
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +33 -23
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/plugins/xlsx/index.js +7 -5
- package/src/api/api-loader.js +12 -95
- package/src/api/define-api.js +1 -0
- package/src/app-context.js +41 -5
- package/src/client-runtime/constants.js +10 -0
- package/src/client-runtime/mount.js +2 -3
- package/src/dev/banner.js +6 -2
- package/src/dev/client-gen.js +458 -0
- package/src/discovery/scan-routes.js +5 -2
- package/src/file-router.js +3 -3
- package/src/helpers.js +212 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +13 -154
- package/src/plugin-manager.js +99 -18
- package/src/server.js +335 -133
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +95 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +15 -1
- package/src/services/validator.js +38 -20
- 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
|
|
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
|
|
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
|
|
79
|
+
return sha256(str).slice(0, 16);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
/**
|
package/plugins/sitemap.js
CHANGED
|
@@ -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:
|
|
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
|
|
233
|
+
const hostnameNormalized = stripTrailingSlash(hostname);
|
|
233
234
|
|
|
234
235
|
// Filter static routes for sitemap
|
|
235
236
|
const sitemapRoutes = routes
|
package/plugins/swagger.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
+
};
|