webspresso 0.0.104 → 0.0.105

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 (132) hide show
  1. package/.agents/AGENTS.md +6 -0
  2. package/.agents/ARCHITECTURE.md +23 -0
  3. package/.agents/CACHE.md +147 -0
  4. package/.agents/CLI.md +1 -1
  5. package/.agents/EARLY_HINTS.md +120 -0
  6. package/.agents/HEALTH.md +118 -0
  7. package/.agents/LIMITS.md +109 -0
  8. package/.agents/ORM.md +155 -1
  9. package/.agents/PLUGINS.md +28 -0
  10. package/.agents/RESILIENCE.md +235 -0
  11. package/AGENTS.md +6 -0
  12. package/README.md +2 -2
  13. package/bin/commands/add-docker.js +6 -6
  14. package/bin/commands/dev.js +1 -1
  15. package/bin/commands/new.js +5 -5
  16. package/bin/commands/openapi.js +1 -1
  17. package/bin/commands/start.js +1 -1
  18. package/bin/utils/project.js +2 -2
  19. package/core/auth/hash.js +5 -4
  20. package/core/auth/jwt.js +5 -54
  21. package/core/auth/magic-link.js +4 -5
  22. package/core/auth/manager.js +3 -5
  23. package/core/auth/oauth.js +8 -17
  24. package/core/auth/totp.js +2 -1
  25. package/core/cache/byte-size.js +123 -0
  26. package/core/cache/index.js +21 -0
  27. package/core/cache/lru.js +477 -0
  28. package/core/cache/tagged-lru.js +346 -0
  29. package/core/commands/registry.js +6 -20
  30. package/core/early-hints/index.js +267 -0
  31. package/core/errors/domain.js +48 -0
  32. package/core/errors/http.js +31 -0
  33. package/core/errors/index.js +10 -0
  34. package/core/errors/normalize.js +88 -2
  35. package/core/events/discovery.js +6 -24
  36. package/core/events/dispatcher.js +2 -7
  37. package/core/health/index.js +41 -0
  38. package/core/health/middleware.js +213 -0
  39. package/core/health/normalize.js +140 -0
  40. package/core/health/probes.js +203 -0
  41. package/core/limits/index.js +34 -0
  42. package/core/limits/middleware.js +317 -0
  43. package/core/limits/normalize.js +222 -0
  44. package/core/orm/batch.js +70 -0
  45. package/core/orm/cache/fingerprint.js +2 -2
  46. package/core/orm/cache/memory-provider.js +27 -110
  47. package/core/orm/index.js +71 -8
  48. package/core/orm/json-fields.js +22 -8
  49. package/core/orm/model.js +10 -0
  50. package/core/orm/query-builder.js +63 -9
  51. package/core/orm/repository.js +318 -19
  52. package/core/orm/schema-helpers.js +8 -10
  53. package/core/orm/scopes.js +321 -1
  54. package/core/orm/transaction.js +10 -2
  55. package/core/orm/utils.js +4 -86
  56. package/core/queue/adapters/database.js +4 -8
  57. package/core/queue/manager.js +55 -41
  58. package/core/resilience/circuit-breaker.js +391 -0
  59. package/core/resilience/errors.js +63 -0
  60. package/core/resilience/index.js +53 -0
  61. package/core/resilience/registry.js +97 -0
  62. package/core/resilience/retry.js +203 -0
  63. package/core/schema-cache.js +9 -2
  64. package/core/shutdown/http-adapter.js +4 -1
  65. package/core/shutdown/shutdown-manager.js +120 -19
  66. package/core/utils/array.js +12 -0
  67. package/core/utils/base64url.js +8 -0
  68. package/core/utils/crypto.js +118 -0
  69. package/core/utils/ensure-leading-slash.js +6 -0
  70. package/core/utils/escape-regexp.js +7 -0
  71. package/core/utils/fetch.js +6 -5
  72. package/core/utils/fs.js +144 -0
  73. package/core/utils/hmac.js +6 -0
  74. package/core/utils/json.js +98 -0
  75. package/core/utils/object.js +106 -0
  76. package/core/utils/random.js +9 -0
  77. package/core/utils/regex.js +77 -0
  78. package/core/utils/sha256.js +6 -0
  79. package/core/utils/string.js +87 -0
  80. package/core/utils/strip-trailing-slash.js +6 -0
  81. package/core/utils/time.js +96 -0
  82. package/core/utils/timeout.js +348 -0
  83. package/core/utils/url.js +106 -0
  84. package/core/utils/walk.js +7 -0
  85. package/index.d.ts +616 -1
  86. package/index.js +148 -0
  87. package/package.json +1 -2
  88. package/plugins/admin-panel/core/api-extensions.js +2 -5
  89. package/plugins/admin-panel/index.js +1 -1
  90. package/plugins/admin-panel/modules/menu.js +1 -1
  91. package/plugins/api-key/crypto.js +3 -3
  92. package/plugins/api-key/scopes.js +5 -7
  93. package/plugins/audit-log/api-handlers.js +6 -15
  94. package/plugins/audit-log/parse.js +3 -3
  95. package/plugins/csrf/index.js +5 -9
  96. package/plugins/dashboard/app.js +2 -2
  97. package/plugins/dashboard/index.js +5 -5
  98. package/plugins/data-exchange/record-selection.js +2 -5
  99. package/plugins/email/api-handlers.js +5 -18
  100. package/plugins/email/auth-bridge.js +3 -2
  101. package/plugins/health-check.js +2 -1
  102. package/plugins/index.js +2 -0
  103. package/plugins/lazy.js +5 -6
  104. package/plugins/mcp/discovery.js +18 -17
  105. package/plugins/polar/src/api.js +2 -1
  106. package/plugins/polar/src/webhooks.js +5 -6
  107. package/plugins/rate-limit/index.js +3 -1
  108. package/plugins/redirect/index.js +9 -7
  109. package/plugins/rls/index.js +170 -0
  110. package/plugins/site-analytics/tracking.js +2 -2
  111. package/plugins/sitemap.js +3 -2
  112. package/plugins/swagger.js +4 -3
  113. package/plugins/upload/index.js +4 -3
  114. package/plugins/upload/orphan-pruner.js +4 -3
  115. package/plugins/webhooks/crypto.js +6 -11
  116. package/plugins/webhooks/dispatcher.js +8 -8
  117. package/plugins/webhooks/index.js +6 -0
  118. package/plugins/webhooks/matcher.js +7 -11
  119. package/plugins/webhooks/sender.js +8 -11
  120. package/src/app-context.js +41 -5
  121. package/src/dev/banner.js +6 -2
  122. package/src/file-router.js +1 -3
  123. package/src/helpers.js +159 -24
  124. package/src/modules/module-discovery.js +1 -9
  125. package/src/pages/page-loader.js +53 -1
  126. package/src/plugin-manager.js +90 -17
  127. package/src/server.js +136 -25
  128. package/src/services/discovery.js +19 -40
  129. package/src/services/executor.js +86 -25
  130. package/src/services/memoize.js +3 -25
  131. package/src/services/registry.js +6 -0
  132. package/src/template/precompile.js +8 -19
