@jingyi0605/codingns4dsh 0.1.2 → 0.1.7-rc.2

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 (171) hide show
  1. package/README.en.md +4 -4
  2. package/README.md +14 -5
  3. package/data/build/dist/client/account-bar.js +57 -3
  4. package/data/build/dist/client/bundle.js +36246 -268
  5. package/data/build/dist/client/cli-slots.js +60 -18
  6. package/data/build/dist/client/dsh-h5-bootstrap.d.ts +2 -0
  7. package/data/build/dist/client/dsh-h5-bootstrap.js +79 -13
  8. package/data/build/dist/client/features/file-management-panel.d.ts +5 -0
  9. package/data/build/dist/client/features/file-management-panel.js +48 -0
  10. package/data/build/dist/client/features/file-management.d.ts +4 -0
  11. package/data/build/dist/client/features/file-management.js +50 -0
  12. package/data/build/dist/client/features/index.d.ts +10 -1
  13. package/data/build/dist/client/features/index.js +12 -1
  14. package/data/build/dist/client/features/login-protection-panel.js +1 -1
  15. package/data/build/dist/client/features/login-protection-session.d.ts +2 -0
  16. package/data/build/dist/client/features/login-protection-session.js +18 -0
  17. package/data/build/dist/client/features/peer-host.d.ts +4 -0
  18. package/data/build/dist/client/features/peer-host.js +89 -0
  19. package/data/build/dist/client/features/reverse-proxy-panel.js +23 -7
  20. package/data/build/dist/client/features/reverse-proxy.d.ts +1 -2
  21. package/data/build/dist/client/features/reverse-proxy.js +75 -5
  22. package/data/build/dist/client/features/types.d.ts +9 -0
  23. package/data/build/dist/client/features/workspace-session-enhancement-panel.js +10 -0
  24. package/data/build/dist/client/features/workspace-session-enhancement.js +18 -1
  25. package/data/build/dist/client/file-management-dom.d.ts +12 -0
  26. package/data/build/dist/client/file-management-dom.js +527 -0
  27. package/data/build/dist/client/git-management.js +24 -5
  28. package/data/build/dist/client/host-navigation.d.ts +29 -0
  29. package/data/build/dist/client/host-navigation.js +29 -0
  30. package/data/build/dist/client/host-router.d.ts +3 -0
  31. package/data/build/dist/client/host-router.js +2 -0
  32. package/data/build/dist/client/index.d.ts +6 -1
  33. package/data/build/dist/client/index.js +30 -10
  34. package/data/build/dist/client/locale.js +36 -7
  35. package/data/build/dist/client/peer-host-connection-button.d.ts +13 -0
  36. package/data/build/dist/client/peer-host-connection-button.js +69 -0
  37. package/data/build/dist/client/peer-host-management-api.d.ts +40 -0
  38. package/data/build/dist/client/peer-host-management-api.js +33 -0
  39. package/data/build/dist/client/peer-host-management-panel.d.ts +13 -0
  40. package/data/build/dist/client/peer-host-management-panel.js +181 -0
  41. package/data/build/dist/client/peer-host-native-session-ui.d.ts +60 -0
  42. package/data/build/dist/client/peer-host-native-session-ui.js +372 -0
  43. package/data/build/dist/client/peer-host-scoped-client.d.ts +67 -0
  44. package/data/build/dist/client/peer-host-scoped-client.js +316 -0
  45. package/data/build/dist/client/peer-host-session-controller.d.ts +30 -0
  46. package/data/build/dist/client/peer-host-session-controller.js +95 -0
  47. package/data/build/dist/client/quick-phrase-slot.js +14 -0
  48. package/data/build/dist/client/remote-web-context.js +26 -4
  49. package/data/build/dist/client/session-changed-files-view.d.ts +15 -0
  50. package/data/build/dist/client/session-changed-files-view.js +337 -0
  51. package/data/build/dist/client/settings-bridge.d.ts +14 -4
  52. package/data/build/dist/client/settings-bridge.js +59 -12
  53. package/data/build/dist/client/settings-section.js +17 -4
  54. package/data/build/dist/client/subscription-slot.js +21 -8
  55. package/data/build/dist/client/workspace-session-rightbar-dom.d.ts +28 -0
  56. package/data/build/dist/client/workspace-session-rightbar-dom.js +291 -0
  57. package/data/build/dist/dsh-capabilities/client/config-forms-adapter.js +25 -33
  58. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.d.ts +7 -1
  59. package/data/build/dist/dsh-capabilities/client/settings-scope-adapter.js +34 -11
  60. package/data/build/dist/dsh-capabilities/matrix.js +9 -0
  61. package/data/build/dist/dsh-capabilities/routes.js +35 -0
  62. package/data/build/dist/dsh-capabilities/settings-store.d.ts +12 -0
  63. package/data/build/dist/dsh-capabilities/settings-store.js +34 -0
  64. package/data/build/dist/dsh-capabilities/types.d.ts +1 -1
  65. package/data/build/dist/features/host-router.d.ts +26 -0
  66. package/data/build/dist/features/host-router.js +110 -0
  67. package/data/build/dist/features/registry.js +3 -0
  68. package/data/build/dist/host/cli-adapters/codex-driver.d.ts +13 -1
  69. package/data/build/dist/host/cli-adapters/codex-driver.js +528 -72
  70. package/data/build/dist/host/cli-adapters/command-code-driver.d.ts +21 -2
  71. package/data/build/dist/host/cli-adapters/command-code-driver.js +408 -105
  72. package/data/build/dist/host/cli-adapters/driver.d.ts +2 -0
  73. package/data/build/dist/host/cli-adapters/dsh-message-projector.d.ts +2 -0
  74. package/data/build/dist/host/cli-adapters/dsh-message-projector.js +47 -28
  75. package/data/build/dist/host/cli-adapters/dsh-tool-history.js +37 -0
  76. package/data/build/dist/host/cli-adapters/feature.js +94 -10
  77. package/data/build/dist/host/cli-adapters/json-rpc-process.d.ts +6 -0
  78. package/data/build/dist/host/cli-adapters/json-rpc-process.js +14 -3
  79. package/data/build/dist/host/cli-adapters/kimi-driver.js +3 -3
  80. package/data/build/dist/host/cli-adapters/model-catalog.d.ts +6 -0
  81. package/data/build/dist/host/cli-adapters/model-catalog.js +8 -0
  82. package/data/build/dist/host/cli-adapters/opencode-driver.js +14 -2
  83. package/data/build/dist/host/cli-adapters/pi-driver.js +2 -2
  84. package/data/build/dist/host/cli-adapters/process-utils.d.ts +8 -0
  85. package/data/build/dist/host/cli-adapters/process-utils.js +48 -0
  86. package/data/build/dist/host/cli-adapters/registry.d.ts +6 -1
  87. package/data/build/dist/host/cli-adapters/registry.js +41 -7
  88. package/data/build/dist/host/cli-adapters/rpc-driver-utils.js +23 -4
  89. package/data/build/dist/host/cli-adapters/standard-stream-driver.d.ts +3 -0
  90. package/data/build/dist/host/cli-adapters/standard-stream-driver.js +36 -15
  91. package/data/build/dist/host/dsh-device-runtime.d.ts +2 -0
  92. package/data/build/dist/host/dsh-device-runtime.js +55 -29
  93. package/data/build/dist/host/dsh-runtime-version.d.ts +3 -2
  94. package/data/build/dist/host/dsh-runtime-version.js +71 -2
  95. package/data/build/dist/host/features/auth.js +10 -2
  96. package/data/build/dist/host/features/file-management.d.ts +5 -0
  97. package/data/build/dist/host/features/file-management.js +267 -0
  98. package/data/build/dist/host/features/git-management.js +24 -2
  99. package/data/build/dist/host/features/index.d.ts +2 -0
  100. package/data/build/dist/host/features/index.js +6 -0
  101. package/data/build/dist/host/features/peer-host.d.ts +22 -0
  102. package/data/build/dist/host/features/peer-host.js +322 -0
  103. package/data/build/dist/host/features/types.d.ts +4 -0
  104. package/data/build/dist/host/host-router.d.ts +3 -0
  105. package/data/build/dist/host/host-router.js +2 -0
  106. package/data/build/dist/host/index-injection.d.ts +17 -0
  107. package/data/build/dist/host/index-injection.js +24 -0
  108. package/data/build/dist/host/index.d.ts +4 -1
  109. package/data/build/dist/host/index.js +41 -7
  110. package/data/build/dist/host/lan-access-dsh.d.ts +34 -1
  111. package/data/build/dist/host/lan-access-dsh.js +145 -26
  112. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.d.ts +52 -0
  113. package/data/build/dist/host/modules/peer-host/host-api-proxy-service.js +166 -0
  114. package/data/build/dist/host/modules/peer-host/host-ws-connector.d.ts +31 -0
  115. package/data/build/dist/host/modules/peer-host/host-ws-connector.js +108 -0
  116. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.d.ts +26 -0
  117. package/data/build/dist/host/modules/peer-host/host-ws-proxy-service.js +159 -0
  118. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.d.ts +50 -0
  119. package/data/build/dist/host/modules/peer-host/peer-host-aggregate-service.js +97 -0
  120. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.d.ts +19 -0
  121. package/data/build/dist/host/modules/peer-host/peer-host-diagnostics.js +54 -0
  122. package/data/build/dist/host/modules/peer-host/peer-host-handshake.d.ts +44 -0
  123. package/data/build/dist/host/modules/peer-host/peer-host-handshake.js +133 -0
  124. package/data/build/dist/host/modules/peer-host/peer-host-relay.d.ts +65 -0
  125. package/data/build/dist/host/modules/peer-host/peer-host-relay.js +196 -0
  126. package/data/build/dist/host/modules/peer-host/peer-host-session.d.ts +42 -0
  127. package/data/build/dist/host/modules/peer-host/peer-host-session.js +152 -0
  128. package/data/build/dist/host/modules/peer-host/peer-host-store.d.ts +92 -0
  129. package/data/build/dist/host/modules/peer-host/peer-host-store.js +323 -0
  130. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.d.ts +34 -0
  131. package/data/build/dist/host/modules/peer-host/peer-host-ws-gateway.js +156 -0
  132. package/data/build/dist/host/native-session-bridge.d.ts +18 -2
  133. package/data/build/dist/host/native-session-bridge.js +232 -8
  134. package/data/build/dist/host/remote-web-runtime.js +26 -1
  135. package/data/build/dist/host/rpc.js +8 -3
  136. package/data/build/dist/host/session-changes.d.ts +5 -0
  137. package/data/build/dist/host/session-changes.js +163 -0
  138. package/data/build/dist/host/settings.js +6 -0
  139. package/data/build/dist/index.d.ts +1 -1
  140. package/data/build/dist/index.js +1 -1
  141. package/data/build/dist/shared/contracts/cli-adapter.d.ts +23 -2
  142. package/data/build/dist/shared/contracts/config.d.ts +15 -0
  143. package/data/build/dist/shared/contracts/config.js +10 -0
  144. package/data/build/dist/shared/contracts/dsh-device.d.ts +2 -0
  145. package/data/build/dist/shared/contracts/errors.d.ts +14 -0
  146. package/data/build/dist/shared/contracts/errors.js +15 -0
  147. package/data/build/dist/shared/contracts/feature.d.ts +2 -0
  148. package/data/build/dist/shared/contracts/file-management.d.ts +6 -0
  149. package/data/build/dist/shared/contracts/file-management.js +3 -0
  150. package/data/build/dist/shared/contracts/peer-host.d.ts +116 -15
  151. package/data/build/dist/shared/contracts/peer-host.js +19 -0
  152. package/data/build/dist/shared/contracts/version.d.ts +1 -1
  153. package/data/build/dist/shared/contracts/version.js +1 -1
  154. package/data/build/dist/shared/index.d.ts +4 -3
  155. package/data/build/dist/shared/index.js +2 -2
  156. package/data/build/dist/transport/debug.d.ts +5 -0
  157. package/data/build/dist/transport/debug.js +23 -1
  158. package/data/build/dist/transport/dsh-transport.d.ts +2 -0
  159. package/data/build/dist/transport/dsh-transport.js +11 -0
  160. package/data/build/dist/transport/webrtc-client.d.ts +2 -0
  161. package/data/build/dist/transport/webrtc-client.js +2 -1
  162. package/data/build/dist/transport/webrtc-host.d.ts +2 -0
  163. package/data/build/dist/transport/webrtc-host.js +6 -3
  164. package/data/build/dist/typert.host.d.ts +1 -1
  165. package/data/build/dist/typert.host.js +2 -1
  166. package/dsh.bundle.patch +10 -0
  167. package/package.json +19 -11
  168. package/profile/README.md +2 -2
  169. package/profile/package.json +2 -2
  170. package/profile/version.json +1 -1
  171. package/version.json +1 -1
