@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/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @lark-apaas/nestjs-http-forwarder
2
+
3
+ NestJS 模块:通用内网转发能力。**自动注册 controller**,把请求的出口从浏览器变成 node。
4
+
5
+ > ⚠️ SDK 不感知代理 / 鉴权 / 隧道。实际客户内网访问的代理 / JWT / 隧道路由由部署环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)处理。
6
+
7
+ > ⚠️ SDK 不内置鉴权 guard。消费方在 AppModule 层面给路由加自己的 guard(如全局 NeedLoginGuard)。
8
+
9
+ ## 自动注册的接口
10
+
11
+ ```
12
+ ALL /anycross/forward?targetUrl=<内网 URL>
13
+ ```
14
+
15
+ - **method**:取自请求自身(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS)
16
+ - **targetUrl**:query 参数
17
+ - **headers**:自动透传(剔除 host / content-length / hop-by-hop)
18
+ - **body**:自动透传
19
+ - **响应**:上游 status / headers / body 直接投射到 HTTP response
20
+
21
+ 路径前缀可由 `routePrefix` 配置(默认 `anycross`)。
22
+
23
+ ## 使用
24
+
25
+ ### 1. 注册模块
26
+
27
+ ```ts
28
+ import { Module } from '@nestjs/common';
29
+ import { HttpForwarderModule } from '@lark-apaas/nestjs-http-forwarder';
30
+
31
+ @Module({
32
+ imports: [
33
+ HttpForwarderModule.forRoot({
34
+ // 全部可选
35
+ requestTimeoutMs: 30_000,
36
+ maxResponseBytes: 10 * 1024 * 1024,
37
+ routePrefix: 'anycross',
38
+ }),
39
+ ],
40
+ })
41
+ export class AppModule {}
42
+ ```
43
+
44
+ ### 2. 前端调用(搭配 `axiosForBackend`)
45
+
46
+ ```ts
47
+ import { axiosForBackend } from '@lark-apaas/client-toolkit';
48
+
49
+ // GET 内网接口
50
+ const res = await axiosForBackend.get('/anycross/forward', {
51
+ params: { targetUrl: 'http://api.corp.com/v1/users' },
52
+ });
53
+
54
+ // POST 内网接口
55
+ await axiosForBackend.post('/anycross/forward', { qty: 3 }, {
56
+ params: { targetUrl: 'http://api.corp.com/v1/orders' },
57
+ });
58
+
59
+ // 其他 method 同理
60
+ await axiosForBackend.delete('/anycross/forward', {
61
+ params: { targetUrl: 'http://api.corp.com/v1/orders/42' },
62
+ });
63
+ ```
64
+
65
+ 前端拿到的就是真实上游响应:`response.status`、`response.headers`、`response.data` 跟直连内网一样。
66
+
67
+ ### 3. 自定义编排(高级用法)
68
+
69
+ 如果默认 controller 行为不够灵活,可以直接注入 service 写自己的 controller:
70
+
71
+ ```ts
72
+ import { Body, Controller, Post } from '@nestjs/common';
73
+ import {
74
+ HttpForwarderService,
75
+ type ForwardRequestDto,
76
+ } from '@lark-apaas/nestjs-http-forwarder';
77
+
78
+ @Controller('my/custom')
79
+ export class MyController {
80
+ constructor(private readonly forwarder: HttpForwarderService) {}
81
+
82
+ @Post()
83
+ async forward(@Body() body: ForwardRequestDto) {
84
+ return this.forwarder.forward(body);
85
+ }
86
+ }
87
+ ```
88
+
89
+ ## ForwardRequestDto / ForwardResponseDto
90
+
91
+ ```ts
92
+ interface ForwardRequestDto {
93
+ method: string; // 由 fetch 自身校验
94
+ targetUrl: string; // 必须 http:// 或 https://
95
+ headers?: Record<string, string>;
96
+ body?: string | null;
97
+ }
98
+
99
+ interface ForwardResponseDto {
100
+ status: number;
101
+ headers: Record<string, string>;
102
+ body: string;
103
+ }
104
+ ```
105
+
106
+ ## 配置参考
107
+
108
+ | 字段 | 必填 | 默认 | 说明 |
109
+ |------|------|------|------|
110
+ | `requestTimeoutMs` | — | `30000` | 单次请求超时(AbortController) |
111
+ | `maxResponseBytes` | — | `10485760` (10 MB) | 上游响应体最大字节数;超阈值返回 502 RESPONSE_TOO_LARGE |
112
+ | `routePrefix` | — | `anycross` | controller 路径前缀,最终路径 = `{routePrefix}/forward` |
113
+
114
+ ## service 做的 5 件核心事
115
+
116
+ | # | 能力 | 说明 |
117
+ |---|------|------|
118
+ | 1 | 协议白名单 | targetUrl 仅允许 `http://` / `https://`(防 SSRF) |
119
+ | 2 | host / content-length 接管 | host 重写为 targetUrl.host;content-length 让 fetch 自算 |
120
+ | 3 | 超时控制 | AbortController + setTimeout 默认 30s |
121
+ | 4 | 响应体大小上限 | 流式累计字节,超阈值 abort + 抛 502 RESPONSE_TOO_LARGE(防 OOM) |
122
+ | 5 | 错误码映射 | fetch 抛错按 signal.aborted / TypeError 等结构化信号映射为稳定 code |
123
+
124
+ method 白名单 / body 类型校验 / payload 非空校验等都**没做**——交给 fetch 自身(fetch 对配置错统一抛 TypeError,由 SDK 映射为 400 INVALID_REQUEST)。
125
+
126
+ ## 错误码
127
+
128
+ | HTTP | code | 含义 |
129
+ |------|------|------|
130
+ | 400 | `INVALID_REQUEST` | targetUrl 缺失/非法 URL / fetch 抛 TypeError(method/headers 配置错) |
131
+ | 400 | `INVALID_TARGET_PROTOCOL` | targetUrl 协议非 http/https |
132
+ | 502 | `UPSTREAM_UNREACHABLE` | 上游不可达 / 网络错 |
133
+ | 502 | `RESPONSE_TOO_LARGE` | 上游响应体超 `maxResponseBytes` |
134
+ | 504 | `UPSTREAM_TIMEOUT` | 单次请求超时(基于 `controller.signal.aborted`) |
135
+
136
+ 上游 4xx / 5xx 不抛错,原样投射到 HTTP response。
137
+
138
+ ## 限制(v0.1.0)
139
+
140
+ - 仅支持 `string` / `null` 请求体(不支持 binary / FormData / 流)
141
+ - 响应体统一 `Buffer.concat(chunks).toString('utf-8')`(不支持流式)
142
+ - 不做请求重试(透传语义)
143
+ - 默认 controller 路径固定 `{prefix}/forward`,路径自定义请走自定义 controller
package/dist/index.cjs ADDED
@@ -0,0 +1,411 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ ALLOWED_PROTOCOLS: () => ALLOWED_PROTOCOLS,
25
+ CONTROLLER_ROUTE: () => CONTROLLER_ROUTE,
26
+ DEFAULT_MAX_RESPONSE_BYTES: () => DEFAULT_MAX_RESPONSE_BYTES,
27
+ DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
28
+ ERROR_CODES: () => ERROR_CODES,
29
+ HEADERS_NOT_FORWARDED_TO_UPSTREAM: () => HEADERS_NOT_FORWARDED_TO_UPSTREAM,
30
+ HEADERS_NOT_RETURNED_TO_CLIENT: () => HEADERS_NOT_RETURNED_TO_CLIENT,
31
+ HTTP_FORWARDER_MODULE_OPTIONS: () => HTTP_FORWARDER_MODULE_OPTIONS,
32
+ HttpForwarderController: () => HttpForwarderController,
33
+ HttpForwarderModule: () => HttpForwarderModule,
34
+ HttpForwarderService: () => HttpForwarderService,
35
+ resolveOptions: () => resolveOptions
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/http-forwarder.module.ts
40
+ var import_common3 = require("@nestjs/common");
41
+
42
+ // src/const.ts
43
+ var HTTP_FORWARDER_MODULE_OPTIONS = /* @__PURE__ */ Symbol("HTTP_FORWARDER_MODULE_OPTIONS");
44
+ var CONTROLLER_ROUTE = "anycross/forward";
45
+ var DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
46
+ var DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;
47
+ var ALLOWED_PROTOCOLS = [
48
+ "http:",
49
+ "https:"
50
+ ];
51
+ var HEADERS_NOT_FORWARDED_TO_UPSTREAM = /* @__PURE__ */ new Set([
52
+ "connection",
53
+ "keep-alive",
54
+ "transfer-encoding",
55
+ "upgrade",
56
+ "proxy-connection",
57
+ "proxy-authenticate",
58
+ "proxy-authorization",
59
+ "te",
60
+ "trailer",
61
+ "accept-encoding"
62
+ ]);
63
+ var HEADERS_NOT_RETURNED_TO_CLIENT = /* @__PURE__ */ new Set([
64
+ "content-encoding",
65
+ "content-length",
66
+ "transfer-encoding",
67
+ "connection",
68
+ "keep-alive",
69
+ "upgrade"
70
+ ]);
71
+ var ERROR_CODES = {
72
+ INVALID_REQUEST: "INVALID_REQUEST",
73
+ INVALID_TARGET_PROTOCOL: "INVALID_TARGET_PROTOCOL",
74
+ UPSTREAM_UNREACHABLE: "UPSTREAM_UNREACHABLE",
75
+ UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
76
+ RESPONSE_TOO_LARGE: "RESPONSE_TOO_LARGE"
77
+ };
78
+
79
+ // src/controllers/http-forwarder.controller.ts
80
+ var import_common2 = require("@nestjs/common");
81
+
82
+ // src/services/http-forwarder.service.ts
83
+ var import_common = require("@nestjs/common");
84
+ function _ts_decorate(decorators, target, key, desc) {
85
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
86
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
87
+ 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;
88
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
89
+ }
90
+ __name(_ts_decorate, "_ts_decorate");
91
+ function _ts_metadata(k, v) {
92
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
93
+ }
94
+ __name(_ts_metadata, "_ts_metadata");
95
+ function _ts_param(paramIndex, decorator) {
96
+ return function(target, key) {
97
+ decorator(target, key, paramIndex);
98
+ };
99
+ }
100
+ __name(_ts_param, "_ts_param");
101
+ var HttpForwarderService = class {
102
+ static {
103
+ __name(this, "HttpForwarderService");
104
+ }
105
+ options;
106
+ constructor(options) {
107
+ this.options = options;
108
+ }
109
+ async forward(payload) {
110
+ const targetUrl = this.assertValidTargetUrl(payload?.targetUrl);
111
+ const headers = this.buildOutgoingHeaders(payload.headers, targetUrl);
112
+ const controller = new AbortController();
113
+ const timeoutTimer = setTimeout(() => controller.abort(), this.options.requestTimeoutMs);
114
+ if (typeof timeoutTimer.unref === "function") timeoutTimer.unref();
115
+ try {
116
+ const response = await fetch(targetUrl.toString(), {
117
+ method: payload.method,
118
+ headers,
119
+ body: payload.body ?? void 0,
120
+ signal: controller.signal
121
+ });
122
+ const body = await this.readBoundedBody(response, controller);
123
+ return {
124
+ status: response.status,
125
+ headers: Object.fromEntries(response.headers),
126
+ body
127
+ };
128
+ } catch (err) {
129
+ throw this.mapNetworkError(err, controller);
130
+ } finally {
131
+ clearTimeout(timeoutTimer);
132
+ }
133
+ }
134
+ /**
135
+ * URL 解析 + 协议白名单。其他入参校验交给 fetch 自身。
136
+ */
137
+ assertValidTargetUrl(raw) {
138
+ if (typeof raw !== "string" || !raw) {
139
+ throw new import_common.HttpException({
140
+ code: ERROR_CODES.INVALID_REQUEST,
141
+ message: "targetUrl is required"
142
+ }, import_common.HttpStatus.BAD_REQUEST);
143
+ }
144
+ let parsed;
145
+ try {
146
+ parsed = new URL(raw);
147
+ } catch {
148
+ throw new import_common.HttpException({
149
+ code: ERROR_CODES.INVALID_REQUEST,
150
+ message: "Invalid targetUrl"
151
+ }, import_common.HttpStatus.BAD_REQUEST);
152
+ }
153
+ if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
154
+ throw new import_common.HttpException({
155
+ code: ERROR_CODES.INVALID_TARGET_PROTOCOL,
156
+ message: `Unsupported protocol: ${parsed.protocol}`
157
+ }, import_common.HttpStatus.BAD_REQUEST);
158
+ }
159
+ return parsed;
160
+ }
161
+ /**
162
+ * host / content-length 的**单点处理点**:
163
+ * - host:强制覆盖为 targetUrl.host(防上游误识别,无论调用方传什么)
164
+ * - content-length:剥离(fetch 会按真实 body 自算)
165
+ *
166
+ * 其他 hop-by-hop / accept-encoding 等由 controller 已剥离,service 信任入参。
167
+ */
168
+ buildOutgoingHeaders(incoming, targetUrl) {
169
+ const out = {};
170
+ if (incoming) {
171
+ for (const [key, value] of Object.entries(incoming)) {
172
+ const lower = key.toLowerCase();
173
+ if (lower === "host" || lower === "content-length") continue;
174
+ out[key] = value;
175
+ }
176
+ }
177
+ out["host"] = targetUrl.host;
178
+ return out;
179
+ }
180
+ /**
181
+ * 流式读取响应体并累计字节数,超 `maxResponseBytes` 即 abort + 抛 502。
182
+ */
183
+ async readBoundedBody(response, controller) {
184
+ const limit = this.options.maxResponseBytes;
185
+ if (!response.body) return "";
186
+ const reader = response.body.getReader();
187
+ const chunks = [];
188
+ let total = 0;
189
+ try {
190
+ while (true) {
191
+ const { done, value } = await reader.read();
192
+ if (done) break;
193
+ if (value) {
194
+ total += value.byteLength;
195
+ if (total > limit) {
196
+ controller.abort();
197
+ throw new import_common.HttpException({
198
+ code: ERROR_CODES.RESPONSE_TOO_LARGE,
199
+ message: `response exceeds maxResponseBytes (${limit})`
200
+ }, import_common.HttpStatus.BAD_GATEWAY);
201
+ }
202
+ chunks.push(value);
203
+ }
204
+ }
205
+ } finally {
206
+ void reader.cancel().catch(() => void 0);
207
+ }
208
+ return Buffer.concat(chunks).toString("utf-8");
209
+ }
210
+ /**
211
+ * 把 fetch 抛错 / 超时统一映射为 HttpException。
212
+ *
213
+ * 1. HttpException → 直接透传(含 INVALID_TARGET_PROTOCOL / RESPONSE_TOO_LARGE)
214
+ * 2. controller.signal.aborted → UPSTREAM_TIMEOUT 504
215
+ * 3. TypeError → INVALID_REQUEST 400(fetch 对配置错统一抛 TypeError)
216
+ * 4. 兜底 → UPSTREAM_UNREACHABLE 502
217
+ */
218
+ mapNetworkError(err, controller) {
219
+ if (err instanceof import_common.HttpException) return err;
220
+ if (controller.signal.aborted) {
221
+ return new import_common.HttpException({
222
+ code: ERROR_CODES.UPSTREAM_TIMEOUT,
223
+ message: "upstream timeout"
224
+ }, import_common.HttpStatus.GATEWAY_TIMEOUT);
225
+ }
226
+ if (err instanceof TypeError) {
227
+ return new import_common.HttpException({
228
+ code: ERROR_CODES.INVALID_REQUEST,
229
+ message: "invalid request configuration"
230
+ }, import_common.HttpStatus.BAD_REQUEST);
231
+ }
232
+ return new import_common.HttpException({
233
+ code: ERROR_CODES.UPSTREAM_UNREACHABLE,
234
+ message: "upstream unreachable"
235
+ }, import_common.HttpStatus.BAD_GATEWAY);
236
+ }
237
+ };
238
+ HttpForwarderService = _ts_decorate([
239
+ (0, import_common.Injectable)(),
240
+ _ts_param(0, (0, import_common.Inject)(HTTP_FORWARDER_MODULE_OPTIONS)),
241
+ _ts_metadata("design:type", Function),
242
+ _ts_metadata("design:paramtypes", [
243
+ typeof HttpForwarderOptionsResolved === "undefined" ? Object : HttpForwarderOptionsResolved
244
+ ])
245
+ ], HttpForwarderService);
246
+
247
+ // src/controllers/http-forwarder.controller.ts
248
+ function _ts_decorate2(decorators, target, key, desc) {
249
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
250
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
251
+ 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;
252
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
253
+ }
254
+ __name(_ts_decorate2, "_ts_decorate");
255
+ function _ts_metadata2(k, v) {
256
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
257
+ }
258
+ __name(_ts_metadata2, "_ts_metadata");
259
+ function _ts_param2(paramIndex, decorator) {
260
+ return function(target, key) {
261
+ decorator(target, key, paramIndex);
262
+ };
263
+ }
264
+ __name(_ts_param2, "_ts_param");
265
+ var HttpForwarderController = class _HttpForwarderController {
266
+ static {
267
+ __name(this, "HttpForwarderController");
268
+ }
269
+ svc;
270
+ constructor(svc) {
271
+ this.svc = svc;
272
+ }
273
+ async forward(req, res) {
274
+ const targetUrl = req.query?.targetUrl;
275
+ if (typeof targetUrl !== "string" || !targetUrl) {
276
+ throw new import_common2.HttpException({
277
+ code: ERROR_CODES.INVALID_REQUEST,
278
+ message: "query parameter `targetUrl` is required"
279
+ }, import_common2.HttpStatus.BAD_REQUEST);
280
+ }
281
+ const out = await this.svc.forward({
282
+ method: req.method.toUpperCase(),
283
+ targetUrl,
284
+ headers: _HttpForwarderController.pickIncomingHeaders(req.headers),
285
+ body: _HttpForwarderController.serializeIncomingBody(req)
286
+ });
287
+ res.status(out.status);
288
+ for (const [key, value] of Object.entries(out.headers)) {
289
+ if (HEADERS_NOT_RETURNED_TO_CLIENT.has(key.toLowerCase())) continue;
290
+ res.setHeader(key, value);
291
+ }
292
+ res.send(out.body);
293
+ }
294
+ /**
295
+ * 从入站请求 headers 选出可透传给上游的字段。剔除 hop-by-hop / accept-encoding。
296
+ * 保留 cookie / authorization / x-* 等业务头。
297
+ * host / content-length 不在此处剥离——由 service.buildOutgoingHeaders 单点处理。
298
+ */
299
+ static pickIncomingHeaders(incoming) {
300
+ const out = {};
301
+ for (const [key, raw] of Object.entries(incoming)) {
302
+ if (HEADERS_NOT_FORWARDED_TO_UPSTREAM.has(key.toLowerCase())) continue;
303
+ if (raw === void 0) continue;
304
+ out[key] = Array.isArray(raw) ? raw.join(", ") : String(raw);
305
+ }
306
+ return out;
307
+ }
308
+ /**
309
+ * 把 express 已解析的 body 序列化回字符串供 service 透传。
310
+ */
311
+ static serializeIncomingBody(req) {
312
+ const method = req.method.toUpperCase();
313
+ if (method === "GET" || method === "HEAD") return null;
314
+ const body = req.body;
315
+ if (body === void 0 || body === null) return null;
316
+ if (typeof body === "string") return body;
317
+ if (Buffer.isBuffer(body)) return body.toString("utf-8");
318
+ if (typeof body === "object" && Object.keys(body).length === 0) {
319
+ return null;
320
+ }
321
+ return JSON.stringify(body);
322
+ }
323
+ };
324
+ _ts_decorate2([
325
+ (0, import_common2.All)(CONTROLLER_ROUTE),
326
+ _ts_param2(0, (0, import_common2.Req)()),
327
+ _ts_param2(1, (0, import_common2.Res)()),
328
+ _ts_metadata2("design:type", Function),
329
+ _ts_metadata2("design:paramtypes", [
330
+ typeof Request === "undefined" ? Object : Request,
331
+ typeof Response === "undefined" ? Object : Response
332
+ ]),
333
+ _ts_metadata2("design:returntype", Promise)
334
+ ], HttpForwarderController.prototype, "forward", null);
335
+ HttpForwarderController = _ts_decorate2([
336
+ (0, import_common2.Controller)(),
337
+ _ts_metadata2("design:type", Function),
338
+ _ts_metadata2("design:paramtypes", [
339
+ typeof HttpForwarderService === "undefined" ? Object : HttpForwarderService
340
+ ])
341
+ ], HttpForwarderController);
342
+
343
+ // src/services/options.resolved.ts
344
+ function resolveOptions(options) {
345
+ const opts = options ?? {};
346
+ const requestTimeoutMs = opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
347
+ const maxResponseBytes = opts.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
348
+ if (!Number.isInteger(requestTimeoutMs) || requestTimeoutMs <= 0) {
349
+ throw new Error("HttpForwarderModule.forRoot: `requestTimeoutMs` must be a positive integer");
350
+ }
351
+ if (!Number.isInteger(maxResponseBytes) || maxResponseBytes <= 0) {
352
+ throw new Error("HttpForwarderModule.forRoot: `maxResponseBytes` must be a positive integer");
353
+ }
354
+ return {
355
+ requestTimeoutMs,
356
+ maxResponseBytes
357
+ };
358
+ }
359
+ __name(resolveOptions, "resolveOptions");
360
+
361
+ // src/http-forwarder.module.ts
362
+ function _ts_decorate3(decorators, target, key, desc) {
363
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
364
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
365
+ 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;
366
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
367
+ }
368
+ __name(_ts_decorate3, "_ts_decorate");
369
+ var HttpForwarderModule = class _HttpForwarderModule {
370
+ static {
371
+ __name(this, "HttpForwarderModule");
372
+ }
373
+ static forRoot(options) {
374
+ const resolved = resolveOptions(options);
375
+ return {
376
+ module: _HttpForwarderModule,
377
+ controllers: [
378
+ HttpForwarderController
379
+ ],
380
+ providers: [
381
+ {
382
+ provide: HTTP_FORWARDER_MODULE_OPTIONS,
383
+ useValue: resolved
384
+ },
385
+ HttpForwarderService
386
+ ],
387
+ exports: [
388
+ HttpForwarderService
389
+ ]
390
+ };
391
+ }
392
+ };
393
+ HttpForwarderModule = _ts_decorate3([
394
+ (0, import_common3.Module)({})
395
+ ], HttpForwarderModule);
396
+ // Annotate the CommonJS export names for ESM import in node:
397
+ 0 && (module.exports = {
398
+ ALLOWED_PROTOCOLS,
399
+ CONTROLLER_ROUTE,
400
+ DEFAULT_MAX_RESPONSE_BYTES,
401
+ DEFAULT_REQUEST_TIMEOUT_MS,
402
+ ERROR_CODES,
403
+ HEADERS_NOT_FORWARDED_TO_UPSTREAM,
404
+ HEADERS_NOT_RETURNED_TO_CLIENT,
405
+ HTTP_FORWARDER_MODULE_OPTIONS,
406
+ HttpForwarderController,
407
+ HttpForwarderModule,
408
+ HttpForwarderService,
409
+ resolveOptions
410
+ });
411
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../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":["/**\n * @lark-apaas/nestjs-http-forwarder\n *\n * NestJS 模块:通用内网转发能力。自动注册 controller `/anycross/forward`\n * 接收前端请求并通过 node fetch 透传到目标 URL;同时导出 `HttpForwarderService`\n * 供需要自定义 controller 的场景使用。\n *\n * 用途:把请求的出口从浏览器变成 node。客户内网访问的代理 / 鉴权 / 隧道由部署\n * 环境(如沙箱内的 mihomo 透明代理 + iptables 拦截)负责,SDK 不感知。\n */\n\n// 主模块\nexport { HttpForwarderModule } from './http-forwarder.module';\n\n// 控制器\nexport * from './controllers';\n\n// 服务\nexport * from './services';\n\n// 类型定义\nexport * from './types';\n\n// 常量\nexport {\n HTTP_FORWARDER_MODULE_OPTIONS,\n CONTROLLER_ROUTE,\n DEFAULT_REQUEST_TIMEOUT_MS,\n DEFAULT_MAX_RESPONSE_BYTES,\n ALLOWED_PROTOCOLS,\n HEADERS_NOT_FORWARDED_TO_UPSTREAM,\n HEADERS_NOT_RETURNED_TO_CLIENT,\n ERROR_CODES,\n} from './const';\n","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":["import_common","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","import_common","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"]}