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.
Files changed (184) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -3
  3. package/bin/commands/add-esbuild.js +7 -3
  4. package/bin/commands/add-tailwind.js +9 -5
  5. package/bin/commands/build.js +20 -4
  6. package/bin/commands/dev.js +57 -7
  7. package/bin/commands/doctor.js +42 -1
  8. package/bin/commands/favicon-generate.js +10 -3
  9. package/bin/commands/new.js +8 -3
  10. package/bin/commands/start.js +3 -1
  11. package/bin/utils/add-command.js +16 -0
  12. package/core/app.js +45 -3
  13. package/core/auth/authenticate.js +3 -3
  14. package/core/auth/hash.js +167 -43
  15. package/core/auth/index.js +13 -1
  16. package/core/auth/jwt.js +21 -2
  17. package/core/auth/manager.js +63 -14
  18. package/core/auth/middleware.js +4 -2
  19. package/core/auth/oauth.js +5 -1
  20. package/core/auth/tokens.js +7 -2
  21. package/core/auth/totp.js +42 -6
  22. package/core/auth/user-agent.js +2 -0
  23. package/core/client/adapter/datastar.js +1401 -0
  24. package/core/client/api.js +1337 -0
  25. package/core/client/controller.js +1706 -0
  26. package/core/client/debug.js +367 -0
  27. package/core/client/devtool.js +1134 -0
  28. package/core/client/forms.js +1164 -0
  29. package/core/client/fragments.js +894 -0
  30. package/core/client/index.d.ts +59 -0
  31. package/core/client/index.js +391 -0
  32. package/core/client/lite.js +175 -0
  33. package/core/client/server-fragments.js +868 -0
  34. package/core/client/storage.js +339 -0
  35. package/core/client/utils.js +281 -0
  36. package/core/compression/middleware.js +277 -26
  37. package/core/context/app-context.js +25 -3
  38. package/core/context/request.js +32 -4
  39. package/core/errors/middleware.js +14 -0
  40. package/core/events/dispatcher.js +33 -0
  41. package/core/forms/flash.js +136 -102
  42. package/core/health/middleware.js +36 -6
  43. package/core/http/pipeline.js +45 -6
  44. package/core/index.js +25 -7
  45. package/core/kernel/view.js +41 -10
  46. package/core/metrics/middleware.js +30 -6
  47. package/core/metrics/registry.js +13 -5
  48. package/core/orm/cache/fingerprint.js +32 -11
  49. package/core/orm/cache/layer.js +19 -2
  50. package/core/orm/hidden.js +145 -0
  51. package/core/orm/model.js +48 -2
  52. package/core/orm/query-builder.js +343 -6
  53. package/core/orm/query-filters.js +14 -2
  54. package/core/orm/repository.js +64 -13
  55. package/core/orm/schema-helpers.js +9 -6
  56. package/core/orm/transaction.js +50 -6
  57. package/core/orm/types.js +4 -0
  58. package/core/queue/adapters/database.js +184 -2
  59. package/core/queue/adapters/memory.js +153 -5
  60. package/core/queue/adapters/redis.js +59 -4
  61. package/core/queue/job.js +14 -1
  62. package/core/queue/manager.js +115 -0
  63. package/core/resilience/circuit-breaker.js +27 -8
  64. package/core/services/builtins/exchange.js +174 -7
  65. package/core/services/builtins/file-manager.js +36 -0
  66. package/core/services/builtins/mail.js +27 -0
  67. package/core/services/builtins/queue.js +144 -0
  68. package/core/services/builtins/security.js +118 -0
  69. package/core/services/builtins/webhook.js +110 -0
  70. package/core/services/executor.js +38 -16
  71. package/core/services/index.js +4 -1
  72. package/core/sse/stream.js +28 -5
  73. package/core/utils/timeout.js +8 -1
  74. package/core/validation/compiler.js +10 -7
  75. package/index.d.ts +1320 -74
  76. package/index.js +128 -8
  77. package/package.json +75 -27
  78. package/plugins/admin-panel/client/parts/08-record-list.js +717 -70
  79. package/plugins/admin-panel/index.js +19 -8
  80. package/plugins/csrf/index.js +3 -3
  81. package/plugins/data-exchange/export.js +214 -0
  82. package/plugins/data-exchange/import.js +48 -5
  83. package/plugins/data-exchange/index.js +70 -28
  84. package/plugins/data-exchange/preview.js +266 -0
  85. package/plugins/data-exchange/template.js +242 -0
  86. package/plugins/email/admin-component.js +783 -141
  87. package/plugins/email/api-handlers.js +51 -1
  88. package/plugins/email/index.js +5 -0
  89. package/plugins/email/service.js +48 -0
  90. package/plugins/file-manager/admin-component.js +1546 -70
  91. package/plugins/file-manager/api-handlers.js +166 -1
  92. package/plugins/file-manager/core.js +598 -8
  93. package/plugins/file-manager/index.js +8 -0
  94. package/plugins/index.js +7 -0
  95. package/plugins/mcp/auth.js +3 -3
  96. package/plugins/oauth/index.js +36 -3
  97. package/plugins/queue/admin-component.js +1092 -0
  98. package/plugins/queue/api-handlers.js +248 -0
  99. package/plugins/queue/index.js +69 -0
  100. package/plugins/rest-resources/index.js +7 -2
  101. package/plugins/security/admin-component.js +689 -0
  102. package/plugins/security/api-handlers.js +169 -0
  103. package/plugins/security/audit.js +153 -0
  104. package/plugins/security/bruteforce.js +244 -0
  105. package/plugins/security/firewall.js +459 -0
  106. package/plugins/security/headers.js +214 -0
  107. package/plugins/security/honeypot.js +234 -0
  108. package/plugins/security/incident-recorder.js +137 -0
  109. package/plugins/security/index.js +211 -0
  110. package/plugins/seo-checker/analyzer.js +20 -7
  111. package/plugins/upload/index.js +36 -0
  112. package/plugins/webhooks/admin-component.js +702 -175
  113. package/plugins/webhooks/api-handlers.js +75 -1
  114. package/plugins/webhooks/dispatcher.js +290 -11
  115. package/plugins/webhooks/index.js +37 -2
  116. package/plugins/webhooks/sender.js +56 -0
  117. package/plugins/xlsx/engine.js +19 -4
  118. package/src/api/api-loader.js +19 -1
  119. package/src/bundle/index.js +443 -17
  120. package/src/client-runtime/bootstrap-swup.js +9 -0
  121. package/src/client-runtime/mount.js +15 -20
  122. package/src/client-runtime/resolve.js +3 -2
  123. package/src/dev/api-typegen.js +1300 -0
  124. package/src/dev/banner.js +10 -1
  125. package/src/dev/colors.js +1 -1
  126. package/src/dev/index.js +7 -1
  127. package/src/dev/typegen.js +173 -17
  128. package/src/dev-reload/index.js +1 -1
  129. package/src/discovery/file-route-parser.js +17 -3
  130. package/src/discovery/scan-routes.js +4 -1
  131. package/src/file-router.js +253 -69
  132. package/src/helpers.js +46 -26
  133. package/src/njk-frontmatter.js +30 -4
  134. package/src/routing/cascading.js +26 -8
  135. package/src/routing/mount-discovered-routes.js +76 -0
  136. package/src/server.js +231 -70
  137. package/views/partials/webspresso-client-runtime.njk +1 -9
  138. package/.agents/ADMIN.md +0 -426
  139. package/.agents/AGENTS.md +0 -96
  140. package/.agents/ARCHITECTURE.md +0 -163
  141. package/.agents/AUTH.md +0 -581
  142. package/.agents/CACHE.md +0 -148
  143. package/.agents/CLI.md +0 -649
  144. package/.agents/COMPRESSION.md +0 -59
  145. package/.agents/CONTEXT.md +0 -152
  146. package/.agents/CONVENTIONS.md +0 -50
  147. package/.agents/EARLY_HINTS.md +0 -120
  148. package/.agents/EMAIL.md +0 -223
  149. package/.agents/ERRORS.md +0 -260
  150. package/.agents/EVENTS.md +0 -179
  151. package/.agents/FLAGS.md +0 -118
  152. package/.agents/HEALTH.md +0 -118
  153. package/.agents/LIMITS.md +0 -109
  154. package/.agents/LOGGING.md +0 -93
  155. package/.agents/MCP.md +0 -103
  156. package/.agents/METRICS.md +0 -127
  157. package/.agents/MIGRATION_JOBS.md +0 -95
  158. package/.agents/ORM.md +0 -700
  159. package/.agents/PLUGINS.md +0 -729
  160. package/.agents/QUEUE.md +0 -290
  161. package/.agents/RATE_LIMIT.md +0 -147
  162. package/.agents/REALTIME.md +0 -213
  163. package/.agents/RESILIENCE.md +0 -235
  164. package/.agents/ROUTING.md +0 -605
  165. package/.agents/SCALE_OUT.md +0 -109
  166. package/.agents/SECRETS.md +0 -117
  167. package/.agents/SERVICES.md +0 -341
  168. package/.agents/SHUTDOWN.md +0 -74
  169. package/.agents/STREAMING.md +0 -108
  170. package/.agents/WORKFLOW.md +0 -27
  171. package/.agents/internal/DOCUMENTATION_REVIEW.md +0 -485
  172. package/.agents/internal/TECHNICAL_REVIEW.md +0 -428
  173. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +0 -122
  174. package/.agents/skills/webspresso-framework/SKILL.md +0 -89
  175. package/.agents/skills/webspresso-orm-and-database/SKILL.md +0 -127
  176. package/.agents/skills/webspresso-routing-and-api/SKILL.md +0 -130
  177. package/.agents/skills/webspresso-services/SKILL.md +0 -134
  178. package/.cursorrules +0 -14
  179. package/AGENTS.md +0 -203
  180. package/CLAUDE.md +0 -54
  181. package/core/kernel/run-demo.js +0 -58
  182. package/plugins/admin-panel/styles.js +0 -9
  183. package/plugins/data-exchange/export-xlsx.js +0 -119
  184. 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
