@lark-apaas/nestjs-http-forwarder 0.1.1-alpha.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/dist/index.js ADDED
@@ -0,0 +1,375 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ // src/http-forwarder.module.ts
5
+ import { Module } from "@nestjs/common";
6
+
7
+ // src/const.ts
8
+ var HTTP_FORWARDER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("HTTP_FORWARDER_MODULE_OPTIONS");
9
+ var CONTROLLER_ROUTE = "anycross/forward";
10
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
11
+ var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
12
+ var ALLOWED_PROTOCOLS = [
13
+ "http:",
14
+ "https:"
15
+ ];
16
+ var HEADERS_NOT_FORWARDED_TO_UPSTREAM = /* @__PURE__ */ new Set([
17
+ "connection",
18
+ "keep-alive",
19
+ "transfer-encoding",
20
+ "upgrade",
21
+ "proxy-connection",
22
+ "proxy-authenticate",
23
+ "proxy-authorization",
24
+ "te",
25
+ "trailer",
26
+ "accept-encoding"
27
+ ]);
28
+ var HEADERS_NOT_RETURNED_TO_CLIENT = /* @__PURE__ */ new Set([
29
+ "content-encoding",
30
+ "content-length",
31
+ "transfer-encoding",
32
+ "connection",
33
+ "keep-alive",
34
+ "upgrade"
35
+ ]);
36
+ var ERROR_CODES = {
37
+ INVALID_REQUEST: "INVALID_REQUEST",
38
+ INVALID_TARGET_PROTOCOL: "INVALID_TARGET_PROTOCOL",
39
+ UPSTREAM_UNREACHABLE: "UPSTREAM_UNREACHABLE",
40
+ UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
41
+ RESPONSE_TOO_LARGE: "RESPONSE_TOO_LARGE"
42
+ };
43
+
44
+ // src/controllers/http-forwarder.controller.ts
45
+ import { All, Controller, HttpException as HttpException2, HttpStatus as HttpStatus2, Req, Res } from "@nestjs/common";
46
+
47
+ // src/services/http-forwarder.service.ts
48
+ import { HttpException, HttpStatus, Inject, Injectable } from "@nestjs/common";
49
+ function _ts_decorate(decorators, target, key, desc) {
50
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
51
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
52
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
53
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
54
+ }
55
+ __name(_ts_decorate, "_ts_decorate");
56
+ function _ts_metadata(k, v) {
57
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
58
+ }
59
+ __name(_ts_metadata, "_ts_metadata");
60
+ function _ts_param(paramIndex, decorator) {
61
+ return function(target, key) {
62
+ decorator(target, key, paramIndex);
63
+ };
64
+ }
65
+ __name(_ts_param, "_ts_param");
66
+ var HttpForwarderService = class {
67
+ static {
68
+ __name(this, "HttpForwarderService");
69
+ }
70
+ options;
71
+ constructor(options) {
72
+ this.options = options;
73
+ }
74
+ async forward(payload) {
75
+ const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);
76
+ const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);
77
+ const controller = new AbortController();
78
+ const timeoutTimer = setTimeout(() => controller.abort(), this.options.requestTimeoutMs);
79
+ if (typeof timeoutTimer.unref === "function") timeoutTimer.unref();
80
+ try {
81
+ const response = await fetch(targetUrl.toString(), {
82
+ method: payload.method,
83
+ headers,
84
+ body: payload.body ?? void 0,
85
+ signal: controller.signal
86
+ });
87
+ const body = await this.readBoundedBody(response, controller);
88
+ return {
89
+ status: response.status,
90
+ headers: Object.fromEntries(response.headers),
91
+ body
92
+ };
93
+ } catch (err) {
94
+ throw this.mapNetworkError(err, controller);
95
+ } finally {
96
+ clearTimeout(timeoutTimer);
97
+ }
98
+ }
99
+ /**
100
+ * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。
101
+ */
102
+ assertValidTargetUrl(raw) {
103
+ if (typeof raw !== "string" || !raw) {
104
+ throw new HttpException({
105
+ code: ERROR_CODES.INVALID_REQUEST,
106
+ message: "targetUrl is required"
107
+ }, HttpStatus.BAD_REQUEST);
108
+ }
109
+ let parsed;
110
+ try {
111
+ parsed = new URL(raw);
112
+ } catch {
113
+ throw new HttpException({
114
+ code: ERROR_CODES.INVALID_REQUEST,
115
+ message: "Invalid targetUrl"
116
+ }, HttpStatus.BAD_REQUEST);
117
+ }
118
+ if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
119
+ throw new HttpException({
120
+ code: ERROR_CODES.INVALID_TARGET_PROTOCOL,
121
+ message: `Unsupported protocol: ${parsed.protocol}`
122
+ }, HttpStatus.BAD_REQUEST);
123
+ }
124
+ return parsed;
125
+ }
126
+ /**
127
+ * host / content-length 的**单点处理点**:
128
+ * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)
129
+ * - content-length:剥离(fetch 会按真实 body 自算)
130
+ *
131
+ * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。
132
+ */
133
+ buildOutgoingHeaders(incoming, targetUrl) {
134
+ const out = {};
135
+ if (incoming) {
136
+ for (const [key, value] of Object.entries(incoming)) {
137
+ const lower = key.toLowerCase();
138
+ if (lower === "host" || lower === "content-length") continue;
139
+ out[key] = value;
140
+ }
141
+ }
142
+ out["host"] = targetUrl.host;
143
+ return out;
144
+ }
145
+ /**
146
+ * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。
147
+ */
148
+ async readBoundedBody(response, controller) {
149
+ const limit = this.options.maxResponseBytes;
150
+ if (!response.body) return "";
151
+ const reader = response.body.getReader();
152
+ const chunks = [];
153
+ let total = 0;
154
+ try {
155
+ while (true) {
156
+ const { done, value } = await reader.read();
157
+ if (done) break;
158
+ if (value) {
159
+ total += value.byteLength;
160
+ if (total > limit) {
161
+ controller.abort();
162
+ throw new HttpException({
163
+ code: ERROR_CODES.RESPONSE_TOO_LARGE,
164
+ message: `response exceeds maxResponseBytes (${limit})`
165
+ }, HttpStatus.BAD_GATEWAY);
166
+ }
167
+ chunks.push(value);
168
+ }
169
+ }
170
+ } finally {
171
+ void reader.cancel().catch(() => void 0);
172
+ }
173
+ return Buffer.concat(chunks).toString("utf-8");
174
+ }
175
+ /**
176
+ * 把 fetch 抛错 / 超时统一映射为 HttpException。
177
+ *
178
+ * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)
179
+ * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504
180
+ * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)
181
+ * 4. 兜底 → UPSTREAM_UNREACHABLE 502
182
+ */
183
+ mapNetworkError(err, controller) {
184
+ if (err instanceof HttpException) return err;
185
+ if (controller.signal.aborted) {
186
+ return new HttpException({
187
+ code: ERROR_CODES.UPSTREAM_TIMEOUT,
188
+ message: "upstream timeout"
189
+ }, HttpStatus.GATEWAY_TIMEOUT);
190
+ }
191
+ if (err instanceof TypeError) {
192
+ return new HttpException({
193
+ code: ERROR_CODES.INVALID_REQUEST,
194
+ message: "invalid request configuration"
195
+ }, HttpStatus.BAD_REQUEST);
196
+ }
197
+ return new HttpException({
198
+ code: ERROR_CODES.UPSTREAM_UNREACHABLE,
199
+ message: "upstream unreachable"
200
+ }, HttpStatus.BAD_GATEWAY);
201
+ }
202
+ };
203
+ HttpForwarderService = _ts_decorate([
204
+ Injectable(),
205
+ _ts_param(0, Inject(HTTP_FORWARDER_MODULE_OPTIONS)),
206
+ _ts_metadata("design:type", Function),
207
+ _ts_metadata("design:paramtypes", [
208
+ typeof HttpForwarderOptionsResolved === "undefined" ? Object : HttpForwarderOptionsResolved
209
+ ])
210
+ ], HttpForwarderService);
211
+
212
+ // src/controllers/http-forwarder.controller.ts
213
+ function _ts_decorate2(decorators, target, key, desc) {
214
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
215
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
216
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
217
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
218
+ }
219
+ __name(_ts_decorate2, "_ts_decorate");
220
+ function _ts_metadata2(k, v) {
221
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
222
+ }
223
+ __name(_ts_metadata2, "_ts_metadata");
224
+ function _ts_param2(paramIndex, decorator) {
225
+ return function(target, key) {
226
+ decorator(target, key, paramIndex);
227
+ };
228
+ }
229
+ __name(_ts_param2, "_ts_param");
230
+ var HttpForwarderController = class _HttpForwarderController {
231
+ static {
232
+ __name(this, "HttpForwarderController");
233
+ }
234
+ svc;
235
+ constructor(svc) {
236
+ this.svc = svc;
237
+ }
238
+ async forward(req, res) {
239
+ const targetUrl = req.query?.targetUrl;
240
+ if (typeof targetUrl !== "string" || !targetUrl) {
241
+ throw new HttpException2({
242
+ code: ERROR_CODES.INVALID_REQUEST,
243
+ message: "query parameter `targetUrl` is required"
244
+ }, HttpStatus2.BAD_REQUEST);
245
+ }
246
+ const out = await this.svc.forward({
247
+ method: req.method.toUpperCase(),
248
+ targetUrl,
249
+ headers: _HttpForwarderController.pickIncomingHeaders(req.headers),
250
+ body: _HttpForwarderController.serializeIncomingBody(req)
251
+ });
252
+ res.status(out.status);
253
+ for (const [key, value] of Object.entries(out.headers)) {
254
+ if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;
255
+ res.setHeader(key, value);
256
+ }
257
+ res.send(out.body);
258
+ }
259
+ /**
260
+ * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。
261
+ * 保留 cookie / authorization / x-* 等业务头。
262
+ * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。
263
+ */
264
+ static pickIncomingHeaders(incoming) {
265
+ const out = {};
266
+ for (const [key, raw] of Object.entries(incoming)) {
267
+ if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;
268
+ if (raw === void 0) continue;
269
+ out[key] = Array.isArray(raw) ? raw.join(", ") : String(raw);
270
+ }
271
+ return out;
272
+ }
273
+ /**
274
+ * 把 express 已解析的 body 序列化回字符串供 service 透传。
275
+ */
276
+ static serializeIncomingBody(req) {
277
+ const method = req.method.toUpperCase();
278
+ if (method === "GET" || method === "HEAD") return null;
279
+ const body = req.body;
280
+ if (body === void 0 || body === null) return null;
281
+ if (typeof body === "string") return body;
282
+ if (Buffer.isBuffer(body)) return body.toString("utf-8");
283
+ if (typeof body === "object" && Object.keys(body).length === 0) {
284
+ return null;
285
+ }
286
+ return JSON.stringify(body);
287
+ }
288
+ };
289
+ _ts_decorate2([
290
+ All(CONTROLLER_ROUTE),
291
+ _ts_param2(0, Req()),
292
+ _ts_param2(1, Res()),
293
+ _ts_metadata2("design:type", Function),
294
+ _ts_metadata2("design:paramtypes", [
295
+ typeof Request === "undefined" ? Object : Request,
296
+ typeof Response === "undefined" ? Object : Response
297
+ ]),
298
+ _ts_metadata2("design:returntype", Promise)
299
+ ], HttpForwarderController.prototype, "forward", null);
300
+ HttpForwarderController = _ts_decorate2([
301
+ Controller(),
302
+ _ts_metadata2("design:type", Function),
303
+ _ts_metadata2("design:paramtypes", [
304
+ typeof HttpForwarderService === "undefined" ? Object : HttpForwarderService
305
+ ])
306
+ ], HttpForwarderController);
307
+
308
+ // src/services/options.resolved.ts
309
+ function resolveOptions(options) {
310
+ const opts = options ?? {};
311
+ const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
312
+ const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
313
+ if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {
314
+ throw new Error("HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer");
315
+ }
316
+ if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {
317
+ throw new Error("HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer");
318
+ }
319
+ return {
320
+ requestTimeoutMs,
321
+ maxResponseBytes
322
+ };
323
+ }
324
+ __name(resolveOptions, "resolveOptions");
325
+
326
+ // src/http-forwarder.module.ts
327
+ function _ts_decorate3(decorators, target, key, desc) {
328
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
329
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
330
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
331
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
332
+ }
333
+ __name(_ts_decorate3, "_ts_decorate");
334
+ var HttpForwarderModule = class _HttpForwarderModule {
335
+ static {
336
+ __name(this, "HttpForwarderModule");
337
+ }
338
+ static forRoot(options) {
339
+ const resolved = resolveOptions(options);
340
+ return {
341
+ module: _HttpForwarderModule,
342
+ controllers: [
343
+ HttpForwarderController
344
+ ],
345
+ providers: [
346
+ {
347
+ provide: HTTP_FORWARDER_MODULE_OPTIONS,
348
+ useValue: resolved
349
+ },
350
+ HttpForwarderService
351
+ ],
352
+ exports: [
353
+ HttpForwarderService
354
+ ]
355
+ };
356
+ }
357
+ };
358
+ HttpForwarderModule = _ts_decorate3([
359
+ Module({})
360
+ ], HttpForwarderModule);
361
+ export {
362
+ ALLOWED_PROTOCOLS,
363
+ CONTROLLER_ROUTE,
364
+ DEFAULT_MAX_RESPONSE_BYTES,
365
+ DEFAULT_REQUEST_TIMEOUT_MS,
366
+ ERROR_CODES,
367
+ HEADERS_NOT_FORWARDED_TO_UPSTREAM,
368
+ HEADERS_NOT_RETURNED_TO_CLIENT,
369
+ HTTP_FORWARDER_MODULE_OPTIONS,
370
+ HttpForwarderController,
371
+ HttpForwarderModule,
372
+ HttpForwarderService,
373
+ resolveOptions
374
+ };
375
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/http-forwarder.module.ts","../src/const.ts","../src/controllers/http-forwarder.controller.ts","../src/services/http-forwarder.service.ts","../src/services/options.resolved.ts"],"sourcesContent":["import { DynamicModule, Module } from '@nestjs/common';\nimport { HTTP_FORWARDER_MODULE_OPTIONS } from './const';\nimport { HttpForwarderController } from './controllers/http-forwarder.controller';\nimport { HttpForwarderService } from './services/http-forwarder.service';\nimport { resolveOptions } from './services/options.resolved';\nimport type { HttpForwarderModuleOptions } from './types';\n\n/**\n * HttpForwarderModule\n *\n * 自动注册:\n * - `HttpForwarderController` —— 通用内网转发 endpoint `ALL /anycross/forward?targetUrl=...`\n * 支持所有 HTTP method,headers / body 自动透传,上游响应直接投射到客户端。\n * - `HttpForwarderService`(可注入到自定义 controller,编排特殊场景)\n *\n * **不内置鉴权**——消费方在 AppModule 层面给路由加 guard(如全局 NeedLoginGuard)。\n *\n * 使用示例:\n * ```ts\n * @Module({\n * imports: [\n * HttpForwarderModule.forRoot({\n * requestTimeoutMs: 30_000,\n * maxResponseBytes: 10 * 1024 * 1024,\n * }),\n * ],\n * })\n * export class AppModule {}\n * ```\n *\n * 前端调用:\n * ```ts\n * axiosForBackend.get('/anycross/forward', { params: { targetUrl: 'http://api.corp.com/v1/users' } });\n * axiosForBackend.post('/anycross/forward', body, { params: { targetUrl: 'http://api.corp.com/v1/orders' } });\n * ```\n *\n * **注**:实际代理 / 鉴权 / 隧道由部署环境处理(如沙箱内的 mihomo 透明代理 +\n * iptables 拦截)。SDK 不感知 anycross / proxy_group_id / jwtToken。\n */\n@Module({})\nexport class HttpForwarderModule {\n static forRoot(options?: HttpForwarderModuleOptions): DynamicModule {\n const resolved = resolveOptions(options);\n return {\n module: HttpForwarderModule,\n controllers: [HttpForwarderController],\n providers: [\n {\n provide: HTTP_FORWARDER_MODULE_OPTIONS,\n useValue: resolved,\n },\n HttpForwarderService,\n ],\n exports: [HttpForwarderService],\n };\n }\n}\n","/** HttpForwarder module options injection token */\nexport const HTTP_FORWARDER_MODULE_OPTIONS = Symbol('HTTP_FORWARDER_MODULE_OPTIONS');\n\n/** Controller route prefix (硬编码,不可配置以保持调用方式统一) */\nexport const CONTROLLER_ROUTE = 'anycross/forward';\n\n/** Default request timeout in milliseconds */\nexport const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;\n\n/** Default maximum response body size in bytes (10 MB) */\nexport const DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\n\n/** Allowed target URL protocols */\nexport const ALLOWED_PROTOCOLS = ['http:', 'https:'] as const;\n\n/**\n * 入站 headers 中不能透传给上游的 hop-by-hop / transport / encoding 字段。\n * RFC 7230 §6.1 hop-by-hop + accept-encoding(由 fetch / undici 接管)。\n *\n * 注:host / content-length 由 service.buildOutgoingHeaders 在写入 fetch 入参时\n * 单点处理(host 重写为 targetUrl.host;content-length 让 fetch 自算),此处不重复。\n */\nexport const HEADERS_NOT_FORWARDED_TO_UPSTREAM = new Set([\n 'connection',\n 'keep-alive',\n 'transfer-encoding',\n 'upgrade',\n 'proxy-connection',\n 'proxy-authenticate',\n 'proxy-authorization',\n 'te',\n 'trailer',\n 'accept-encoding',\n]);\n\n/**\n * 上游响应 headers 中不应回写到下游 response 的字段。\n * - content-encoding:fetch 已解压,原 encoding 不再适用\n * - content-length:node http 会根据真实 body 重算\n * - transfer-encoding / connection / keep-alive:hop-by-hop\n */\nexport const HEADERS_NOT_RETURNED_TO_CLIENT = new Set([\n 'content-encoding',\n 'content-length',\n 'transfer-encoding',\n 'connection',\n 'keep-alive',\n 'upgrade',\n]);\n\n/** Error codes returned to callers */\nexport const ERROR_CODES = {\n INVALID_REQUEST: 'INVALID_REQUEST',\n INVALID_TARGET_PROTOCOL: 'INVALID_TARGET_PROTOCOL',\n UPSTREAM_UNREACHABLE: 'UPSTREAM_UNREACHABLE',\n UPSTREAM_TIMEOUT: 'UPSTREAM_TIMEOUT',\n RESPONSE_TOO_LARGE: 'RESPONSE_TOO_LARGE',\n} as const;\n","import {\n All,\n Controller,\n HttpException,\n HttpStatus,\n Req,\n Res,\n} from '@nestjs/common';\nimport type { Request, Response } from 'express';\nimport {\n CONTROLLER_ROUTE,\n ERROR_CODES,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n} from '../const';\nimport { HttpForwarderService } from '../services/http-forwarder.service';\n\n/**\n * 通用内网转发 controller。\n *\n * 路径:`/anycross/forward`(硬编码,所有消费方一致)。\n *\n * 接受所有 HTTP method(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)。\n * - method:直接取自入站请求自身\n * - targetUrl:取自 query `?targetUrl=...`\n * - headers:透传入站请求 headers(剔除 hop-by-hop / accept-encoding;\n * host / content-length 由 service 单点处理)\n * - body:透传入站请求 body(已经过 express body-parser,按情况序列化为字符串)\n *\n * 响应:上游 status / headers / body **直接投射到 HTTP response**,让前端 axios\n * 看到的就是真实上游响应(而非 wrap 的 JSON)。\n *\n * **SDK 不内置鉴权**——消费方在 AppModule 层面给本路由加 guard(如全局 NeedLoginGuard)。\n */\n@Controller()\nexport class HttpForwarderController {\n constructor(private readonly svc: HttpForwarderService) {}\n\n @All(CONTROLLER_ROUTE)\n async forward(@Req() req: Request, @Res() res: Response): Promise<void> {\n const targetUrl = req.query?.targetUrl;\n if (typeof targetUrl !== 'string' || !targetUrl) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_REQUEST,\n message: 'query parameter `targetUrl` is required',\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n\n const out = await this.svc.forward({\n method: req.method.toUpperCase(),\n targetUrl,\n headers: HttpForwarderController.pickIncomingHeaders(req.headers),\n body: HttpForwarderController.serializeIncomingBody(req),\n });\n\n res.status(out.status);\n for (const [key, value] of Object.entries(out.headers)) {\n if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;\n res.setHeader(key, value);\n }\n res.send(out.body);\n }\n\n /**\n * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。\n * 保留 cookie / authorization / x-* 等业务头。\n * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。\n */\n static pickIncomingHeaders(incoming: Request['headers']): Record<string, string> {\n const out: Record<string, string> = {};\n for (const [key, raw] of Object.entries(incoming)) {\n if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;\n if (raw === undefined) continue;\n out[key] = Array.isArray(raw) ? raw.join(', ') : String(raw);\n }\n return out;\n }\n\n /**\n * 把 express 已解析的 body 序列化回字符串供 service 透传。\n */\n static serializeIncomingBody(req: Request): string | null {\n const method = req.method.toUpperCase();\n if (method === 'GET' || method === 'HEAD') return null;\n const body = (req as { body?: unknown }).body;\n if (body === undefined || body === null) return null;\n if (typeof body === 'string') return body;\n if (Buffer.isBuffer(body)) return body.toString('utf-8');\n if (typeof body === 'object' && Object.keys(body as object).length === 0) {\n return null;\n }\n return JSON.stringify(body);\n }\n}\n","import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common';\nimport {\n ALLOWED_PROTOCOLS,\n ERROR_CODES,\n HTTP_FORWARDER_MODULE_OPTIONS,\n} from '../const';\nimport type { ForwardRequestDto, ForwardResponseDto } from '../types';\nimport type { HttpForwarderOptionsResolved } from './options.resolved';\n\n/**\n * 反向 HTTP 转发器:把入站请求\"原样\"转发到目标 URL。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由\n * 部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n *\n * 只做 5 件核心事:\n * 1. 协议白名单(仅 http/https,防 SSRF)\n * 2. host 重写 + content-length 剥离(语义正确性,单点处理)\n * 3. 单次请求超时(AbortController,默认 30s)\n * 4. 响应体大小上限(流式累计字节,防 OOM,默认 10MB)\n * 5. 错误码映射(给调用方稳定的 error contract)\n */\n@Injectable()\nexport class HttpForwarderService {\n constructor(\n @Inject(HTTP_FORWARDER_MODULE_OPTIONS)\n private readonly options: HttpForwarderOptionsResolved,\n ) {}\n\n async forward(payload: ForwardRequestDto): Promise<ForwardResponseDto> {\n const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);\n const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);\n const controller = new AbortController();\n const timeoutTimer = setTimeout(\n () => controller.abort(),\n this.options.requestTimeoutMs,\n );\n if (typeof timeoutTimer.unref === 'function') timeoutTimer.unref();\n\n try {\n const response = await fetch(targetUrl.toString(), {\n method: payload.method,\n headers,\n body: payload.body ?? undefined,\n signal: controller.signal,\n });\n const body = await this.readBoundedBody(response, controller);\n return {\n status: response.status,\n headers: Object.fromEntries(response.headers),\n body,\n };\n } catch (err) {\n throw this.mapNetworkError(err, controller);\n } finally {\n clearTimeout(timeoutTimer);\n }\n }\n\n /**\n * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。\n */\n private assertValidTargetUrl(raw: unknown): URL {\n if (typeof raw !== 'string' || !raw) {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'targetUrl is required' },\n HttpStatus.BAD_REQUEST,\n );\n }\n let parsed: URL;\n try {\n parsed = new URL(raw);\n } catch {\n throw new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'Invalid targetUrl' },\n HttpStatus.BAD_REQUEST,\n );\n }\n if (\n !ALLOWED_PROTOCOLS.includes(parsed.protocol as (typeof ALLOWED_PROTOCOLS)[number])\n ) {\n throw new HttpException(\n {\n code: ERROR_CODES.INVALID_TARGET_PROTOCOL,\n message: `Unsupported protocol: ${parsed.protocol}`,\n },\n HttpStatus.BAD_REQUEST,\n );\n }\n return parsed;\n }\n\n /**\n * host / content-length 的**单点处理点**:\n * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)\n * - content-length:剥离(fetch 会按真实 body 自算)\n *\n * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。\n */\n private buildOutgoingHeaders(\n incoming: Record<string, string> | undefined,\n targetUrl: URL,\n ): Record<string, string> {\n const out: Record<string, string> = {};\n if (incoming) {\n for (const [key, value] of Object.entries(incoming)) {\n const lower = key.toLowerCase();\n if (lower === 'host' || lower === 'content-length') continue;\n out[key] = value;\n }\n }\n out['host'] = targetUrl.host;\n return out;\n }\n\n /**\n * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。\n */\n private async readBoundedBody(\n response: Response,\n controller: AbortController,\n ): Promise<string> {\n const limit = this.options.maxResponseBytes;\n if (!response.body) return '';\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n let total = 0;\n try {\n // eslint-disable-next-line no-constant-condition\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n if (value) {\n total += value.byteLength;\n if (total > limit) {\n controller.abort();\n throw new HttpException(\n {\n code: ERROR_CODES.RESPONSE_TOO_LARGE,\n message: `response exceeds maxResponseBytes (${limit})`,\n },\n HttpStatus.BAD_GATEWAY,\n );\n }\n chunks.push(value);\n }\n }\n } finally {\n void reader.cancel().catch(() => undefined);\n }\n return Buffer.concat(chunks).toString('utf-8');\n }\n\n /**\n * 把 fetch 抛错 / 超时统一映射为 HttpException。\n *\n * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)\n * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504\n * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)\n * 4. 兜底 → UPSTREAM_UNREACHABLE 502\n */\n private mapNetworkError(err: unknown, controller: AbortController): HttpException {\n if (err instanceof HttpException) return err;\n if (controller.signal.aborted) {\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_TIMEOUT, message: 'upstream timeout' },\n HttpStatus.GATEWAY_TIMEOUT,\n );\n }\n if (err instanceof TypeError) {\n return new HttpException(\n { code: ERROR_CODES.INVALID_REQUEST, message: 'invalid request configuration' },\n HttpStatus.BAD_REQUEST,\n );\n }\n return new HttpException(\n { code: ERROR_CODES.UPSTREAM_UNREACHABLE, message: 'upstream unreachable' },\n HttpStatus.BAD_GATEWAY,\n );\n }\n}\n","import type { HttpForwarderModuleOptions } from '../types';\nimport {\n DEFAULT_MAX_RESPONSE_BYTES,\n DEFAULT_REQUEST_TIMEOUT_MS,\n} from '../const';\n\n/**\n * forRoot 入参经过默认值合并后的形态。SDK 内部统一使用此类型。\n */\nexport interface HttpForwarderOptionsResolved\n extends Required<HttpForwarderModuleOptions> {}\n\n/**\n * 校验并补默认值。配置错误(非法字段值)在模块装配时抛出,启动期 fail-fast。\n */\nexport function resolveOptions(\n options: HttpForwarderModuleOptions | undefined,\n): HttpForwarderOptionsResolved {\n const opts = options ?? {};\n const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;\n const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;\n\n if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer',\n );\n }\n if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {\n throw new Error(\n 'HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer',\n );\n }\n return { requestTimeoutMs, maxResponseBytes };\n}\n"],"mappings":"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","names":["Module","HTTP_FORWARDER_MODULE_OPTIONS","Symbol","CONTROLLER_ROUTE","DEFAULT_REQUEST_TIMEOUT_MS","DEFAULT_MAX_RESPONSE_BYTES","ALLOWED_PROTOCOLS","HEADERS_NOT_FORWARDED_TO_UPSTREAM","Set","HEADERS_NOT_RETURNED_TO_CLIENT","ERROR_CODES","INVALID_REQUEST","INVALID_TARGET_PROTOCOL","UPSTREAM_UNREACHABLE","UPSTREAM_TIMEOUT","RESPONSE_TOO_LARGE","All","Controller","HttpException","HttpStatus","Req","Res","HttpException","HttpStatus","Inject","Injectable","HttpForwarderService","options","forward","payload","targetUrl","assertValidTargetUrl","headers","buildOutgoingHeaders","controller","AbortController","timeoutTimer","setTimeout","abort","requestTimeoutMs","unref","response","fetch","toString","method","body","undefined","signal","readBoundedBody","status","Object","fromEntries","err","mapNetworkError","clearTimeout","raw","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","parsed","URL","ALLOWED_PROTOCOLS","includes","protocol","INVALID_TARGET_PROTOCOL","incoming","out","key","value","entries","lower","toLowerCase","host","limit","maxResponseBytes","reader","getReader","chunks","total","done","read","byteLength","RESPONSE_TOO_LARGE","BAD_GATEWAY","push","cancel","catch","Buffer","concat","aborted","UPSTREAM_TIMEOUT","GATEWAY_TIMEOUT","TypeError","UPSTREAM_UNREACHABLE","HttpForwarderController","svc","forward","req","res","targetUrl","query","HttpException","code","ERROR_CODES","INVALID_REQUEST","message","HttpStatus","BAD_REQUEST","out","method","toUpperCase","headers","pickIncomingHeaders","body","serializeIncomingBody","status","key","value","Object","entries","HEADERS_NOT_RETURNED_TO_CLIENT","has","toLowerCase","setHeader","send","incoming","raw","HEADERS_NOT_FORWARDED_TO_UPSTREAM","undefined","Array","isArray","join","String","Buffer","isBuffer","toString","keys","length","JSON","stringify","resolveOptions","options","opts","requestTimeoutMs","DEFAULT_REQUEST_TIMEOUT_MS","maxResponseBytes","DEFAULT_MAX_RESPONSE_BYTES","Number","isInteger","Error","HttpForwarderModule","forRoot","options","resolved","resolveOptions","module","controllers","HttpForwarderController","providers","provide","HTTP_FORWARDER_MODULE_OPTIONS","useValue","HttpForwarderService","exports"]}
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@lark-apaas/nestjs-http-forwarder",
3
+ "version": "0.1.1-alpha.0",
4
+ "description": "FullStack Nestjs server-side HTTP request forwarder (node egress, mihomo-aware)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "plugin",
14
+ "nestjs",
15
+ "server",
16
+ "http",
17
+ "forwarder",
18
+ "egress",
19
+ "typescript"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18.17.0"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "import": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "clean": "rm -rf dist",
38
+ "lint": "eslint src/**/*.ts",
39
+ "typecheck": "tsc --noEmit",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest --watch",
42
+ "prepublishOnly": "npm run build"
43
+ },
44
+ "dependencies": {
45
+ "reflect-metadata": "^0.1.13"
46
+ },
47
+ "devDependencies": {
48
+ "@nestjs/common": "^10.0.0",
49
+ "@nestjs/core": "^10.0.0",
50
+ "@nestjs/testing": "^10.0.0",
51
+ "@types/express": "^5.0.0",
52
+ "@types/node": "^20.0.0",
53
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
54
+ "@typescript-eslint/parser": "^6.0.0",
55
+ "eslint": "^8.42.0",
56
+ "ts-node": "^10.9.2",
57
+ "tsup": "^8.0.0",
58
+ "typescript": "^5.0.0",
59
+ "vitest": "^3.2.4"
60
+ },
61
+ "peerDependencies": {
62
+ "@nestjs/common": "^10.0.0",
63
+ "@nestjs/core": "^10.0.0"
64
+ }
65
+ }