package/.agents/AGENTS.md CHANGED
@@ -18,6 +18,7 @@ When tasked with a feature or bugfix, **immediately consult the corresponding to
18
18
  | **Authentication (Session / JWT / Guard)** | [`.agents/AUTH.md`](AUTH.md) | `core/auth/` |
19
19
  | **Background Jobs & Worker Queue** | [`.agents/QUEUE.md`](QUEUE.md) | `jobs/` |
20
20
  | **Decoupled Events & Signals** | [`.agents/EVENTS.md`](EVENTS.md) | `core/events/`, `listeners/` |
21
+ | **Fault Tolerance / Circuit Breaker / Retry** | [`.agents/RESILIENCE.md`](RESILIENCE.md) | `core/resilience/` |
21
22
  | **API Signatures & Types Verification** | [`index.d.ts`](../index.d.ts) | **Canonical Ground Truth** |
22
23
 
23
24
  ---
@@ -56,6 +57,11 @@ This `.agents/` customization root is organized into modular topic guides:
56
57
  17. **[SSR Streaming & Chunked Transfer](.agents/STREAMING.md)** — Native HTTP chunked streaming (`res.renderStream()`), deferred data loader slots (`defer: { key: promise }`), and TTFB optimization.
57
58
  18. **Polar billing production checklist** — [docs/guides/polar-production-checklist.md](../docs/guides/polar-production-checklist.md) (env, webhooks, rate limits, external tier hooks, CSP). Plugin reference: [PLUGINS.md § polarPlugin](PLUGINS.md).
58
59
  19. **[Decoupled Events & Signals Architecture](.agents/EVENTS.md)** — Zero-dependency enterprise event dispatcher (`core/events`), sync/async/parallel/serial/waterfall dispatch, wildcard listeners (`order.*`), Django-style `Signal` and sender filtering, queue bridging, and listener auto-discovery.
