@everystack/server 0.4.35 → 0.4.37
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/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/index.ts +12 -1
- package/src/plugin.ts +23 -1
- package/src/strip-query-params.ts +41 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `@everystack/server` are documented here.
|
|
|
4
4
|
|
|
5
5
|
Format based on [Keep a Changelog](https://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
+
## [0.4.37] - 2026-10-18
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- an action named __proto__ in a plugin or app action map is refused at init
|
|
12
|
+
|
|
13
|
+
## [0.4.36] - 2026-10-18
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- the Lambda entry strips edge query params (_v by default) before routing
|
|
18
|
+
|
|
7
19
|
## [0.4.35] - 2026-10-14
|
|
8
20
|
|
|
9
21
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@everystack/server",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.37",
|
|
4
4
|
"description": "Server runtime primitives for Lambda — event adapters, routing, SSR, image processing",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"author": "Scalable Technology, Inc. <licensing@scalable.technology>",
|
|
@@ -193,7 +193,7 @@
|
|
|
193
193
|
"ts-jest": "29.4.9",
|
|
194
194
|
"typescript": "5.9.3",
|
|
195
195
|
"@everystack/auth": "0.4.9",
|
|
196
|
-
"@everystack/cli": "0.4.
|
|
196
|
+
"@everystack/cli": "0.4.100"
|
|
197
197
|
},
|
|
198
198
|
"scripts": {
|
|
199
199
|
"test": "jest",
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
|
|
18
18
|
import { log } from './log';
|
|
19
19
|
import { buildTimingHeaders } from './timing';
|
|
20
|
+
import { DEFAULT_STRIP_QUERY_PARAMS, stripQueryParams } from './strip-query-params';
|
|
20
21
|
|
|
21
22
|
export { buildTimingHeaders } from './timing';
|
|
22
23
|
export type { TimingHeaderOptions } from './timing';
|
|
@@ -99,6 +100,13 @@ export interface LambdaHandlerOptions {
|
|
|
99
100
|
logSink?: LogSink;
|
|
100
101
|
/** Emit Server-Timing headers on responses. Default true. */
|
|
101
102
|
observability?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Query params removed from every HTTP request before routing, matched by decoded name whatever
|
|
105
|
+
* the value. The CDN edge appends `?_v=<epoch>` and CloudFront forwards the whole query string,
|
|
106
|
+
* so without this the REST handler would answer 400 to a param with no operator. List any params
|
|
107
|
+
* your own edge adds. Default `['_v']`; `[]` disables stripping.
|
|
108
|
+
*/
|
|
109
|
+
stripQueryParams?: string[];
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
// --- Event Adapter ---
|
|
@@ -256,7 +264,10 @@ export function createLambdaHandler(
|
|
|
256
264
|
|
|
257
265
|
try {
|
|
258
266
|
const { router } = await ensureInitialized();
|
|
259
|
-
const request =
|
|
267
|
+
const request = stripQueryParams(
|
|
268
|
+
eventToRequest(httpEvent),
|
|
269
|
+
options.stripQueryParams ?? DEFAULT_STRIP_QUERY_PARAMS,
|
|
270
|
+
);
|
|
260
271
|
const path = httpEvent.rawPath;
|
|
261
272
|
const method = httpEvent.requestContext.http.method;
|
|
262
273
|
const handlerFn = router(path, method);
|
package/src/plugin.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
log,
|
|
37
37
|
} from './index';
|
|
38
38
|
import { splitSqlStatements, stripSqlComments } from './sql-lex';
|
|
39
|
+
import { DEFAULT_STRIP_QUERY_PARAMS, stripQueryParams } from './strip-query-params';
|
|
39
40
|
|
|
40
41
|
// --- Plugin Types ---
|
|
41
42
|
|
|
@@ -138,6 +139,13 @@ export interface PluginLambdaHandlerOptions {
|
|
|
138
139
|
* here, not in the HTTP-facing API function.
|
|
139
140
|
*/
|
|
140
141
|
http?: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* Query params removed from every HTTP request before routing, matched by decoded name whatever
|
|
144
|
+
* the value. The CDN edge appends `?_v=<epoch>` and CloudFront forwards the whole query string,
|
|
145
|
+
* so without this the REST handler would answer 400 to a param with no operator. List any params
|
|
146
|
+
* your own edge adds. Default `['_v']`; `[]` disables stripping.
|
|
147
|
+
*/
|
|
148
|
+
stripQueryParams?: string[];
|
|
141
149
|
}
|
|
142
150
|
|
|
143
151
|
// --- Plugin Lambda Handler Implementation ---
|
|
@@ -164,6 +172,17 @@ export function createPluginLambdaHandler(
|
|
|
164
172
|
contributions.push(await plugin(ctx));
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
// An OWN `__proto__` key (parsed JSON, a computed key) cannot be an action: Object.assign would
|
|
176
|
+
// run the `__proto__` setter on the map instead of adding it, and the spread below would define
|
|
177
|
+
// it as an own key. Refuse it at init rather than drop or dispatch it.
|
|
178
|
+
const refuseProtoKey = (source: string, actions: Record<string, ActionHandler> | undefined) => {
|
|
179
|
+
if (actions && Object.hasOwn(actions, '__proto__')) {
|
|
180
|
+
throw new Error(`[everystack/server] ${source} action name "__proto__" is not allowed`);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
for (const contribution of contributions) refuseProtoKey('plugin', contribution.actions);
|
|
184
|
+
refuseProtoKey('app', options.actions);
|
|
185
|
+
|
|
167
186
|
// Collect actions (app-level takes precedence on collision)
|
|
168
187
|
const pluginActions: Record<string, ActionHandler> = {};
|
|
169
188
|
for (const contribution of contributions) {
|
|
@@ -272,7 +291,10 @@ export function createPluginLambdaHandler(
|
|
|
272
291
|
|
|
273
292
|
try {
|
|
274
293
|
const { router, logSink } = await ensureInitialized();
|
|
275
|
-
const request =
|
|
294
|
+
const request = stripQueryParams(
|
|
295
|
+
eventToRequest(httpEvent),
|
|
296
|
+
options.stripQueryParams ?? DEFAULT_STRIP_QUERY_PARAMS,
|
|
297
|
+
);
|
|
276
298
|
const path = httpEvent.rawPath;
|
|
277
299
|
const method = httpEvent.requestContext.http.method;
|
|
278
300
|
const handlerFn = router!(path, method);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal (not exported from the package): removes edge-added query params from a Request before
|
|
3
|
+
* routing. Both Lambda entry points (createLambdaHandler, createPluginLambdaHandler) call it right
|
|
4
|
+
* after eventToRequest.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** The CDN edge's cache-version param (cacheVersionFeature, docs/caching.md). */
|
|
8
|
+
export const DEFAULT_STRIP_QUERY_PARAMS: readonly string[] = ['_v'];
|
|
9
|
+
|
|
10
|
+
/** Decodes a raw query-pair name the way a form-encoded parser does; null if it cannot decode. */
|
|
11
|
+
function decodeName(raw: string): string | null {
|
|
12
|
+
try {
|
|
13
|
+
return decodeURIComponent(raw.replace(/\+/g, ' '));
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns `request` with every query pair whose decoded name is in `names` removed. The kept pairs
|
|
21
|
+
* keep their exact bytes and order (no URLSearchParams re-serialization). When nothing is removed
|
|
22
|
+
* the same Request object is returned; otherwise a new Request carries the original method,
|
|
23
|
+
* headers and body.
|
|
24
|
+
*/
|
|
25
|
+
export function stripQueryParams(request: Request, names: readonly string[]): Request {
|
|
26
|
+
if (names.length === 0) return request;
|
|
27
|
+
const url = request.url;
|
|
28
|
+
const q = url.indexOf('?');
|
|
29
|
+
if (q === -1) return request;
|
|
30
|
+
|
|
31
|
+
const pairs = url.slice(q + 1).split('&');
|
|
32
|
+
const kept = pairs.filter((pair) => {
|
|
33
|
+
const eq = pair.indexOf('=');
|
|
34
|
+
const name = decodeName(eq === -1 ? pair : pair.slice(0, eq));
|
|
35
|
+
return name === null || !names.includes(name);
|
|
36
|
+
});
|
|
37
|
+
if (kept.length === pairs.length) return request;
|
|
38
|
+
|
|
39
|
+
const query = kept.join('&');
|
|
40
|
+
return new Request(url.slice(0, q) + (query ? '?' + query : ''), request);
|
|
41
|
+
}
|