- if (!name.includes('\\') && !name.includes('views')) return name;
127
- let normalized = name.replace(/\\/g, '/');
128
- // Collapse any repeated views/views/... to a single views/ or clean path
129
- while (/(?:^|\/)views\/(?:\.\/)?views(?:\/|$)/.test(normalized)) {
130
- normalized = normalized.replace(/((?:^|\/)views)\/(?:\.\/)?views(?=\/|$)/g, '$1');
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
- if (typeof norm !== 'string') return String(norm);
138
- if (!norm.startsWith('.') && !norm.startsWith('views/')) return norm;
139
- // Canonical key strips leading ./ or views/ for cycle detection
140
- return norm.replace(/^\.\//, '').replace(/^views\//, '');
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
- // Disable CSP in development for easier development (Nunjucks hot reload, etc.)
352
- contentSecurityPolicy: isDev ? false : {
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: `{ alpine?: boolean|object, swup?: boolean|object }`. Overridable by env `WEBSPRESSO_ALPINE` / `WEBSPRESSO_SWUP` (=1 or true). Serves `/__webspresso/client-runtime/*` when either flag is on.
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 || 'development';
600
- const isDev = NODE_ENV !== 'production';
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 (options.queue) {
756
- req.queue = options.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
- req.service = (name, input, opts) =>
771
- core.service(name, input, req.context, opts);
772
- req.service.call = (name, input, opts) => req.service(name, input, opts);
773
- req.service.execute = (name, input, opts) => req.service(name, input, opts);
774
- req.service.invalidate = (name, input, ctx) =>
775
- serviceRegistry.invalidate(name, input, ctx || req.context);
776
- req.service.clearCache = (name) =>
777
- serviceRegistry.clearCache(name);
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.alpine / .swup are enabled. See src/client-runtime/. #}
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 %}