@shivaedev/test-story 0.12.0 → 0.13.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/CHANGELOG.md +6 -0
- package/README.md +5 -1
- package/dist/server/forwardingProxy.d.ts +3 -1
- package/dist/server/forwardingProxy.d.ts.map +1 -1
- package/dist/server/forwardingProxy.js +9 -1
- package/dist/server/forwardingProxy.js.map +1 -1
- package/package.json +2 -2
- package/src/server/forwardingProxy.ts +13 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.13.0 - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `ForwardingProxyOptions.localAddress` selects the proxy's source socket address and rejects unavailable addresses during acquisition. `ForwardingProxyOptions.forwardedHops` appends visitor and edge hops after the client's `x-forwarded-for` chain instead of the caller address; fixed `headers` still override the generated chain.
|
|
8
|
+
|
|
3
9
|
## 0.12.0 - 2026-10-18
|
|
4
10
|
|
|
5
11
|
### Added
|
package/README.md
CHANGED
|
@@ -241,6 +241,8 @@ Use `forwardingProxy` from `@shivaedev/test-story/forwardingProxy.ts` in the kit
|
|
|
241
241
|
import { forwardingProxy } from "@shivaedev/test-story/forwardingProxy.ts";
|
|
242
242
|
|
|
243
243
|
const proxy = yield* forwardingProxy(upstream, {
|
|
244
|
+
localAddress: "::1",
|
|
245
|
+
forwardedHops: ["203.0.113.9", "2001:db8::1"],
|
|
244
246
|
headers: {
|
|
245
247
|
host: "internal.test",
|
|
246
248
|
"x-forwarded-host": "app.example",
|
|
@@ -249,7 +251,9 @@ const proxy = yield* forwardingProxy(upstream, {
|
|
|
249
251
|
});
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
Point story requests at `proxy.url`. The proxy listens on IPv4 and IPv6 loopback on the same allocated port, forwards the request's method, path, query, body and headers to `upstream`, and returns the host's status, headers and body. It appends the connecting caller's address to `x-forwarded-for`, preserving any existing chain. The optional `headers` map overrides incoming and generated headers, including `x-forwarded-for` when the story needs a fixed chain.
|
|
254
|
+
Point story requests at `proxy.url`. The proxy listens on IPv4 and IPv6 loopback on the same allocated port, forwards the request's method, path, query, body and headers to `upstream`, and returns the host's status, headers and body. It appends the connecting caller's address to `x-forwarded-for`, preserving any existing chain. Set `forwardedHops` to append those addresses in order instead of the caller, so a client chain of `spoofed` with hops `visitor` and `edge` becomes `spoofed, visitor, edge`. The optional `headers` map overrides incoming and generated headers, including `x-forwarded-for` when the story needs a fixed chain.
|
|
255
|
+
|
|
256
|
+
Set `localAddress` to choose the source socket address of the proxy's connection to the host. The upstream must accept that address family; for a hostname, the proxy resolves the matching family. Use `127.0.0.1` or `::1` for portable loopback stories. An unavailable local address fails acquisition with a message naming it. The proxy returns 502 if it cannot connect to the host. Both listeners close with the story's Scope.
|
|
253
257
|
|
|
254
258
|
#### Reading OTLP exports
|
|
255
259
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
export interface ForwardingProxyOptions {
|
|
3
|
+
readonly forwardedHops?: readonly string[];
|
|
3
4
|
readonly headers?: Readonly<Record<string, string>>;
|
|
5
|
+
readonly localAddress?: string;
|
|
4
6
|
}
|
|
5
7
|
export declare const forwardingProxy: (upstream: string, options?: ForwardingProxyOptions | undefined) => Effect.Effect<{
|
|
6
8
|
url: string;
|
|
7
|
-
},
|
|
9
|
+
}, Error, import("effect/Scope").Scope>;
|
|
8
10
|
//# sourceMappingURL=forwardingProxy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forwardingProxy.d.ts","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"forwardingProxy.d.ts","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGhC,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,eAAO,MAAM,eAAe;;uCAuC1B,CAAC"}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import { createServer, request } from "node:http";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
import { loopbackServer } from "#internal/loopback.ts";
|
|
4
5
|
export const forwardingProxy = Effect.fnUntraced(function* (upstream, options = {}) {
|
|
6
|
+
if (options.localAddress !== undefined) {
|
|
7
|
+
yield* Effect.scoped(loopbackServer(createServer(), { host: options.localAddress })).pipe(Effect.mapError((cause) => new Error(`Cannot acquire forwarding proxy with local address ${options.localAddress}`, { cause })));
|
|
8
|
+
}
|
|
5
9
|
const forward = (incoming, response) => {
|
|
6
10
|
const outgoing = request(`${upstream}${incoming.url}`, {
|
|
11
|
+
family: options.localAddress === undefined ? undefined : isIP(options.localAddress),
|
|
7
12
|
headers: {
|
|
8
13
|
...incoming.headers,
|
|
9
|
-
"x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress]
|
|
14
|
+
"x-forwarded-for": [incoming.headers["x-forwarded-for"], ...(options.forwardedHops ?? [incoming.socket.remoteAddress])]
|
|
15
|
+
.filter(Boolean)
|
|
16
|
+
.join(", "),
|
|
10
17
|
...options.headers,
|
|
11
18
|
},
|
|
19
|
+
localAddress: options.localAddress,
|
|
12
20
|
method: incoming.method,
|
|
13
21
|
}, (result) => {
|
|
14
22
|
response.writeHead(result.statusCode ?? 502, result.headers);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forwardingProxy.js","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,OAAO,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"forwardingProxy.js","sourceRoot":"","sources":["../../src/server/forwardingProxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAwB,OAAO,EAAE,MAAM,WAAW,CAAC;AACxE,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAQvD,MAAM,CAAC,MAAM,eAAe,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,OAAO,GAA2B,EAAE;IACjH,IAAI,OAAO,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;QACxC,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CACxF,MAAM,CAAC,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,KAAK,CAAC,sDAAsD,OAAO,CAAC,YAAY,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAC9H,CAAC;IACH,CAAC;IACD,MAAM,OAAO,GAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE;QACvD,MAAM,QAAQ,GAAG,OAAO,CACvB,GAAG,QAAQ,GAAG,QAAQ,CAAC,GAAG,EAAE,EAC5B;YACC,MAAM,EAAE,OAAO,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC;YACnF,OAAO,EAAE;gBACR,GAAG,QAAQ,CAAC,OAAO;gBACnB,iBAAiB,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,iBAAiB,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,aAAa,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;qBACrH,MAAM,CAAC,OAAO,CAAC;qBACf,IAAI,CAAC,IAAI,CAAC;gBACZ,GAAG,OAAO,CAAC,OAAO;aAClB;YACD,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,MAAM,EAAE,QAAQ,CAAC,MAAM;SACvB,EACD,CAAC,MAAM,EAAE,EAAE;YACV,QAAQ,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,IAAI,GAAG,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC;YAC7D,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACvB,CAAC,CACD,CAAC;QACF,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YAC9B,IAAI,QAAQ,CAAC,WAAW,EAAE,CAAC;gBAC1B,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACP,QAAQ,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC;YAC/B,CAAC;QACF,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;QAC/C,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACzB,CAAC,CAAC;IACF,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;IACzD,KAAK,CAAC,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/F,OAAO,EAAE,GAAG,EAAE,CAAC;AAChB,CAAC,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/test-story",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Tests that read like stories over the real engine: a story kit per engine with its own it, traits per feature, and a failure that prints the story, where it stopped and the engine's state",
|
|
5
5
|
"homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/test-story#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@effect/vitest": "4.0.4",
|
|
49
|
-
"@shivaedev/quality": "0.28.
|
|
49
|
+
"@shivaedev/quality": "0.28.1",
|
|
50
50
|
"@types/node": "24.19.2",
|
|
51
51
|
"effect": "4.0.4",
|
|
52
52
|
"happy-dom": "20.14.6",
|
|
@@ -1,21 +1,33 @@
|
|
|
1
1
|
import { createServer, type RequestListener, request } from "node:http";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import { Effect } from "effect";
|
|
3
4
|
import { loopbackServer } from "#internal/loopback.ts";
|
|
4
5
|
|
|
5
6
|
export interface ForwardingProxyOptions {
|
|
7
|
+
readonly forwardedHops?: readonly string[];
|
|
6
8
|
readonly headers?: Readonly<Record<string, string>>;
|
|
9
|
+
readonly localAddress?: string;
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
export const forwardingProxy = Effect.fnUntraced(function* (upstream: string, options: ForwardingProxyOptions = {}) {
|
|
13
|
+
if (options.localAddress !== undefined) {
|
|
14
|
+
yield* Effect.scoped(loopbackServer(createServer(), { host: options.localAddress })).pipe(
|
|
15
|
+
Effect.mapError((cause) => new Error(`Cannot acquire forwarding proxy with local address ${options.localAddress}`, { cause })),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
10
18
|
const forward: RequestListener = (incoming, response) => {
|
|
11
19
|
const outgoing = request(
|
|
12
20
|
`${upstream}${incoming.url}`,
|
|
13
21
|
{
|
|
22
|
+
family: options.localAddress === undefined ? undefined : isIP(options.localAddress),
|
|
14
23
|
headers: {
|
|
15
24
|
...incoming.headers,
|
|
16
|
-
"x-forwarded-for": [incoming.headers["x-forwarded-for"], incoming.socket.remoteAddress]
|
|
25
|
+
"x-forwarded-for": [incoming.headers["x-forwarded-for"], ...(options.forwardedHops ?? [incoming.socket.remoteAddress])]
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
.join(", "),
|
|
17
28
|
...options.headers,
|
|
18
29
|
},
|
|
30
|
+
localAddress: options.localAddress,
|
|
19
31
|
method: incoming.method,
|
|
20
32
|
},
|
|
21
33
|
(result) => {
|