60
+ 20. **[HTTP 103 Early Hints](.agents/EARLY_HINTS.md)** — Zero-dependency RFC 8288 / RFC 8297 Early Hints dispatcher (`core/early-hints`), preloading route CSS/JS and fonts before SSR loader execution, `createApp({ earlyHints })`, and `res.earlyHints(links)` / `ctx.earlyHints(links)` helpers.
61
+ 21. **[Payload Limits & DoS Protection](.agents/LIMITS.md)** — Zero-dependency request body limits and DoS protection (`core/limits`), `createApp({ limits })`, automatic `PayloadTooLargeError` (HTTP 413) mapping, route overrides (`limits.routes`), streaming bypass, and native HMAC webhook `req.rawBody` capture.
62
+ 22. **[Cloud & Kubernetes Health Probes](.agents/HEALTH.md)** — Zero-dependency RFC-compliant liveness (`/healthz`), readiness (`/readyz`), and combined diagnostic (`/health`) probes (`core/health`), event loop lag telemetry, database/queue health monitors, automatic HTTP 503 draining on graceful shutdown, and `createApp({ healthCheck: true })`.
63
+ 23. **[LRU & Tagged Cache Engine](.agents/CACHE.md)** — Zero-dependency, memory-bounded in-memory LRU cache (`core/cache`), byte budget constraints (`maxBytes` / `maxMemoryMb`), TTL expiration, stampede protection (`getOrSet`), leak-free tag invalidation (`TaggedLruCache`), ORM query cache provider, and `createApp({ cache })`.
64
+ 24. **[Enterprise Resilience & Circuit Breaker](.agents/RESILIENCE.md)** — Zero-dependency fault tolerance engine (`core/resilience`), three-state `CircuitBreaker` (`CLOSED`, `OPEN`, `HALF_OPEN`), rolling window failure rate, per-call execution timeout, fallback handlers, and exponential backoff retry engine (`retry`, `withRetry`) with randomized jitter and transient error detection.
59
65
 
60
66
  ---
61
67
 
@@ -51,6 +51,29 @@
51
51
  - **HTTP Response Compression (`core/compression`)**:
52
52
  - Zero-dependency streaming zlib compression configured via `createApp({ server: { compression: true } })`.
53
53
  - Content negotiation for Brotli (`br`), Gzip (`gzip`), Deflate (`deflate`), threshold filtering (1 KB), and `res.compress(false)` route opt-out.
54
+ - **Configurable Payload Size Limits & DoS Protection (`core/limits`)**:
55
+ - Zero-dependency request body limits configured via `createApp({ limits: { json: '1mb', urlencoded: '1mb', raw: false, text: false, parameterLimit: 1000 } })` or shorthand string/number (`limits: '5mb'`). Also accepted under `createApp({ server: { limits } })`.
56
+ - **HTTP 413 & 400 Error Mapping**: Payload limit violations and excessive form parameters automatically produce `PayloadTooLargeError` (HTTP 413) with `{ limit, length, expected }` metadata; malformed JSON bodies produce `BadRequestError` (HTTP 400) with `INVALID_JSON_PAYLOAD`.
57
+ - **Raw Body Capture for Webhooks**: `captureRawBody: true | string[] | Function` captures incoming request payload `Buffer` on `req.rawBody` for zero-overhead HMAC cryptographic verification (Polar, Stripe, GitHub).
58
+ - **Route Overrides & Exclusions**: `limits.routes` supports per-route limit adjustments (e.g. `routes: { '/api/upload': { json: '25mb' } }`), while `limits.exclude` completely bypasses body parsing for raw socket/streaming endpoints.
59
+ - **Cloud & Kubernetes Health Probes (`core/health`, `/healthz`, `/readyz`, `/health`)**:
60
+ - RFC-compliant cloud-native endpoints enabled via `createApp({ healthCheck: true })` or fine-grained `{ path: '/healthz', readyPath: '/readyz', combinedPath: '/health', db: true, queue: true, eventLoopLag: true }`.
61
+ - **Liveness Probe (`/healthz`)**: Non-blocking probe reporting basic process health, memory usage (`rss`, `heapUsed`, `heapTotal`, `external`), and system uptime.
62
+ - **Readiness Probe (`/readyz`)**: Deep dependency inspection checking database ping (`knex.raw('SELECT 1')`), worker queue status, custom user checks, and `ShutdownManager` draining state (immediately returning HTTP 503 so upstream load balancers detach the instance).
63
+ - **Diagnostic Health (`/health`)**: Full operational dashboard with event loop lag percentiles (`perf_hooks.monitorEventLoopDelay`), dependency breakdown, and latency metrics.
64
+ - **In-Memory LRU & Tagged Cache Engine (`core/cache`)**:
65
+ - Zero-dependency O(1) Least-Recently-Used caching engine configured via `createApp({ cache: { maxEntries, maxMemoryMb, defaultTtl } })` or `cache: false`.
66
+ - **Memory Budget & Leak Prevention**: Enforces byte boundaries (`maxBytes` / `maxMemoryMb`) via `approximateByteSize` to prevent process OOM errors, combined with passive and active TTL pruning (`pruneExpired()`).
67
+ - **Bidirectional Tag Invalidation (`TaggedLruCache`)**: Supports model and query tagging (`invalidateTag`, `invalidateTags`). When items are evicted via LRU, all associated tag sets are automatically and symmetrically cleaned up, eliminating dangling Set memory leaks.
68
+ - **Stampede / Thundering Herd Protection**: `getOrSet(key, asyncFactory, options)` deduplicates concurrent in-flight requests for cold keys so expensive factories execute only once.
69
+ - **Framework Integration**: Directly powers `core/orm/cache/memory-provider.js` (ORM query caching) and `core/schema-cache.js` (Zod schema cache), and is automatically exposed on `req.cache`, `req.context.cache`, and `app.cache`.
70
+ - **Enterprise Resilience, Circuit Breaker & Retry Engine (`core/resilience`)**:
71
+ - Zero-dependency fault tolerance and cascading failure prevention system built natively on `events` and `perf_hooks`.
72
+ - **Three-State Circuit Breaker (`CircuitBreaker`)**: Standard `CLOSED`, `OPEN`, and `HALF_OPEN` states. Trips to `OPEN` when consecutive failures reach `failureThreshold` or failure rate percentage exceeds `failureRateThreshold` in a rolling sample window (`rollingWindow`).
73
+ - **Fail-Fast & Graceful Fallbacks**: Fast-fails calls with `CircuitBreakerOpenError` (HTTP 503) without executing downstream code when open, or seamlessly executes fallback handlers (`fallback: (err, ...args) => ...`).
74
+ - **Trial Recovery Probing**: Automatically transitions to `HALF_OPEN` after `resetTimeout` and tests downstream health using trial probes (`successThreshold`).
75
+ - **Exponential Backoff Retry with Full Jitter (`retry`, `withRetry`)**: Automatically retries transient network errors (`ECONNRESET`, `ETIMEDOUT`, 429 Too Many Requests, and 5xx errors) with randomized full jitter to prevent thundering herd spikes while avoiding retrying 4xx client errors.
76
+ - **Services Layer Integration**: Declarative `defineService({ circuitBreaker, retry })` seamlessly wraps service execution. Also accessible on `app.breakers`, `app.getBreaker()`, `req.breaker()`, `req.retry()`, `ctx.breaker()`, and `ctx.retry()`.
54
77
  - **Custom Error & 404 Pages (`errorPages`)**:
