@invisibleloop/pulse 0.21.6 → 0.22.0
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/package.json +1 -1
- package/public/.pulse-ui-version +1 -1
- package/src/agent/guide-server.md +1 -1
- package/src/server/index.js +5 -4
- package/src/spec/schema.js +7 -0
- package/types/schema.d.ts +3 -0
package/package.json
CHANGED
package/public/.pulse-ui-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.22.0
|
|
@@ -295,7 +295,7 @@ await ctx.formData() // URL-encoded body → plain object | null — NOT a Form
|
|
|
295
295
|
await ctx.buffer() // raw Buffer
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
-
Bodies larger than `maxBody` (default 1 MB,
|
|
298
|
+
Bodies larger than the limit are rejected with a 413 when the handler reads them. The limit is `maxBody` in `createServer` (default 1 MB); set `maxBody` on a spec to override it for that route only, e.g. `maxBody: 6 * 1024 * 1024` on an image-upload route. Prefer a per-route limit to raising the global one, and authenticate in `guard` first: the framework reads the body only after the guard runs (unless your guard reads it itself), so an unauthenticated caller never gets the larger buffer. A `submit` route with CSRF is the exception to rely on: the body is read before the CSRF check, so give a public form only the limit it needs.
|
|
299
299
|
|
|
300
300
|
**`ctx.formData()` returns a plain object, not a browser `FormData`.** Read fields directly — `data?.email`, not `data.get('email')` — the latter throws `TypeError: data.get is not a function`. This trips people up because client-side action code (`actions.*.run(state, serverState, formData)`) receives a real `FormData` object with a `.get()` method — same method name, different context, different shape. If you're writing `spec.submit` and reach for `.get()`, that's the tell you've copied the client-side pattern by mistake.
|
|
301
301
|
|
package/src/server/index.js
CHANGED
|
@@ -1054,7 +1054,7 @@ export async function createServer(entries, options = {}) {
|
|
|
1054
1054
|
const nonce = crypto.randomBytes(16).toString('base64url')
|
|
1055
1055
|
|
|
1056
1056
|
// Build request context passed to guard, server data fetchers, and render
|
|
1057
|
-
const ctx = buildContext(req, url, match.params, nonce, maxBody)
|
|
1057
|
+
const ctx = buildContext(req, url, match.params, nonce, match.spec.maxBody ?? maxBody)
|
|
1058
1058
|
|
|
1059
1059
|
// Brand resolution — attach ctx.brand before guard or server data runs
|
|
1060
1060
|
if (resolveBrand) {
|
|
@@ -1793,13 +1793,14 @@ function buildContext(req, url, params, nonce = '', maxBody = 1024 * 1024) {
|
|
|
1793
1793
|
function _readBody() {
|
|
1794
1794
|
if (_bodyPromise) return _bodyPromise
|
|
1795
1795
|
_bodyPromise = new Promise((resolve, reject) => {
|
|
1796
|
+
const tooLarge = () => reject(Object.assign(new Error(`Request body exceeds ${maxBody} bytes`), { status: 413 }))
|
|
1797
|
+
// An honest Content-Length over the limit is refused without reading the body.
|
|
1798
|
+
if (Number(req.headers['content-length']) > maxBody) return tooLarge()
|
|
1796
1799
|
const chunks = []
|
|
1797
1800
|
let size = 0
|
|
1798
1801
|
req.on('data', chunk => {
|
|
1799
1802
|
size += chunk.length
|
|
1800
|
-
if (size > maxBody) {
|
|
1801
|
-
return reject(Object.assign(new Error(`Request body exceeds ${maxBody} bytes`), { status: 413 }))
|
|
1802
|
-
}
|
|
1803
|
+
if (size > maxBody) { chunks.length = 0; return tooLarge() }
|
|
1803
1804
|
chunks.push(chunk)
|
|
1804
1805
|
})
|
|
1805
1806
|
req.on('end', () => resolve(Buffer.concat(chunks)))
|
package/src/spec/schema.js
CHANGED
|
@@ -384,6 +384,13 @@ export function validateSpec(spec) {
|
|
|
384
384
|
}
|
|
385
385
|
}
|
|
386
386
|
|
|
387
|
+
// maxBody — per-route request body limit in bytes
|
|
388
|
+
if (spec.maxBody !== undefined) {
|
|
389
|
+
if (!Number.isFinite(spec.maxBody) || spec.maxBody <= 0) {
|
|
390
|
+
errors.push('spec.maxBody must be a positive number (bytes)')
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
387
394
|
// guard — per-route authorization check
|
|
388
395
|
if (spec.guard !== undefined) {
|
|
389
396
|
if (typeof spec.guard !== 'function') {
|
package/types/schema.d.ts
CHANGED
|
@@ -253,6 +253,9 @@ export interface PulseSpec<S extends object = Record<string, unknown>> {
|
|
|
253
253
|
/** Timeout in ms for all server fetchers on this page. Overrides createServer fetcherTimeout. */
|
|
254
254
|
serverTimeout?: number
|
|
255
255
|
|
|
256
|
+
/** Request body limit in bytes for this route. Overrides createServer maxBody (default 1 MB), up or down. */
|
|
257
|
+
maxBody?: number
|
|
258
|
+
|
|
256
259
|
/**
|
|
257
260
|
* Fallback renderer called when view() throws at runtime.
|
|
258
261
|
* Return an HTML string to display instead of the default error message.
|