@@ -0,0 +1,133 @@
1
+ import { PEER_HOST_ERROR_CODES } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostStore, } from './peer-host-store.js';
3
+ export const PEER_HOST_HANDSHAKE_PATH = '/api/public/host-handshake';
4
+ /** 目标 Host 握手协调器;只把脱敏结果写回 PeerHostRecord。 */
5
+ export class PeerHostHandshakeService {
6
+ store;
7
+ credentials;
8
+ options;
9
+ fetchImpl;
10
+ timeoutMs;
11
+ now;
12
+ constructor(store, credentials, options) {
13
+ this.store = store;
14
+ this.credentials = credentials;
15
+ this.options = options;
16
+ this.fetchImpl = options.fetchImpl ?? fetch;
17
+ this.timeoutMs = options.timeoutMs ?? 5_000;
18
+ this.now = options.now ?? Date.now;
19
+ }
20
+ async check(peerHostId) {
21
+ const current = await this.requireRecord(peerHostId);
22
+ await this.store.updateHandshake(peerHostId, this.emptyUpdate('checking'));
23
+ try {
24
+ const payload = await this.fetchHandshake(current.route);
25
+ const update = this.resolve(current, payload);
26
+ if (update.status === 'identity_changed')
27
+ await this.credentials.clear(peerHostId);
28
+ return await this.store.updateHandshake(peerHostId, update);
29
+ }
30
+ catch (error) {
31
+ const code = errorCode(error, PEER_HOST_ERROR_CODES.UNREACHABLE);
32
+ return this.store.updateHandshake(peerHostId, {
33
+ ...this.emptyUpdate('unreachable'),
34
+ lastErrorCode: code,
35
+ });
36
+ }
37
+ }
38
+ async fetchHandshake(route) {
39
+ if (route.kind !== 'lan')
40
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE, '中转 PeerHost 暂不可用');
41
+ const controller = new AbortController();
42
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
43
+ try {
44
+ const response = await this.fetchImpl(new URL(PEER_HOST_HANDSHAKE_PATH, route.normalizedOrigin), { method: 'GET', signal: controller.signal, headers: { accept: 'application/json' } });
45
+ if (!response.ok)
46
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.UNREACHABLE, `握手请求失败: ${response.status}`);
47
+ let value;
48
+ try {
49
+ value = await response.json();
50
+ }
51
+ catch {
52
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '握手响应不是 JSON');
53
+ }
54
+ if (!isHandshakePayload(value))
55
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '握手响应字段无效');
56
+ return value;
57
+ }
58
+ catch (error) {
59
+ if (error instanceof HandshakeError)
60
+ throw error;
61
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.UNREACHABLE, '目标 Host 不可达');
62
+ }
63
+ finally {
64
+ clearTimeout(timer);
65
+ }
66
+ }
67
+ resolve(current, payload) {
68
+ const common = {
69
+ pluginId: payload.pluginId,
70
+ pluginVersion: payload.pluginVersion,
71
+ dshVersion: payload.dshVersion,
72
+ apiCompatibility: payload.apiCompatibility,
73
+ fingerprint: payload.fingerprint,
74
+ lastCheckedAt: this.now(),
75
+ lastErrorCode: null,
76
+ };
77
+ if (payload.productId !== this.options.productId || payload.pluginId !== this.options.pluginId) {
78
+ return { ...common, status: 'plugin_missing', lastErrorCode: PEER_HOST_ERROR_CODES.PLUGIN_MISSING };
79
+ }
80
+ if (payload.pluginVersion !== this.options.pluginVersion || !this.options.isDshVersionSupported(payload.dshVersion) || payload.apiCompatibility !== this.options.apiCompatibility) {
81
+ return { ...common, status: 'version_mismatch', lastErrorCode: PEER_HOST_ERROR_CODES.VERSION_MISMATCH };
82
+ }
83
+ if (current.fingerprint !== null && payload.fingerprint !== current.fingerprint) {
84
+ return { ...common, status: 'identity_changed', lastErrorCode: PEER_HOST_ERROR_CODES.IDENTITY_CHANGED };
85
+ }
86
+ return { ...common, status: 'ready' };
87
+ }
88
+ emptyUpdate(status) {
89
+ return {
90
+ status,
91
+ pluginId: null,
92
+ pluginVersion: null,
93
+ dshVersion: null,
94
+ apiCompatibility: null,
95
+ fingerprint: null,
96
+ lastCheckedAt: this.now(),
97
+ lastErrorCode: null,
98
+ };
99
+ }
100
+ async requireRecord(peerHostId) {
101
+ const record = await this.store.get(peerHostId);
102
+ if (record === null)
103
+ throw new HandshakeError(PEER_HOST_ERROR_CODES.NOT_FOUND, 'PeerHost 不存在');
104
+ return record;
105
+ }
106
+ }
107
+ export class HandshakeError extends Error {
108
+ code;
109
+ constructor(code, message) {
110
+ super(message);
111
+ this.code = code;
112
+ this.name = 'HandshakeError';
113
+ }
114
+ }
115
+ function isHandshakePayload(value) {
116
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
117
+ return false;
118
+ const input = value;
119
+ return typeof input.productId === 'string'
120
+ && (typeof input.pluginId === 'string' || input.pluginId === null)
121
+ && (typeof input.pluginVersion === 'string' || input.pluginVersion === null)
122
+ && typeof input.dshVersion === 'string'
123
+ && typeof input.apiCompatibility === 'string'
124
+ && (typeof input.fingerprint === 'string' || input.fingerprint === null)
125
+ && Array.isArray(input.capabilities)
126
+ && input.capabilities.every((item) => typeof item === 'string');
127
+ }
128
+ function errorCode(error, fallback) {
129
+ if (error instanceof HandshakeError)
130
+ return error.code;
131
+ return fallback;
132
+ }
133
+ //# sourceMappingURL=peer-host-handshake.js.map
@@ -0,0 +1,65 @@
1
+ import type { HostScope, PeerHostRecord } from '../../../shared/contracts/peer-host.js';
2
+ import type { PeerHostRemoteConnector, PeerHostSocket } from './host-ws-proxy-service.js';
3
+ /** 已验证的 Host 侧 Relay 适配器;适配器内部必须复用现有 Transport。 */
4
+ export interface PeerHostRelayTransportFactory {
5
+ readonly transportVersion: string;
6
+ readonly open: (input: {
7
+ readonly record: PeerHostRecord;
8
+ readonly scope: HostScope;
9
+ /** 只在当前调用栈传递,禁止写入持久状态。 */
10
+ readonly accessToken: string;
11
+ }) => Promise<PeerHostSocket>;
12
+ }
13
+ export interface PeerHostRelayConnectorOptions {
14
+ readonly transport?: PeerHostRelayTransportFactory;
15
+ }
16
+ /**
17
+ * 构造中转 connector。没有经过 Host 侧 Transport 验证的实现一律保持不可用。
18
+ * relayEntryId/deviceId 只作为稳定路由标识传给适配器,不会被解释为 URL 或 ticket。
19
+ */
20
+ export declare function createPeerHostRelayConnector(options?: PeerHostRelayConnectorOptions): PeerHostRemoteConnector;
21
+ export type PeerHostReconnectState = 'idle' | 'connecting' | 'ready' | 'reconnecting' | 'unreachable' | 'stopped' | 'relay_unavailable';
22
+ export interface PeerHostReconnectSnapshot {
23
+ readonly peerHostId: string;
24
+ readonly state: PeerHostReconnectState;
25
+ readonly attempts: number;
26
+ readonly generation: number;
27
+ /** 重连成功后供上层重建订阅的完整作用域快照。 */
28
+ readonly scope: HostScope;
29
+ readonly lastErrorCode: string | null;
30
+ }
31
+ export interface PeerHostReconnectManagerOptions {
32
+ readonly connect: PeerHostRemoteConnector;
33
+ readonly onState?: (snapshot: PeerHostReconnectSnapshot) => void | Promise<void>;
34
+ readonly initialDelayMs?: number;
35
+ readonly maxDelayMs?: number;
36
+ readonly maxAttempts?: number;
37
+ readonly setTimer?: (callback: () => void, delayMs: number) => ReturnType<typeof setTimeout>;
38
+ readonly clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
39
+ }
40
+ /**
41
+ * 单个 PeerHost 的有界断线恢复状态机。ticket/token 只存在 Entry 内存,close 后立即释放。
42
+ * 重连成功会递增 generation;调用方必须用新快照重新建立 HostScope 订阅。
43
+ */
44
+ export declare class PeerHostReconnectManager {
45
+ private readonly options;
46
+ private readonly entries;
47
+ private readonly setTimer;
48
+ private readonly clearTimer;
49
+ private readonly initialDelayMs;
50
+ private readonly maxDelayMs;
51
+ private readonly maxAttempts;
52
+ private closed;
53
+ constructor(options: PeerHostReconnectManagerOptions);
54
+ connect(record: PeerHostRecord, accessToken: string, scope: HostScope): Promise<PeerHostSocket>;
55
+ snapshot(peerHostId: string): PeerHostReconnectSnapshot | null;
56
+ close(peerHostId?: string): Promise<void>;
57
+ private attach;
58
+ private onClosed;
59
+ private markFailure;
60
+ private schedule;
61
+ private reconnect;
62
+ private stopEntry;
63
+ private publish;
64
+ }
65
+ //# sourceMappingURL=peer-host-relay.d.ts.map
@@ -0,0 +1,196 @@
1
+ import { PEER_HOST_ERROR_CODES } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostConnectorError } from './host-ws-connector.js';
3
+ /**
4
+ * 构造中转 connector。没有经过 Host 侧 Transport 验证的实现一律保持不可用。
5
+ * relayEntryId/deviceId 只作为稳定路由标识传给适配器,不会被解释为 URL 或 ticket。
6
+ */
7
+ export function createPeerHostRelayConnector(options = {}) {
8
+ return async (record, accessToken, scope) => {
9
+ if (record.route.kind !== 'relay') {
10
+ throw new PeerHostConnectorError('PEER_HOST_PROXY_UNREACHABLE', 'PeerHost relay connector 收到非中转路由');
11
+ }
12
+ assertScope(scope, record.id);
13
+ const transport = options.transport;
14
+ if (transport === undefined || transport.transportVersion.trim() === '' || transport.transportVersion !== record.route.transportVersion) {
15
+ throw new PeerHostConnectorError('PEER_HOST_RELAY_UNAVAILABLE', '中转 PeerHost 尚未具备已验证的 Host Transport');
16
+ }
17
+ if (accessToken.trim() === '')
18
+ throw new PeerHostConnectorError('PEER_HOST_SESSION_REQUIRED', '目标 Host 登录态为空');
19
+ try {
20
+ return await transport.open({ record, scope, accessToken });
21
+ }
22
+ catch (error) {
23
+ if (error instanceof PeerHostConnectorError)
24
+ throw error;
25
+ throw new PeerHostConnectorError('PEER_HOST_PROXY_UNREACHABLE', '中转 PeerHost 连接失败');
26
+ }
27
+ };
28
+ }
29
+ /**
30
+ * 单个 PeerHost 的有界断线恢复状态机。ticket/token 只存在 Entry 内存,close 后立即释放。
31
+ * 重连成功会递增 generation;调用方必须用新快照重新建立 HostScope 订阅。
32
+ */
33
+ export class PeerHostReconnectManager {
34
+ options;
35
+ entries = new Map();
36
+ setTimer;
37
+ clearTimer;
38
+ initialDelayMs;
39
+ maxDelayMs;
40
+ maxAttempts;
41
+ closed = false;
42
+ constructor(options) {
43
+ this.options = options;
44
+ this.setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
45
+ this.clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
46
+ this.initialDelayMs = bounded(options.initialDelayMs ?? 250, 10, 60_000);
47
+ this.maxDelayMs = bounded(options.maxDelayMs ?? 10_000, this.initialDelayMs, 120_000);
48
+ this.maxAttempts = bounded(options.maxAttempts ?? 5, 0, 100);
49
+ }
50
+ async connect(record, accessToken, scope) {
51
+ if (this.closed)
52
+ throw new PeerHostConnectorError('PEER_HOST_PROXY_UNREACHABLE', 'PeerHost reconnect manager 已关闭');
53
+ assertScope(scope, record.id);
54
+ const previous = this.entries.get(record.id);
55
+ if (previous)
56
+ this.stopEntry(previous);
57
+ const entry = {
58
+ record,
59
+ accessToken,
60
+ scope,
61
+ socket: null,
62
+ timer: null,
63
+ state: 'connecting',
64
+ attempts: 0,
65
+ generation: scope.scopeGeneration,
66
+ lastErrorCode: null,
67
+ stopped: false,
68
+ };
69
+ this.entries.set(record.id, entry);
70
+ await this.publish(entry);
71
+ try {
72
+ const socket = await this.options.connect(record, accessToken, scope);
73
+ this.attach(entry, socket);
74
+ entry.state = 'ready';
75
+ entry.attempts = 0;
76
+ entry.lastErrorCode = null;
77
+ await this.publish(entry);
78
+ return socket;
79
+ }
80
+ catch (error) {
81
+ this.markFailure(entry, error);
82
+ this.schedule(entry);
83
+ throw error;
84
+ }
85
+ }
86
+ snapshot(peerHostId) {
87
+ const entry = this.entries.get(peerHostId);
88
+ return entry ? toSnapshot(entry) : null;
89
+ }
90
+ async close(peerHostId) {
91
+ if (peerHostId === undefined) {
92
+ this.closed = true;
93
+ for (const entry of this.entries.values())
94
+ this.stopEntry(entry);
95
+ this.entries.clear();
96
+ return;
97
+ }
98
+ const entry = this.entries.get(peerHostId);
99
+ if (!entry)
100
+ return;
101
+ this.stopEntry(entry);
102
+ this.entries.delete(peerHostId);
103
+ }
104
+ attach(entry, socket) {
105
+ entry.socket = socket;
106
+ socket.on('close', () => this.onClosed(entry));
107
+ socket.on('error', () => this.onClosed(entry));
108
+ }
109
+ onClosed(entry) {
110
+ if (entry.stopped || this.closed || entry.socket === null)
111
+ return;
112
+ entry.socket = null;
113
+ entry.state = 'reconnecting';
114
+ this.schedule(entry);
115
+ void this.publish(entry);
116
+ }
117
+ markFailure(entry, error) {
118
+ entry.attempts += 1;
119
+ entry.lastErrorCode = error instanceof PeerHostConnectorError ? error.code : PEER_HOST_ERROR_CODES.PROXY_UNREACHABLE;
120
+ entry.state = entry.lastErrorCode === PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE ? 'relay_unavailable' : 'unreachable';
121
+ }
122
+ schedule(entry) {
123
+ if (entry.stopped || this.closed || entry.timer !== null)
124
+ return;
125
+ if (entry.lastErrorCode === PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE) {
126
+ entry.state = 'relay_unavailable';
127
+ void this.publish(entry);
128
+ return;
129
+ }
130
+ if (this.maxAttempts === 0 || entry.attempts >= this.maxAttempts) {
131
+ entry.state = entry.lastErrorCode === PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE ? 'relay_unavailable' : 'unreachable';
132
+ void this.publish(entry);
133
+ return;
134
+ }
135
+ const delay = Math.min(this.maxDelayMs, this.initialDelayMs * 2 ** Math.max(0, entry.attempts - 1));
136
+ entry.timer = this.setTimer(() => {
137
+ entry.timer = null;
138
+ if (entry.stopped || this.closed)
139
+ return;
140
+ void this.reconnect(entry);
141
+ }, delay);
142
+ void this.publish(entry);
143
+ }
144
+ async reconnect(entry) {
145
+ if (entry.stopped || this.closed)
146
+ return;
147
+ entry.state = 'reconnecting';
148
+ await this.publish(entry);
149
+ try {
150
+ const socket = await this.options.connect(entry.record, entry.accessToken, { ...entry.scope, scopeGeneration: ++entry.generation });
151
+ if (entry.stopped || this.closed) {
152
+ socket.close(1000, 'PeerHost reconnect manager 已关闭');
153
+ return;
154
+ }
155
+ entry.scope = { ...entry.scope, scopeGeneration: entry.generation };
156
+ this.attach(entry, socket);
157
+ entry.state = 'ready';
158
+ entry.attempts = 0;
159
+ entry.lastErrorCode = null;
160
+ await this.publish(entry);
161
+ }
162
+ catch (error) {
163
+ this.markFailure(entry, error);
164
+ this.schedule(entry);
165
+ }
166
+ }
167
+ stopEntry(entry) {
168
+ entry.stopped = true;
169
+ entry.state = 'stopped';
170
+ if (entry.timer !== null)
171
+ this.clearTimer(entry.timer);
172
+ entry.timer = null;
173
+ const socket = entry.socket;
174
+ entry.socket = null;
175
+ if (socket?.readyState === 1)
176
+ socket.close(1000, 'PeerHost reconnect manager 已清理');
177
+ void this.publish(entry);
178
+ }
179
+ async publish(entry) {
180
+ await this.options.onState?.(toSnapshot(entry));
181
+ }
182
+ }
183
+ function assertScope(scope, peerHostId) {
184
+ if (scope.targetHostId !== peerHostId || scope.hostId.trim() === '' || scope.workspaceId.trim() === '' || !Number.isSafeInteger(scope.scopeGeneration) || scope.scopeGeneration < 0 || (scope.sessionId !== null && scope.sessionId.trim() === '')) {
185
+ throw new PeerHostConnectorError('PEER_HOST_PROXY_UNREACHABLE', 'PeerHost relay 作用域无效');
186
+ }
187
+ }
188
+ function toSnapshot(entry) {
189
+ return { peerHostId: entry.record.id, state: entry.state, attempts: entry.attempts, generation: entry.generation, scope: entry.scope, lastErrorCode: entry.lastErrorCode };
190
+ }
191
+ function bounded(value, min, max) {
192
+ if (!Number.isSafeInteger(value) || value < min || value > max)
193
+ throw new TypeError('PeerHost reconnect 参数无效');
194
+ return value;
195
+ }
196
+ //# sourceMappingURL=peer-host-relay.js.map
@@ -0,0 +1,42 @@
1
+ import type { PeerHostErrorCode } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostStore, type PeerHostCredentialStore } from './peer-host-store.js';
3
+ export interface PeerHostLoginInput {
4
+ readonly username: string;
5
+ readonly password: string;
6
+ }
7
+ export interface PeerHostSessionView {
8
+ readonly peerHostId: string;
9
+ readonly status: 'logged_in' | 'logged_out';
10
+ readonly expiresAt: number | null;
11
+ }
12
+ export interface PeerHostSessionOptions {
13
+ readonly fetchImpl?: typeof fetch;
14
+ readonly now?: () => number;
15
+ readonly refreshSkewMs?: number;
16
+ }
17
+ /** PeerHost 目标登录态协调器;token 只在 Host 进程和敏感存储中流转。 */
18
+ export declare class PeerHostSessionService {
19
+ private readonly store;
20
+ private readonly credentials;
21
+ private readonly options;
22
+ private readonly fetchImpl;
23
+ private readonly now;
24
+ private readonly refreshSkewMs;
25
+ constructor(store: PeerHostStore, credentials: PeerHostCredentialStore, options?: {
26
+ readonly apiPath?: string;
27
+ } & PeerHostSessionOptions);
28
+ login(peerHostId: string, input: PeerHostLoginInput): Promise<PeerHostSessionView>;
29
+ refresh(peerHostId: string): Promise<PeerHostSessionView>;
30
+ getAccessToken(peerHostId: string): Promise<string>;
31
+ /** 目标 Host 明确拒绝当前 token 时,只清理该 PeerHost 的登录态。 */
32
+ invalidate(peerHostId: string): Promise<void>;
33
+ logout(peerHostId: string): Promise<PeerHostSessionView>;
34
+ private ensureReady;
35
+ private request;
36
+ private sessionRequired;
37
+ }
38
+ export declare class PeerHostSessionError extends Error {
39
+ readonly code: PeerHostErrorCode;
40
+ constructor(code: PeerHostErrorCode, message: string);
41
+ }
42
+ //# sourceMappingURL=peer-host-session.d.ts.map
@@ -0,0 +1,152 @@
1
+ import { PEER_HOST_ERROR_CODES } from '../../../shared/contracts/peer-host.js';
2
+ import { PeerHostStore } from './peer-host-store.js';
3
+ /** PeerHost 目标登录态协调器;token 只在 Host 进程和敏感存储中流转。 */
4
+ export class PeerHostSessionService {
5
+ store;
6
+ credentials;
7
+ options;
8
+ fetchImpl;
9
+ now;
10
+ refreshSkewMs;
11
+ constructor(store, credentials, options = {}) {
12
+ this.store = store;
13
+ this.credentials = credentials;
14
+ this.options = options;
15
+ this.fetchImpl = options.fetchImpl ?? fetch;
16
+ this.now = options.now ?? Date.now;
17
+ this.refreshSkewMs = options.refreshSkewMs ?? 30_000;
18
+ }
19
+ async login(peerHostId, input) {
20
+ const record = await this.ensureReady(peerHostId);
21
+ const username = requiredText(input.username, 'username');
22
+ const password = requiredText(input.password, 'password');
23
+ const response = await this.request(record, '/api/auth/login', { username, password });
24
+ const payload = parseTokenResponse(response);
25
+ const credential = toCredential(payload, this.now());
26
+ await this.credentials.write(peerHostId, credential);
27
+ return toView(peerHostId, credential);
28
+ }
29
+ async refresh(peerHostId) {
30
+ const record = await this.ensureReady(peerHostId);
31
+ const previous = await this.credentials.read(peerHostId);
32
+ if (previous === null)
33
+ return this.sessionRequired(peerHostId);
34
+ try {
35
+ const response = await this.request(record, '/api/auth/refresh', { refreshToken: previous.refreshToken });
36
+ const payload = parseTokenResponse(response, previous.refreshToken);
37
+ const credential = toCredential(payload, this.now());
38
+ await this.credentials.write(peerHostId, credential);
39
+ return toView(peerHostId, credential);
40
+ }
41
+ catch (error) {
42
+ await this.credentials.clear(peerHostId);
43
+ await this.store.updateStatus(peerHostId, 'session_required', errorCode(error, PEER_HOST_ERROR_CODES.SESSION_REQUIRED));
44
+ return this.sessionRequired(peerHostId);
45
+ }
46
+ }
47
+ async getAccessToken(peerHostId) {
48
+ const record = await this.ensureReady(peerHostId);
49
+ const credential = await this.credentials.read(peerHostId);
50
+ if (credential === null)
51
+ return this.sessionRequired(peerHostId).then(() => '');
52
+ if (credential.expiresAt - this.now() <= this.refreshSkewMs) {
53
+ const refreshed = await this.refresh(peerHostId);
54
+ const latest = await this.credentials.read(peerHostId);
55
+ if (refreshed.status !== 'logged_in' || latest === null)
56
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.SESSION_REQUIRED, '目标 Host 登录态已失效');
57
+ return latest.accessToken;
58
+ }
59
+ return credential.accessToken;
60
+ }
61
+ /** 目标 Host 明确拒绝当前 token 时,只清理该 PeerHost 的登录态。 */
62
+ async invalidate(peerHostId) {
63
+ await this.credentials.clear(peerHostId);
64
+ const record = await this.store.get(peerHostId);
65
+ if (record !== null && record.status === 'ready')
66
+ await this.store.updateStatus(peerHostId, 'session_required', PEER_HOST_ERROR_CODES.SESSION_REQUIRED);
67
+ }
68
+ async logout(peerHostId) {
69
+ const record = await this.store.get(peerHostId);
70
+ if (record === null)
71
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.NOT_FOUND, 'PeerHost 不存在');
72
+ const credential = await this.credentials.read(peerHostId);
73
+ if (credential !== null) {
74
+ try {
75
+ await this.request(record, '/api/auth/logout', undefined, credential.accessToken);
76
+ }
77
+ catch {
78
+ // 远端退出失败不能阻止本地凭据清理,避免旧 token 继续被代理使用。
79
+ }
80
+ }
81
+ await this.credentials.clear(peerHostId);
82
+ if (record.status === 'ready')
83
+ await this.store.updateStatus(peerHostId, 'ready', null);
84
+ return { peerHostId, status: 'logged_out', expiresAt: null };
85
+ }
86
+ async ensureReady(peerHostId) {
87
+ const record = await this.store.get(peerHostId);
88
+ if (record === null)
89
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.NOT_FOUND, 'PeerHost 不存在');
90
+ if (record.status !== 'ready')
91
+ throw new PeerHostSessionError(record.status === 'session_required' ? PEER_HOST_ERROR_CODES.SESSION_REQUIRED : PEER_HOST_ERROR_CODES.NOT_READY, 'PeerHost 尚未通过握手检查');
92
+ return record;
93
+ }
94
+ async request(record, path, body, accessToken) {
95
+ if (record.route.kind !== 'lan')
96
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.RELAY_UNAVAILABLE, '中转 PeerHost 暂不可用');
97
+ const headers = new Headers({ accept: 'application/json' });
98
+ if (body !== undefined)
99
+ headers.set('content-type', 'application/json');
100
+ if (accessToken !== undefined)
101
+ headers.set('authorization', `Bearer ${accessToken}`);
102
+ const response = await this.fetchImpl(new URL(path, record.route.normalizedOrigin), {
103
+ method: 'POST', headers, ...(body === undefined ? {} : { body: JSON.stringify(body) }),
104
+ });
105
+ let payload = null;
106
+ try {
107
+ payload = await response.json();
108
+ }
109
+ catch { /* logout 允许空响应 */ }
110
+ if (!response.ok)
111
+ throw new PeerHostSessionError(response.status === 401 ? PEER_HOST_ERROR_CODES.SESSION_REQUIRED : PEER_HOST_ERROR_CODES.PROXY_UNREACHABLE, '目标 Host 登录请求失败');
112
+ return payload;
113
+ }
114
+ async sessionRequired(peerHostId) {
115
+ await this.store.updateStatus(peerHostId, 'session_required', PEER_HOST_ERROR_CODES.SESSION_REQUIRED);
116
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.SESSION_REQUIRED, '目标 Host 需要登录');
117
+ }
118
+ }
119
+ export class PeerHostSessionError extends Error {
120
+ code;
121
+ constructor(code, message) {
122
+ super(message);
123
+ this.code = code;
124
+ this.name = 'PeerHostSessionError';
125
+ }
126
+ }
127
+ function parseTokenResponse(value, fallbackRefreshToken) {
128
+ if (typeof value !== 'object' || value === null || Array.isArray(value))
129
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '目标 Host 登录响应无效');
130
+ const input = value;
131
+ const accessToken = typeof input.accessToken === 'string' ? input.accessToken : '';
132
+ const refreshToken = typeof input.refreshToken === 'string' ? input.refreshToken : fallbackRefreshToken ?? '';
133
+ const expiresIn = typeof input.expiresIn === 'number' && Number.isFinite(input.expiresIn) && input.expiresIn > 0 ? input.expiresIn : 0;
134
+ if (!accessToken || !refreshToken || expiresIn <= 0)
135
+ throw new PeerHostSessionError(PEER_HOST_ERROR_CODES.RESPONSE_INVALID, '目标 Host token 响应无效');
136
+ return { accessToken, refreshToken, expiresIn };
137
+ }
138
+ function toCredential(payload, now) {
139
+ return { accessToken: payload.accessToken, refreshToken: payload.refreshToken, expiresAt: now + payload.expiresIn * 1000 };
140
+ }
141
+ function toView(peerHostId, credential) {
142
+ return { peerHostId, status: 'logged_in', expiresAt: credential.expiresAt };
143
+ }
144
+ function requiredText(value, field) {
145
+ if (typeof value !== 'string' || value.trim() === '')
146
+ throw new TypeError(`${field} 不能为空`);
147
+ return value.trim();
148
+ }
149
+ function errorCode(error, fallback) {
150
+ return error instanceof PeerHostSessionError ? error.code : fallback;
151
+ }
152
+ //# sourceMappingURL=peer-host-session.js.map
@@ -0,0 +1,92 @@
1
+ import type { PeerHostErrorCode, PeerHostRecord, PeerHostRoute } from '../../../shared/contracts/peer-host.js';
2
+ export interface PeerHostTokenRecord {
3
+ readonly accessToken: string;
4
+ readonly refreshToken: string;
5
+ readonly expiresAt: number;
6
+ }
7
+ export interface PeerHostRecordStore {
8
+ read(): Promise<readonly PeerHostRecord[]>;
9
+ write(records: readonly PeerHostRecord[]): Promise<void>;
10
+ }
11
+ export interface PeerHostCredentialStore {
12
+ read(peerHostId: string): Promise<PeerHostTokenRecord | null>;
13
+ write(peerHostId: string, credential: PeerHostTokenRecord): Promise<void>;
14
+ clear(peerHostId: string): Promise<void>;
15
+ }
16
+ export interface PeerHostCreateInput {
17
+ readonly displayName: string;
18
+ readonly route: PeerHostRoute;
19
+ }
20
+ export interface PeerHostUpdateInput {
21
+ readonly displayName?: string;
22
+ readonly route?: PeerHostRoute;
23
+ }
24
+ export interface PeerHostHandshakeUpdate {
25
+ readonly status: PeerHostRecord['status'];
26
+ readonly pluginId: string | null;
27
+ readonly pluginVersion: string | null;
28
+ readonly dshVersion: string | null;
29
+ readonly apiCompatibility: string | null;
30
+ readonly fingerprint: string | null;
31
+ readonly lastCheckedAt: number;
32
+ readonly lastErrorCode: PeerHostErrorCode | null;
33
+ }
34
+ export declare class PeerHostStoreError extends Error {
35
+ readonly code: PeerHostErrorCode;
36
+ constructor(code: PeerHostErrorCode, message: string);
37
+ }
38
+ /** 当前 Host 的 PeerHost 配置服务;所有 mutation 都在 Host 内完成校验。 */
39
+ export declare class PeerHostStore {
40
+ private readonly ownerUserId;
41
+ private readonly recordStore;
42
+ private readonly credentials;
43
+ private readonly now;
44
+ private readonly createId;
45
+ private records;
46
+ private operation;
47
+ constructor(ownerUserId: string, recordStore: PeerHostRecordStore, credentials: PeerHostCredentialStore, now?: () => number, createId?: () => string);
48
+ list(): Promise<readonly PeerHostRecord[]>;
49
+ get(peerHostId: string): Promise<PeerHostRecord | null>;
50
+ create(input: PeerHostCreateInput): Promise<PeerHostRecord>;
51
+ update(peerHostId: string, input: PeerHostUpdateInput): Promise<PeerHostRecord>;
52
+ remove(peerHostId: string): Promise<void>;
53
+ updateHandshake(peerHostId: string, input: PeerHostHandshakeUpdate): Promise<PeerHostRecord>;
54
+ updateStatus(peerHostId: string, status: PeerHostRecord['status'], lastErrorCode: PeerHostErrorCode | null): Promise<PeerHostRecord>;
55
+ private load;
56
+ private persist;
57
+ private findIndex;
58
+ private assertUnique;
59
+ private enqueue;
60
+ }
61
+ export declare class InMemoryPeerHostRecordStore implements PeerHostRecordStore {
62
+ private records;
63
+ constructor(records?: readonly PeerHostRecord[]);
64
+ read(): Promise<readonly PeerHostRecord[]>;
65
+ write(records: readonly PeerHostRecord[]): Promise<void>;
66
+ }
67
+ export declare class InMemoryPeerHostCredentialStore implements PeerHostCredentialStore {
68
+ private readonly records;
69
+ read(peerHostId: string): Promise<PeerHostTokenRecord | null>;
70
+ write(peerHostId: string, credential: PeerHostTokenRecord): Promise<void>;
71
+ clear(peerHostId: string): Promise<void>;
72
+ }
73
+ /** 非敏感记录的原子文件存储;token 不允许进入此文件。 */
74
+ export declare class FilePeerHostRecordStore implements PeerHostRecordStore {
75
+ private readonly filePath;
76
+ constructor(filePath: string);
77
+ read(): Promise<readonly PeerHostRecord[]>;
78
+ write(records: readonly PeerHostRecord[]): Promise<void>;
79
+ }
80
+ /** AES-256-GCM 文件存储,密钥必须由 Host 启动边界注入。 */
81
+ export declare class EncryptedFilePeerHostCredentialStore implements PeerHostCredentialStore {
82
+ private readonly filePath;
83
+ constructor(filePath: string, key: Uint8Array);
84
+ private readonly key;
85
+ read(peerHostId: string): Promise<PeerHostTokenRecord | null>;
86
+ write(peerHostId: string, credential: PeerHostTokenRecord): Promise<void>;
87
+ clear(peerHostId: string): Promise<void>;
88
+ private readAll;
89
+ private writeAll;
90
+ }
91
+ export declare function normalizePeerHostRoute(route: PeerHostRoute): PeerHostRoute;
92
+ //# sourceMappingURL=peer-host-store.d.ts.map