55
78
  - Configured via `createApp({ errorPages: { notFound, serverError, timeout } })`.
56
79
  - **404 Not Found (`notFound`)**: Template path (`'404.njk'`) or handler `(req, res, ctx) => ...`.
@@ -0,0 +1,147 @@
1
+ # High-Performance In-Memory LRU & Tagged Cache Engine (`core/cache`)
2
+
3
+ Webspresso includes a zero-dependency, memory-bounded, leak-free in-memory cache engine featuring O(1) Least-Recently-Used (LRU) eviction, byte budget constraints (`maxBytes` / `maxMemoryMb`), TTL expiration, stampede protection (thundering herd prevention), and O(1) bidirectional tag-based invalidation.
4
+
5
+ ---
6
+
7
+ ## 🎯 Key Capabilities & Design Guarantees
8
+
9
+ 1. **True O(1) LRU Eviction**: Built on V8's native insertion-order Map mechanics with zero extra memory allocation for linked-list node wrappers.
10
+ 2. **Dual Capacity Boundaries**:
11
+ - **`maxEntries`**: Restricts the maximum number of items stored.
12
+ - **`maxBytes` / `maxMemoryMb`**: Enforces strict memory budgets using `approximateByteSize` to prevent process Out-Of-Memory (OOM) crashes under heavy or large payload loads.
13
+ 3. **Symmetrical Tag Cleanup (Zero Leaks)**: When an item is evicted via LRU, expired via TTL, or deleted, all associated tag index sets are automatically pruned. Empty tag sets are deleted immediately, preventing the classic "dangling Set memory leak".
14
+ 4. **Stampede / Thundering Herd Protection**: `getOrSet(key, asyncFactory)` coalesces concurrent requests for the same missing key, invoking the expensive calculation or DB query exactly once.
15
+ 5. **Universal Integration**:
16
+ - **ORM Query Cache**: Powers `core/orm/cache/memory-provider.js` with true LRU and memory budgets.
17
+ - **Zod Schema Cache**: Powers `core/schema-cache.js` with bounded 2,000 schemas capacity.
18
+ - **Application & Context**: Automatically mounted on `app.cache`, `req.cache`, and `req.context.cache` unless `cache: false` is configured.
19
+
20
+ ---
21
+
22
+ ## 🚀 Quick Usage
23
+
24
+ ### 1. Standalone `LruCache`
25
+ ```javascript
26
+ const { createLruCache } = require('webspresso');
27
+
28
+ const cache = createLruCache({
29
+ maxEntries: 1000,
30
+ maxMemoryMb: 50, // 50 MB byte budget
31
+ defaultTtl: '15m', // 15 minutes default TTL
32
+ clone: false, // Set true if cached objects must not be mutated
33
+ });
34
+
35
+ cache.set('user:42', { name: 'Alice', role: 'admin' });
36
+
37
+ const user = cache.get('user:42'); // Refreshes LRU position
38
+ const peekUser = cache.peek('user:42'); // Reads without refreshing LRU position
39
+ ```
40
+
41
+ ### 2. Tagged Invalidation (`TaggedLruCache`)
42
+ ```javascript
43
+ const { createTaggedLruCache } = require('webspresso');
44
+
45
+ const cache = createTaggedLruCache({
46
+ maxEntries: 5000,
47
+ maxMemoryMb: 100,
48
+ });
49
+
50
+ // Tag items by model and primary key
51
+ cache.set('query:users:active', usersList, { tags: ['model:User', 'q:active'] });
52
+ cache.set('user:1:profile', userProfile, { tags: ['model:User', 'pk:1'] });
53
+
54
+ // Invalidate single primary key
55
+ cache.invalidateTag('pk:1');
56
+
57
+ // Invalidate all queries related to the User model
58
+ cache.invalidateTags(['model:User']);
59
+ ```
60
+
61
+ ### 3. Stampede / Thundering Herd Protection (`getOrSet`)
62
+ ```javascript
63
+ // 50 concurrent requests for 'expensive:report' will execute the factory only once:
64
+ const report = await cache.getOrSet('expensive:report', async (key) => {
65
+ return await generateHeavyReport();
66
+ }, { ttl: '1h' });
67
+ ```
68
+
69
+ ---
70
+
71
+ ## ⚙️ Application Configuration (`createApp`)
72
+
73
+ Configure the process-wide cache directly when bootstrapping Webspresso:
74
+
75
+ ```javascript
76
+ const { createApp } = require('webspresso');
77
+
78
+ const app = createApp({
79
+ pagesDir: './pages',
80
+ cache: {
81
+ maxEntries: 5000,
82
+ maxMemoryMb: 64, // 64 MB budget
83
+ defaultTtl: '10m',
84
+ },
85
+ });
86
+ ```
87
+
88
+ To disable the app cache completely:
89
+ ```javascript
90
+ const app = createApp({
91
+ pagesDir: './pages',
92
+ cache: false,
93
+ });
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 🌐 Context & Route Usage
99
+
100
+ Inside page loaders, services, or API route handlers:
101
+
102
+ ```javascript
103
+ // In API routes (pages/api/stats.get.js)
104
+ module.exports = defineApi({
105
+ handler: async (req, res) => {
106
+ const stats = await req.cache.getOrSet('site:stats', async () => {
107
+ return await req.db.getRepository('Order').getSummaryStats();
108
+ }, { ttl: '5m', tags: ['stats', 'model:Order'] });
109
+
110
+ return res.json(stats);
111
+ },
112
+ });
113
+ ```
114
+
115
+ ```javascript
116
+ // From any background job or script using app-context
117
+ const { getCache } = require('webspresso');
118
+
119
+ const cache = getCache();
120
+ if (cache) {
121
+ cache.invalidateTag('stats');
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## 📊 Telemetry & Observability Metrics
128
+
129
+ Access real-time cache performance and memory metrics:
130
+
131
+ ```javascript
132
+ const stats = app.cache.getStats();
133
+ console.log(stats);
134
+ // {
135
+ // entries: 42,
136
+ // tags: 5,
137
+ // currentBytes: 124850,
138
+ // maxBytes: 67108864,
139
+ // maxEntries: 5000,
140
+ // hits: 1540,
141
+ // misses: 23,
142
+ // hitRate: 0.985,
143
+ // sets: 65,
144
+ // evictions: 0,
145
+ // expirations: 12
146
+ // }
147
+ ```
package/.agents/CLI.md CHANGED
@@ -313,7 +313,7 @@ Generate production-ready, security-hardened, multi-stage `Dockerfile`, `.docker
313
313
  npx webspresso add:docker
314
314
 
315
315
  # Customize Node.js version, database service, or container port
316
- npx webspresso add:docker --node 22-alpine --db postgres --redis --port 3000
316
+ npx webspresso add:docker --node 22-alpine --db postgres --redis --port 7331
317
317
 
318
318
  # Overwrite existing files
319
319
  npx webspresso add:docker --force
@@ -0,0 +1,120 @@
1
+ # HTTP 103 Early Hints & Critical Asset Preloading
2
+
3
+ Webspresso features built-in, zero-dependency support for **HTTP 103 Early Hints** (RFC 8288 / RFC 8297).
4
+ Early Hints allow Webspresso to send informational HTTP headers (containing CSS, JavaScript, and font preload instructions) to the client **before** server-side rendering (`load()`, database queries, external API calls) finishes, allowing the browser to download critical page assets in parallel.
5
+
6
+ ---
7
+
8
+ ## 🚀 How It Works
9
+
10
+ 1. The client sends an HTTP request (`GET /products/123`).
11
+ 2. Webspresso resolves the matching route and its companion CSS/JS asset candidates via `AssetManager`.
12
+ 3. If Early Hints are enabled, Webspresso immediately writes an `HTTP/1.1 103 Early Hints` response with `Link: </css/products.css>; rel=preload; as=style, </js/products.js>; rel=preload; as=script`.
13
+ 4. While the browser starts downloading stylesheets and scripts, Webspresso runs the page's async `load()` function and Nunjucks rendering.
14
+ 5. Once the HTML is ready, Webspresso returns `HTTP/1.1 200 OK` with the final document.
15
+ 6. The browser can immediately render without waiting for secondary asset network round-trips (reducing TTFB, FCP and LCP).
16
+
17
+ ---
18
+
19
+ ## 🛠️ Configuration
20
+
21
+ ### Global Configuration (`createApp`)
22
+
23
+ Enable automatic route asset preloading globally:
24
+
25
+ ```javascript
26
+ const { createApp } = require('webspresso');
27
+
28
+ const app = createApp({
29
+ earlyHints: true, // Automatically preloads route CSS & JS
30
+ // Or with custom default links:
31
+ // earlyHints: {
32
+ // auto: true,
33
+ // links: ['/css/global.css', '/fonts/inter.woff2'],
34
+ // }
35
+ });
36
+ ```
37
+
38
+ ### Route / Page Configuration (`definePage`)
39
+
40
+ Configure Early Hints per-page inside your page module:
41
+
42
+ ```javascript
43
+ const { definePage } = require('webspresso');
44
+
45
+ module.exports = definePage({
46
+ // Preload route assets automatically
47
+ earlyHints: true,
48
+
49
+ // Or specify exact links:
50
+ // earlyHints: [
51
+ // '/css/special.css',
52
+ // { url: '/fonts/inter.woff2', as: 'font', crossOrigin: 'anonymous' }
53
+ // ],
54
+
55
+ load: async (ctx) => {
56
+ // You can also dispatch hints manually during load:
57
+ ctx.earlyHints(['/extra-chunk.js']);
58
+
59
+ const product = await ctx.db.getRepository('Product').findById(ctx.params.id);
60
+ return { product };
61
+ },
62
+ });
63
+ ```
64
+
65
+ ### In Route Handlers and API Middleware
66
+
67
+ `res.earlyHints(links, [callback])` is attached to `res` on every request:
68
+
69
+ ```javascript
70
+ app.get('/dashboard', (req, res) => {
71
+ // Send 103 Early Hints
72
+ res.earlyHints([
73
+ '/css/dashboard.css',
74
+ '/js/charts.js',
75
+ ]);
76
+
77
+ // Run async queries...
78
+ setTimeout(() => {
79
+ res.send('<h1>Dashboard</h1>');
80
+ }, 200);
81
+ });
82
+ ```
83
+
84
+ ---
85
+
86
+ ## 📋 Link Options & Deduction
87
+
88
+ Webspresso automatically deduces `as` and `crossorigin` attributes based on URL file extensions:
89
+
90
+ | Extension | Inferred `as` | Browser CORS Requirement |
91
+ | :--- | :--- | :--- |
92
+ | `.css` | `style` | - |
93
+ | `.js`, `.mjs`, `.cjs` | `script` | - |
94
+ | `.woff2`, `.woff`, `.ttf` | `font` | Auto-adds `crossorigin` |
95
+ | `.png`, `.jpg`, `.webp`, `.svg` | `image` | - |
96
+ | `.json` | `fetch` | - |
97
+
98
+ You can also pass full descriptor objects:
99
+
100
+ ```javascript
101
+ res.earlyHints([
102
+ {
103
+ url: '/fonts/inter.woff2',
104
+ as: 'font',
105
+ crossOrigin: 'anonymous',
106
+ fetchPriority: 'high',
107
+ },
108
+ {
109
+ url: 'https://cdn.example.com',
110
+ rel: 'preconnect',
111
+ },
112
+ ]);
113
+ ```
114
+
115
+ ---
116
+
117
+ ## 🛡️ Resilience & Fallbacks
118
+
119
+ - If the client or proxy does not support HTTP 103, or if running in an environment without `res.writeEarlyHints` (HTTP/1.0, test mocks), `sendEarlyHints` returns `false` safely without throwing.
120
+ - Calling `earlyHints` after response headers have been sent (`res.headersSent === true`) is a safe no-op.
@@ -0,0 +1,118 @@
1
+ # Cloud & Kubernetes Health & Readiness Probes (`core/health`)
2
+
3
+ Webspresso includes a zero-dependency, Docker & Kubernetes-native health inspection system. It provides standard liveness (`/healthz`), readiness (`/readyz`), and combined diagnostics endpoints with automatic database pings, background worker queue checks, event loop delay metrics, and instant graceful shutdown draining integration.
4
+
5
+ ---
6
+
7
+ ## 🎯 Key Capabilities
8
+
9
+ 1. **Kubernetes Liveness Probe (`/healthz`)**:
10
+ - Confirms process vitality and responsiveness.
11
+ - Gathers memory footprint (`rss`, `heapUsed`, `heapTotal` formatted and raw bytes), process uptime, PID, platform, and event loop lag statistics (`meanMs`, `maxMs`, `p99Ms`).
12
+ - If event loop delay exceeds threshold (`eventLoopThresholdMs`), flags status as `degraded`.
13
+
14
+ 2. **Kubernetes Readiness Probe (`/readyz`)**:
15
+ - Confirms the container is fully prepared to handle live client traffic.
16
+ - **Database Connectivity**: Pings active Knex instance (`SELECT 1`) with timeout protection.
17
+ - **Queue Readiness**: Probes worker queue instance status.
18
+ - **Custom Probes**: Executes user-registered async checks concurrently (`checks: { redis, stripe }`).
19
+ - Returns `HTTP 200` (`{ ready: true, status: 'ready' }`) when all components are healthy.
20
+ - Returns `HTTP 503` (`{ ready: false, status: 'unhealthy', checks: ... }`) if any component fails.
21
+
22
+ 3. **Graceful Draining Integration (`ShutdownManager`)**:
23
+ - As soon as the server enters shutdown draining (`app.close()` or `SIGTERM`/`SIGINT`), `/readyz` immediately returns `HTTP 503` (`{ ready: false, status: 'draining' }`).
24
+ - Cloud load balancers (AWS ALB, K8s Ingress, Google Cloud, Cloudflare) immediately stop routing new traffic to this instance, while existing active requests drain cleanly without dropping a single connection.
25
+
26
+ ---
27
+
28
+ ## 🚀 Configuration Examples
29
+
30
+ ### 1. Simple Enablement
31
+
32
+ ```javascript
33
+ const { createApp } = require('webspresso');
34
+
35
+ const app = createApp({
36
+ pagesDir: 'pages',
37
+ // Enables /healthz (liveness), /readyz (readiness), and /health (combined)
38
+ healthCheck: true,
39
+ });
40
+ ```
41
+
42
+ Shorthand custom path:
43
+ ```javascript
44
+ const app = createApp({
45
+ pagesDir: 'pages',
46
+ healthCheck: '/api/status', // Mounts combined status on /api/status
47
+ });
48
+ ```
49
+
50
+ ### 2. Full Production Configuration
51
+
52
+ ```javascript
53
+ const app = createApp({
54
+ pagesDir: 'pages',
55
+ db: myDatabaseInstance,
56
+ queue: myQueueInstance,
57
+ healthCheck: {
58
+ enabled: true,
59
+ path: '/healthz', // Liveness probe URL
60
+ readyPath: '/readyz', // Readiness probe URL
61
+ combinedPath: '/health', // Diagnostic summary URL
62
+ db: true, // Automatically probe database (SELECT 1)
63
+ queue: true, // Automatically probe worker queue
64
+ eventLoopLag: true, // Monitor event loop delay
65
+ eventLoopThresholdMs: 300, // Lag threshold in ms
66
+ timeoutMs: 2500, // Probe timeout before marking down
67
+ checks: { // Custom dependency probes
68
+ redis: async () => redisClient.ping(),
69
+ stripe: async () => checkStripeHealth(),
70
+ },
71
+ headers: {
72
+ 'Cache-Control': 'no-store, no-cache, must-revalidate',
73
+ 'X-Content-Type-Options': 'nosniff',
74
+ },
75
+ },
76
+ });
77
+ ```
78
+
79
+ ---
80
+
81
+ ## ☸️ Kubernetes Pod Spec Example
82
+
83
+ ```yaml
84
+ livenessProbe:
85
+ httpGet:
86
+ path: /healthz
87
+ port: 3000
88
+ initialDelaySeconds: 5
89
+ periodSeconds: 10
90
+ timeoutSeconds: 3
91
+ failureThreshold: 3
92
+
93
+ readinessProbe:
94
+ httpGet:
95
+ path: /readyz
96
+ port: 3000
97
+ initialDelaySeconds: 2
98
+ periodSeconds: 5
99
+ timeoutSeconds: 2
100
+ failureThreshold: 2
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 🔍 Programmatic Inspection
106
+
107
+ Access health state directly in code or CLI scripts:
108
+
109
+ ```javascript
110
+ // Get complete diagnostic payload
111
+ const status = await app.getHealthStatus();
112
+ console.log('Liveness:', status.liveness);
113
+ console.log('Readiness:', status.readiness);
114
+
115
+ // Or via healthManager
116
+ const liveness = await app.healthManager.getLiveness();
117
+ const readiness = await app.healthManager.getReadiness();
118
+ ```
@@ -0,0 +1,109 @@
1
+ # Configurable Payload Size Limits & DoS Protection (`core/limits`)
2
+
3
+ Webspresso provides a zero-dependency, configurable request body size limiter and Denial of Service (DoS) protection layer. It safeguards Node.js memory against payload exhaustion attacks while supporting per-route overrides, streaming bypass, and native raw body capture for HMAC webhook verification.
4
+
5
+ ---
6
+
7
+ ## 🎯 Key Capabilities
8
+
9
+ 1. **Multi-Parser Configuration**:
10
+ - `json`: Size limit for `application/json` (default: `'1mb'`).
11
+ - `urlencoded`: Size limit for `application/x-www-form-urlencoded` (default: `'1mb'`).
12
+ - `parameterLimit`: Maximum number of URL-encoded form parameters (default: `1000`).
13
+ - `raw`: Size limit for `application/octet-stream` (default: `false` / disabled).
14
+ - `text`: Size limit for `text/plain` (default: `false` / disabled).
15
+ 2. **Standard Exception Mapping**:
16
+ - Limit exceeded -> Throws `PayloadTooLargeError` (HTTP 413) with `{ limit, length, expected }` details and `code: 'PAYLOAD_TOO_LARGE'`.
17
+ - Parameter exhaustion -> Throws `PayloadTooLargeError` (HTTP 413) with `code: 'PARAMETERS_TOO_MANY'`.
18
+ - Malformed JSON -> Throws `BadRequestError` (HTTP 400) with `code: 'INVALID_JSON_PAYLOAD'`.
19
+ 3. **Webhook & HMAC Signature Verification**:
20
+ - `captureRawBody: true` stores incoming unparsed `Buffer` on `req.rawBody` for zero-overhead cryptographic signatures (Polar, Stripe, LemonSqueezy, GitHub).
21
+ 4. **Route Overrides & Streaming Bypass**:
22
+ - `limits.routes`: Generous limits for specific paths (e.g. `/api/upload` -> 25MB) without loosening server-wide protection.
23
+ - `limits.exclude`: Completely skips global body parsers for streaming and custom multipart endpoints.
24
+
25
+ ---
26
+
27
+ ## 🚀 Configuration Examples
28
+
29
+ ### 1. Global Configuration
30
+
31
+ ```javascript
32
+ const { createApp } = require('webspresso');
33
+
34
+ const app = createApp({
35
+ pagesDir: 'pages',
36
+ limits: {
37
+ json: '2mb', // 2 Megabytes for JSON
38
+ urlencoded: '1mb', // 1 Megabyte for Forms
39
+ parameterLimit: 2000, // Up to 2000 parameters
40
+ raw: '10mb', // Enable raw buffer parsing for binary data
41
+ captureRawBody: true, // Automatically populate req.rawBody
42
+ },
43
+ });
44
+ ```
45
+
46
+ Shorthand (applies to both JSON and URL-encoded):
47
+ ```javascript
48
+ const app = createApp({
49
+ pagesDir: 'pages',
50
+ limits: '5mb', // Both json and urlencoded set to 5 MB
51
+ });
52
+ ```
53
+
54
+ ### 2. Route Overrides & Exclusions
55
+
56
+ ```javascript
57
+ const app = createApp({
58
+ pagesDir: 'pages',
59
+ limits: {
60
+ json: '500kb', // Tight global limit for standard APIs
61
+ routes: {
62
+ '/api/import/json': { json: '25mb' }, // Relaxed limit for batch import
63
+ '/api/webhooks/*': { raw: '5mb', captureRawBody: true },
64
+ },
65
+ exclude: [
66
+ '/api/stream/upload', // Skip global body parser entirely for socket streaming
67
+ ],
68
+ },
69
+ });
70
+ ```
71
+
72
+ ### 3. Webhook Signature Verification with `req.rawBody`
73
+
74
+ ```javascript
75
+ // pages/api/webhooks/polar.post.js
76
+ const crypto = require('crypto');
77
+ const { defineApi, UnauthorizedError } = require('webspresso');
78
+
79
+ module.exports = defineApi({
80
+ handler: async (req, res) => {
81
+ const signature = req.headers['webhook-signature'];
82
+ const expected = crypto
83
+ .createHmac('sha256', process.env.POLAR_WEBHOOK_SECRET)
84
+ .update(req.rawBody) // Native Buffer captured via captureRawBody
85
+ .digest('hex');
86
+
87
+ if (signature !== expected) {
88
+ throw new UnauthorizedError('Invalid signature');
89
+ }
90
+
91
+ return res.json({ received: true });
92
+ },
93
+ });
94
+ ```
95
+
96
+ ---
97
+
98
+ ## 🛡️ Standalone Middleware Usage
99
+
100
+ For manual route mounting or custom Express routers:
101
+
102
+ ```javascript
103
+ const { createLimitsMiddleware } = require('webspresso');
104
+
105
+ // Mount on custom router or endpoint
106
+ app.post('/custom-endpoint', createLimitsMiddleware({ json: '50mb' }), (req, res) => {
107
+ res.json({ size: req.body.length });
108
+ });
109
+ ```