webspresso 0.0.103 → 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.
- package/.agents/AGENTS.md +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +4 -3
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/orm-map-html.js +2 -8
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +6 -57
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +45 -30
- package/core/auth/totp.js +5 -16
- package/core/auth/user-agent.js +1 -21
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/content/renderer.js +1 -13
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/middleware.js +2 -8
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/forms/form-builder.js +1 -9
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +76 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +132 -31
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +191 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +121 -0
- package/core/utils/fs.js +144 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +627 -2
- package/index.js +158 -0
- package/package.json +1 -2
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +3 -80
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -26
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +8 -7
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +7 -23
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +11 -3
- package/plugins/polar/src/webhooks.js +7 -13
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +57 -7
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/rls/index.js +170 -0
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/tracking.js +21 -18
- package/plugins/sitemap.js +21 -17
- package/plugins/swagger.js +13 -17
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -28
- package/plugins/webhooks/dispatcher.js +14 -9
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +28 -17
- package/plugins/webhooks/sender.js +40 -13
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/dev-reload/index.js +47 -6
- package/src/file-router.js +1 -3
- package/src/helpers.js +180 -33
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +137 -26
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- 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
|
|
package/.agents/ARCHITECTURE.md
CHANGED
|
@@ -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) => ...`.
|
package/.agents/CACHE.md
ADDED
|
@@ -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
|
|
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
|
+
```
|