@ai-zen/agents-host 0.0.0-stage → 0.1.0-alpha.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 (146) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/LICENSE +21 -0
  3. package/README.md +759 -2
  4. package/dist/app.d.ts +384 -0
  5. package/dist/app.js +1148 -0
  6. package/dist/cli.d.ts +22 -0
  7. package/dist/cli.js +354 -0
  8. package/dist/config.d.ts +97 -0
  9. package/dist/config.js +171 -0
  10. package/dist/handlers/agentHandlers.d.ts +18 -0
  11. package/dist/handlers/agentHandlers.js +20 -0
  12. package/dist/handlers/assetHandlers.d.ts +29 -0
  13. package/dist/handlers/assetHandlers.js +35 -0
  14. package/dist/handlers/capabilities.d.ts +26 -0
  15. package/dist/handlers/capabilities.js +138 -0
  16. package/dist/handlers/completionHandlers.d.ts +18 -0
  17. package/dist/handlers/completionHandlers.js +13 -0
  18. package/dist/handlers/configHandlers.d.ts +31 -0
  19. package/dist/handlers/configHandlers.js +59 -0
  20. package/dist/handlers/conversationHandlers.d.ts +118 -0
  21. package/dist/handlers/conversationHandlers.js +314 -0
  22. package/dist/handlers/handoffHandlers.d.ts +17 -0
  23. package/dist/handlers/handoffHandlers.js +14 -0
  24. package/dist/handlers/hostHandlers.d.ts +39 -0
  25. package/dist/handlers/hostHandlers.js +56 -0
  26. package/dist/handlers/index.d.ts +128 -0
  27. package/dist/handlers/index.js +92 -0
  28. package/dist/handlers/messageHandlers.d.ts +11 -0
  29. package/dist/handlers/messageHandlers.js +10 -0
  30. package/dist/handlers/modelHandlers.d.ts +17 -0
  31. package/dist/handlers/modelHandlers.js +19 -0
  32. package/dist/handlers/noticeHandlers.d.ts +24 -0
  33. package/dist/handlers/noticeHandlers.js +24 -0
  34. package/dist/handlers/questionHandlers.d.ts +16 -0
  35. package/dist/handlers/questionHandlers.js +16 -0
  36. package/dist/handlers/searchHandlers.d.ts +12 -0
  37. package/dist/handlers/searchHandlers.js +12 -0
  38. package/dist/handlers/workspaceHandlers.d.ts +29 -0
  39. package/dist/handlers/workspaceHandlers.js +47 -0
  40. package/dist/i18n/en.d.ts +243 -0
  41. package/dist/i18n/en.js +270 -0
  42. package/dist/i18n/index.d.ts +43 -0
  43. package/dist/i18n/index.js +72 -0
  44. package/dist/i18n/zh.d.ts +240 -0
  45. package/dist/i18n/zh.js +257 -0
  46. package/dist/index.d.ts +17 -0
  47. package/dist/index.js +15 -0
  48. package/dist/net/UploadReceiver.d.ts +186 -0
  49. package/dist/net/UploadReceiver.js +494 -0
  50. package/dist/net/WsHub.d.ts +231 -0
  51. package/dist/net/WsHub.js +689 -0
  52. package/dist/net/auth.d.ts +73 -0
  53. package/dist/net/auth.js +74 -0
  54. package/dist/net/discovery.d.ts +72 -0
  55. package/dist/net/discovery.js +171 -0
  56. package/dist/net/index.d.ts +14 -0
  57. package/dist/net/index.js +10 -0
  58. package/dist/runtime/ChatRunner.d.ts +374 -0
  59. package/dist/runtime/ChatRunner.js +1204 -0
  60. package/dist/runtime/ConversationDrafts.d.ts +43 -0
  61. package/dist/runtime/ConversationDrafts.js +47 -0
  62. package/dist/runtime/RunRegistry.d.ts +79 -0
  63. package/dist/runtime/RunRegistry.js +108 -0
  64. package/dist/runtime/ScopeRegistry.d.ts +153 -0
  65. package/dist/runtime/ScopeRegistry.js +259 -0
  66. package/dist/runtime/TransientRunner.d.ts +90 -0
  67. package/dist/runtime/TransientRunner.js +179 -0
  68. package/dist/runtime/index.d.ts +14 -0
  69. package/dist/runtime/index.js +10 -0
  70. package/dist/services/AgentService.d.ts +150 -0
  71. package/dist/services/AgentService.js +718 -0
  72. package/dist/services/AssetService.d.ts +338 -0
  73. package/dist/services/AssetService.js +739 -0
  74. package/dist/services/AutoNaming.d.ts +92 -0
  75. package/dist/services/AutoNaming.js +235 -0
  76. package/dist/services/CompletionService.d.ts +122 -0
  77. package/dist/services/CompletionService.js +442 -0
  78. package/dist/services/ConfigService.d.ts +171 -0
  79. package/dist/services/ConfigService.js +695 -0
  80. package/dist/services/ConversationService.d.ts +274 -0
  81. package/dist/services/ConversationService.js +902 -0
  82. package/dist/services/CredentialService.d.ts +33 -0
  83. package/dist/services/CredentialService.js +86 -0
  84. package/dist/services/DiscoveryService.d.ts +182 -0
  85. package/dist/services/DiscoveryService.js +998 -0
  86. package/dist/services/HandoffService.d.ts +189 -0
  87. package/dist/services/HandoffService.js +414 -0
  88. package/dist/services/Logger.d.ts +130 -0
  89. package/dist/services/Logger.js +256 -0
  90. package/dist/services/MessageService.d.ts +88 -0
  91. package/dist/services/MessageService.js +280 -0
  92. package/dist/services/ModelService.d.ts +89 -0
  93. package/dist/services/ModelService.js +489 -0
  94. package/dist/services/NoticeService.d.ts +93 -0
  95. package/dist/services/NoticeService.js +217 -0
  96. package/dist/services/QuestionService.d.ts +184 -0
  97. package/dist/services/QuestionService.js +457 -0
  98. package/dist/services/SearchService.d.ts +54 -0
  99. package/dist/services/SearchService.js +117 -0
  100. package/dist/services/WorkspaceService.d.ts +96 -0
  101. package/dist/services/WorkspaceService.js +213 -0
  102. package/dist/services/imageSize.d.ts +41 -0
  103. package/dist/services/imageSize.js +146 -0
  104. package/dist/services/index.d.ts +20 -0
  105. package/dist/services/index.js +15 -0
  106. package/dist/services/secretMask.d.ts +24 -0
  107. package/dist/services/secretMask.js +34 -0
  108. package/dist/storage/AssetRepository.d.ts +87 -0
  109. package/dist/storage/AssetRepository.js +180 -0
  110. package/dist/storage/ConversationRepository.d.ts +55 -0
  111. package/dist/storage/ConversationRepository.js +128 -0
  112. package/dist/storage/CredentialRepository.d.ts +28 -0
  113. package/dist/storage/CredentialRepository.js +47 -0
  114. package/dist/storage/Database.d.ts +95 -0
  115. package/dist/storage/Database.js +180 -0
  116. package/dist/storage/MessageRepository.d.ts +104 -0
  117. package/dist/storage/MessageRepository.js +323 -0
  118. package/dist/storage/MetaRepository.d.ts +21 -0
  119. package/dist/storage/MetaRepository.js +33 -0
  120. package/dist/storage/NoticeRepository.d.ts +31 -0
  121. package/dist/storage/NoticeRepository.js +89 -0
  122. package/dist/storage/QuestionRepository.d.ts +34 -0
  123. package/dist/storage/QuestionRepository.js +96 -0
  124. package/dist/storage/WorkspaceRepository.d.ts +25 -0
  125. package/dist/storage/WorkspaceRepository.js +44 -0
  126. package/dist/storage/dbWorker.d.ts +10 -0
  127. package/dist/storage/dbWorker.js +132 -0
  128. package/dist/storage/index.d.ts +20 -0
  129. package/dist/storage/index.js +16 -0
  130. package/dist/storage/migrations.d.ts +26 -0
  131. package/dist/storage/migrations.js +207 -0
  132. package/dist/storage/types.d.ts +298 -0
  133. package/dist/storage/types.js +14 -0
  134. package/dist/wire/errors.d.ts +105 -0
  135. package/dist/wire/errors.js +143 -0
  136. package/dist/wire/events.d.ts +102 -0
  137. package/dist/wire/events.js +13 -0
  138. package/dist/wire/frames.d.ts +106 -0
  139. package/dist/wire/frames.js +156 -0
  140. package/dist/wire/index.d.ts +31 -0
  141. package/dist/wire/index.js +31 -0
  142. package/dist/wire/methods.d.ts +1896 -0
  143. package/dist/wire/methods.js +99 -0
  144. package/dist/wire/states.d.ts +487 -0
  145. package/dist/wire/states.js +61 -0
  146. package/package.json +63 -3
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 凭证的校验与本地凭证的生成/交付(architecture §2 / PRD A5、L11)。
3
+ *
4
+ * 真相在库里:`credentials` 表一列存 sha256 摘要,明文只在两处出现 —— 创建时回给创建者的那一次,
5
+ * 以及本地凭证写进 `<dataRoot>/run/token` 的那一份。于是「多串、带备注、可单独吊销」全部由库承担,
6
+ * 本文件只做两件事:
7
+ *
8
+ * 1. {@link verify}:一串明文 → 库里仍有效的那一行(没命中就是 `null`)。
9
+ * 2. {@link createLocalCredential}:本地实例首次启动时造一串(备注 `local`)并写进交付文件。
10
+ *
11
+ * **所有连接都要凭证**(PRD A5):本地实例的「无感」来自「凭证已在交付文件里被客户端取走」,
12
+ * 不是来自「回环地址免检」—— 所以这里没有任何「从 127.0.0.1 来就放行」的分支。
13
+ */
14
+ import type { HostPaths } from "../config.js";
15
+ import type { Database } from "../storage/Database.js";
16
+ /** 本地凭证的备注:与用户在 `agents-host token create --note` 里自己写的备注区分开 */
17
+ export declare const LOCAL_CREDENTIAL_NOTE = "local";
18
+ /** 校验通过的凭证:只给「哪一串」,不含摘要与明文 */
19
+ export interface VerifiedCredential {
20
+ id: string;
21
+ note: string;
22
+ }
23
+ /**
24
+ * 校验一串凭证。
25
+ *
26
+ * 交给接线层的注入形状:`hello` 带上来的是明文,校验方只需要回答「这串还算数吗、是哪一串」。
27
+ * 用法是「每条连接在 `hello` 时校验一次」,所以吊销对已经握过手的连接不回头生效
28
+ * (吊销是「以后连不上」,不是「把别人踢下线」)。
29
+ */
30
+ export interface TokenVerifier {
31
+ verify(token: string): Promise<VerifiedCredential | null>;
32
+ }
33
+ /**
34
+ * 一串明文 → 库里仍有效的那一行。
35
+ *
36
+ * 空串一律不通过:库里不会存在空串的摘要,但把「客户端漏传 token」与「token 不对」分成两种
37
+ * 结果没有意义 —— 两者都是「这串不被接受」。
38
+ */
39
+ export declare function verify(db: Database, token: string): Promise<VerifiedCredential | null>;
40
+ /** 把 {@link verify} 包成注入用的对象(`WsHub` 收的就是这个形状) */
41
+ export declare function createTokenVerifier(db: Database): TokenVerifier;
42
+ /** `createLocalCredential` 的可选项 */
43
+ export interface LocalCredentialOptions {
44
+ /** 备注,缺省 `local` */
45
+ note?: string;
46
+ /** 时钟,测试注入用;缺省 `Date.now` */
47
+ now?: () => number;
48
+ /** 随机字节数,缺省 32(测试里可以调小) */
49
+ tokenBytes?: number;
50
+ /** 强制轮换:不管交付文件里那串是否还有效,都造一串新的并重写文件 */
51
+ rotate?: boolean;
52
+ }
53
+ /** 本地凭证的生成结果 */
54
+ export interface LocalCredentialResult {
55
+ /** 库里那一行的 id(吊销时用它) */
56
+ credentialId: string;
57
+ /** 明文;此后再也拿不到第二份,除非又读交付文件 */
58
+ token: string;
59
+ /** 这次是不是真的新造了一串(`false` 表示复用了交付文件里那串) */
60
+ created: boolean;
61
+ }
62
+ /**
63
+ * 取(必要时先生成)本地凭证,并保证 `<dataRoot>/run/token` 里是**当前可用**的那一串。
64
+ *
65
+ * 三种情形:
66
+ * - 交付文件有内容且库里仍有效 → 直接复用(绝大多数启动走这条,不会每次重启都造新串);
67
+ * - 交付文件有内容但已吊销 / 库里查不到 → 造一串新的并重写文件(旧串已经无效,留着文件只会让人连不上);
68
+ * - 交付文件不在 → 造一串新的并写进去。
69
+ *
70
+ * 「库里已有别的有效凭证、但交付文件被用户删了」也会造新串:交付文件是明文的唯一副本,
71
+ * 删掉之后那串明文无从复原,再造一串是唯一能让本地客户端连上的做法。
72
+ */
73
+ export declare function createLocalCredential(db: Database, paths: HostPaths, options?: LocalCredentialOptions): Promise<LocalCredentialResult>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * 凭证的校验与本地凭证的生成/交付(architecture §2 / PRD A5、L11)。
3
+ *
4
+ * 真相在库里:`credentials` 表一列存 sha256 摘要,明文只在两处出现 —— 创建时回给创建者的那一次,
5
+ * 以及本地凭证写进 `<dataRoot>/run/token` 的那一份。于是「多串、带备注、可单独吊销」全部由库承担,
6
+ * 本文件只做两件事:
7
+ *
8
+ * 1. {@link verify}:一串明文 → 库里仍有效的那一行(没命中就是 `null`)。
9
+ * 2. {@link createLocalCredential}:本地实例首次启动时造一串(备注 `local`)并写进交付文件。
10
+ *
11
+ * **所有连接都要凭证**(PRD A5):本地实例的「无感」来自「凭证已在交付文件里被客户端取走」,
12
+ * 不是来自「回环地址免检」—— 所以这里没有任何「从 127.0.0.1 来就放行」的分支。
13
+ */
14
+ import { randomBytes, randomUUID } from "node:crypto";
15
+ import { CredentialRepository, hashCredential } from "../storage/CredentialRepository.js";
16
+ import { readLocalToken, writeLocalToken } from "./discovery.js";
17
+ /** 本地凭证的备注:与用户在 `agents-host token create --note` 里自己写的备注区分开 */
18
+ export const LOCAL_CREDENTIAL_NOTE = "local";
19
+ /** 凭证明文(base64url)的随机字节数;32 字节 → 43 个字符 */
20
+ const DEFAULT_TOKEN_BYTES = 32;
21
+ /**
22
+ * 一串明文 → 库里仍有效的那一行。
23
+ *
24
+ * 空串一律不通过:库里不会存在空串的摘要,但把「客户端漏传 token」与「token 不对」分成两种
25
+ * 结果没有意义 —— 两者都是「这串不被接受」。
26
+ */
27
+ export async function verify(db, token) {
28
+ const plain = typeof token === "string" ? token.trim() : "";
29
+ if (plain === "")
30
+ return null;
31
+ const credentials = new CredentialRepository(db);
32
+ const row = await credentials.findActiveByHash(hashCredential(plain));
33
+ return row === undefined ? null : { id: row.id, note: row.note };
34
+ }
35
+ /** 把 {@link verify} 包成注入用的对象(`WsHub` 收的就是这个形状) */
36
+ export function createTokenVerifier(db) {
37
+ return {
38
+ verify: (token) => verify(db, token),
39
+ };
40
+ }
41
+ /**
42
+ * 取(必要时先生成)本地凭证,并保证 `<dataRoot>/run/token` 里是**当前可用**的那一串。
43
+ *
44
+ * 三种情形:
45
+ * - 交付文件有内容且库里仍有效 → 直接复用(绝大多数启动走这条,不会每次重启都造新串);
46
+ * - 交付文件有内容但已吊销 / 库里查不到 → 造一串新的并重写文件(旧串已经无效,留着文件只会让人连不上);
47
+ * - 交付文件不在 → 造一串新的并写进去。
48
+ *
49
+ * 「库里已有别的有效凭证、但交付文件被用户删了」也会造新串:交付文件是明文的唯一副本,
50
+ * 删掉之后那串明文无从复原,再造一串是唯一能让本地客户端连上的做法。
51
+ */
52
+ export async function createLocalCredential(db, paths, options = {}) {
53
+ const credentials = new CredentialRepository(db);
54
+ const note = options.note ?? LOCAL_CREDENTIAL_NOTE;
55
+ if (options.rotate !== true) {
56
+ const existing = await readLocalToken(paths);
57
+ if (existing !== null) {
58
+ const verified = await verify(db, existing);
59
+ if (verified !== null) {
60
+ return { credentialId: verified.id, token: existing, created: false };
61
+ }
62
+ }
63
+ }
64
+ const token = randomBytes(options.tokenBytes ?? DEFAULT_TOKEN_BYTES).toString("base64url");
65
+ const id = randomUUID();
66
+ await credentials.create({
67
+ id,
68
+ note,
69
+ hash: hashCredential(token),
70
+ createdAt: (options.now ?? Date.now)(),
71
+ });
72
+ await writeLocalToken(paths, token);
73
+ return { credentialId: id, token, created: true };
74
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * 发现文件与本地凭证交付口(architecture §2)。
3
+ *
4
+ * 两个文件、两种用途:
5
+ *
6
+ * - `<dataRoot>/run/host.json` 是「这台机器上谁在服务」的唯一真相:进程 id、监听的地址与端口、
7
+ * 版本、是不是本地实例、启动时刻。客户端探活与宿主启动时自查都读它,两边不各写一份。
8
+ * - `<dataRoot>/run/token` 是**本地凭证的交付口**:内容只有本地那串凭证,客户端首次连接时
9
+ * 自动取走。凭证自己的真相在库里(可列出、带备注、可单独吊销),这里只是一份「给本机进程读」的副本。
10
+ *
11
+ * 目录由本文件按需创建:`run/` 与 `assets/` 都可能在首次启动时还不存在。
12
+ */
13
+ import type { HostPaths } from "../config.js";
14
+ /**
15
+ * `run/host.json` 的形状。
16
+ *
17
+ * `host` 是**实际监听**的地址(本地实例是 `127.0.0.1`,对外实例是调用方指定的地址),
18
+ * `port` 是实际端口 —— 端口被占时宿主会退回系统分配的临时端口,所以客户端不能按「启动参数里的端口」
19
+ * 去连,只能读这个文件。
20
+ */
21
+ export interface HostFileInfo {
22
+ /** 宿主进程 id(客户端据此判断「上次那个进程还在不在」) */
23
+ pid: number;
24
+ host: string;
25
+ port: number;
26
+ /** 宿主版本(升级判定要比对磁盘上的版本,PRD D4) */
27
+ version: string;
28
+ /** 是不是本地实例(`true` 只监听回环,`false` 是创建者起的对外实例) */
29
+ local: boolean;
30
+ /** 启动时刻,ISO 8601 */
31
+ startedAt: string;
32
+ }
33
+ /**
34
+ * 写发现文件。
35
+ *
36
+ * 先写同目录的临时文件再 `rename`:读的一方要么看到上一份完整内容,要么看到这一份完整内容,
37
+ * 不会读到「写了一半的 JSON」(客户端探活正是高频读它的那一个)。
38
+ */
39
+ export declare function writeHostFile(paths: HostPaths, info: HostFileInfo): Promise<void>;
40
+ /**
41
+ * 读发现文件。
42
+ *
43
+ * 三种结果分开给:文件不存在 → `null`(还没起过宿主);内容坏了 → 抛 `config_invalid`
44
+ * (「文件不存在」与「文件坏了」必须能分开,PRD F-F6 对配置文件说的是同一件事);
45
+ * 正常 → 解析后的形状。
46
+ *
47
+ * 本函数**不判断**那个 pid 是否还活着:探活是调用方的事(这里只有文件,没有进程表)。
48
+ */
49
+ export declare function readHostFile(paths: HostPaths): Promise<HostFileInfo | null>;
50
+ /**
51
+ * 清掉发现文件(进程收尾时用)。
52
+ *
53
+ * 文件本来就不在时不算失败:收尾动作要能被重复执行,不能因为「已经删过一次」而报错。
54
+ */
55
+ export declare function clearHostFile(paths: HostPaths): Promise<void>;
56
+ /**
57
+ * 写本地凭证交付文件(权限尽力而为地设成 `0600`)。
58
+ *
59
+ * 关于 `0600`:在 POSIX 上它让「同机其他用户」读不到这串凭证;在 Windows 上 `mode` 与
60
+ * `chmod` 都不改变 ACL(那里是空操作),实际保护来自用户目录本身的访问控制。
61
+ * 所以这里是**尽力而为**:设不上不报错,也不构成任何安全保证。
62
+ */
63
+ export declare function writeLocalToken(paths: HostPaths, token: string): Promise<void>;
64
+ /**
65
+ * 读本地凭证交付文件。
66
+ *
67
+ * 文件不存在 → `null`(还没生成过);内容为空 → `null`(等同于没有)。其余读写错误向上抛:
68
+ * 权限问题与「文件不在」是两件事,混成 `null` 会让首次启动悄悄再造一串凭证。
69
+ */
70
+ export declare function readLocalToken(paths: HostPaths): Promise<string | null>;
71
+ /** 清掉本地凭证交付文件(凭证被吊销 / 轮换时顺手做的事) */
72
+ export declare function clearLocalToken(paths: HostPaths): Promise<void>;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * 发现文件与本地凭证交付口(architecture §2)。
3
+ *
4
+ * 两个文件、两种用途:
5
+ *
6
+ * - `<dataRoot>/run/host.json` 是「这台机器上谁在服务」的唯一真相:进程 id、监听的地址与端口、
7
+ * 版本、是不是本地实例、启动时刻。客户端探活与宿主启动时自查都读它,两边不各写一份。
8
+ * - `<dataRoot>/run/token` 是**本地凭证的交付口**:内容只有本地那串凭证,客户端首次连接时
9
+ * 自动取走。凭证自己的真相在库里(可列出、带备注、可单独吊销),这里只是一份「给本机进程读」的副本。
10
+ *
11
+ * 目录由本文件按需创建:`run/` 与 `assets/` 都可能在首次启动时还不存在。
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { promises as fs } from "node:fs";
15
+ import { basename, dirname, join } from "node:path";
16
+ import { HostError } from "../wire/errors.js";
17
+ /**
18
+ * 写发现文件。
19
+ *
20
+ * 先写同目录的临时文件再 `rename`:读的一方要么看到上一份完整内容,要么看到这一份完整内容,
21
+ * 不会读到「写了一半的 JSON」(客户端探活正是高频读它的那一个)。
22
+ */
23
+ export async function writeHostFile(paths, info) {
24
+ await fs.mkdir(paths.runDir, { recursive: true });
25
+ const content = `${JSON.stringify(info, null, 2)}\n`;
26
+ await writeAtomic(paths.hostFile, content);
27
+ }
28
+ /**
29
+ * 读发现文件。
30
+ *
31
+ * 三种结果分开给:文件不存在 → `null`(还没起过宿主);内容坏了 → 抛 `config_invalid`
32
+ * (「文件不存在」与「文件坏了」必须能分开,PRD F-F6 对配置文件说的是同一件事);
33
+ * 正常 → 解析后的形状。
34
+ *
35
+ * 本函数**不判断**那个 pid 是否还活着:探活是调用方的事(这里只有文件,没有进程表)。
36
+ */
37
+ export async function readHostFile(paths) {
38
+ let text;
39
+ try {
40
+ text = await fs.readFile(paths.hostFile, "utf8");
41
+ }
42
+ catch (error) {
43
+ if (isNotFound(error))
44
+ return null;
45
+ throw error;
46
+ }
47
+ let value;
48
+ try {
49
+ value = JSON.parse(text);
50
+ }
51
+ catch (error) {
52
+ throw brokenHostFile(paths.hostFile, "it is not valid JSON", error);
53
+ }
54
+ return parseHostFileInfo(value, paths.hostFile);
55
+ }
56
+ /**
57
+ * 清掉发现文件(进程收尾时用)。
58
+ *
59
+ * 文件本来就不在时不算失败:收尾动作要能被重复执行,不能因为「已经删过一次」而报错。
60
+ */
61
+ export async function clearHostFile(paths) {
62
+ try {
63
+ await fs.unlink(paths.hostFile);
64
+ }
65
+ catch (error) {
66
+ if (isNotFound(error))
67
+ return;
68
+ throw error;
69
+ }
70
+ }
71
+ /**
72
+ * 写本地凭证交付文件(权限尽力而为地设成 `0600`)。
73
+ *
74
+ * 关于 `0600`:在 POSIX 上它让「同机其他用户」读不到这串凭证;在 Windows 上 `mode` 与
75
+ * `chmod` 都不改变 ACL(那里是空操作),实际保护来自用户目录本身的访问控制。
76
+ * 所以这里是**尽力而为**:设不上不报错,也不构成任何安全保证。
77
+ */
78
+ export async function writeLocalToken(paths, token) {
79
+ await fs.mkdir(paths.runDir, { recursive: true });
80
+ await fs.writeFile(paths.tokenFile, `${token}\n`, { mode: 0o600 });
81
+ // 文件已存在时 `mode` 不生效(它只作用于创建),所以显式再设一次;失败按「尽力而为」忽略
82
+ await fs.chmod(paths.tokenFile, 0o600).catch(() => undefined);
83
+ }
84
+ /**
85
+ * 读本地凭证交付文件。
86
+ *
87
+ * 文件不存在 → `null`(还没生成过);内容为空 → `null`(等同于没有)。其余读写错误向上抛:
88
+ * 权限问题与「文件不在」是两件事,混成 `null` 会让首次启动悄悄再造一串凭证。
89
+ */
90
+ export async function readLocalToken(paths) {
91
+ let text;
92
+ try {
93
+ text = await fs.readFile(paths.tokenFile, "utf8");
94
+ }
95
+ catch (error) {
96
+ if (isNotFound(error))
97
+ return null;
98
+ throw error;
99
+ }
100
+ const token = text.trim();
101
+ return token === "" ? null : token;
102
+ }
103
+ /** 清掉本地凭证交付文件(凭证被吊销 / 轮换时顺手做的事) */
104
+ export async function clearLocalToken(paths) {
105
+ await fs.rm(paths.tokenFile, { force: true });
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // 内部
109
+ // ---------------------------------------------------------------------------
110
+ function isNotFound(error) {
111
+ return typeof error === "object" && error !== null && error.code === "ENOENT";
112
+ }
113
+ function brokenHostFile(filePath, reason, cause) {
114
+ return new HostError("config_invalid", {
115
+ params: { reason },
116
+ detail: { path: filePath, reason },
117
+ cause,
118
+ });
119
+ }
120
+ /**
121
+ * 校验发现文件的形状。
122
+ *
123
+ * 逐字段检查而不是整块断言:坏文件要能指出是哪一项不对(客户端/运维排查时唯一有用的信息)。
124
+ */
125
+ function parseHostFileInfo(value, filePath) {
126
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
127
+ throw brokenHostFile(filePath, "it is not a JSON object");
128
+ }
129
+ const record = value;
130
+ const pid = record.pid;
131
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) {
132
+ throw brokenHostFile(filePath, "`pid` is not a process id");
133
+ }
134
+ const host = record.host;
135
+ if (typeof host !== "string" || host.trim() === "") {
136
+ throw brokenHostFile(filePath, "`host` is missing");
137
+ }
138
+ const port = record.port;
139
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 0 || port > 65535) {
140
+ throw brokenHostFile(filePath, "`port` is not a port number");
141
+ }
142
+ const version = record.version;
143
+ if (typeof version !== "string" || version.trim() === "") {
144
+ throw brokenHostFile(filePath, "`version` is missing");
145
+ }
146
+ const local = record.local;
147
+ if (typeof local !== "boolean") {
148
+ throw brokenHostFile(filePath, "`local` is not a boolean");
149
+ }
150
+ const startedAt = record.startedAt;
151
+ if (typeof startedAt !== "string" || startedAt.trim() === "") {
152
+ throw brokenHostFile(filePath, "`startedAt` is missing");
153
+ }
154
+ return { pid, host, port, version, local, startedAt };
155
+ }
156
+ /**
157
+ * 原子写:先写同目录临时文件,再改名。
158
+ *
159
+ * 临时文件放在目标同目录,`rename` 才是同一个文件系统内的改名(跨设备的改名会退化成复制)。
160
+ */
161
+ async function writeAtomic(filePath, content) {
162
+ const temp = join(dirname(filePath), `.${basename(filePath)}.${process.pid}.${randomUUID()}.tmp`);
163
+ try {
164
+ await fs.writeFile(temp, content, { mode: 0o600 });
165
+ await fs.rename(temp, filePath);
166
+ }
167
+ catch (error) {
168
+ await fs.rm(temp, { force: true }).catch(() => undefined);
169
+ throw error;
170
+ }
171
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 接线层出口:发现文件、凭证、上传接收、WebSocket 总线。
3
+ *
4
+ * 这一层只做「进程与外界的边界」:握手、分发、推送、二进制分帧。
5
+ * 业务逻辑在 `services/` 与 `runtime/`,由单根(`app.ts`)注入进来。
6
+ */
7
+ export { clearLocalToken, clearHostFile, readLocalToken, readHostFile, writeLocalToken, writeHostFile, } from "./discovery.js";
8
+ export type { HostFileInfo } from "./discovery.js";
9
+ export { createLocalCredential, createTokenVerifier, LOCAL_CREDENTIAL_NOTE, verify } from "./auth.js";
10
+ export type { LocalCredentialOptions, LocalCredentialResult, TokenVerifier, VerifiedCredential } from "./auth.js";
11
+ export { decodeChunkFrame, encodeChunkFrame, formatBytes, UploadReceiver } from "./UploadReceiver.js";
12
+ export type { UploadFinalizeRequest, UploadFinalizer, UploadReceiverOptions, } from "./UploadReceiver.js";
13
+ export { WsHub } from "./WsHub.js";
14
+ export type { ConnectionInfo, EventLocalizer, HandlerTable, HubLogger, MethodCallContext, MethodHandler, WsHubOptions, } from "./WsHub.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 接线层出口:发现文件、凭证、上传接收、WebSocket 总线。
3
+ *
4
+ * 这一层只做「进程与外界的边界」:握手、分发、推送、二进制分帧。
5
+ * 业务逻辑在 `services/` 与 `runtime/`,由单根(`app.ts`)注入进来。
6
+ */
7
+ export { clearLocalToken, clearHostFile, readLocalToken, readHostFile, writeLocalToken, writeHostFile, } from "./discovery.js";
8
+ export { createLocalCredential, createTokenVerifier, LOCAL_CREDENTIAL_NOTE, verify } from "./auth.js";
9
+ export { decodeChunkFrame, encodeChunkFrame, formatBytes, UploadReceiver } from "./UploadReceiver.js";
10
+ export { WsHub } from "./WsHub.js";