mn-ws 0.1.0 → 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.
Files changed (178) hide show
  1. package/assets/groups.json +10 -0
  2. package/assets/mn-ws/SKILL.md +55 -0
  3. package/assets/mn-ws/hooks.json +14 -0
  4. package/assets/mn-ws/install/inject-root.mjs +26 -0
  5. package/bin/ws.mjs +13 -0
  6. package/dist/core/auth.d.ts +194 -0
  7. package/dist/core/auth.d.ts.map +1 -0
  8. package/dist/core/auth.js +331 -0
  9. package/dist/core/auth.js.map +1 -0
  10. package/dist/core/brand.d.ts +11 -0
  11. package/dist/core/brand.d.ts.map +1 -0
  12. package/dist/core/brand.js +7 -0
  13. package/dist/core/brand.js.map +1 -0
  14. package/dist/core/bridge.d.ts +106 -0
  15. package/dist/core/bridge.d.ts.map +1 -0
  16. package/dist/core/bridge.js +308 -0
  17. package/dist/core/bridge.js.map +1 -0
  18. package/dist/core/config.d.ts +174 -0
  19. package/dist/core/config.d.ts.map +1 -0
  20. package/dist/core/config.js +417 -0
  21. package/dist/core/config.js.map +1 -0
  22. package/dist/core/controller.d.ts +64 -0
  23. package/dist/core/controller.d.ts.map +1 -0
  24. package/dist/core/controller.js +100 -0
  25. package/dist/core/controller.js.map +1 -0
  26. package/dist/core/errors/index.d.ts +30 -0
  27. package/dist/core/errors/index.d.ts.map +1 -0
  28. package/dist/core/errors/index.js +88 -0
  29. package/dist/core/errors/index.js.map +1 -0
  30. package/dist/core/open.d.ts +3 -0
  31. package/dist/core/open.d.ts.map +1 -0
  32. package/dist/core/open.js +27 -0
  33. package/dist/core/open.js.map +1 -0
  34. package/dist/core/paths.d.ts +15 -0
  35. package/dist/core/paths.d.ts.map +1 -0
  36. package/dist/core/paths.js +50 -0
  37. package/dist/core/paths.js.map +1 -0
  38. package/dist/core/serve-port.d.ts +73 -0
  39. package/dist/core/serve-port.d.ts.map +1 -0
  40. package/dist/core/serve-port.js +134 -0
  41. package/dist/core/serve-port.js.map +1 -0
  42. package/dist/core/session.d.ts +78 -0
  43. package/dist/core/session.d.ts.map +1 -0
  44. package/dist/core/session.js +124 -0
  45. package/dist/core/session.js.map +1 -0
  46. package/dist/core/skills/bus.d.ts +29 -0
  47. package/dist/core/skills/bus.d.ts.map +1 -0
  48. package/dist/core/skills/bus.js +114 -0
  49. package/dist/core/skills/bus.js.map +1 -0
  50. package/dist/core/skills/get.d.ts +17 -0
  51. package/dist/core/skills/get.d.ts.map +1 -0
  52. package/dist/core/skills/get.js +87 -0
  53. package/dist/core/skills/get.js.map +1 -0
  54. package/dist/core/skills/handlers.d.ts +22 -0
  55. package/dist/core/skills/handlers.d.ts.map +1 -0
  56. package/dist/core/skills/handlers.js +86 -0
  57. package/dist/core/skills/handlers.js.map +1 -0
  58. package/dist/core/skills/index.d.ts +47 -0
  59. package/dist/core/skills/index.d.ts.map +1 -0
  60. package/dist/core/skills/index.js +268 -0
  61. package/dist/core/skills/index.js.map +1 -0
  62. package/dist/core/skills/install.d.ts +18 -0
  63. package/dist/core/skills/install.d.ts.map +1 -0
  64. package/dist/core/skills/install.js +123 -0
  65. package/dist/core/skills/install.js.map +1 -0
  66. package/dist/core/skills/text-util.d.ts +2 -0
  67. package/dist/core/skills/text-util.d.ts.map +1 -0
  68. package/dist/core/skills/text-util.js +14 -0
  69. package/dist/core/skills/text-util.js.map +1 -0
  70. package/dist/core/store.d.ts +29 -0
  71. package/dist/core/store.d.ts.map +1 -0
  72. package/dist/core/store.js +100 -0
  73. package/dist/core/store.js.map +1 -0
  74. package/dist/core/views.d.ts +11 -0
  75. package/dist/core/views.d.ts.map +1 -0
  76. package/dist/core/views.js +20 -0
  77. package/dist/core/views.js.map +1 -0
  78. package/dist/index.d.ts +6 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +8 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/modules/auth/index.d.ts +73 -0
  83. package/dist/modules/auth/index.d.ts.map +1 -0
  84. package/dist/modules/auth/index.js +64 -0
  85. package/dist/modules/auth/index.js.map +1 -0
  86. package/dist/modules/auth/service.d.ts +98 -0
  87. package/dist/modules/auth/service.d.ts.map +1 -0
  88. package/dist/modules/auth/service.js +200 -0
  89. package/dist/modules/auth/service.js.map +1 -0
  90. package/dist/modules/bridge/index.d.ts +73 -0
  91. package/dist/modules/bridge/index.d.ts.map +1 -0
  92. package/dist/modules/bridge/index.js +89 -0
  93. package/dist/modules/bridge/index.js.map +1 -0
  94. package/dist/modules/bridge/service.d.ts +75 -0
  95. package/dist/modules/bridge/service.d.ts.map +1 -0
  96. package/dist/modules/bridge/service.js +134 -0
  97. package/dist/modules/bridge/service.js.map +1 -0
  98. package/dist/modules/home/index.d.ts +95 -0
  99. package/dist/modules/home/index.d.ts.map +1 -0
  100. package/dist/modules/home/index.js +123 -0
  101. package/dist/modules/home/index.js.map +1 -0
  102. package/dist/modules/home/service.d.ts +48 -0
  103. package/dist/modules/home/service.d.ts.map +1 -0
  104. package/dist/modules/home/service.js +195 -0
  105. package/dist/modules/home/service.js.map +1 -0
  106. package/dist/modules/skill/index.d.ts +78 -0
  107. package/dist/modules/skill/index.d.ts.map +1 -0
  108. package/dist/modules/skill/index.js +132 -0
  109. package/dist/modules/skill/index.js.map +1 -0
  110. package/dist/runtime/api.d.ts +24 -0
  111. package/dist/runtime/api.d.ts.map +1 -0
  112. package/dist/runtime/api.js +140 -0
  113. package/dist/runtime/api.js.map +1 -0
  114. package/dist/runtime/cli.d.ts +23 -0
  115. package/dist/runtime/cli.d.ts.map +1 -0
  116. package/dist/runtime/cli.js +410 -0
  117. package/dist/runtime/cli.js.map +1 -0
  118. package/dist/runtime/help.d.ts +29 -0
  119. package/dist/runtime/help.d.ts.map +1 -0
  120. package/dist/runtime/help.js +89 -0
  121. package/dist/runtime/help.js.map +1 -0
  122. package/dist/runtime/registry.d.ts +318 -0
  123. package/dist/runtime/registry.d.ts.map +1 -0
  124. package/dist/runtime/registry.js +75 -0
  125. package/dist/runtime/registry.js.map +1 -0
  126. package/dist/runtime/server.d.ts +10 -0
  127. package/dist/runtime/server.d.ts.map +1 -0
  128. package/dist/runtime/server.js +127 -0
  129. package/dist/runtime/server.js.map +1 -0
  130. package/dist/runtime/spec.d.ts +32 -0
  131. package/dist/runtime/spec.d.ts.map +1 -0
  132. package/dist/runtime/spec.js +180 -0
  133. package/dist/runtime/spec.js.map +1 -0
  134. package/dist/runtime/types/action.d.ts +101 -0
  135. package/dist/runtime/types/action.d.ts.map +1 -0
  136. package/dist/runtime/types/action.js +19 -0
  137. package/dist/runtime/types/action.js.map +1 -0
  138. package/dist/runtime/types/client.d.ts +39 -0
  139. package/dist/runtime/types/client.d.ts.map +1 -0
  140. package/dist/runtime/types/client.js +2 -0
  141. package/dist/runtime/types/client.js.map +1 -0
  142. package/dist/runtime/types/http.d.ts +22 -0
  143. package/dist/runtime/types/http.d.ts.map +1 -0
  144. package/dist/runtime/types/http.js +6 -0
  145. package/dist/runtime/types/http.js.map +1 -0
  146. package/dist/runtime/types/module.d.ts +72 -0
  147. package/dist/runtime/types/module.d.ts.map +1 -0
  148. package/dist/runtime/types/module.js +15 -0
  149. package/dist/runtime/types/module.js.map +1 -0
  150. package/dist/web/public/assets/CliHints-BOxU-Duc.js +1 -0
  151. package/dist/web/public/assets/index-BTRKdMIW.js +50 -0
  152. package/dist/web/public/assets/index-BjGe8ggI.css +1 -0
  153. package/dist/web/public/assets/view-CE4tnCuy.js +1 -0
  154. package/dist/web/public/assets/view-CEQ2uPxE.js +1 -0
  155. package/dist/web/public/favicon-16.png +0 -0
  156. package/dist/web/public/favicon-32.png +0 -0
  157. package/dist/web/public/favicon-48.png +0 -0
  158. package/dist/web/public/favicon.ico +0 -0
  159. package/dist/web/public/index.html +19 -0
  160. package/dist/web/public/logo-rounded.png +0 -0
  161. package/dist/web/public/logo-rounded.svg +6 -0
  162. package/dist/web/public/logo.png +0 -0
  163. package/dist/web/public/logo.svg +6 -0
  164. package/package.json +56 -35
  165. package/README.md +0 -251
  166. package/bin/index.js +0 -714
  167. package/client/bridge-client.js +0 -160
  168. package/client/web.js +0 -234
  169. package/client/ws-client.js +0 -191
  170. package/lib/apps.js +0 -237
  171. package/lib/auth.js +0 -319
  172. package/lib/config.js +0 -534
  173. package/lib/index.js +0 -15
  174. package/lib/logger.js +0 -63
  175. package/lib/protocol.js +0 -208
  176. package/lib/store.js +0 -66
  177. package/server/auth.js +0 -274
  178. package/server/server.js +0 -752
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/errors/index.ts"],"names":[],"mappings":"AAoBA,eAAO,MAAM,KAAK;;;;;;;;CAQR,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,KAAK,CAAC,CAAC,MAAM,OAAO,KAAK,CAAC,CAAC;AAyB3D,qBAAa,OAAQ,SAAQ,KAAK;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;gBAEN,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO;CAMhE;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAEpD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAElD;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,GAAG,YAAY,CAOzD;AAID,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACP,CAAC;AACrD,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACX,CAAC;AACjD,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACZ,CAAC;AAChD,eAAO,MAAM,OAAO,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACZ,CAAC;AAC/C,eAAO,MAAM,QAAQ,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACZ,CAAC;AAKhD,eAAO,MAAM,SAAS,GAAI,SAAS,MAAM,EAAE,UAAU,OAAO,YACX,CAAC"}
@@ -0,0 +1,88 @@
1
+ // 错误契约:**失败抛 NxError,业务结果返回 {status}**。
2
+ //
3
+ // 这条边界是整个项目最重要的一条约定,判定标准是「调用方要不要处理它」:
4
+ //
5
+ // - 失败(参数非法、目标不存在、外部命令挂掉、数据损坏)
6
+ // → throw。调用方无从处理,只能中断当前操作并上报。API 映射成 4xx/5xx,
7
+ // CLI 映射成 exit 1 + 可解析文本。
8
+ // - 业务结果(同步遇到冲突等待选侧、幂等跳过、规则阻止删除)
9
+ // → return { status: 'ok' | 'skipped' | 'conflict' | 'blocked', ... }。
10
+ // 这不是错误——UI 要拿它弹窗让用户决策,所以绝不能抛。
11
+ //
12
+ // 历史教训:改造前同一层里并存 throw / {ok:false} / {status} / {available} 四种
13
+ // 表达,上层只能靠字符串猜,无法程序化分支。
14
+ //
15
+ // 命名说明:类名是 NxError 而非 AppError——「App」在这个家族里指代不明
16
+ // (哪个 app?),而这类项目通名是 nx-xx,NxError 表意更准。
17
+ //
18
+ // 兼容约束:assets 里的 agent 文档教 agent 用错误文本中的「用法:」「未设置」
19
+ // 「不存在」三个子串分类失败。message 可以追加内容,但不得删除它们。
20
+ export const CODES = {
21
+ INVALID_INPUT: 'INVALID_INPUT', // 参数缺失/非法/越界
22
+ NOT_FOUND: 'NOT_FOUND', // 目标不存在
23
+ CONFLICT: 'CONFLICT', // 状态冲突,需调用方决策
24
+ BLOCKED: 'BLOCKED', // 业务规则主动阻止
25
+ EXTERNAL: 'EXTERNAL', // 外部命令(git / gh)失败
26
+ SPEC_ERROR: 'SPEC_ERROR', // action 声明本身有误(写错了,不是用户输入错)
27
+ INTERNAL: 'INTERNAL', // 兜底
28
+ };
29
+ // code -> HTTP 状态。只在这里映射一次,api.js 不再自行判断。
30
+ // Record 穷尽性:新增错误码漏映射,编译期立即报错。
31
+ const HTTP_STATUS = {
32
+ [CODES.INVALID_INPUT]: 400,
33
+ [CODES.NOT_FOUND]: 404,
34
+ [CODES.CONFLICT]: 409,
35
+ [CODES.BLOCKED]: 409,
36
+ [CODES.EXTERNAL]: 502,
37
+ [CODES.SPEC_ERROR]: 500, // 声明写错是开发者的错,不是调用方的错
38
+ [CODES.INTERNAL]: 500,
39
+ };
40
+ // code -> 进程退出码。CLI 只区分「成功 / 失败」,保持 agent-workflow.md 声明的契约。
41
+ const EXIT_CODE = {
42
+ [CODES.INVALID_INPUT]: 1,
43
+ [CODES.NOT_FOUND]: 1,
44
+ [CODES.CONFLICT]: 1,
45
+ [CODES.BLOCKED]: 1,
46
+ [CODES.EXTERNAL]: 1,
47
+ [CODES.SPEC_ERROR]: 1,
48
+ [CODES.INTERNAL]: 1,
49
+ };
50
+ export class NxError extends Error {
51
+ code;
52
+ details;
53
+ constructor(code, message, details) {
54
+ super(message);
55
+ this.name = 'NxError';
56
+ this.code = code;
57
+ if (details !== undefined)
58
+ this.details = details;
59
+ }
60
+ }
61
+ export function httpStatusOf(code) {
62
+ return HTTP_STATUS[code] ?? 500;
63
+ }
64
+ export function exitCodeOf(code) {
65
+ return EXIT_CODE[code] ?? 1;
66
+ }
67
+ // 把任意抛出物归一成 ErrorPayload,供 API / CLI 统一序列化。
68
+ // 非 NxError 一律归为 INTERNAL——不把内部堆栈细节泄漏给 agent。
69
+ export function toErrorPayload(err) {
70
+ if (err instanceof NxError) {
71
+ const out = { code: err.code, message: err.message };
72
+ if (err.details !== undefined)
73
+ out.details = err.details;
74
+ return out;
75
+ }
76
+ return { code: CODES.INTERNAL, message: String(err?.message || err) };
77
+ }
78
+ // ---- 构造快捷方式:让 service 层读起来是「为什么失败」而不是「怎么构造」 ----
79
+ export const badInput = (message, details) => new NxError(CODES.INVALID_INPUT, message, details);
80
+ export const notFound = (message, details) => new NxError(CODES.NOT_FOUND, message, details);
81
+ export const conflict = (message, details) => new NxError(CODES.CONFLICT, message, details);
82
+ export const blocked = (message, details) => new NxError(CODES.BLOCKED, message, details);
83
+ export const external = (message, details) => new NxError(CODES.EXTERNAL, message, details);
84
+ // action 声明写错了(不是用户输入错)。
85
+ // 由 core 抛出、spec.js 调用——这正是把它放在 core 的原因:spec 是 runtime 层,
86
+ // core 不能依赖它,而「声明有误」这个错误码是两层共用的名词。
87
+ export const specError = (message, details) => new NxError(CODES.SPEC_ERROR, message, details);
88
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/core/errors/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare function openBrowser(url: string): void;
2
+ export declare function openPath(dir: string): void;
3
+ //# sourceMappingURL=open.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.d.ts","sourceRoot":"","sources":["../../src/core/open.ts"],"names":[],"mappings":"AAmBA,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAE1C"}
@@ -0,0 +1,27 @@
1
+ // 交给操作系统的两个动作:打开浏览器、在文件管理器里打开目录。
2
+ // 零业务语义,且刻意不抛错——打不开就算了,调用方已经打印了地址。
3
+ import { spawn } from 'node:child_process';
4
+ function open(target) {
5
+ try {
6
+ const plat = process.platform;
7
+ if (plat === 'win32') {
8
+ spawn('cmd', ['/c', 'start', '', target], { detached: true, stdio: 'ignore' }).unref();
9
+ }
10
+ else if (plat === 'darwin') {
11
+ spawn('open', [target], { detached: true, stdio: 'ignore' }).unref();
12
+ }
13
+ else {
14
+ spawn('xdg-open', [target], { detached: true, stdio: 'ignore' }).unref();
15
+ }
16
+ }
17
+ catch {
18
+ // 静默降级:调用方已经把 URL / 路径打印到终端
19
+ }
20
+ }
21
+ export function openBrowser(url) {
22
+ open(url);
23
+ }
24
+ export function openPath(dir) {
25
+ open(dir);
26
+ }
27
+ //# sourceMappingURL=open.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open.js","sourceRoot":"","sources":["../../src/core/open.ts"],"names":[],"mappings":"AAAA,iCAAiC;AACjC,mCAAmC;AACnC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,SAAS,IAAI,CAAC,MAAc;IAC1B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC;QAC9B,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,KAAK,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACzF,CAAC;aAAM,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC7B,KAAK,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QACvE,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC;QAC3E,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,4BAA4B;IAC9B,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC,GAAG,CAAC,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,GAAW;IAClC,IAAI,CAAC,GAAG,CAAC,CAAC;AACZ,CAAC"}
@@ -0,0 +1,15 @@
1
+ import type { SafeRelPath } from './brand.js';
2
+ export declare const APP_NAME = "mn-ws";
3
+ export declare const APP_TITLE = "mn-ws";
4
+ export declare const APP_DESC = "HTTP \u4E0E WebSocket \u53CC\u5411\u6865\u7684\u5BA2\u6237\u7AEF\u534A\uFF1A\u628A\u672C\u673A nx \u670D\u52A1\u6865\u7ED9\u8FDC\u7AEF HTTP \u5BA2\u6237\u7AEF";
5
+ export declare const APP_DIR: string;
6
+ export declare const STORE_PATH: string;
7
+ export declare const DEFAULT_PORT = 7777;
8
+ export declare const STORE_ENV = "MN_WS_STORE";
9
+ export declare function storePathFromEnv(): string;
10
+ export declare function assertSafeName(name: unknown, label?: string): string;
11
+ export declare function assertSafeRelPath(input: unknown, { label, allowSubdir }?: {
12
+ label?: string;
13
+ allowSubdir?: boolean;
14
+ }): SafeRelPath;
15
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.d.ts","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAG9C,eAAO,MAAM,QAAQ,UAAU,CAAC;AAChC,eAAO,MAAM,SAAS,UAAU,CAAC;AACjC,eAAO,MAAM,QAAQ,mKAAqD,CAAC;AAG3E,eAAO,MAAM,OAAO,QAA4B,CAAC;AACjD,eAAO,MAAM,UAAU,QAA8B,CAAC;AAKtD,eAAO,MAAM,YAAY,OAAO,CAAC;AAGjC,eAAO,MAAM,SAAS,gBAAgB,CAAC;AAEvC,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAQD,wBAAgB,cAAc,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,SAAO,GAAG,MAAM,CAMlE;AAGD,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,EAAE,KAAc,EAAE,WAAkB,EAAE,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,OAAO,CAAA;CAAO,GACrF,WAAW,CAUb"}
@@ -0,0 +1,50 @@
1
+ // 路径与安全校验:项目里所有「东西放哪」都从这里取,不在别处拼字符串。
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { badInput } from './errors/index.js';
5
+ // ---- 项目标识(生成时由模板变量替换)----
6
+ export const APP_NAME = 'mn-ws';
7
+ export const APP_TITLE = 'mn-ws';
8
+ export const APP_DESC = 'HTTP 与 WebSocket 双向桥的客户端半:把本机 nx 服务桥给远端 HTTP 客户端';
9
+ // ---- 数据目录(用户主目录下,可用环境变量覆盖)----
10
+ export const APP_DIR = join(homedir(), '.mn-ws');
11
+ export const STORE_PATH = join(APP_DIR, 'store.json');
12
+ // ---- 端口 ----
13
+ // serve 的 HTTP 端口(生成时写入探测到的空闲端口)。
14
+ // vite dev server 的端口在 vite.config.ts 里单独配。
15
+ export const DEFAULT_PORT = 7777;
16
+ // 测试必须能指向临时目录,否则会写脏用户的真实数据。
17
+ export const STORE_ENV = 'MN_WS_STORE';
18
+ export function storePathFromEnv() {
19
+ return process.env[STORE_ENV] || STORE_PATH;
20
+ }
21
+ // ---- 输入校验 ----
22
+ //
23
+ // 名称与路径分开校验:名称禁前导点(`.foo` 不像个名字),
24
+ // 路径允许(`.gitignore` 是完全正常的文件)。
25
+ // 早期把两者混用一套规则,结果拒绝了 `.gitignore`。
26
+ export function assertSafeName(name, label = '名称') {
27
+ if (!name || typeof name !== 'string')
28
+ throw badInput(`${label}不能为空`);
29
+ if (/[\\/]/.test(name) || name.includes('..') || name.startsWith('.')) {
30
+ throw badInput(`非法 ${label}: ${name}`);
31
+ }
32
+ return name;
33
+ }
34
+ // 校验通过即品牌类型 SafeRelPath:下游拿到它就知道路径已经过安全检查。
35
+ export function assertSafeRelPath(input, { label = '文件路径', allowSubdir = true } = {}) {
36
+ if (!input || typeof input !== 'string')
37
+ throw badInput(label + '不能为空');
38
+ if (/^([a-zA-Z]:|[\\/])/.test(input))
39
+ throw badInput(label + '必须是相对路径: ' + input);
40
+ const segments = input.split(/[\\/]+/).filter((x) => x !== '' && x !== '.');
41
+ if (!segments.length)
42
+ throw badInput('非法 ' + label + ': ' + input);
43
+ if (segments.includes('..'))
44
+ throw badInput(label + '不得包含 ..: ' + input);
45
+ if (!allowSubdir && segments.length > 1) {
46
+ throw badInput(label + '不能包含路径分隔符: ' + input);
47
+ }
48
+ return segments.join('/');
49
+ }
50
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/core/paths.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAG7C,4BAA4B;AAC5B,MAAM,CAAC,MAAM,QAAQ,GAAG,OAAO,CAAC;AAChC,MAAM,CAAC,MAAM,SAAS,GAAG,OAAO,CAAC;AACjC,MAAM,CAAC,MAAM,QAAQ,GAAG,kDAAkD,CAAC;AAE3E,iCAAiC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AACjD,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAEtD,eAAe;AACf,kCAAkC;AAClC,4CAA4C;AAC5C,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AAEjC,4BAA4B;AAC5B,MAAM,CAAC,MAAM,SAAS,GAAG,aAAa,CAAC;AAEvC,MAAM,UAAU,gBAAgB;IAC9B,OAAO,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,UAAU,CAAC;AAC9C,CAAC;AAED,iBAAiB;AACjB,EAAE;AACF,kCAAkC;AAClC,+BAA+B;AAC/B,kCAAkC;AAElC,MAAM,UAAU,cAAc,CAAC,IAAa,EAAE,KAAK,GAAG,IAAI;IACxD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,MAAM,QAAQ,CAAC,GAAG,KAAK,MAAM,CAAC,CAAC;IACtE,IAAI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACtE,MAAM,QAAQ,CAAC,MAAM,KAAK,KAAK,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,2CAA2C;AAC3C,MAAM,UAAU,iBAAiB,CAC/B,KAAc,EACd,EAAE,KAAK,GAAG,MAAM,EAAE,WAAW,GAAG,IAAI,KAAgD,EAAE;IAEtF,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,MAAM,QAAQ,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;IACxE,IAAI,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,MAAM,QAAQ,CAAC,KAAK,GAAG,WAAW,GAAG,KAAK,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC;IAC5E,IAAI,CAAC,QAAQ,CAAC,MAAM;QAAE,MAAM,QAAQ,CAAC,KAAK,GAAG,KAAK,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC;IACnE,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,MAAM,QAAQ,CAAC,KAAK,GAAG,WAAW,GAAG,KAAK,CAAC,CAAC;IACzE,IAAI,CAAC,WAAW,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxC,MAAM,QAAQ,CAAC,KAAK,GAAG,aAAa,GAAG,KAAK,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAgB,CAAC;AAC3C,CAAC"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 所有 nx-xx 项目共享的关闭开关。
3
+ * 设成 1 / true / yes / on 即禁止 `serve` 自动打开浏览器(正常启动与端口复用都算)。
4
+ * 命令行 `--no-open` 只影响这一次;这个环境变量对整个 shell 生效。
5
+ */
6
+ export declare const NO_OPEN_ENV = "NX_SERVE_NO_OPEN";
7
+ /**
8
+ * 探测面板时给的应答窗口。
9
+ *
10
+ * 1500ms 而不是更短:面板的 bootstrap 不一定是纯本机计算——nx-kv 登录后要去后端
11
+ * 拉分组,链路一慢就超过 800ms。窗口太窄的后果不是「探测失败」这么轻,
12
+ * 而是**把自己的面板误判成别人的服务**,于是白起一个实例、占掉第二个端口——
13
+ * 恰好是这套机制要消灭的那种体验。
14
+ */
15
+ export declare const PROBE_TIMEOUT_MS = 1500;
16
+ /** 真值判定:1 / true / yes / on(大小写不敏感)算开,其他一律算关。 */
17
+ export declare function isEnvTruthy(value: string | undefined | null): boolean;
18
+ /**
19
+ * 是否允许自动打开浏览器。
20
+ * 优先级:`--no-open` 命令行开关 > NX_SERVE_NO_OPEN 环境变量 > 默认允许。
21
+ */
22
+ export declare function autoOpenEnabled(flagNoOpen?: boolean): boolean;
23
+ /** 探测到的面板身份:bootstrap 响应里的 app 段。 */
24
+ export interface PanelIdentity {
25
+ name?: string;
26
+ version?: string;
27
+ [key: string]: unknown;
28
+ }
29
+ /** 探测到的 bootstrap 负载(只用到 app 段,其余字段原样带回)。 */
30
+ export interface PanelProbe {
31
+ app?: PanelIdentity;
32
+ [key: string]: unknown;
33
+ }
34
+ /**
35
+ * 探测端口上有没有面板在跑:GET /api/bootstrap,短超时。
36
+ *
37
+ * 所有 nx-xx 面板都暴露 /api/bootstrap,响应里带 app.name / app.version ——
38
+ * 这就是跨项目唯一的认领签名(别的服务不会恰好返回它)。
39
+ * 不是面板、无响应、超时一律返回 null。
40
+ *
41
+ * headers 形参给需要鉴权的项目用(nx-as 的管理面要 Bearer 密钥);
42
+ * 默认不传,匿名探测。
43
+ */
44
+ export declare function probePanel(port: number, host?: string, timeoutMs?: number, headers?: Record<string, string> | null): Promise<PanelProbe | null>;
45
+ /**
46
+ * 从 start 起逐个试,返回第一个能监听的端口(最多试 50 个)。
47
+ * 用临时 net.Server 探测、立刻关闭;与 startServer 走同一套 EADDRINUSE 判定。
48
+ */
49
+ export declare function findFreePort(start: number, host?: string): Promise<number>;
50
+ /** 端口冲突的处置方式。 */
51
+ export type PortConflictAction = 'reuse' | 'shift';
52
+ /** 端口冲突的统一决策结果。 */
53
+ export interface PortConflictDecision {
54
+ /** reuse —— 端口上跑的就是本项目的面板;shift —— 顺延到下一个空闲端口。 */
55
+ action: PortConflictAction;
56
+ /** reuse 时是原端口;shift 时是下一个空闲端口。 */
57
+ port: number;
58
+ /** 占用者的 app 名(探测不到就是 null)。 */
59
+ occupant: string | null;
60
+ /** 探测到的 bootstrap 负载(探测不到就是 null)。 */
61
+ existing: PanelProbe | null;
62
+ }
63
+ /**
64
+ * 端口冲突的统一决策。
65
+ *
66
+ * reuse —— 端口上跑的就是本项目的面板:复用它,不再起第二个进程。
67
+ * 起第二个会让「切项目」变成「切端口」,而用户想看的其实是已经开着的那一个。
68
+ * shift —— 端口被别的东西占着:顺延到下一个空闲端口,再起自己的面板。
69
+ */
70
+ export declare function resolvePortConflict(port: number, appName: string, host?: string, headers?: Record<string, string> | null): Promise<PortConflictDecision>;
71
+ /** 占用者的一句话描述,用于终端提示。探测不到就是「未知服务」。 */
72
+ export declare function describeOccupant(occupant: string | null): string;
73
+ //# sourceMappingURL=serve-port.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-port.d.ts","sourceRoot":"","sources":["../../src/core/serve-port.ts"],"names":[],"mappings":"AASA;;;;GAIG;AACH,eAAO,MAAM,WAAW,qBAAqB,CAAC;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,iDAAiD;AACjD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAIrE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO,CAG7D;AAED,qCAAqC;AACrC,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,6CAA6C;AAC7C,MAAM,WAAW,UAAU;IACzB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AA0CD;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,SAAc,EAClB,SAAS,SAAmB,EAC5B,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAW,GAC5C,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAG5B;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,SAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB/E;AAED,iBAAiB;AACjB,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,OAAO,CAAC;AAEnD,mBAAmB;AACnB,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,MAAM,EAAE,kBAAkB,CAAC;IAC3B,mCAAmC;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sCAAsC;IACtC,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,SAAc,EAClB,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAW,GAC5C,OAAO,CAAC,oBAAoB,CAAC,CAc/B;AAED,qCAAqC;AACrC,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAEhE"}
@@ -0,0 +1,134 @@
1
+ // serve 的端口协商 + 浏览器打开策略。
2
+ //
3
+ // 零业务语义:所有 nx-xx 面板共用这一份,靠环境变量 NX_SERVE_NO_OPEN 统一关闭自动开浏览器。
4
+ // 放 core/ 是因为它只依赖 Node 内置模块与「本项目的 app 名」,不涉及任何功能域。
5
+ //
6
+ // 这份文件在所有 nx-xx 项目之间**逐字节相同**——改一处就要全量同步,
7
+ // 否则「口径统一」这件事本身会悄悄分叉。
8
+ import net from 'node:net';
9
+ /**
10
+ * 所有 nx-xx 项目共享的关闭开关。
11
+ * 设成 1 / true / yes / on 即禁止 `serve` 自动打开浏览器(正常启动与端口复用都算)。
12
+ * 命令行 `--no-open` 只影响这一次;这个环境变量对整个 shell 生效。
13
+ */
14
+ export const NO_OPEN_ENV = 'NX_SERVE_NO_OPEN';
15
+ /**
16
+ * 探测面板时给的应答窗口。
17
+ *
18
+ * 1500ms 而不是更短:面板的 bootstrap 不一定是纯本机计算——nx-kv 登录后要去后端
19
+ * 拉分组,链路一慢就超过 800ms。窗口太窄的后果不是「探测失败」这么轻,
20
+ * 而是**把自己的面板误判成别人的服务**,于是白起一个实例、占掉第二个端口——
21
+ * 恰好是这套机制要消灭的那种体验。
22
+ */
23
+ export const PROBE_TIMEOUT_MS = 1500;
24
+ /** 真值判定:1 / true / yes / on(大小写不敏感)算开,其他一律算关。 */
25
+ export function isEnvTruthy(value) {
26
+ if (value === undefined || value === null)
27
+ return false;
28
+ const s = String(value).trim().toLowerCase();
29
+ return s === '1' || s === 'true' || s === 'yes' || s === 'on';
30
+ }
31
+ /**
32
+ * 是否允许自动打开浏览器。
33
+ * 优先级:`--no-open` 命令行开关 > NX_SERVE_NO_OPEN 环境变量 > 默认允许。
34
+ */
35
+ export function autoOpenEnabled(flagNoOpen) {
36
+ if (flagNoOpen)
37
+ return false;
38
+ return !isEnvTruthy(process.env[NO_OPEN_ENV]);
39
+ }
40
+ /**
41
+ * 探一次端口,返回 { data, timedOut }。
42
+ *
43
+ * 把「超时」和「不是面板」分开,是因为两者的后续动作不同:
44
+ * 超时说明端口上可能正是一个应答慢的面板,值得再等一次;
45
+ * 明确不是面板就没有再等的必要。
46
+ */
47
+ async function probePanelOnce(port, host, timeoutMs, headers) {
48
+ try {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
51
+ let res;
52
+ try {
53
+ res = await fetch(`http://${host}:${port}/api/bootstrap`, {
54
+ signal: controller.signal,
55
+ headers: headers || undefined,
56
+ });
57
+ }
58
+ finally {
59
+ clearTimeout(timer);
60
+ }
61
+ if (!res.ok)
62
+ return { data: null, timedOut: false };
63
+ const body = (await res.json());
64
+ return { data: (body && body.data) || null, timedOut: false };
65
+ }
66
+ catch (err) {
67
+ const e = err;
68
+ return { data: null, timedOut: !!(e && e.name === 'AbortError') };
69
+ }
70
+ }
71
+ /**
72
+ * 探测端口上有没有面板在跑:GET /api/bootstrap,短超时。
73
+ *
74
+ * 所有 nx-xx 面板都暴露 /api/bootstrap,响应里带 app.name / app.version ——
75
+ * 这就是跨项目唯一的认领签名(别的服务不会恰好返回它)。
76
+ * 不是面板、无响应、超时一律返回 null。
77
+ *
78
+ * headers 形参给需要鉴权的项目用(nx-as 的管理面要 Bearer 密钥);
79
+ * 默认不传,匿名探测。
80
+ */
81
+ export async function probePanel(port, host = '127.0.0.1', timeoutMs = PROBE_TIMEOUT_MS, headers = null) {
82
+ const { data } = await probePanelOnce(port, host, timeoutMs, headers);
83
+ return data;
84
+ }
85
+ /**
86
+ * 从 start 起逐个试,返回第一个能监听的端口(最多试 50 个)。
87
+ * 用临时 net.Server 探测、立刻关闭;与 startServer 走同一套 EADDRINUSE 判定。
88
+ */
89
+ export function findFreePort(start, host = '127.0.0.1') {
90
+ return new Promise((resolve_, reject) => {
91
+ const tryPort = (p) => {
92
+ if (p > start + 50) {
93
+ reject(new Error(`找不到空闲端口(从 ${start} 起连续 50 个被占)`));
94
+ return;
95
+ }
96
+ const probe = net.createServer();
97
+ probe.once('error', (err) => {
98
+ if (err && err.code === 'EADDRINUSE')
99
+ tryPort(p + 1);
100
+ else
101
+ reject(err);
102
+ });
103
+ probe.listen(p, host, () => probe.close(() => resolve_(p)));
104
+ };
105
+ tryPort(start);
106
+ });
107
+ }
108
+ /**
109
+ * 端口冲突的统一决策。
110
+ *
111
+ * reuse —— 端口上跑的就是本项目的面板:复用它,不再起第二个进程。
112
+ * 起第二个会让「切项目」变成「切端口」,而用户想看的其实是已经开着的那一个。
113
+ * shift —— 端口被别的东西占着:顺延到下一个空闲端口,再起自己的面板。
114
+ */
115
+ export async function resolvePortConflict(port, appName, host = '127.0.0.1', headers = null) {
116
+ let probe = await probePanelOnce(port, host, PROBE_TIMEOUT_MS, headers);
117
+ if (probe.timedOut) {
118
+ // 慢应答的面板也认:再等一次。别把「自己的面板」误判成「别人的服务」,
119
+ // 白白再起一个实例——那正是这套机制要消灭的行为。
120
+ probe = await probePanelOnce(port, host, PROBE_TIMEOUT_MS, headers);
121
+ }
122
+ const existing = probe.data;
123
+ const occupant = existing && existing.app ? existing.app.name || null : null;
124
+ if (existing && occupant === appName) {
125
+ return { action: 'reuse', port, occupant, existing };
126
+ }
127
+ const next = await findFreePort(port + 1, host);
128
+ return { action: 'shift', port: next, occupant, existing };
129
+ }
130
+ /** 占用者的一句话描述,用于终端提示。探测不到就是「未知服务」。 */
131
+ export function describeOccupant(occupant) {
132
+ return occupant || '未知服务';
133
+ }
134
+ //# sourceMappingURL=serve-port.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-port.js","sourceRoot":"","sources":["../../src/core/serve-port.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,78 @@
1
+ import { isExpired, type ClientConfig } from './config.js';
2
+ /**
3
+ * 面板的登录态。
4
+ *
5
+ * ## 「必须登录才能用」到底指什么
6
+ *
7
+ * 面板跑在 127.0.0.1 上,但它**握着桥的凭据** —— 能连 server、能改白名单。
8
+ * 所以「打开就能用」等于「任何能访问这个端口的东西都能用」。
9
+ *
10
+ * 门禁因此落在**服务端的登录态**上:
11
+ * · 没有有效登录态 → 所有业务 API 回 401,前端渲染登录页
12
+ * · 有有效登录态 → 放行
13
+ *
14
+ * 两种拿登录态的方式(按优先级):
15
+ * 1. **复用 CLI 的设备令牌** —— 本机 `mn-ws auth login` 存下来的那一份。
16
+ * 面板启动时读账号簿,有未过期的设备令牌就直接进入登录态(via=cli)。
17
+ * 这样「命令行登一次,面板直接能用」,不必登两遍。
18
+ * 2. **面板里现登** —— 前端点「登录」→ 弹浏览器 → PKCE 回环回调到
19
+ * 面板自己的 `/callback` → 换到设备令牌 → 存进账号簿(via=panel)。
20
+ *
21
+ * 凭据一律存在**服务端**,浏览器页面自己不发凭据 ——
22
+ * 页面里没有任何可以泄漏的令牌。
23
+ */
24
+ /** 面板当前的登录态。 */
25
+ export interface PanelSession {
26
+ loggedIn: boolean;
27
+ userId?: string;
28
+ email?: string;
29
+ nickname?: string;
30
+ deviceToken?: string;
31
+ expiresAt?: number;
32
+ /** 登录态的来源:复用 CLI / 面板里现登 / 没有 */
33
+ via: 'cli' | 'panel' | 'none';
34
+ }
35
+ /** 设备令牌是否已过期(实现在 core/config.ts,与账号簿同源)。 */
36
+ export { isExpired };
37
+ export declare class SessionManager {
38
+ private cfg;
39
+ session: PanelSession;
40
+ private pending;
41
+ constructor(cfg: ClientConfig);
42
+ /**
43
+ * 复用 CLI 已存的设备令牌。
44
+ *
45
+ * 读的是**激活账号**那一条:CLI 切了账号(auth switch),面板跟着切,
46
+ * 不然会出现「命令行是 A、面板是 B」的分裂状态。
47
+ */
48
+ adoptFromAccountStore(): boolean;
49
+ /** 当前登录态(对外只给必要字段)。 */
50
+ snapshot(): {
51
+ loggedIn: boolean;
52
+ userId?: string;
53
+ email?: string;
54
+ nickname?: string;
55
+ via: string;
56
+ };
57
+ /** 当前用的凭据(注册到 server 半时用)。 */
58
+ credential(): string;
59
+ /**
60
+ * 面板里点「登录」:起一次授权码流程并弹浏览器。
61
+ *
62
+ * 回调地址是**面板自己的** `http://127.0.0.1:<port>/callback` ——
63
+ * go-ah 里 mn-ws-client 的回调白名单是「127.0.0.1 + 任意端口 + /callback」,
64
+ * 端口通配、路径固定,所以面板端口运行时怎么选都能回调。
65
+ */
66
+ startLogin(panelPort: number): Promise<{
67
+ authorizeUrl: string;
68
+ }>;
69
+ /**
70
+ * 面板的 `/callback` 路由调用它:收回调 → 换 access_token → 换设备令牌。
71
+ */
72
+ completeLogin(code: string, state: string): Promise<PanelSession>;
73
+ /** 登录失败/超时后清掉进行中的流程。 */
74
+ cancelLogin(): void;
75
+ /** 清除登录态(logout 用)。 */
76
+ reset(): void;
77
+ }
78
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/core/session.ts"],"names":[],"mappings":"AAIA,OAAO,EACwD,SAAS,EACtE,KAAK,YAAY,EAClB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,gBAAgB;AAChB,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,GAAG,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;CAC/B;AAWD,4CAA4C;AAC5C,OAAO,EAAE,SAAS,EAAE,CAAC;AAErB,qBAAa,cAAc;IAIb,OAAO,CAAC,GAAG;IAHvB,OAAO,EAAE,YAAY,CAAoC;IACzD,OAAO,CAAC,OAAO,CAA6B;gBAExB,GAAG,EAAE,YAAY;IAErC;;;;;OAKG;IACH,qBAAqB,IAAI,OAAO;IAkBhC,uBAAuB;IACvB,QAAQ,IAAI;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE;IAUlG,8BAA8B;IAC9B,UAAU,IAAI,MAAM;IAIpB;;;;;;OAMG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBtE;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAwCvE,wBAAwB;IACxB,WAAW,IAAI,IAAI;IAInB,uBAAuB;IACvB,KAAK,IAAI,IAAI;CAId"}
@@ -0,0 +1,124 @@
1
+ import { beginOauth, finishOauth, fetchIdpConfig, openBrowser, verifyExchange, } from './auth.js';
2
+ import { listAccounts, saveAuthAccount, withAccounts, readConfigFile, isExpired, } from './config.js';
3
+ /** 设备令牌是否已过期(实现在 core/config.ts,与账号簿同源)。 */
4
+ export { isExpired };
5
+ export class SessionManager {
6
+ cfg;
7
+ session = { loggedIn: false, via: 'none' };
8
+ pending = null;
9
+ constructor(cfg) {
10
+ this.cfg = cfg;
11
+ }
12
+ /**
13
+ * 复用 CLI 已存的设备令牌。
14
+ *
15
+ * 读的是**激活账号**那一条:CLI 切了账号(auth switch),面板跟着切,
16
+ * 不然会出现「命令行是 A、面板是 B」的分裂状态。
17
+ */
18
+ adoptFromAccountStore() {
19
+ const raw = readConfigFile();
20
+ const view = withAccounts(raw);
21
+ const active = view.activeAccountKey ? view.accounts[view.activeAccountKey] : undefined;
22
+ const entry = active || listAccounts(view)[0]?.entry;
23
+ if (!entry || !entry.deviceToken || isExpired(entry))
24
+ return false;
25
+ this.session = {
26
+ loggedIn: true,
27
+ userId: entry.userId,
28
+ email: entry.email,
29
+ nickname: entry.nickname,
30
+ deviceToken: entry.deviceToken,
31
+ expiresAt: entry.expiresAt,
32
+ via: 'cli',
33
+ };
34
+ return true;
35
+ }
36
+ /** 当前登录态(对外只给必要字段)。 */
37
+ snapshot() {
38
+ return {
39
+ loggedIn: this.session.loggedIn,
40
+ userId: this.session.userId,
41
+ email: this.session.email,
42
+ nickname: this.session.nickname,
43
+ via: this.session.via,
44
+ };
45
+ }
46
+ /** 当前用的凭据(注册到 server 半时用)。 */
47
+ credential() {
48
+ return this.session.deviceToken || this.cfg.token;
49
+ }
50
+ /**
51
+ * 面板里点「登录」:起一次授权码流程并弹浏览器。
52
+ *
53
+ * 回调地址是**面板自己的** `http://127.0.0.1:<port>/callback` ——
54
+ * go-ah 里 mn-ws-client 的回调白名单是「127.0.0.1 + 任意端口 + /callback」,
55
+ * 端口通配、路径固定,所以面板端口运行时怎么选都能回调。
56
+ */
57
+ async startLogin(panelPort) {
58
+ if (this.pending)
59
+ throw new Error('已有一个登录在进行中');
60
+ const serverHttp = this.cfg.serverHttp;
61
+ const cfg = await fetchIdpConfig(serverHttp);
62
+ const redirectUri = `http://127.0.0.1:${panelPort}/callback`;
63
+ const { authorizeUrl, req } = beginOauth(cfg, redirectUri);
64
+ this.pending = {
65
+ cfg,
66
+ req,
67
+ serverHttp,
68
+ clientId: this.cfg.clientId,
69
+ deviceName: this.cfg.deviceName,
70
+ };
71
+ openBrowser(authorizeUrl);
72
+ return { authorizeUrl };
73
+ }
74
+ /**
75
+ * 面板的 `/callback` 路由调用它:收回调 → 换 access_token → 换设备令牌。
76
+ */
77
+ async completeLogin(code, state) {
78
+ const pending = this.pending;
79
+ if (!pending)
80
+ throw new Error('没有进行中的登录(可能已超时或重复回调)');
81
+ this.pending = null;
82
+ const tok = await finishOauth(pending.cfg, pending.req, { code, state });
83
+ const res = await verifyExchange({
84
+ serverHttp: pending.serverHttp,
85
+ accessToken: tok.accessToken,
86
+ clientId: pending.clientId,
87
+ deviceName: pending.deviceName,
88
+ });
89
+ const entry = {
90
+ userId: res.userId,
91
+ email: res.email,
92
+ username: res.username,
93
+ nickname: res.nickname,
94
+ deviceToken: res.token,
95
+ expiresAt: res.tokenExpiresAt,
96
+ deviceName: res.deviceName || pending.deviceName,
97
+ clientId: res.clientId || pending.clientId,
98
+ url: this.cfg.url,
99
+ serverHttp: this.cfg.serverHttp,
100
+ loginAt: Date.now(),
101
+ };
102
+ saveAuthAccount({ entry, setActive: true });
103
+ this.session = {
104
+ loggedIn: true,
105
+ userId: res.userId,
106
+ email: res.email,
107
+ nickname: res.nickname,
108
+ deviceToken: res.token,
109
+ expiresAt: res.tokenExpiresAt,
110
+ via: 'panel',
111
+ };
112
+ return this.session;
113
+ }
114
+ /** 登录失败/超时后清掉进行中的流程。 */
115
+ cancelLogin() {
116
+ this.pending = null;
117
+ }
118
+ /** 清除登录态(logout 用)。 */
119
+ reset() {
120
+ this.session = { loggedIn: false, via: 'none' };
121
+ this.pending = null;
122
+ }
123
+ }
124
+ //# sourceMappingURL=session.js.map