hiwork-core 0.0.7 → 0.0.10

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.
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `/hiwork-app` loopback RPC 的 Host 实现:把「当前桌面端版本」报给渲染端。
3
+ *
4
+ * 为什么需要这个频道:侧栏品牌行显示的是**桌面端**版本,而这个值只有 Host 拿得到
5
+ * ——桌面端主进程启动时经 `hiwork:device-info`(stdio:ipc)把 `app.getVersion()` 推给
6
+ * Host,缓存在 `src/browser-login.ts` 的模块级 `pushedDeviceInfo` 里。渲染端看不到
7
+ * 那条 IPC,因此必须经 loopback RPC 取。此前的做法是在 client 侧硬编码产品版本,
8
+ * 每次桌面端发版都要手工同步一次;本频道就是为了去掉这步手工同步。
9
+ *
10
+ * 设计要点(与 `src/daily-rpc.ts` / `src/usage-rpc.ts` 同构):
11
+ * - 只注册一个 channel,endpoint 白名单在 {@link APP_VERSION_RPC_ENDPOINTS},
12
+ * 未知 endpoint 直接 `bad-request`;
13
+ * - `version` 端点无入参:payload 出现任何字段都判 `bad-request`;
14
+ * - **「桌面端没推过」不是错误**:纯 web / 浏览器 dev 下永远不会有值,与本频道要
15
+ * 回答的问题无关,所以返回 `{ version: null }` 的成功信封,由客户端自行回退到
16
+ * 常量——不产生任何错误 UI,也不进日志。
17
+ *
18
+ * 本模块不 import cordis 运行时,只依赖结构化的 {@link AppVersionRpcContext} 端口,
19
+ * 便于单测直接伪造。
20
+ */
21
+ /** RPC 频道;必须与 client 半边 `src/client/app-version.ts` 的 `APP_VERSION_CHANNEL` 一致(由测试锁定)。 */
22
+ export const APP_VERSION_RPC_CHANNEL = '/hiwork-app';
23
+ /** 端点白名单;必须与 client 半边 `APP_VERSION_ENDPOINTS` 一致(由测试锁定)。 */
24
+ export const APP_VERSION_RPC_ENDPOINTS = ['version'];
25
+ /** 通用失败文案:不暴露任何实现细节。 */
26
+ export const APP_VERSION_GENERIC_FAILURE_MESSAGE = '暂时无法获取桌面端版本';
27
+ /** 请求侧错误(只有 `bad-request`:未知端点 / 非法 payload)。 */
28
+ export class AppVersionRequestError extends Error {
29
+ code;
30
+ constructor(code, message) {
31
+ super(message);
32
+ this.code = code;
33
+ this.name = 'AppVersionRequestError';
34
+ }
35
+ }
36
+ /** 是否为白名单端点。 */
37
+ export function isAppVersionRpcEndpoint(endpoint) {
38
+ return APP_VERSION_RPC_ENDPOINTS.includes(endpoint);
39
+ }
40
+ /** 取消判定(用函数包一层,避免 TS 把属性窄化固化到 `await` 之后)。 */
41
+ function isAborted(signal) {
42
+ return signal?.aborted === true;
43
+ }
44
+ /** payload 校验:`version` 无任何入参,出现字段即客户端实现缺陷。 */
45
+ export function parseAppVersionPayload(raw) {
46
+ if (raw === undefined || raw === null)
47
+ return;
48
+ if (typeof raw !== 'object' || Array.isArray(raw)) {
49
+ throw new AppVersionRequestError('bad-request', '桌面端版本请求参数不合法。');
50
+ }
51
+ const keys = Object.keys(raw);
52
+ if (keys.length > 0)
53
+ throw new AppVersionRequestError('bad-request', `桌面端版本请求包含未知字段 '${keys[0]}'。`);
54
+ }
55
+ /** 空串与缺省同义:都表示「拿不到」,一律归一为 null,绝不渲染成 `v`。 */
56
+ function normalizedVersion(raw) {
57
+ const version = typeof raw === 'string' ? raw.trim() : '';
58
+ return version === '' ? null : version;
59
+ }
60
+ /**
61
+ * 处理一次端点调用:返回 DSH Connection 的结果信封,**永不抛异常**。
62
+ * 导出以便单测直接驱动(无需伪造 `ctx.connection`)。
63
+ */
64
+ export async function handleAppVersionEndpoint(endpoint, rawPayload, deps, signal) {
65
+ try {
66
+ if (isAborted(signal))
67
+ return failure('cancelled', '桌面端版本请求已取消。');
68
+ if (!isAppVersionRpcEndpoint(endpoint)) {
69
+ throw new AppVersionRequestError('bad-request', `未知的桌面端版本端点 '${endpoint}'`);
70
+ }
71
+ parseAppVersionPayload(rawPayload);
72
+ const value = { version: normalizedVersion(deps.appVersion()) };
73
+ return { ok: true, value };
74
+ }
75
+ catch (error) {
76
+ if (error instanceof AppVersionRequestError)
77
+ return failure(error.code, error.message);
78
+ deps.log?.(`hiwork-core: 桌面端版本 RPC '${endpoint}' 失败(${error instanceof Error ? error.name : typeof error})`);
79
+ return failure('internal', APP_VERSION_GENERIC_FAILURE_MESSAGE);
80
+ }
81
+ }
82
+ function failure(code, message) {
83
+ return { ok: false, error: { code, message, details: {} } };
84
+ }
85
+ /**
86
+ * 注册 `/hiwork-app` 频道,返回异步 disposer(交给 `ctx.effect` 管理生命周期)。
87
+ */
88
+ export function registerAppVersionRpc(ctx, deps) {
89
+ const handler = (endpoint, payload, signal) => handleAppVersionEndpoint(endpoint, payload, deps, signal);
90
+ const remove = ctx.connection.rpc.handle(APP_VERSION_RPC_CHANNEL, handler, { authority: 'loopback' });
91
+ return async () => { await remove(); };
92
+ }
@@ -47,6 +47,13 @@ export function setDeviceInfo(info) {
47
47
  ...(appVersion === '' ? {} : { appVersion }),
48
48
  };
