nucleus-core-ts 0.10.2 → 0.10.3
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/dist/.build-ok +1 -1
- package/dist/index.js +6 -6
- package/dist/server.d.ts +15 -8
- package/dist/server.js +6 -6
- package/dist/src/Client/Proxy/httpProxy.js +33 -3
- package/dist/src/Client/Proxy/types.d.ts +22 -0
- package/dist/src/Nucleus/index.d.ts +2 -0
- package/dist/src/Nucleus/routes/shared/bodyCeiling.d.ts +31 -2
- package/dist/src/Server/routeRecording.d.ts +16 -0
- package/package.json +1 -1
|
@@ -280,6 +280,24 @@ export function createHttpProxyHandler(config) {
|
|
|
280
280
|
}
|
|
281
281
|
return `${baseUrl}${finalPath}`;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* The caller's address, from the inbound chain when hops are trusted.
|
|
285
|
+
*
|
|
286
|
+
* `X-Forwarded-For` is appended left-to-right, so with N trusted proxies in
|
|
287
|
+
* front the caller is the entry N from the RIGHT. Counting from the right is
|
|
288
|
+
* what makes it unspoofable: a client can prepend anything to the left, but
|
|
289
|
+
* it cannot control what the trusted hops append after it.
|
|
290
|
+
*
|
|
291
|
+
* Returns undefined when the chain is too short to satisfy the configured
|
|
292
|
+
* hop count — a chain that does not reach means the deployment is not what
|
|
293
|
+
* the configuration says, and inventing an address there would be worse than
|
|
294
|
+
* having none.
|
|
295
|
+
*/ function clientFromChain(req, hops) {
|
|
296
|
+
if (hops <= 0) return undefined;
|
|
297
|
+
const chain = (req.headers.get('x-forwarded-for') ?? '').split(',').map((one)=>one.trim()).filter(Boolean);
|
|
298
|
+
if (chain.length < hops) return undefined;
|
|
299
|
+
return chain[chain.length - hops];
|
|
300
|
+
}
|
|
283
301
|
function buildHeaders(req, target, clientIp) {
|
|
284
302
|
const headers = new Headers(req.headers);
|
|
285
303
|
// Strip hop-by-hop headers (RFC 7230 §6.1) — these are connection-scoped and
|
|
@@ -321,9 +339,21 @@ export function createHttpProxyHandler(config) {
|
|
|
321
339
|
'true-client-ip',
|
|
322
340
|
'x-client-ip'
|
|
323
341
|
])headers.delete(h);
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
342
|
+
/*
|
|
343
|
+
* With trusted hops configured, the CHAIN wins over the socket.
|
|
344
|
+
*
|
|
345
|
+
* The socket peer is only the client when this proxy is the edge. Behind an
|
|
346
|
+
* ingress it is the ingress pod, and overwriting the chain with it threw
|
|
347
|
+
* away the one value that identified the caller — so everything downstream
|
|
348
|
+
* keyed on the address saw a single visitor for the whole cluster.
|
|
349
|
+
*
|
|
350
|
+
* Still an OVERWRITE, never an append: what is written is a value derived
|
|
351
|
+
* from a position the client cannot reach, so a spoofed prefix on the left
|
|
352
|
+
* is discarded rather than forwarded.
|
|
353
|
+
*/ const resolved = clientFromChain(req, config.trustedProxyHops ?? 0) ?? clientIp;
|
|
354
|
+
if (resolved) {
|
|
355
|
+
headers.set('x-forwarded-for', resolved);
|
|
356
|
+
headers.set('x-real-ip', resolved);
|
|
327
357
|
} else {
|
|
328
358
|
// Couldn't resolve the socket IP — do not forward a client-controlled value.
|
|
329
359
|
headers.delete('x-forwarded-for');
|
|
@@ -142,6 +142,28 @@ export interface HttpProxyTarget {
|
|
|
142
142
|
}
|
|
143
143
|
export interface HttpProxyConfig {
|
|
144
144
|
targets: HttpProxyTarget[];
|
|
145
|
+
/**
|
|
146
|
+
* How many proxies sit IN FRONT of this one, and are trusted.
|
|
147
|
+
*
|
|
148
|
+
* Default 0, which keeps the old behaviour: the socket peer is the client and
|
|
149
|
+
* `X-Forwarded-For` is overwritten with it. That is right when this proxy is
|
|
150
|
+
* the edge, and wrong the moment anything stands in front — behind a
|
|
151
|
+
* Kubernetes ingress the socket peer is the INGRESS POD, so every request in
|
|
152
|
+
* the cluster arrives with one address and everything keyed on it collapses
|
|
153
|
+
* into a single bucket.
|
|
154
|
+
*
|
|
155
|
+
* Measured on a live install: 353 captcha challenges over 58 hours all
|
|
156
|
+
* attributed to `10.233.104.64`, 50 of them refused by a 10-per-minute
|
|
157
|
+
* per-IP limit, which during the morning sign-in rush meant the login page
|
|
158
|
+
* could not load a challenge at all.
|
|
159
|
+
*
|
|
160
|
+
* Set to the number of trusted hops and the client address is read from the
|
|
161
|
+
* inbound chain instead: `X-Forwarded-For` is a list appended left-to-right,
|
|
162
|
+
* so the caller is the entry `trustedProxyHops` from the RIGHT. Counting from
|
|
163
|
+
* the right is what makes it unspoofable — a client can prepend whatever it
|
|
164
|
+
* likes to the left, but it cannot control what the trusted hops append.
|
|
165
|
+
*/
|
|
166
|
+
trustedProxyHops?: number;
|
|
145
167
|
debug?: boolean;
|
|
146
168
|
onError?: (error: Error, path: string, status?: number) => void;
|
|
147
169
|
onRequest?: (path: string, target: string) => void;
|
|
@@ -29,6 +29,8 @@ export { createTenantRoutes } from './routes/tenant';
|
|
|
29
29
|
* Read by `createNucleusServer`, which puts it on every handler's context.
|
|
30
30
|
*/
|
|
31
31
|
export declare function getMountedRuntime(): unknown;
|
|
32
|
+
/** @see recordResolvedOptions — the config AFTER a path was read and parsed. */
|
|
33
|
+
export declare function getResolvedOptions(): unknown;
|
|
32
34
|
export declare function getMountedRouteGroups(): RouteGroup[];
|
|
33
35
|
export declare function getMountedSocket(): WebSocketRoute | null;
|
|
34
36
|
export declare function getMountedStaticAssets(): string | null;
|
|
@@ -33,13 +33,42 @@ export declare function requiredBodyCeiling(input: {
|
|
|
33
33
|
* configuration promises, or `null` when it can.
|
|
34
34
|
*
|
|
35
35
|
* Deliberately a full instruction rather than a warning. "Body size may be too
|
|
36
|
-
* small" sends somebody reading source; naming the
|
|
37
|
-
*
|
|
36
|
+
* small" sends somebody reading source; naming the setting and the number ends
|
|
37
|
+
* it.
|
|
38
|
+
*
|
|
39
|
+
* The instruction used to name Elysia's constructor, which stopped existing at
|
|
40
|
+
* 0.10 — so every upgraded install logged an ERROR whose one actionable
|
|
41
|
+
* sentence was impossible to act on. A boot error that cannot be obeyed teaches
|
|
42
|
+
* people to ignore boot errors.
|
|
38
43
|
*/
|
|
39
44
|
export declare function bodyCeilingComplaint(input: {
|
|
40
45
|
configured: number | null | undefined;
|
|
41
46
|
required: number | null;
|
|
42
47
|
}): string | null;
|
|
48
|
+
/**
|
|
49
|
+
* What to hand `Bun.serve` as `maxRequestBodySize`, or `undefined` to leave
|
|
50
|
+
* Bun's default alone.
|
|
51
|
+
*
|
|
52
|
+
* This is HEADROOM, not enforcement, and the difference is the whole rule.
|
|
53
|
+
*
|
|
54
|
+
* Handing Bun the product's limit makes Bun answer first — a bare
|
|
55
|
+
* connection-level 413, no content-type, no body — instead of the guard's
|
|
56
|
+
* envelope. That is why the server passes it nothing by default. But Bun still
|
|
57
|
+
* cuts at 128 MiB before any guard runs, so an install advertising 1 GB uploads
|
|
58
|
+
* had them severed with nothing to show, and no way to raise it: this path
|
|
59
|
+
* forwarded nothing, and the boot warning told the host to construct an Elysia
|
|
60
|
+
* that no longer exists.
|
|
61
|
+
*
|
|
62
|
+
* So: never below what the guard enforces, never below what storage advertises,
|
|
63
|
+
* and unset whenever neither exceeds Bun's own default. Bun's number then sits
|
|
64
|
+
* at or above every limit the product applies, which leaves the guard as the
|
|
65
|
+
* first responder for everything it refuses — the envelope survives — while
|
|
66
|
+
* Bun stops severing uploads the product means to accept.
|
|
67
|
+
*/
|
|
68
|
+
export declare function bunBodyCeilingFor(input: {
|
|
69
|
+
storageRequired: number | null;
|
|
70
|
+
guardCeiling: number | undefined;
|
|
71
|
+
}): number | undefined;
|
|
43
72
|
/**
|
|
44
73
|
* The sentence to print when the POD cannot carry what the configuration
|
|
45
74
|
* promises, or `null` when it can.
|
|
@@ -99,6 +99,22 @@ export declare function getRecordedSwagger(): unknown;
|
|
|
99
99
|
export declare function recordRuntime(runtime: unknown): void;
|
|
100
100
|
/** What `recordRuntime` last recorded, or undefined when nothing did. */
|
|
101
101
|
export declare function getMountedRuntime(): unknown;
|
|
102
|
+
/**
|
|
103
|
+
* The config AFTER it was resolved, which is not the config the host passed.
|
|
104
|
+
*
|
|
105
|
+
* `config.options` may be an object or a PATH to a JSON file. When it is a
|
|
106
|
+
* path, `buildNucleus` reads and parses it, and everything inside — the
|
|
107
|
+
* `security` block among it — exists only in there. `createNucleusServer`
|
|
108
|
+
* needed one of those settings and could only see the un-resolved form, so a
|
|
109
|
+
* host that passed a path silently ran without its request-body ceiling: the
|
|
110
|
+
* setting was in the file, read by the plugin, and invisible to the server.
|
|
111
|
+
*
|
|
112
|
+
* Recorded rather than returned from `buildNucleus`, to keep the one channel
|
|
113
|
+
* these two modules already use instead of opening a second.
|
|
114
|
+
*/
|
|
115
|
+
export declare function recordResolvedOptions(options: unknown): void;
|
|
116
|
+
/** What `recordResolvedOptions` last recorded, or null when nothing did. */
|
|
117
|
+
export declare function getResolvedOptions(): unknown;
|
|
102
118
|
export declare function recordStaticAssets(assetsPath: string): void;
|
|
103
119
|
export declare function getRecordedStaticAssets(): string | null;
|
|
104
120
|
export declare function recordBootStep(step: BootStep): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nucleus-core-ts",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Production-ready, enterprise-grade TypeScript framework for building multi-tenant APIs",
|
|
5
5
|
"author": "Hidayet Can Özcan <hidayetcan@gmail.com>",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|