webspresso 0.0.108 → 0.0.110
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/LICENSE +21 -0
- package/README.md +3 -3
- package/bin/commands/add-esbuild.js +7 -3
- package/bin/commands/add-tailwind.js +9 -5
- package/bin/commands/build.js +20 -4
- package/bin/commands/dev.js +57 -7
- package/bin/commands/doctor.js +42 -1
- package/bin/commands/favicon-generate.js +10 -3
- package/bin/commands/new.js +8 -3
- package/bin/commands/start.js +3 -1
- package/bin/utils/add-command.js +16 -0
- package/core/app.js +45 -3
- package/core/auth/authenticate.js +3 -3
- package/core/auth/hash.js +167 -43
- package/core/auth/index.js +13 -1
- package/core/auth/jwt.js +21 -2
- package/core/auth/manager.js +63 -14
- package/core/auth/middleware.js +4 -2
- package/core/auth/oauth.js +5 -1
- package/core/auth/tokens.js +7 -2
- package/core/auth/totp.js +42 -6
- package/core/auth/user-agent.js +2 -0
- package/core/client/adapter/datastar.js +1401 -0
- package/core/client/api.js +1337 -0
- package/core/client/controller.js +1706 -0
- package/core/client/debug.js +367 -0
- package/core/client/devtool.js +1134 -0
- package/core/client/forms.js +1164 -0
- package/core/client/fragments.js +894 -0
- package/core/client/index.d.ts +59 -0
- package/core/client/index.js +391 -0
- package/core/client/lite.js +175 -0
- package/core/client/server-fragments.js +868 -0
- package/core/client/storage.js +339 -0
- package/core/client/utils.js +281 -0
- package/core/compression/middleware.js +277 -26
- package/core/context/app-context.js +25 -3
- package/core/context/request.js +32 -4
- package/core/errors/middleware.js +14 -0
- package/core/events/dispatcher.js +33 -0
- package/core/forms/flash.js +136 -102
- package/core/health/middleware.js +36 -6
- package/core/http/pipeline.js +45 -6
- package/core/index.js +25 -7
- package/core/kernel/view.js +41 -10
- package/core/metrics/middleware.js +30 -6
- package/core/metrics/registry.js +13 -5
- package/core/orm/cache/fingerprint.js +32 -11
- package/core/orm/cache/layer.js +19 -2
- package/core/orm/hidden.js +145 -0
- package/core/orm/model.js +48 -2
- package/core/orm/query-builder.js +343 -6
- package/core/orm/query-filters.js +14 -2
- package/core/orm/repository.js +64 -13
- package/core/orm/schema-helpers.js +9 -6
- package/core/orm/transaction.js +50 -6
- package/core/orm/types.js +4 -0
- package/core/queue/adapters/database.js +184 -2
- package/core/queue/adapters/memory.js +153 -5
- package/core/queue/adapters/redis.js +59 -4
- package/core/queue/job.js +14 -1
- package/core/queue/manager.js +115 -0
- package/core/resilience/circuit-breaker.js +27 -8
- package/core/services/builtins/exchange.js +174 -7
- package/core/services/builtins/file-manager.js +36 -0
- package/core/services/builtins/mail.js +27 -0
- package/core/services/builtins/queue.js +144 -0
- package/core/services/builtins/security.js +118 -0
- package/core/services/builtins/webhook.js +110 -0
- package/core/services/executor.js +38 -16
- package/core/services/index.js +4 -1
- package/core/sse/stream.js +28 -5
- package/core/utils/timeout.js +8 -1
- package/core/validation/compiler.js +10 -7
- package/index.d.ts +1320 -74
- package/index.js +128 -8
- package/package.json +75 -27
- package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
- package/plugins/admin-panel/index.js +19 -8
- package/plugins/csrf/index.js +3 -3
- package/plugins/data-exchange/export.js +214 -0
- package/plugins/data-exchange/import.js +48 -5
- package/plugins/data-exchange/index.js +70 -28
- package/plugins/data-exchange/preview.js +266 -0
- package/plugins/data-exchange/template.js +242 -0
- package/plugins/email/admin-component.js +783 -141
- package/plugins/email/api-handlers.js +51 -1
- package/plugins/email/index.js +5 -0
- package/plugins/email/service.js +48 -0
- package/plugins/file-manager/admin-component.js +1546 -70
- package/plugins/file-manager/api-handlers.js +166 -1
- package/plugins/file-manager/core.js +598 -8
- package/plugins/file-manager/index.js +8 -0
- package/plugins/index.js +7 -0
- package/plugins/mcp/auth.js +3 -3
- package/plugins/oauth/index.js +36 -3
- package/plugins/queue/admin-component.js +1092 -0
- package/plugins/queue/api-handlers.js +248 -0
- package/plugins/queue/index.js +69 -0
- package/plugins/rest-resources/index.js +7 -2
- package/plugins/security/admin-component.js +689 -0
- package/plugins/security/api-handlers.js +169 -0
- package/plugins/security/audit.js +153 -0
- package/plugins/security/bruteforce.js +244 -0
- package/plugins/security/firewall.js +459 -0
- package/plugins/security/headers.js +214 -0
- package/plugins/security/honeypot.js +234 -0
- package/plugins/security/incident-recorder.js +137 -0
- package/plugins/security/index.js +211 -0
- package/plugins/seo-checker/analyzer.js +20 -7
- package/plugins/upload/index.js +36 -0
- package/plugins/webhooks/admin-component.js +702 -175
- package/plugins/webhooks/api-handlers.js +75 -1
- package/plugins/webhooks/dispatcher.js +290 -11
- package/plugins/webhooks/index.js +37 -2
- package/plugins/webhooks/sender.js +56 -0
- package/plugins/xlsx/engine.js +19 -4
- package/src/api/api-loader.js +19 -1
- package/src/bundle/index.js +443 -17
- package/src/client-runtime/bootstrap-swup.js +9 -0
- package/src/client-runtime/mount.js +15 -20
- package/src/client-runtime/resolve.js +3 -2
- package/src/dev/api-typegen.js +1300 -0
- package/src/dev/banner.js +10 -1
- package/src/dev/colors.js +1 -1
- package/src/dev/index.js +7 -1
- package/src/dev/typegen.js +173 -17
- package/src/dev-reload/index.js +1 -1
- package/src/discovery/file-route-parser.js +17 -3
- package/src/discovery/scan-routes.js +4 -1
- package/src/file-router.js +253 -69
- package/src/helpers.js +46 -26
- package/src/njk-frontmatter.js +30 -4
- package/src/routing/cascading.js +26 -8
- package/src/routing/mount-discovered-routes.js +76 -0
- package/src/server.js +231 -70
- package/views/partials/webspresso-client-runtime.njk +1 -9
- package/.agents/ADMIN.md +0 -426
- package/.agents/AGENTS.md +0 -96
- package/.agents/ARCHITECTURE.md +0 -163
- package/.agents/AUTH.md +0 -581
- package/.agents/CACHE.md +0 -148
- package/.agents/CLI.md +0 -649
- package/.agents/COMPRESSION.md +0 -59
- package/.agents/CONTEXT.md +0 -152
- package/.agents/CONVENTIONS.md +0 -50
- package/.agents/EARLY_HINTS.md +0 -120
- package/.agents/EMAIL.md +0 -223
- package/.agents/ERRORS.md +0 -260
- package/.agents/EVENTS.md +0 -179
- package/.agents/FLAGS.md +0 -118
- package/.agents/HEALTH.md +0 -118
- package/.agents/LIMITS.md +0 -109
- package/.agents/LOGGING.md +0 -93
- package/.agents/MCP.md +0 -103
- package/.agents/METRICS.md +0 -127
- package/.agents/MIGRATION_JOBS.md +0 -95
- package/.agents/ORM.md +0 -700
- package/.agents/PLUGINS.md +0 -729
- package/.agents/QUEUE.md +0 -290
- package/.agents/RATE_LIMIT.md +0 -147
- package/.agents/REALTIME.md +0 -213
- package/.agents/RESILIENCE.md +0 -235
- package/.agents/ROUTING.md +0 -605
- package/.agents/SCALE_OUT.md +0 -109
- package/.agents/SECRETS.md +0 -117
- package/.agents/SERVICES.md +0 -341
- package/.agents/SHUTDOWN.md +0 -74
- package/.agents/STREAMING.md +0 -108
- package/.agents/WORKFLOW.md +0 -27
- package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
- package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
- package/.agents/skills/webspresso-framework/SKILL.md +0 -89
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
- package/.agents/skills/webspresso-services/SKILL.md +0 -134
- package/.cursorrules +0 -14
- package/AGENTS.md +0 -203
- package/CLAUDE.md +0 -54
- package/core/kernel/run-demo.js +0 -58
- package/plugins/admin-panel/styles.js +0 -9
- package/plugins/data-exchange/export-xlsx.js +0 -119
- package/src/client-runtime/bootstrap-alpine-swup.js +0 -34
package/src/server.js
CHANGED
|
@@ -121,23 +121,40 @@ function fastETag(body, encoding) {
|
|
|
121
121
|
* @param {Object} options - Nunjucks options
|
|
122
122
|
* @returns {nunjucks.Environment}
|
|
123
123
|
*/
|
|
124
|
+
const normalizedTemplateNameCache = new Map();
|
|
124
125
|
function normalizeTemplateName(name) {
|
|
125
126
|
if (typeof name !== 'string') return name;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
normalized =
|
|
127
|
+
const cached = normalizedTemplateNameCache.get(name);
|
|
128
|
+
if (cached !== undefined) return cached;
|
|
129
|
+
let normalized = name;
|
|
130
|
+
if (name.includes('\\') || name.includes('views')) {
|
|
131
|
+
normalized = name.replace(/\\/g, '/');
|
|
132
|
+
// Collapse any repeated views/views/... to a single views/ or clean path
|
|
133
|
+
while (/(?:^|\/)views\/(?:\.\/)?views(?:\/|$)/.test(normalized)) {
|
|
134
|
+
normalized = normalized.replace(/((?:^|\/)views)\/(?:\.\/)?views(?=\/|$)/g, '$1');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (normalizedTemplateNameCache.size < 512) {
|
|
138
|
+
normalizedTemplateNameCache.set(name, normalized);
|
|
131
139
|
}
|
|
132
140
|
return normalized;
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
const canonicalKeyCache = new Map();
|
|
135
144
|
function canonicalTemplateKey(name) {
|
|
145
|
+
if (typeof name !== 'string') return String(name);
|
|
146
|
+
const cached = canonicalKeyCache.get(name);
|
|
147
|
+
if (cached !== undefined) return cached;
|
|
136
148
|
const norm = normalizeTemplateName(name);
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
let key = norm;
|
|
150
|
+
if (typeof norm === 'string' && (norm.startsWith('.') || norm.startsWith('views/'))) {
|
|
151
|
+
// Canonical key strips leading ./ or views/ for cycle detection
|
|
152
|
+
key = norm.replace(/^\.\//, '').replace(/^views\//, '');
|
|
153
|
+
}
|
|
154
|
+
if (canonicalKeyCache.size < 512) {
|
|
155
|
+
canonicalKeyCache.set(name, key);
|
|
156
|
+
}
|
|
157
|
+
return key;
|
|
141
158
|
}
|
|
142
159
|
|
|
143
160
|
function configureSafeNunjucks(templateDirs, options = {}) {
|
|
@@ -348,8 +365,21 @@ function configureSafeNunjucks(templateDirs, options = {}) {
|
|
|
348
365
|
*/
|
|
349
366
|
function getDefaultHelmetConfig(isDev) {
|
|
350
367
|
return {
|
|
351
|
-
//
|
|
352
|
-
contentSecurityPolicy: isDev ?
|
|
368
|
+
// In development: use reportOnly so developers are alerted to CSP violations without breaking local dev tools
|
|
369
|
+
contentSecurityPolicy: isDev ? {
|
|
370
|
+
reportOnly: true,
|
|
371
|
+
directives: {
|
|
372
|
+
defaultSrc: ["'self'"],
|
|
373
|
+
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Tailwind / Dev reload
|
|
374
|
+
scriptSrc: ["'self'", "'unsafe-inline'"], // Allow inline scripts for client runtime / dev tools
|
|
375
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
376
|
+
fontSrc: ["'self'", "data:"],
|
|
377
|
+
connectSrc: ["'self'", "ws:", "wss:"], // WebSockets for dev live reload
|
|
378
|
+
frameSrc: ["'none'"],
|
|
379
|
+
objectSrc: ["'none'"],
|
|
380
|
+
upgradeInsecureRequests: null, // Browser ignores & warns on upgrade-insecure-requests in Report-Only policy
|
|
381
|
+
}
|
|
382
|
+
} : {
|
|
353
383
|
directives: {
|
|
354
384
|
defaultSrc: ["'self'"],
|
|
355
385
|
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles for Tailwind
|
|
@@ -569,6 +599,34 @@ function haltOnTimedout(req, res, next) {
|
|
|
569
599
|
if (!req.timedout) next();
|
|
570
600
|
}
|
|
571
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Replace Express 5's re-parsing `req.query` getter with a per-request memoized accessor.
|
|
604
|
+
* The first read delegates to the inherited getter (honouring the app's `query parser`);
|
|
605
|
+
* later reads return the same object. Assignments (`req.query = …`) keep working.
|
|
606
|
+
* @param {import('express').Request} req
|
|
607
|
+
*/
|
|
608
|
+
function memoizeRequestQuery(req) {
|
|
609
|
+
if (Object.prototype.hasOwnProperty.call(req, 'query')) return;
|
|
610
|
+
const proto = Object.getPrototypeOf(req);
|
|
611
|
+
let parsed;
|
|
612
|
+
let ready = false;
|
|
613
|
+
Object.defineProperty(req, 'query', {
|
|
614
|
+
configurable: true,
|
|
615
|
+
enumerable: true,
|
|
616
|
+
get() {
|
|
617
|
+
if (!ready) {
|
|
618
|
+
parsed = Reflect.get(proto, 'query', req);
|
|
619
|
+
ready = true;
|
|
620
|
+
}
|
|
621
|
+
return parsed;
|
|
622
|
+
},
|
|
623
|
+
set(value) {
|
|
624
|
+
parsed = value;
|
|
625
|
+
ready = true;
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
572
630
|
/**
|
|
573
631
|
* Create and configure the Express app
|
|
574
632
|
* @param {Object} options - Configuration options
|
|
@@ -590,14 +648,14 @@ function haltOnTimedout(req, res, next) {
|
|
|
590
648
|
* @param {string|boolean} options.timeout - Request timeout (default: '30s', false to disable)
|
|
591
649
|
* @param {Object} options.auth - Authentication manager instance (from createAuth)
|
|
592
650
|
* @param {Object} options.db - Database instance (exposed as ctx.db to plugins)
|
|
593
|
-
* @param {Object} [options.clientRuntime] - Optional client assets: `{
|
|
651
|
+
* @param {Object} [options.clientRuntime] - Optional client assets: `{ swup?: boolean|object }`. Overridable by env `WEBSPRESSO_SWUP` (=1 or true). Serves `/__webspresso/client-runtime/*` when enabled.
|
|
594
652
|
* @param {boolean|{enabled?: boolean, stylesheets?: boolean, scripts?: boolean}} [options.pageAssets] - If truthy, route `load()` return values for `stylesheets` and `scripts` are reserved: removed from the root template context and passed as `pageHead` to Nunjucks, with `pageAssets: true` (for layout to emit `<link>` / `<script>`). Default off.
|
|
595
653
|
* @param {function(import('express').Express, Object): void} [options.setupRoutes] - Called after file routes and plugins, before 404 handler
|
|
596
654
|
* @returns {Object} { app, nunjucksEnv, pluginManager, authMiddleware }
|
|
597
655
|
*/
|
|
598
656
|
function createApp(options = {}) {
|
|
599
|
-
const NODE_ENV = process.env.NODE_ENV || '
|
|
600
|
-
const isDev =
|
|
657
|
+
const NODE_ENV = process.env.NODE_ENV || 'production';
|
|
658
|
+
const isDev = options.isDev !== undefined ? Boolean(options.isDev) : (NODE_ENV === 'development');
|
|
601
659
|
const isTest = NODE_ENV === 'test';
|
|
602
660
|
|
|
603
661
|
const {
|
|
@@ -688,8 +746,16 @@ function createApp(options = {}) {
|
|
|
688
746
|
};
|
|
689
747
|
|
|
690
748
|
try {
|
|
749
|
+
// Adopt or resolve Background Queue
|
|
750
|
+
const queueManager = core.queue || (options.queue && typeof options.queue.dispatch === 'function' ? options.queue : null);
|
|
751
|
+
if (queueManager) {
|
|
752
|
+
app.queue = queueManager;
|
|
753
|
+
options.queue = queueManager;
|
|
754
|
+
}
|
|
755
|
+
|
|
691
756
|
setAppContext({
|
|
692
757
|
db: options.db ?? null,
|
|
758
|
+
queue: queueManager ?? null,
|
|
693
759
|
shutdownManager,
|
|
694
760
|
serviceRegistry,
|
|
695
761
|
events: eventDispatcher,
|
|
@@ -714,14 +780,11 @@ function createApp(options = {}) {
|
|
|
714
780
|
app.service.invalidateByTags = (tags, serviceName) => serviceRegistry.invalidateByTags(tags, serviceName);
|
|
715
781
|
app.service.invalidateTag = app.service.invalidateByTag;
|
|
716
782
|
app.service.invalidateTags = app.service.invalidateByTags;
|
|
717
|
-
if (options.queue) {
|
|
718
|
-
app.queue = options.queue;
|
|
719
|
-
}
|
|
720
783
|
|
|
721
784
|
const { renderStream } = require('../core/ssr/stream');
|
|
722
785
|
const { createSseStream } = require('../core/sse/stream');
|
|
723
786
|
const { streamText, streamJson } = require('../core/stream/text-stream');
|
|
724
|
-
const { runWithContext } = require('../core/context/request');
|
|
787
|
+
const { runWithContext, getContext } = require('../core/context/request');
|
|
725
788
|
app.use((req, res, next) => {
|
|
726
789
|
// 1. Request draining check during graceful shutdown (allow health/readiness probes through)
|
|
727
790
|
const isHealthEndpoint = req.path === '/healthz' || req.path === '/readyz' || req.path === '/health' ||
|
|
@@ -748,67 +811,109 @@ function createApp(options = {}) {
|
|
|
748
811
|
req.id = reqId;
|
|
749
812
|
res.setHeader('X-Request-Id', reqId);
|
|
750
813
|
|
|
814
|
+
// 2b. Express 5 re-parses the query string (qs.parse) on every `req.query` read; the file
|
|
815
|
+
// router, validation and loaders read it ~3x per request. Parse once, keep it assignable.
|
|
816
|
+
memoizeRequestQuery(req);
|
|
817
|
+
|
|
751
818
|
// 3. Request DB, Services, Queue, Events, and Context
|
|
752
819
|
if (options.db) {
|
|
753
820
|
req.db = options.db;
|
|
754
821
|
}
|
|
755
|
-
if (
|
|
756
|
-
req.queue =
|
|
822
|
+
if (queueManager) {
|
|
823
|
+
req.queue = queueManager;
|
|
757
824
|
}
|
|
758
825
|
req.events = eventDispatcher;
|
|
759
826
|
req.cache = appCache;
|
|
760
827
|
req.breakers = appBreakers;
|
|
761
|
-
req.breaker = (name, opts) => appBreakers.getBreaker(name, opts);
|
|
762
828
|
req.retry = retry;
|
|
763
829
|
req.context = core.createContext({
|
|
764
830
|
req,
|
|
765
831
|
res,
|
|
766
832
|
app,
|
|
833
|
+
queue: queueManager ?? null,
|
|
767
834
|
assetManager: appAssetManager,
|
|
768
835
|
...(req.context || {}),
|
|
769
836
|
});
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
req.
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
req.service.invalidateByTag = (tag, serviceName) =>
|
|
779
|
-
serviceRegistry.invalidateByTag(tag, serviceName);
|
|
780
|
-
req.service.invalidateByTags = (tags, serviceName) =>
|
|
781
|
-
serviceRegistry.invalidateByTags(tags, serviceName);
|
|
782
|
-
req.service.invalidateTag = req.service.invalidateByTag;
|
|
783
|
-
req.service.invalidateTags = req.service.invalidateByTags;
|
|
784
|
-
|
|
785
|
-
// 4. SSR Streaming Response Helper
|
|
786
|
-
res.renderStream = (templatePath, context = {}, renderOptions = {}) => {
|
|
787
|
-
return renderStream(res, templatePath, context, {
|
|
788
|
-
env: nunjucksEnv,
|
|
789
|
-
...renderOptions,
|
|
790
|
-
});
|
|
791
|
-
};
|
|
792
|
-
|
|
793
|
-
// 5. Server-Sent Events (SSE) Response Helper
|
|
794
|
-
res.sse = (sourceOrOptions, maybeOptions) => {
|
|
795
|
-
return createSseStream(req, res, sourceOrOptions, maybeOptions);
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
// 6. Text and NDJSON Streaming Helpers (AI / LLM Tokens)
|
|
799
|
-
res.streamText = (sourceOrCallback, maybeOptions) => {
|
|
800
|
-
return streamText(req, res, sourceOrCallback, maybeOptions);
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
res.streamJson = (sourceOrCallback, maybeOptions) => {
|
|
804
|
-
return streamJson(req, res, sourceOrCallback, maybeOptions);
|
|
805
|
-
};
|
|
837
|
+
// Shared helper functions (no per-request closures). Assigned as own properties rather than
|
|
838
|
+
// on app.request/app.response so they survive Express sub-app prototype swaps.
|
|
839
|
+
req.breaker = requestBreaker;
|
|
840
|
+
Object.defineProperty(req, 'service', REQUEST_SERVICE_DESCRIPTOR);
|
|
841
|
+
res.renderStream = renderStreamHelper;
|
|
842
|
+
res.sse = sseHelper;
|
|
843
|
+
res.streamText = streamTextHelper;
|
|
844
|
+
res.streamJson = streamJsonHelper;
|
|
806
845
|
|
|
807
846
|
runWithContext(req.context, () => {
|
|
808
847
|
next();
|
|
809
848
|
});
|
|
810
849
|
});
|
|
811
850
|
|
|
851
|
+
function requestBreaker(name, opts) {
|
|
852
|
+
return appBreakers.getBreaker(name, opts);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/** Build the request-bound service caller (only when a handler actually touches req.service). */
|
|
856
|
+
function createRequestService(req) {
|
|
857
|
+
const service = (name, input, opts) => core.service(name, input, req.context, opts);
|
|
858
|
+
service.call = service;
|
|
859
|
+
service.execute = service;
|
|
860
|
+
service.invalidate = (name, input, ctx) => serviceRegistry.invalidate(name, input, ctx || req.context);
|
|
861
|
+
service.clearCache = (name) => serviceRegistry.clearCache(name);
|
|
862
|
+
service.invalidateByTag = (tag, serviceName) => serviceRegistry.invalidateByTag(tag, serviceName);
|
|
863
|
+
service.invalidateByTags = (tags, serviceName) => serviceRegistry.invalidateByTags(tags, serviceName);
|
|
864
|
+
service.invalidateTag = service.invalidateByTag;
|
|
865
|
+
service.invalidateTags = service.invalidateByTags;
|
|
866
|
+
return service;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const REQUEST_SERVICE_DESCRIPTOR = {
|
|
870
|
+
configurable: true,
|
|
871
|
+
enumerable: true,
|
|
872
|
+
get() {
|
|
873
|
+
const service = createRequestService(this);
|
|
874
|
+
Object.defineProperty(this, 'service', { value: service, writable: true, configurable: true, enumerable: true });
|
|
875
|
+
return service;
|
|
876
|
+
},
|
|
877
|
+
set(value) {
|
|
878
|
+
Object.defineProperty(this, 'service', { value, writable: true, configurable: true, enumerable: true });
|
|
879
|
+
},
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
function resolveStreamReqRes(receiver) {
|
|
883
|
+
const ctx = getContext();
|
|
884
|
+
const res = receiver && typeof receiver.setHeader === 'function'
|
|
885
|
+
? receiver
|
|
886
|
+
: (ctx?.res || null);
|
|
887
|
+
const req = (res && res.req) || (receiver && receiver.req) || (ctx?.req || null);
|
|
888
|
+
return { req, res };
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// 4. SSR Streaming Response Helper
|
|
892
|
+
function renderStreamHelper(templatePath, context = {}, renderOptions = {}) {
|
|
893
|
+
const { res } = resolveStreamReqRes(this);
|
|
894
|
+
return renderStream(res || this, templatePath, context, {
|
|
895
|
+
env: nunjucksEnv,
|
|
896
|
+
...renderOptions,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// 5. Server-Sent Events (SSE) Response Helper
|
|
901
|
+
function sseHelper(sourceOrOptions, maybeOptions) {
|
|
902
|
+
const { req, res } = resolveStreamReqRes(this);
|
|
903
|
+
return createSseStream(req, res, sourceOrOptions, maybeOptions);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// 6. Text and NDJSON Streaming Helpers (AI / LLM Tokens)
|
|
907
|
+
function streamTextHelper(sourceOrCallback, maybeOptions) {
|
|
908
|
+
const { req, res } = resolveStreamReqRes(this);
|
|
909
|
+
return streamText(req, res, sourceOrCallback, maybeOptions);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function streamJsonHelper(sourceOrCallback, maybeOptions) {
|
|
913
|
+
const { req, res } = resolveStreamReqRes(this);
|
|
914
|
+
return streamJson(req, res, sourceOrCallback, maybeOptions);
|
|
915
|
+
}
|
|
916
|
+
|
|
812
917
|
// Async handler wrapper helper for automatic promise rejection handling
|
|
813
918
|
function wrapAsync(fn) {
|
|
814
919
|
if (typeof fn !== 'function') return fn;
|
|
@@ -1128,7 +1233,7 @@ function createApp(options = {}) {
|
|
|
1128
1233
|
}
|
|
1129
1234
|
}
|
|
1130
1235
|
}
|
|
1131
|
-
|
|
1236
|
+
|
|
1132
1237
|
const helmetMw = helmet(finalConfig);
|
|
1133
1238
|
|
|
1134
1239
|
// Check if configuration uses purely static directives (no per-request dynamic functions)
|
|
@@ -1243,9 +1348,6 @@ function createApp(options = {}) {
|
|
|
1243
1348
|
app.set('trust proxy', false);
|
|
1244
1349
|
}
|
|
1245
1350
|
|
|
1246
|
-
// Ensure extended query parser for nested filter/sort query parameters (?filter[field]=val)
|
|
1247
|
-
app.set('query parser', 'extended');
|
|
1248
|
-
|
|
1249
1351
|
// Fast hardware-accelerated Weak ETag (CRC32 + Hex length) as default
|
|
1250
1352
|
// Configurable via options.etag or options.server.etag
|
|
1251
1353
|
const etagSetting = options.etag !== undefined
|
|
@@ -1264,11 +1366,6 @@ function createApp(options = {}) {
|
|
|
1264
1366
|
const limitsOpt = options.limits ?? options.server?.limits;
|
|
1265
1367
|
const activeLimits = setupLimitsMiddleware(app, limitsOpt);
|
|
1266
1368
|
|
|
1267
|
-
// Cookie parser & Flash messages (AdonisJS & Django-inspired old input and errors)
|
|
1268
|
-
const cookieParser = require('cookie-parser');
|
|
1269
|
-
app.use(cookieParser());
|
|
1270
|
-
app.use(createFlashMiddleware());
|
|
1271
|
-
|
|
1272
1369
|
// Ensure default empty object for body on mutating requests if not populated
|
|
1273
1370
|
// and halt processing if request has timed out (after body parsers)
|
|
1274
1371
|
app.use((req, res, next) => {
|
|
@@ -1281,12 +1378,63 @@ function createApp(options = {}) {
|
|
|
1281
1378
|
next();
|
|
1282
1379
|
});
|
|
1283
1380
|
|
|
1381
|
+
|
|
1382
|
+
// Hardened, prototype-safe Query Parser with DoS depth & parameter limits
|
|
1383
|
+
const queryParserOpt = options.queryParser ?? options.server?.queryParser;
|
|
1384
|
+
if (queryParserOpt === false) {
|
|
1385
|
+
app.set('query parser', false);
|
|
1386
|
+
} else if (queryParserOpt === 'simple') {
|
|
1387
|
+
app.set('query parser', 'simple');
|
|
1388
|
+
} else if (typeof queryParserOpt === 'function') {
|
|
1389
|
+
app.set('query parser', queryParserOpt);
|
|
1390
|
+
} else {
|
|
1391
|
+
const qs = require('qs');
|
|
1392
|
+
const parserConfig = (typeof queryParserOpt === 'object' && queryParserOpt !== null) ? queryParserOpt : {};
|
|
1393
|
+
const safeDepth = parserConfig.depth !== undefined ? parserConfig.depth : 5;
|
|
1394
|
+
const safeParamLimit = parserConfig.parameterLimit !== undefined
|
|
1395
|
+
? parserConfig.parameterLimit
|
|
1396
|
+
: (activeLimits?.parameterLimit || 1000);
|
|
1397
|
+
const safeArrayLimit = parserConfig.arrayLimit !== undefined ? parserConfig.arrayLimit : 100;
|
|
1398
|
+
const safeAllowDots = parserConfig.allowDots !== undefined ? Boolean(parserConfig.allowDots) : true;
|
|
1399
|
+
|
|
1400
|
+
app.set('query parser', function safeExtendedQueryParser(str) {
|
|
1401
|
+
return qs.parse(str, {
|
|
1402
|
+
depth: safeDepth,
|
|
1403
|
+
parameterLimit: safeParamLimit,
|
|
1404
|
+
arrayLimit: safeArrayLimit,
|
|
1405
|
+
allowDots: safeAllowDots,
|
|
1406
|
+
allowPrototypes: false,
|
|
1407
|
+
});
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
// Cookie parser & Flash messages (AdonisJS & Django-inspired old input and errors)
|
|
1412
|
+
const cookieSecret =
|
|
1413
|
+
options.secret ||
|
|
1414
|
+
options.cookieSecret ||
|
|
1415
|
+
options.session?.secret ||
|
|
1416
|
+
options.cookies?.secret ||
|
|
1417
|
+
(typeof authManager?.getSessionConfig === 'function' ? authManager.getSessionConfig()?.secret : authManager?.config?.session?.secret) ||
|
|
1418
|
+
process.env.COOKIE_SECRET ||
|
|
1419
|
+
process.env.SESSION_SECRET ||
|
|
1420
|
+
undefined;
|
|
1421
|
+
|
|
1422
|
+
const cookieParser = require('cookie-parser');
|
|
1423
|
+
app.use(cookieParser(cookieSecret));
|
|
1424
|
+
|
|
1425
|
+
const flashConfig = typeof options.flash === 'object' ? { ...options.flash } : {};
|
|
1426
|
+
if (cookieSecret && flashConfig.secret === undefined) {
|
|
1427
|
+
flashConfig.secret = cookieSecret;
|
|
1428
|
+
}
|
|
1429
|
+
app.use(createFlashMiddleware(flashConfig));
|
|
1430
|
+
|
|
1431
|
+
|
|
1284
1432
|
// Native Health & Readiness Probes (/healthz & /readyz)
|
|
1285
1433
|
const { setupHealthMiddleware } = require('../core/health');
|
|
1286
1434
|
const healthOpt = options.healthCheck ?? options.server?.healthCheck;
|
|
1287
1435
|
setupHealthMiddleware(app, healthOpt, {
|
|
1288
1436
|
db: options.db,
|
|
1289
|
-
queue: options.queue,
|
|
1437
|
+
queue: queueManager || options.queue,
|
|
1290
1438
|
shutdownManager,
|
|
1291
1439
|
});
|
|
1292
1440
|
|
|
@@ -1669,6 +1817,7 @@ function createApp(options = {}) {
|
|
|
1669
1817
|
pageAssets: options.pageAssets,
|
|
1670
1818
|
serviceRegistry,
|
|
1671
1819
|
skipExistingRoutes: routeTable,
|
|
1820
|
+
isDev,
|
|
1672
1821
|
});
|
|
1673
1822
|
classicRouteMeta = classicMount.routeMetadata || [];
|
|
1674
1823
|
registerDynamicFileRoutes = classicMount.registerDynamicFileRoutes || (() => {});
|
|
@@ -1677,6 +1826,18 @@ function createApp(options = {}) {
|
|
|
1677
1826
|
// Combine route metadata for plugin manager
|
|
1678
1827
|
const allRouteMeta = [...discoveredMeta, ...classicRouteMeta];
|
|
1679
1828
|
pluginManager.setRoutes(allRouteMeta);
|
|
1829
|
+
|
|
1830
|
+
// The dev banner lists every mounted route: discovered routes plus classic .njk/.js page routes
|
|
1831
|
+
const bannerRoutes = [];
|
|
1832
|
+
const bannerRouteKeys = new Set();
|
|
1833
|
+
for (const r of allRouteMeta) {
|
|
1834
|
+
const method = String(r.method || 'GET').toUpperCase();
|
|
1835
|
+
const routePath = r.path || r.pattern;
|
|
1836
|
+
const key = `${method} ${routePath}`;
|
|
1837
|
+
if (!routePath || bannerRouteKeys.has(key)) continue;
|
|
1838
|
+
bannerRouteKeys.add(key);
|
|
1839
|
+
bannerRoutes.push({ method, path: routePath, type: r.type === 'api' ? 'api' : 'page', source: r.file || '' });
|
|
1840
|
+
}
|
|
1680
1841
|
|
|
1681
1842
|
// Call onRoutesReady hook on plugins with full unified context and mount routes
|
|
1682
1843
|
pluginManager.onRoutesReadySync({
|
|
@@ -2001,7 +2162,7 @@ function createApp(options = {}) {
|
|
|
2001
2162
|
printDevBanner({
|
|
2002
2163
|
app,
|
|
2003
2164
|
server,
|
|
2004
|
-
routeTable,
|
|
2165
|
+
routeTable: bannerRoutes,
|
|
2005
2166
|
options,
|
|
2006
2167
|
db: options.db ?? null,
|
|
2007
2168
|
serviceRegistry,
|
|
@@ -2019,7 +2180,7 @@ function createApp(options = {}) {
|
|
|
2019
2180
|
printDevBanner({
|
|
2020
2181
|
app,
|
|
2021
2182
|
server: app.server,
|
|
2022
|
-
routeTable,
|
|
2183
|
+
routeTable: bannerRoutes,
|
|
2023
2184
|
options,
|
|
2024
2185
|
db: options.db ?? null,
|
|
2025
2186
|
serviceRegistry,
|
|
@@ -2034,7 +2195,7 @@ function createApp(options = {}) {
|
|
|
2034
2195
|
return formatDevBanner({
|
|
2035
2196
|
app,
|
|
2036
2197
|
server: app.server,
|
|
2037
|
-
routeTable,
|
|
2198
|
+
routeTable: bannerRoutes,
|
|
2038
2199
|
options,
|
|
2039
2200
|
db: options.db ?? null,
|
|
2040
2201
|
serviceRegistry,
|
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
{# Served by createApp when clientRuntime.
|
|
2
|
-
{% if clientRuntime and clientRuntime.alpine %}
|
|
3
|
-
<style>[x-cloak] { display: none !important; }</style>
|
|
4
|
-
<script defer src="/__webspresso/client-runtime/alpine.min.js"></script>
|
|
5
|
-
{% endif %}
|
|
1
|
+
{# Served by createApp when clientRuntime.swup is enabled. See src/client-runtime/. #}
|
|
6
2
|
{% if clientRuntime and clientRuntime.swup %}
|
|
7
3
|
<script defer src="/__webspresso/client-runtime/swup.umd.js"></script>
|
|
8
4
|
<script defer src="/__webspresso/client-runtime/swup-head-plugin.umd.js"></script>
|
|
9
5
|
<script defer src="/__webspresso/client-runtime/swup-scripts-plugin.umd.js"></script>
|
|
10
|
-
{% if clientRuntime.alpine %}
|
|
11
|
-
<script defer src="/__webspresso/client-runtime/bootstrap-alpine-swup.js"></script>
|
|
12
|
-
{% else %}
|
|
13
6
|
<script defer src="/__webspresso/client-runtime/bootstrap-swup.js"></script>
|
|
14
7
|
{% endif %}
|
|
15
|
-
{% endif %}
|