49
49
  }
50
+ /**
51
+ * 桌面端经 IPC 推来的版本号(未推过 / 纯 web 时为 undefined)。
52
+ * 只读、无副作用:`/hiwork-app` 频道拿它给侧栏品牌行展示(见 `src/app-version-rpc.ts`)。
53
+ */
54
+ export function pushedAppVersion() {
55
+ return pushedDeviceInfo?.appVersion;
56
+ }
50
57
  /** 清空模块级状态(IPC 设备缓存 + 待完成授权);供测试与插件卸载使用。 */
51
58
  export function resetBrowserLoginState() {
52
59
  pushedDeviceInfo = undefined;
@@ -15,6 +15,8 @@ export const DEFAULT_LOGIN_MODE = 'both';
15
15
  export const CLIENT_POLICY_PATH = '/api/client/policy';
16
16
  /** 策略拉取默认超时:免登录只读接口,超过 5s 视同不可达(fail-open)。 */
17
17
  export const CLIENT_POLICY_TIMEOUT_MS = 5_000;
18
+ /** fail-open 缺省:旧网关不返回 passwordLogin 时,密码表单仍视作可用。 */
19
+ export const DEFAULT_PASSWORD_LOGIN = true;
18
20
  /**
19
21
  * 拼策略 URL:`{baseURL}/api/client/policy?platform=&arch=&version=&device_id=`。
20
22
  * 参数固定顺序、逐个 `encodeURIComponent`(device_id 可能是任意 ASCII,含 `+/=` 也必须安全);
@@ -47,6 +49,16 @@ function parseLoginMode(payload) {
47
49
  const value = payload.loginMode;
48
50
  return value === 'browser' || value === 'form' || value === 'both' ? value : undefined;
49
51
  }
52
+ /**
53
+ * 解析 `passwordLogin`:只认布尔值;缺失 / 非法值一律 {@link DEFAULT_PASSWORD_LOGIN}
54
+ * (fail-open:旧网关不返回该字段,不能因此收起密码入口)。
55
+ */
56
+ function parsePasswordLogin(payload) {
57
+ if (typeof payload !== 'object' || payload === null)
58
+ return DEFAULT_PASSWORD_LOGIN;
59
+ const value = payload.passwordLogin;
60
+ return typeof value === 'boolean' ? value : DEFAULT_PASSWORD_LOGIN;
61
+ }
50
62
  /**
51
63
  * 拉取并解析客户端策略。**永不抛异常**:任何失败都返回 undefined(fail-open)。
52
64
  *
@@ -73,7 +85,7 @@ export async function fetchClientPolicy(opts) {
73
85
  return undefined;
74
86
  }
75
87
  const loginMode = parseLoginMode(payload);
76
- return loginMode === undefined ? undefined : { loginMode };
88
+ return loginMode === undefined ? undefined : { loginMode, passwordLogin: parsePasswordLogin(payload) };
77
89
  }
78
90
  catch {
79
91
  // 网络异常 / 超时(AbortSignal.timeout 抛 TimeoutError)/ 非法 URL:一律 fail-open。