mn-ws-server 0.0.0-stage → 0.2.1

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/config.js ADDED
@@ -0,0 +1,136 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export function configDir() {
5
+ return process.env.MN_WS_HOME || path.join(os.homedir(), '.mn-ws');
6
+ }
7
+ export function serverConfigFile() {
8
+ return path.join(configDir(), 'server.json');
9
+ }
10
+ export function readConfigFile() {
11
+ try {
12
+ const f = serverConfigFile();
13
+ if (!fs.existsSync(f))
14
+ return {};
15
+ return JSON.parse(fs.readFileSync(f, 'utf8'));
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ /** 真值判定:1 / true / yes / on(大小写不敏感)。 */
22
+ export function isEnvTruthy(value) {
23
+ if (value === undefined || value === null)
24
+ return false;
25
+ const s = String(value).trim().toLowerCase();
26
+ return s === '1' || s === 'true' || s === 'yes' || s === 'on';
27
+ }
28
+ /**
29
+ * 布尔开关的严格解析。
30
+ *
31
+ * 空 → undefined(**没表态**,让上层按「跟随其他配置」处理);
32
+ * 1/true/yes/on → true;0/false/no/off → false;其他值 → **直接抛错**。
33
+ *
34
+ * 为什么要严格:部署脚本里 `MN_WS_AUTH_ENABLED=1` 被当成「关闭」时,
35
+ * 服务照常启动、/health 照样绿,而整套按用户隔离的属主判据**静默不存在** ——
36
+ * CI 全绿、人以为开着。这类「配了等于没配」必须在解析层就炸掉。
37
+ */
38
+ export function parseBoolFlag(v) {
39
+ if (v === undefined || v === null || v === '')
40
+ return undefined;
41
+ if (v === true || v === false)
42
+ return v;
43
+ const s = String(v).trim().toLowerCase();
44
+ if (['1', 'true', 'yes', 'on'].includes(s))
45
+ return true;
46
+ if (['0', 'false', 'no', 'off'].includes(s))
47
+ return false;
48
+ throw new Error(`布尔开关取值无法识别:${JSON.stringify(v)}(认 1/true/yes/on 与 0/false/no/off)`);
49
+ }
50
+ /** 命令行解析:--key value / --key=value / --flag / 位置参数。 */
51
+ export function parseArgs(argv) {
52
+ const flags = {};
53
+ const positional = [];
54
+ for (let i = 0; i < argv.length; i += 1) {
55
+ const a = argv[i];
56
+ if (a === '--') {
57
+ positional.push(...argv.slice(i + 1));
58
+ break;
59
+ }
60
+ if (a.startsWith('--')) {
61
+ const eq = a.indexOf('=');
62
+ let key;
63
+ let value;
64
+ if (eq >= 0) {
65
+ key = a.slice(2, eq);
66
+ value = a.slice(eq + 1);
67
+ }
68
+ else {
69
+ key = a.slice(2);
70
+ const next = argv[i + 1];
71
+ if (next !== undefined && !next.startsWith('--')) {
72
+ value = next;
73
+ i += 1;
74
+ }
75
+ else {
76
+ value = true;
77
+ }
78
+ }
79
+ flags[camel(key)] = value;
80
+ }
81
+ else {
82
+ positional.push(a);
83
+ }
84
+ }
85
+ return { flags, positional };
86
+ }
87
+ export function camel(s) {
88
+ return s.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
89
+ }
90
+ function pick(...vals) {
91
+ for (const v of vals) {
92
+ if (v !== undefined && v !== null && v !== '')
93
+ return v;
94
+ }
95
+ return undefined;
96
+ }
97
+ /**
98
+ * 取字符串值,缺省是**空串**。
99
+ *
100
+ * 为什么不能直接 String(pick(...)):pick 全空时返回 undefined,
101
+ * 而 `String(undefined)` 是字符串 **"undefined"** —— 不是空串!
102
+ * 这里是**安全项**:token / jwtSecret 若是 "undefined",
103
+ * `config.token && gate === config.token` 就等于「任何人都能用
104
+ * `X-Mn-Ws-Token: undefined` 通过门禁」。所以缺省必须是空串。
105
+ */
106
+ export function str(v) {
107
+ return v === undefined || v === null ? '' : String(v);
108
+ }
109
+ /**
110
+ * 合并出最终配置。
111
+ *
112
+ * 环境变量前缀 MN_WS_(REMOTETYPE_ 那种大写下划线风格)。
113
+ */
114
+ export function loadServerConfig(flags = {}) {
115
+ const file = readConfigFile();
116
+ const env = process.env;
117
+ return {
118
+ host: str(pick(flags.host, env.MN_WS_HOST, file.host, '0.0.0.0')),
119
+ port: Number(pick(flags.port, env.MN_WS_PORT, file.port, 7777)),
120
+ wsPath: str(pick(flags.wsPath, env.MN_WS_WS_PATH, file.wsPath, '/ws')),
121
+ token: str(pick(flags.token, env.MN_WS_TOKEN, file.token, '')),
122
+ issuer: str(pick(flags.issuer, env.MN_WS_ISSUER, file.issuer, 'http://47.110.80.47:8082')),
123
+ // server 半自己的 OIDC 身份(只用于「我是谁」,不发起授权码流程)
124
+ idpClientId: str(pick(flags.idpClientId, env.MN_WS_IDP_CLIENT_ID, file.idpClientId, 'mn-ws-server')),
125
+ // client 半的身份:真正发起授权码流程的是它(本机 CLI 在回环上收码 + PKCE)
126
+ loginClientId: str(pick(flags.loginClientId, env.MN_WS_LOGIN_CLIENT_ID, file.loginClientId, 'mn-ws-client')),
127
+ redirectUri: str(pick(flags.redirectUri, env.MN_WS_REDIRECT_URI, file.redirectUri, '')),
128
+ jwtSecret: str(pick(flags.jwtSecret, env.MN_WS_JWT_SECRET, file.jwtSecret, '')),
129
+ authEnabled: parseBoolFlag(pick(flags.authEnabled, env.MN_WS_AUTH_ENABLED, file.authEnabled, '')),
130
+ insecure: flags.insecure === true || env.MN_WS_INSECURE === '1',
131
+ requestTimeoutMs: Number(pick(flags.requestTimeout, env.MN_WS_REQUEST_TIMEOUT, file.requestTimeoutMs, 30_000)),
132
+ logLevel: String(pick(flags.logLevel, env.MN_WS_LOG_LEVEL, file.logLevel, 'info')),
133
+ dataDir: path.join(configDir(), 'data'),
134
+ };
135
+ }
136
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * mn-ws-server 的公共面。
3
+ *
4
+ * ⚠️ client 半以 workspace 依赖引入这个包(`mn-ws-server`):
5
+ * 两端说的是同一套帧协议,协议定义**只写一份**,改一处就是改两处。
6
+ * 所以这里导出的东西要当**跨端契约**看 —— 改形状之前先想清楚两边的兼容。
7
+ *
8
+ * 协议与应用目录分开导出(`./protocol` / `./apps`),是为了让 client 半
9
+ * 只取它需要的那部分,不必把 server 的运行时一起打进浏览器包。
10
+ */
11
+ export * from './protocol.js';
12
+ export * from './apps.js';
13
+ export { Logger, LEVELS, type LogLevel } from './logger.js';
14
+ export { Store, type AuditRecord } from './store.js';
15
+ export { AuthHub, DEVICE_TOKEN_TTL_SEC, b64url, b64urlJson, fromB64url, type DeviceTokenPayload, type DeviceRecord, type PublicAuthConfig, } from './auth.js';
16
+ export { createServer, type ServerOptions } from './server.js';
17
+ export { loadServerConfig, parseArgs, parseBoolFlag, isEnvTruthy, configDir, serverConfigFile, type ServerConfig, } from './config.js';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAE,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EACL,OAAO,EACP,oBAAoB,EACpB,MAAM,EACN,UAAU,EACV,UAAU,EACV,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACtB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,WAAW,EACX,SAAS,EACT,gBAAgB,EAChB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * mn-ws-server 的公共面。
3
+ *
4
+ * ⚠️ client 半以 workspace 依赖引入这个包(`mn-ws-server`):
5
+ * 两端说的是同一套帧协议,协议定义**只写一份**,改一处就是改两处。
6
+ * 所以这里导出的东西要当**跨端契约**看 —— 改形状之前先想清楚两边的兼容。
7
+ *
8
+ * 协议与应用目录分开导出(`./protocol` / `./apps`),是为了让 client 半
9
+ * 只取它需要的那部分,不必把 server 的运行时一起打进浏览器包。
10
+ */
11
+ export * from './protocol.js';
12
+ export * from './apps.js';
13
+ export { Logger, LEVELS } from './logger.js';
14
+ export { Store } from './store.js';
15
+ export { AuthHub, DEVICE_TOKEN_TTL_SEC, b64url, b64urlJson, fromB64url, } from './auth.js';
16
+ export { createServer } from './server.js';
17
+ export { loadServerConfig, parseArgs, parseBoolFlag, isEnvTruthy, configDir, serverConfigFile, } from './config.js';
18
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,EAAiB,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,KAAK,EAAoB,MAAM,YAAY,CAAC;AACrD,OAAO,EACL,OAAO,EACP,oBAAoB,EACpB,MAAM,EACN,UAAU,EACV,UAAU,GAIX,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAC/D,OAAO,EACL,gBAAgB,EAChB,SAAS,EACT,aAAa,EACb,WAAW,EACX,SAAS,EACT,gBAAgB,GAEjB,MAAM,aAAa,CAAC"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 极简分级日志器。
3
+ *
4
+ * 与 mn-rt 的 client/lib/logger.js 同款:写文件 + 控制台,
5
+ * 不引第三方依赖(这个包要能被 npx 一把跑起来,依赖越少越好)。
6
+ */
7
+ export declare const LEVELS: Record<string, number>;
8
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
9
+ export declare class Logger {
10
+ level: number;
11
+ levelName: string;
12
+ dir: string | null;
13
+ name: string;
14
+ private stream;
15
+ constructor({ level, dir, name }?: {
16
+ level?: string;
17
+ dir?: string | null;
18
+ name?: string;
19
+ });
20
+ private _log;
21
+ debug(msg: string, extra?: unknown): void;
22
+ info(msg: string, extra?: unknown): void;
23
+ warn(msg: string, extra?: unknown): void;
24
+ error(msg: string, extra?: unknown): void;
25
+ close(): void;
26
+ }
27
+ //# sourceMappingURL=logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAgD,CAAC;AAE3F,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,qBAAa,MAAM;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,MAAM,CAAwB;gBAE1B,EAAE,KAAc,EAAE,GAAU,EAAE,IAAc,EAAE,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO;IAkBvH,OAAO,CAAC,IAAI;IAYZ,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IACzC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IACxC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IACxC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI;IAEzC,KAAK,IAAI,IAAI;CAMd"}
package/dist/logger.js ADDED
@@ -0,0 +1,73 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * 极简分级日志器。
5
+ *
6
+ * 与 mn-rt 的 client/lib/logger.js 同款:写文件 + 控制台,
7
+ * 不引第三方依赖(这个包要能被 npx 一把跑起来,依赖越少越好)。
8
+ */
9
+ export const LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
10
+ export class Logger {
11
+ level;
12
+ levelName;
13
+ dir;
14
+ name;
15
+ stream;
16
+ constructor({ level = 'info', dir = null, name = 'mn-ws' } = {}) {
17
+ this.level = LEVELS[level] ?? LEVELS.info ?? 20;
18
+ this.levelName = level;
19
+ this.dir = dir;
20
+ this.name = name;
21
+ this.stream = null;
22
+ if (dir) {
23
+ try {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ const file = path.join(dir, `${name}.log`);
26
+ this.stream = fs.createWriteStream(file, { flags: 'a' });
27
+ }
28
+ catch {
29
+ this.stream = null; // 写不了日志不是致命错误,降级成只打控制台
30
+ }
31
+ }
32
+ }
33
+ _log(level, msg, extra) {
34
+ if ((LEVELS[level] || 0) < this.level)
35
+ return;
36
+ const ts = new Date().toISOString();
37
+ const line = extra
38
+ ? `${ts} [${level.toUpperCase()}] ${msg} ${safeJson(extra)}`
39
+ : `${ts} [${level.toUpperCase()}] ${msg}`;
40
+ try {
41
+ process.stdout.write(`${line}\n`);
42
+ }
43
+ catch { /* ignore */ }
44
+ if (this.stream) {
45
+ try {
46
+ this.stream.write(`${line}\n`);
47
+ }
48
+ catch { /* ignore */ }
49
+ }
50
+ }
51
+ debug(msg, extra) { this._log('debug', msg, extra); }
52
+ info(msg, extra) { this._log('info', msg, extra); }
53
+ warn(msg, extra) { this._log('warn', msg, extra); }
54
+ error(msg, extra) { this._log('error', msg, extra); }
55
+ close() {
56
+ if (this.stream) {
57
+ try {
58
+ this.stream.end();
59
+ }
60
+ catch { /* ignore */ }
61
+ this.stream = null;
62
+ }
63
+ }
64
+ }
65
+ function safeJson(obj) {
66
+ try {
67
+ return JSON.stringify(obj);
68
+ }
69
+ catch {
70
+ return '[unserializable]';
71
+ }
72
+ }
73
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * 通道协议:mn-ws server 半 ⇄ client 半 之间的 WebSocket 帧格式。
3
+ *
4
+ * 帧是 JSON 文本。**请求类帧带 id 用于配对** —— 一条 WS 连接上同时跑多个
5
+ * HTTP 请求,响应按 id 归位(见 §多路复用)。
6
+ *
7
+ * 这套协议刻意做成**零语义**的:帧里装的就是「一次 HTTP 请求」与
8
+ * 「一次 HTTP 响应」,server 半原样转发、client 半原样还原。
9
+ * 任何业务语义(nx 的 action、鉴权规则)都不进协议层。
10
+ *
11
+ * ⚠️ 这个模块被 **client 半以 workspace 依赖引入**(`mn-ws-server` 的公共面)。
12
+ * 改这里的形状 = 同时改两端,所以改动必须先想清楚两边的兼容。
13
+ */
14
+ export declare const PROTOCOL_VERSION = 1;
15
+ /** 帧类型 */
16
+ export declare const FRAME: {
17
+ readonly WELCOME: "welcome";
18
+ readonly REGISTERED: "registered";
19
+ readonly REQUEST: "request";
20
+ readonly ABORT: "abort";
21
+ readonly PING: "ping";
22
+ readonly SHUTDOWN: "shutdown";
23
+ readonly ERROR: "error";
24
+ readonly REGISTER: "register";
25
+ readonly RESPONSE: "response";
26
+ readonly CHUNK: "chunk";
27
+ readonly PONG: "pong";
28
+ };
29
+ export type FrameType = (typeof FRAME)[keyof typeof FRAME];
30
+ /**
31
+ * 不能跨代理转发的头(hop-by-hop)。
32
+ *
33
+ * 这些头描述的是**某一次连接**的属性,转发到下一跳会出错或无意义:
34
+ * · Connection: keep-alive —— 新连接会自己协商
35
+ * · Transfer-Encoding: chunked —— body 已经被完整读成 buffer,重新算长度
36
+ * · Upgrade / Proxy-* —— 代理自己的协商,不该替下游决定
37
+ */
38
+ export declare const HOP_BY_HOP_HEADERS: ReadonlySet<string>;
39
+ /**
40
+ * 请求头里必须**剥离**的头:
41
+ * · hop-by-hop —— 描述的是某一次连接,不该跨代理
42
+ * · Origin —— nx-kv 的跨站防护只放行本机来源,透传浏览器 Origin 会 403
43
+ * · X-Mn-Ws-Token —— **mn-ws 自己的 L1 凭据**,属于桥接层内部事务,
44
+ * 泄给目标服务等于把门禁令牌交出去
45
+ */
46
+ export declare const STRIP_REQUEST_HEADERS: ReadonlySet<string>;
47
+ /**
48
+ * 响应头里不能转发的头。
49
+ *
50
+ * `content-length` 会在 client half 重新按实际 body 长度计算;
51
+ * `content-encoding` 由 client half 的 http 客户端处理,body 到手时已解码。
52
+ */
53
+ export declare const STRIP_RESPONSE_HEADERS: ReadonlySet<string>;
54
+ /** 请求体上限(字节)。与 nx-kv 的 24MB 对齐;base64 后约 32MB。 */
55
+ export declare const MAX_BODY_BYTES: number;
56
+ /** 单个 HTTP 请求在 server 半的最长等待时间(毫秒)。 */
57
+ export declare const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
58
+ /** 协议层的头:键统一小写,值统一成数组(保住 set-cookie 这类多值头)。 */
59
+ export type ProtocolHeaders = Record<string, string[]>;
60
+ /** 规范化前的头(Node 的 IncomingHttpHeaders 形状:值可能是 string | string[] | undefined)。 */
61
+ export type RawHeaders = Record<string, string | string[] | number | undefined>;
62
+ /** 帧的通用形状。具体帧再各自收窄。 */
63
+ export interface Frame {
64
+ type: string;
65
+ id?: string;
66
+ [k: string]: unknown;
67
+ }
68
+ /** 帧编码:把一个对象序列化成 WS 文本帧。 */
69
+ export declare function encodeFrame(obj: unknown): string;
70
+ /**
71
+ * 帧解码。返回 null 表示「这不是一帧合法的协议消息」——
72
+ * 调用方应当忽略并计数,而不是把解析异常抛给连接层。
73
+ */
74
+ export declare function decodeFrame(raw: unknown): Frame | null;
75
+ export declare function nextRequestId(prefix?: string): string;
76
+ /** 头对象规范化:键统一小写,值统一成数组。 */
77
+ export declare function normalizeHeaders(headers: RawHeaders | undefined | null): ProtocolHeaders;
78
+ /** 取头的第一个值(大小写不敏感)。 */
79
+ export declare function headerValue(headers: ProtocolHeaders | Record<string, unknown> | undefined | null, name: string): string | undefined;
80
+ /** 剔除不该转发的请求头。 */
81
+ export declare function stripRequestHeaders(headers: Record<string, string[] | string> | undefined | null): Record<string, string[] | string>;
82
+ /** 剔除不该转发的响应头。 */
83
+ export declare function stripResponseHeaders(headers: Record<string, string[] | string> | undefined | null): Record<string, string[] | string>;
84
+ /**
85
+ * 把协议层的头(值统一是数组)压平成 Node http 能吃的形状。
86
+ *
87
+ * ## 为什么需要这一步
88
+ *
89
+ * 协议层把头的值统一存成数组,是为了**保住 set-cookie 这类多值头**
90
+ * (一个响应里可以有几十条 Set-Cookie,用 string 就只剩最后一条)。
91
+ * 但 Node 的 `http.request({headers})` 与 `res.writeHead(status, headers)`
92
+ * 要求**除 set-cookie 外全是字符串** —— 直接把数组塞进去会抛
93
+ * `options.headers.host property must be of type string. Received an instance of Array`。
94
+ *
95
+ * @param headers 规范化后的头
96
+ * @param arrayAllowed 允许保留数组的键(响应侧用 set-cookie)
97
+ */
98
+ export declare function flattenHeaders(headers: Record<string, string[] | string> | undefined | null, arrayAllowed?: readonly string[]): Record<string, string | string[]>;
99
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,UAAU;AACV,eAAO,MAAM,KAAK;;;;;;;;;;;;CAcR,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC;AAE3D;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAUjD,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAIpD,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAGrD,CAAC;AAEH,kDAAkD;AAClD,eAAO,MAAM,cAAc,QAAmB,CAAC;AAE/C,uCAAuC;AACvC,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;AAEvD,gFAAgF;AAChF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;AAEhF,uBAAuB;AACvB,MAAM,WAAW,KAAK;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACtB;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAkBtD;AASD,wBAAgB,aAAa,CAAC,MAAM,SAAM,GAAG,MAAM,CAIlD;AAED,2BAA2B;AAC3B,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,GAAG,SAAS,GAAG,IAAI,GAAG,eAAe,CASxF;AAED,uBAAuB;AACvB,wBAAgB,WAAW,CACzB,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,EACrE,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAOpB;AAED,kBAAkB;AAClB,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,GAC5D,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,CAOnC;AAED,kBAAkB;AAClB,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,GAC5D,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,CAOnC;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,SAAS,GAAG,IAAI,EAC7D,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAgBnC"}
@@ -0,0 +1,201 @@
1
+ /**
2
+ * 通道协议:mn-ws server 半 ⇄ client 半 之间的 WebSocket 帧格式。
3
+ *
4
+ * 帧是 JSON 文本。**请求类帧带 id 用于配对** —— 一条 WS 连接上同时跑多个
5
+ * HTTP 请求,响应按 id 归位(见 §多路复用)。
6
+ *
7
+ * 这套协议刻意做成**零语义**的:帧里装的就是「一次 HTTP 请求」与
8
+ * 「一次 HTTP 响应」,server 半原样转发、client 半原样还原。
9
+ * 任何业务语义(nx 的 action、鉴权规则)都不进协议层。
10
+ *
11
+ * ⚠️ 这个模块被 **client 半以 workspace 依赖引入**(`mn-ws-server` 的公共面)。
12
+ * 改这里的形状 = 同时改两端,所以改动必须先想清楚两边的兼容。
13
+ */
14
+ export const PROTOCOL_VERSION = 1;
15
+ /** 帧类型 */
16
+ export const FRAME = {
17
+ // server → client
18
+ WELCOME: 'welcome',
19
+ REGISTERED: 'registered',
20
+ REQUEST: 'request',
21
+ ABORT: 'abort',
22
+ PING: 'ping',
23
+ SHUTDOWN: 'shutdown',
24
+ ERROR: 'error',
25
+ // client → server
26
+ REGISTER: 'register',
27
+ RESPONSE: 'response',
28
+ CHUNK: 'chunk',
29
+ PONG: 'pong',
30
+ };
31
+ /**
32
+ * 不能跨代理转发的头(hop-by-hop)。
33
+ *
34
+ * 这些头描述的是**某一次连接**的属性,转发到下一跳会出错或无意义:
35
+ * · Connection: keep-alive —— 新连接会自己协商
36
+ * · Transfer-Encoding: chunked —— body 已经被完整读成 buffer,重新算长度
37
+ * · Upgrade / Proxy-* —— 代理自己的协商,不该替下游决定
38
+ */
39
+ export const HOP_BY_HOP_HEADERS = new Set([
40
+ 'connection',
41
+ 'keep-alive',
42
+ 'proxy-authenticate',
43
+ 'proxy-authorization',
44
+ 'proxy-connection',
45
+ 'te',
46
+ 'trailer',
47
+ 'transfer-encoding',
48
+ 'upgrade',
49
+ ]);
50
+ /**
51
+ * 请求头里必须**剥离**的头:
52
+ * · hop-by-hop —— 描述的是某一次连接,不该跨代理
53
+ * · Origin —— nx-kv 的跨站防护只放行本机来源,透传浏览器 Origin 会 403
54
+ * · X-Mn-Ws-Token —— **mn-ws 自己的 L1 凭据**,属于桥接层内部事务,
55
+ * 泄给目标服务等于把门禁令牌交出去
56
+ */
57
+ export const STRIP_REQUEST_HEADERS = new Set([
58
+ ...HOP_BY_HOP_HEADERS,
59
+ 'origin',
60
+ 'x-mn-ws-token',
61
+ ]);
62
+ /**
63
+ * 响应头里不能转发的头。
64
+ *
65
+ * `content-length` 会在 client half 重新按实际 body 长度计算;
66
+ * `content-encoding` 由 client half 的 http 客户端处理,body 到手时已解码。
67
+ */
68
+ export const STRIP_RESPONSE_HEADERS = new Set([
69
+ ...HOP_BY_HOP_HEADERS,
70
+ 'content-length',
71
+ ]);
72
+ /** 请求体上限(字节)。与 nx-kv 的 24MB 对齐;base64 后约 32MB。 */
73
+ export const MAX_BODY_BYTES = 24 * 1024 * 1024;
74
+ /** 单个 HTTP 请求在 server 半的最长等待时间(毫秒)。 */
75
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
76
+ /** 帧编码:把一个对象序列化成 WS 文本帧。 */
77
+ export function encodeFrame(obj) {
78
+ return JSON.stringify(obj);
79
+ }
80
+ /**
81
+ * 帧解码。返回 null 表示「这不是一帧合法的协议消息」——
82
+ * 调用方应当忽略并计数,而不是把解析异常抛给连接层。
83
+ */
84
+ export function decodeFrame(raw) {
85
+ if (raw === null || raw === undefined)
86
+ return null;
87
+ let text;
88
+ if (typeof raw === 'string')
89
+ text = raw;
90
+ else if (Buffer.isBuffer(raw))
91
+ text = raw.toString('utf8');
92
+ else if (Array.isArray(raw))
93
+ text = Buffer.concat(raw).toString('utf8');
94
+ else
95
+ return null;
96
+ let obj;
97
+ try {
98
+ obj = JSON.parse(text);
99
+ }
100
+ catch {
101
+ return null;
102
+ }
103
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj))
104
+ return null;
105
+ const type = obj.type;
106
+ if (typeof type !== 'string')
107
+ return null;
108
+ return obj;
109
+ }
110
+ /**
111
+ * 生成一个跨连接唯一的请求 id。
112
+ *
113
+ * server 半的 pending 表是**全局**的(不按连接分表),所以 id 必须全局唯一:
114
+ * 只带连接内计数的话,两条连接上的 "1" 会撞车,响应会被配到错误的请求上。
115
+ */
116
+ let _seq = 0;
117
+ export function nextRequestId(prefix = 'r') {
118
+ _seq = (_seq + 1) % 0xffffff;
119
+ const rand = Math.random().toString(36).slice(2, 8);
120
+ return `${prefix}-${Date.now().toString(36)}-${_seq.toString(36)}-${rand}`;
121
+ }
122
+ /** 头对象规范化:键统一小写,值统一成数组。 */
123
+ export function normalizeHeaders(headers) {
124
+ const out = {};
125
+ if (!headers || typeof headers !== 'object')
126
+ return out;
127
+ for (const [k, v] of Object.entries(headers)) {
128
+ const key = String(k).toLowerCase();
129
+ if (v === undefined || v === null)
130
+ continue;
131
+ out[key] = Array.isArray(v) ? v.map((x) => String(x)) : [String(v)];
132
+ }
133
+ return out;
134
+ }
135
+ /** 取头的第一个值(大小写不敏感)。 */
136
+ export function headerValue(headers, name) {
137
+ if (!headers)
138
+ return undefined;
139
+ const key = String(name).toLowerCase();
140
+ const v = headers[key];
141
+ if (v === undefined || v === null || v === '')
142
+ return undefined;
143
+ if (Array.isArray(v))
144
+ return v.length ? String(v[0]) : undefined;
145
+ return String(v);
146
+ }
147
+ /** 剔除不该转发的请求头。 */
148
+ export function stripRequestHeaders(headers) {
149
+ const out = {};
150
+ for (const [k, v] of Object.entries(headers || {})) {
151
+ if (STRIP_REQUEST_HEADERS.has(k))
152
+ continue;
153
+ out[k] = v;
154
+ }
155
+ return out;
156
+ }
157
+ /** 剔除不该转发的响应头。 */
158
+ export function stripResponseHeaders(headers) {
159
+ const out = {};
160
+ for (const [k, v] of Object.entries(headers || {})) {
161
+ if (STRIP_RESPONSE_HEADERS.has(k))
162
+ continue;
163
+ out[k] = v;
164
+ }
165
+ return out;
166
+ }
167
+ /**
168
+ * 把协议层的头(值统一是数组)压平成 Node http 能吃的形状。
169
+ *
170
+ * ## 为什么需要这一步
171
+ *
172
+ * 协议层把头的值统一存成数组,是为了**保住 set-cookie 这类多值头**
173
+ * (一个响应里可以有几十条 Set-Cookie,用 string 就只剩最后一条)。
174
+ * 但 Node 的 `http.request({headers})` 与 `res.writeHead(status, headers)`
175
+ * 要求**除 set-cookie 外全是字符串** —— 直接把数组塞进去会抛
176
+ * `options.headers.host property must be of type string. Received an instance of Array`。
177
+ *
178
+ * @param headers 规范化后的头
179
+ * @param arrayAllowed 允许保留数组的键(响应侧用 set-cookie)
180
+ */
181
+ export function flattenHeaders(headers, arrayAllowed = []) {
182
+ const out = {};
183
+ for (const [k, v] of Object.entries(headers || {})) {
184
+ if (v === undefined || v === null)
185
+ continue;
186
+ if (Array.isArray(v)) {
187
+ if (arrayAllowed.includes(k)) {
188
+ if (v.length)
189
+ out[k] = v.map((x) => String(x));
190
+ continue;
191
+ }
192
+ if (!v.length)
193
+ continue;
194
+ out[k] = String(v[0]);
195
+ continue;
196
+ }
197
+ out[k] = String(v);
198
+ }
199
+ return out;
200
+ }
201
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}