@ysgroup/core 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ysgroup/core",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -16,4 +16,4 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  }
19
- }
19
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,19 @@
1
1
  /* @ysgroup/core —— 前端运行时:WS RPC 客户端 / 登录态 / 差量订阅 / REST。 */
2
2
 
3
- export { YsWsClient, type Envelope } from "./ws";
3
+ export { YsWsClient, YsRpcError, type Envelope } from "./ws";
4
4
  export { useYsStore } from "./store";
5
- export { YsRest, YsRestError, type YsAdminRpcCall, type DeleteBlocker, type DeleteImpact, type RevealedCredential, type TopologyNode } from "./rest";
5
+ export {
6
+ YsRest,
7
+ YsRestError,
8
+ type YsAdminRpcCall,
9
+ type DeleteBlocker,
10
+ type DeleteImpact,
11
+ type RevealedCredential,
12
+ type TopologyNode,
13
+ } from "./rest";
14
+ export {
15
+ applyPasswordWrap,
16
+ sealLoginPassword,
17
+ sealPassword,
18
+ type PasswordWrap,
19
+ } from "./password";
@@ -0,0 +1,141 @@
1
+ /* 登录口令密封:X25519 临时密钥 + HKDF-SHA256 + AES-256-GCM,与 Auth/ys_base 对齐。 */
2
+
3
+ export interface PasswordWrap {
4
+ password_wrap: string;
5
+ password_public_key_kid: string;
6
+ password_public_key: string;
7
+ }
8
+
9
+ const ALG = "x25519-seal";
10
+ const PREFIX = "pwd.v1.";
11
+ const INFO = new TextEncoder().encode("ys-pwd-v1");
12
+ const SALT = new Uint8Array(32);
13
+ const NONCE = new Uint8Array(12);
14
+ const KID_RE = /^[A-Za-z0-9_-]+$/;
15
+
16
+ let current: PasswordWrap | null = null;
17
+ const waiters: Array<() => void> = [];
18
+
19
+ export function applyPasswordWrap(data: unknown): void {
20
+ if (!data || typeof data !== "object") return;
21
+ const rec = data as Record<string, unknown>;
22
+ const wrap = String(rec.password_wrap ?? "");
23
+ const kid = String(rec.password_public_key_kid ?? "");
24
+ const key = String(rec.password_public_key ?? "");
25
+ if (wrap !== ALG || !KID_RE.test(kid) || !key) return;
26
+ current = {
27
+ password_wrap: wrap,
28
+ password_public_key_kid: kid,
29
+ password_public_key: key,
30
+ };
31
+ while (waiters.length) waiters.shift()?.();
32
+ }
33
+
34
+ export function clearPasswordWrap(): void {
35
+ current = null;
36
+ }
37
+
38
+ export function passwordWrap(): PasswordWrap | null {
39
+ return current;
40
+ }
41
+
42
+ export async function waitForPasswordWrap(
43
+ timeoutMs = 10_000,
44
+ ): Promise<PasswordWrap> {
45
+ if (current) return current;
46
+ return new Promise((resolve, reject) => {
47
+ const timer = setTimeout(() => {
48
+ const index = waiters.indexOf(onReady);
49
+ if (index >= 0) waiters.splice(index, 1);
50
+ reject(new Error("登录公钥尚未就绪"));
51
+ }, timeoutMs);
52
+ const onReady = () => {
53
+ clearTimeout(timer);
54
+ if (current) resolve(current);
55
+ else reject(new Error("登录公钥尚未就绪"));
56
+ };
57
+ waiters.push(onReady);
58
+ });
59
+ }
60
+
61
+ export async function sealLoginPassword(plaintext: string): Promise<string> {
62
+ const wrap = await waitForPasswordWrap();
63
+ return sealPassword(
64
+ plaintext,
65
+ wrap.password_public_key,
66
+ wrap.password_public_key_kid,
67
+ );
68
+ }
69
+
70
+ export async function sealPassword(
71
+ plaintext: string,
72
+ publicKeyB64: string,
73
+ kid = "e1",
74
+ ): Promise<string> {
75
+ if (!plaintext) throw new Error("密码不能为空");
76
+ if (!KID_RE.test(kid)) throw new Error("密码信封密钥标识无效");
77
+ const recipientRaw = b64uDecode(publicKeyB64);
78
+ if (recipientRaw.byteLength !== 32) throw new Error("登录公钥长度无效");
79
+
80
+ const ephemeral = await crypto.subtle.generateKey({ name: "X25519" }, true, [
81
+ "deriveBits",
82
+ ]);
83
+ const recipient = await crypto.subtle.importKey(
84
+ "raw",
85
+ recipientRaw,
86
+ { name: "X25519" },
87
+ false,
88
+ [],
89
+ );
90
+ const shared = new Uint8Array(
91
+ await crypto.subtle.deriveBits(
92
+ { name: "X25519", public: recipient },
93
+ ephemeral.privateKey,
94
+ 256,
95
+ ),
96
+ );
97
+ const hkdfKey = await crypto.subtle.importKey("raw", shared, "HKDF", false, [
98
+ "deriveBits",
99
+ ]);
100
+ const aesRaw = await crypto.subtle.deriveBits(
101
+ { name: "HKDF", hash: "SHA-256", salt: SALT, info: INFO },
102
+ hkdfKey,
103
+ 256,
104
+ );
105
+ const aesKey = await crypto.subtle.importKey(
106
+ "raw",
107
+ aesRaw,
108
+ { name: "AES-GCM" },
109
+ false,
110
+ ["encrypt"],
111
+ );
112
+ const aad = new TextEncoder().encode(`${PREFIX}${kid}`);
113
+ const packedCt = new Uint8Array(
114
+ await crypto.subtle.encrypt(
115
+ { name: "AES-GCM", iv: NONCE, additionalData: aad, tagLength: 128 },
116
+ aesKey,
117
+ new TextEncoder().encode(plaintext),
118
+ ),
119
+ );
120
+ const ephPk = new Uint8Array(
121
+ await crypto.subtle.exportKey("raw", ephemeral.publicKey),
122
+ );
123
+ const packed = new Uint8Array(ephPk.byteLength + packedCt.byteLength);
124
+ packed.set(ephPk, 0);
125
+ packed.set(packedCt, ephPk.byteLength);
126
+ return `${PREFIX}${kid}.${b64uEncode(packed)}`;
127
+ }
128
+
129
+ function b64uEncode(raw: Uint8Array): string {
130
+ let bin = "";
131
+ for (const byte of raw) bin += String.fromCharCode(byte);
132
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
133
+ }
134
+
135
+ function b64uDecode(text: string): Uint8Array {
136
+ const pad = "=".repeat((4 - (text.length % 4)) % 4);
137
+ const bin = atob(text.replace(/-/g, "+").replace(/_/g, "/") + pad);
138
+ const out = new Uint8Array(bin.length);
139
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
140
+ return out;
141
+ }
package/src/rest.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  /* @ysgroup/core —— 管理接口客户端(支持 HTTP 或统一 WS RPC 传输)。 */
2
2
 
3
+ import { sealLoginPassword } from "./password";
4
+ import { YsRpcError } from "./ws";
5
+
3
6
  export type YsAdminRpcCall = (data: {
4
7
  method: string;
5
8
  path: string;
@@ -45,7 +48,11 @@ export class YsRest {
45
48
  private rpcCall?: YsAdminRpcCall,
46
49
  ) {}
47
50
 
48
- private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
51
+ private async request<T>(
52
+ method: string,
53
+ path: string,
54
+ body?: unknown,
55
+ ): Promise<T> {
49
56
  if (this.rpcCall) {
50
57
  try {
51
58
  return (await this.rpcCall({
@@ -107,7 +114,9 @@ export class YsRest {
107
114
  body: form,
108
115
  });
109
116
  if (!resp.ok) {
110
- const parsed = await resp.json().catch(() => ({ detail: resp.statusText }));
117
+ const parsed = await resp
118
+ .json()
119
+ .catch(() => ({ detail: resp.statusText }));
111
120
  throw new YsRestError(resp.status, parsed.detail ?? parsed);
112
121
  }
113
122
  return (await resp.json()) as T;
@@ -120,16 +129,25 @@ export class YsRest {
120
129
  list<T = Record<string, unknown>>(table: string) {
121
130
  return this.request<T[]>("GET", `/api/tables/${table}`);
122
131
  }
123
- upsert<T = Record<string, unknown>>(table: string, doc: Record<string, unknown>) {
132
+ upsert<T = Record<string, unknown>>(
133
+ table: string,
134
+ doc: Record<string, unknown>,
135
+ ) {
124
136
  return this.request<T>("PUT", `/api/tables/${table}`, doc);
125
137
  }
126
138
  remove(table: string, id: string) {
127
- return this.request<{ deleted: string }>("DELETE", `/api/tables/${table}/${id}`);
139
+ return this.request<{ deleted: string }>(
140
+ "DELETE",
141
+ `/api/tables/${table}/${id}`,
142
+ );
128
143
  }
129
144
  deleteImpact(table: string, id: string) {
130
- return this.request<DeleteImpact>("GET", `/api/tables/${table}/${id}/delete-impact`);
145
+ return this.request<DeleteImpact>(
146
+ "GET",
147
+ `/api/tables/${table}/${id}/delete-impact`,
148
+ );
131
149
  }
132
- revealCredentials(
150
+ async revealCredentials(
133
151
  table: string,
134
152
  id: string,
135
153
  body: {
@@ -141,9 +159,12 @@ export class YsRest {
141
159
  return this.request<{
142
160
  credentials: Record<string, RevealedCredential>;
143
161
  expires_in: number;
144
- }>("POST", `/api/tables/${table}/${id}/credentials/reveal`, body);
162
+ }>("POST", `/api/tables/${table}/${id}/credentials/reveal`, {
163
+ ...body,
164
+ password: await sealLoginPassword(body.password),
165
+ });
145
166
  }
146
- updateCredentials(
167
+ async updateCredentials(
147
168
  table: string,
148
169
  id: string,
149
170
  body: {
@@ -152,13 +173,23 @@ export class YsRest {
152
173
  reason: string;
153
174
  },
154
175
  ) {
155
- return this.request<{ updated_fields: string[] }>("POST", `/api/tables/${table}/${id}/credentials/update`, body);
176
+ return this.request<{ updated_fields: string[] }>(
177
+ "POST",
178
+ `/api/tables/${table}/${id}/credentials/update`,
179
+ {
180
+ ...body,
181
+ password: await sealLoginPassword(body.password),
182
+ },
183
+ );
156
184
  }
157
185
  listApps() {
158
186
  return this.request<Record<string, unknown>[]>("GET", "/api/apps");
159
187
  }
160
188
  listAppReferences() {
161
- return this.request<Record<string, unknown>[]>("GET", "/api/apps/reference-list");
189
+ return this.request<Record<string, unknown>[]>(
190
+ "GET",
191
+ "/api/apps/reference-list",
192
+ );
162
193
  }
163
194
  createApp(body: Record<string, unknown>) {
164
195
  return this.request<Record<string, unknown>>("POST", "/api/apps", body);
@@ -170,54 +201,92 @@ export class YsRest {
170
201
  return this.request("POST", `/api/apps/${appId}/grants`, grants);
171
202
  }
172
203
  updateApp(appId: string, body: Record<string, unknown>) {
173
- return this.request<Record<string, unknown>>("PUT", `/api/apps/${appId}`, body);
204
+ return this.request<Record<string, unknown>>(
205
+ "PUT",
206
+ `/api/apps/${appId}`,
207
+ body,
208
+ );
174
209
  }
175
210
  rotateSecret(appId: string) {
176
- return this.request<{ secret: string }>("POST", `/api/apps/${appId}/rotate_secret`, {});
211
+ return this.request<{ secret: string }>(
212
+ "POST",
213
+ `/api/apps/${appId}/rotate_secret`,
214
+ {},
215
+ );
177
216
  }
178
217
  disableUser(userId: string) {
179
218
  return this.request("POST", `/api/users/${userId}/disable`, {});
180
219
  }
181
220
  issueSso(appName: string) {
182
- return this.request<{ code: string; expires_in: number }>("POST", "/api/sso/issue", {
183
- app_name: appName,
184
- });
221
+ return this.request<{ code: string; expires_in: number }>(
222
+ "POST",
223
+ "/api/sso/issue",
224
+ {
225
+ app_name: appName,
226
+ },
227
+ );
185
228
  }
186
229
  portalApps() {
187
230
  return this.request<Record<string, unknown>[]>("GET", "/api/portal/apps");
188
231
  }
189
- createUser(body: Record<string, unknown>) {
190
- return this.request<Record<string, unknown>>("POST", "/api/users", body);
232
+ async createUser(body: Record<string, unknown>) {
233
+ const password =
234
+ typeof body.password === "string"
235
+ ? await sealLoginPassword(body.password)
236
+ : body.password;
237
+ return this.request<Record<string, unknown>>("POST", "/api/users", {
238
+ ...body,
239
+ password,
240
+ });
191
241
  }
192
242
  updateUser(id: string, body: Record<string, unknown>) {
193
- return this.request<Record<string, unknown>>("PUT", `/api/users/${id}`, body);
243
+ return this.request<Record<string, unknown>>(
244
+ "PUT",
245
+ `/api/users/${id}`,
246
+ body,
247
+ );
194
248
  }
195
249
  setUserEnabled(id: string, enabled: boolean) {
196
- return this.request("POST", `/api/users/${id}/${enabled ? "enable" : "disable"}`, {});
250
+ return this.request(
251
+ "POST",
252
+ `/api/users/${id}/${enabled ? "enable" : "disable"}`,
253
+ {},
254
+ );
197
255
  }
198
- resetUserPassword(id: string, newPassword: string, reason: string) {
256
+ async resetUserPassword(id: string, newPassword: string, reason: string) {
199
257
  return this.request("POST", `/api/users/${id}/reset-password`, {
200
- new_password: newPassword,
258
+ new_password: await sealLoginPassword(newPassword),
201
259
  reason,
202
260
  });
203
261
  }
204
- changeMyPassword(currentPassword: string, newPassword: string) {
262
+ async changeMyPassword(currentPassword: string, newPassword: string) {
205
263
  return this.request("POST", "/api/me/change-password", {
206
- current_password: currentPassword,
207
- new_password: newPassword,
264
+ current_password: await sealLoginPassword(currentPassword),
265
+ new_password: await sealLoginPassword(newPassword),
208
266
  });
209
267
  }
210
268
  removeUser(id: string) {
211
269
  return this.request("DELETE", `/api/users/${id}`);
212
270
  }
213
271
  getUserApps(id: string) {
214
- return this.request<{ app_id: string; admin: boolean }[]>("GET", `/api/users/${id}/apps`);
272
+ return this.request<{ app_id: string; admin: boolean }[]>(
273
+ "GET",
274
+ `/api/users/${id}/apps`,
275
+ );
215
276
  }
216
- setUserApps(id: string, grants: { app_id: string; admin: boolean }[], reason = "") {
217
- return this.request<{ grants: { app_id: string; admin: boolean }[] }>("PUT", `/api/users/${id}/apps`, {
218
- grants,
219
- reason,
220
- });
277
+ setUserApps(
278
+ id: string,
279
+ grants: { app_id: string; admin: boolean }[],
280
+ reason = "",
281
+ ) {
282
+ return this.request<{ grants: { app_id: string; admin: boolean }[] }>(
283
+ "PUT",
284
+ `/api/users/${id}/apps`,
285
+ {
286
+ grants,
287
+ reason,
288
+ },
289
+ );
221
290
  }
222
291
  uploadOpenAccountConfirmation(ownerId: string, file: File) {
223
292
  return this.upload<{
@@ -228,21 +297,49 @@ export class YsRest {
228
297
  }>(`/api/owners/${ownerId}/open-account-confirmation`, file);
229
298
  }
230
299
  upsertMountExchangeAccount(body: Record<string, unknown>) {
231
- return this.request<Record<string, unknown>>("PUT", "/api/mount-exchange-accounts", body);
300
+ return this.request<Record<string, unknown>>(
301
+ "PUT",
302
+ "/api/mount-exchange-accounts",
303
+ body,
304
+ );
232
305
  }
233
306
  setProjectOwners(projectId: string, ownerIds: string[]) {
234
- return this.request<{ owner_ids: string[] }>("PUT", `/api/projects/${projectId}/owners`, {
235
- owner_ids: ownerIds,
236
- });
307
+ return this.request<{ owner_ids: string[] }>(
308
+ "PUT",
309
+ `/api/projects/${projectId}/owners`,
310
+ {
311
+ owner_ids: ownerIds,
312
+ },
313
+ );
237
314
  }
238
- getRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies") {
239
- return this.request<{ ids: string[] }>("GET", `/api/${entity}/${id}/relations/${relation}`);
315
+ getRelations(
316
+ entity: "owners" | "accounts",
317
+ id: string,
318
+ relation: "apps" | "strategies",
319
+ ) {
320
+ return this.request<{ ids: string[] }>(
321
+ "GET",
322
+ `/api/${entity}/${id}/relations/${relation}`,
323
+ );
240
324
  }
241
- setRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies", ids: string[], reason = "") {
242
- return this.request<{ ids: string[] }>("PUT", `/api/${entity}/${id}/relations/${relation}`, { ids, reason });
325
+ setRelations(
326
+ entity: "owners" | "accounts",
327
+ id: string,
328
+ relation: "apps" | "strategies",
329
+ ids: string[],
330
+ reason = "",
331
+ ) {
332
+ return this.request<{ ids: string[] }>(
333
+ "PUT",
334
+ `/api/${entity}/${id}/relations/${relation}`,
335
+ { ids, reason },
336
+ );
243
337
  }
244
338
  listOwnerFiles(ownerId: string) {
245
- return this.request<Record<string, unknown>[]>("GET", `/api/owners/${ownerId}/files`);
339
+ return this.request<Record<string, unknown>[]>(
340
+ "GET",
341
+ `/api/owners/${ownerId}/files`,
342
+ );
246
343
  }
247
344
  async downloadFile(fileId: string) {
248
345
  if (this.rpcCall) {
@@ -265,6 +362,15 @@ export class YsRest {
265
362
  }
266
363
 
267
364
  function rpcError(error: unknown): YsRestError {
365
+ if (error instanceof YsRpcError) {
366
+ let detail: unknown = error.clientMessage;
367
+ try {
368
+ detail = JSON.parse(error.clientMessage);
369
+ } catch {
370
+ // 普通文本错误保持原文。
371
+ }
372
+ return new YsRestError(error.code, detail);
373
+ }
268
374
  const message = String(error).replace(/^Error:\s*/, "");
269
375
  const match = message.match(/^\[(\d+)\]\s*(.*)$/s);
270
376
  if (!match) return new YsRestError(500, message);
@@ -280,7 +386,8 @@ function rpcError(error: unknown): YsRestError {
280
386
  function fileToBase64(file: File): Promise<string> {
281
387
  return new Promise((resolve, reject) => {
282
388
  const reader = new FileReader();
283
- reader.onerror = () => reject(reader.error ?? new Error("读取上传文件失败"));
389
+ reader.onerror = () =>
390
+ reject(reader.error ?? new Error("读取上传文件失败"));
284
391
  reader.onload = () => resolve(String(reader.result).split(",", 2)[1] ?? "");
285
392
  reader.readAsDataURL(file);
286
393
  });
@@ -289,7 +396,8 @@ function fileToBase64(file: File): Promise<string> {
289
396
  function base64ToBlob(value: string, contentType: string): Blob {
290
397
  const binary = atob(value);
291
398
  const bytes = new Uint8Array(binary.length);
292
- for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
399
+ for (let index = 0; index < binary.length; index += 1)
400
+ bytes[index] = binary.charCodeAt(index);
293
401
  return new Blob([bytes], { type: contentType });
294
402
  }
295
403
 
package/src/store.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /* @ysgroup/core —— 登录态 + 差量订阅缓存(Pinia store)。 */
2
2
  import { defineStore } from "pinia";
3
3
  import { ref, computed } from "vue";
4
+ import { sealLoginPassword } from "./password";
4
5
  import { YsWsClient } from "./ws";
5
6
 
6
7
  interface LoginResult {
@@ -42,7 +43,9 @@ export const useYsStore = defineStore("ys", () => {
42
43
  ws.on("__disconnected", () => (connected.value = false));
43
44
  ws.on("snapshot", (d: unknown) => applySnapshot(d as SnapshotData));
44
45
  ws.on("delta", (d: unknown) => applyDelta(d as DeltaData));
45
- ws.on("user_revoked", (d: unknown) => onRevoked(d as { user_id: string; reason?: string }));
46
+ ws.on("user_revoked", (d: unknown) =>
47
+ onRevoked(d as { user_id: string; reason?: string }),
48
+ );
46
49
  ws.on("resync", () => {}); // 前端订阅本子系统后端,resync 由后端 SDK 处理
47
50
  ws.start();
48
51
  }
@@ -98,12 +101,16 @@ export const useYsStore = defineStore("ys", () => {
98
101
  return "登录状态已失效,请重新登录。";
99
102
  }
100
103
 
101
- async function login(username: string, password: string, otp = ""): Promise<void> {
104
+ async function login(
105
+ username: string,
106
+ password: string,
107
+ otp = "",
108
+ ): Promise<void> {
102
109
  if (!ws) throw new Error("未连接");
103
110
  sessionMessage.value = "";
104
111
  const result = (await ws.call("login", {
105
112
  username,
106
- password,
113
+ password: await sealLoginPassword(password),
107
114
  otp,
108
115
  app_name: appName.value,
109
116
  user_agent: navigator.userAgent,
@@ -115,7 +122,9 @@ export const useYsStore = defineStore("ys", () => {
115
122
  async function loginByCode(code: string): Promise<void> {
116
123
  await waitUntilConnected();
117
124
  if (!ws) throw new Error("未连接");
118
- const result = (await ws.call("exchange_sso_code", { code })) as LoginResult;
125
+ const result = (await ws.call("exchange_sso_code", {
126
+ code,
127
+ })) as LoginResult;
119
128
  setSession(result);
120
129
  }
121
130
 
@@ -135,7 +144,10 @@ export const useYsStore = defineStore("ys", () => {
135
144
  });
136
145
  }
137
146
 
138
- async function changeMyPassword(currentPassword: string, newPassword: string): Promise<void> {
147
+ async function changeMyPassword(
148
+ currentPassword: string,
149
+ newPassword: string,
150
+ ): Promise<void> {
139
151
  if (!ws || !accessToken.value) throw new Error("未登录");
140
152
  if (
141
153
  String(user.value?.role ?? "") === "Root" ||
@@ -147,8 +159,8 @@ export const useYsStore = defineStore("ys", () => {
147
159
  }
148
160
  await ws.call("change_password", {
149
161
  access_token: accessToken.value,
150
- current_password: currentPassword,
151
- new_password: newPassword,
162
+ current_password: await sealLoginPassword(currentPassword),
163
+ new_password: await sealLoginPassword(newPassword),
152
164
  });
153
165
  clearSession();
154
166
  }
package/src/ws.ts CHANGED
@@ -1,22 +1,45 @@
1
1
  /* @ysgroup/core —— 浏览器端 WS RPC 客户端(与 ys_base.ws.WsRpcClient 信封协议对齐)。 */
2
2
 
3
+ import { applyPasswordWrap } from "./password";
4
+
3
5
  export interface Envelope {
4
6
  event: string;
5
7
  id?: string;
6
8
  reply_to?: string;
7
9
  code?: number;
8
10
  message?: string;
11
+ request_id?: string;
9
12
  ts?: number;
10
13
  data?: unknown;
11
14
  }
12
15
 
16
+ /** 服务端非零信封。500 时展示文案附带编号,便于对照日志。 */
17
+ export class YsRpcError extends Error {
18
+ readonly clientMessage: string;
19
+
20
+ constructor(
21
+ public code: number,
22
+ message: string,
23
+ public requestId = "",
24
+ ) {
25
+ const display =
26
+ code >= 500 && requestId ? `${message}(编号 ${requestId})` : message;
27
+ super(display);
28
+ this.name = "YsRpcError";
29
+ this.clientMessage = message;
30
+ }
31
+ }
32
+
13
33
  type EventHandler = (data: unknown) => void;
14
34
 
15
35
  export class YsWsClient {
16
36
  private ws: WebSocket | null = null;
17
37
  private seq = 0;
18
38
  private connectionId = "";
19
- private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
39
+ private pending = new Map<
40
+ string,
41
+ { resolve: (v: unknown) => void; reject: (e: Error) => void }
42
+ >();
20
43
  private handlers = new Map<string, EventHandler[]>();
21
44
  private reconnectDelay = 1000;
22
45
  private stopped = false;
@@ -84,11 +107,15 @@ export class YsWsClient {
84
107
  };
85
108
  ws.onmessage = (e) => this.onMessage(e);
86
109
  ws.onclose = () => {
87
- for (const p of this.pending.values()) p.reject(new Error("connection lost"));
110
+ for (const p of this.pending.values())
111
+ p.reject(new Error("connection lost"));
88
112
  this.pending.clear();
89
113
  this.emit("__disconnected", null);
90
114
  if (!this.stopped) {
91
- setTimeout(() => this.connect(), this.reconnectDelay + Math.random() * 1000);
115
+ setTimeout(
116
+ () => this.connect(),
117
+ this.reconnectDelay + Math.random() * 1000,
118
+ );
92
119
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
93
120
  }
94
121
  };
@@ -106,14 +133,25 @@ export class YsWsClient {
106
133
  if (p) {
107
134
  this.pending.delete(msg.reply_to);
108
135
  if ((msg.code ?? 0) === 0) p.resolve(msg.data);
109
- else p.reject(new Error(`[${msg.code}] ${msg.message ?? ""}`));
136
+ else
137
+ p.reject(
138
+ new YsRpcError(
139
+ msg.code ?? 500,
140
+ msg.message ?? "",
141
+ msg.request_id ?? "",
142
+ ),
143
+ );
110
144
  }
111
145
  return;
112
146
  }
113
147
  if (msg.event === "hello") {
114
- this.connectionId = String((msg.data as { connection_id?: string })?.connection_id ?? "");
148
+ this.connectionId = String(
149
+ (msg.data as { connection_id?: string })?.connection_id ?? "",
150
+ );
151
+ applyPasswordWrap(msg.data);
115
152
  this.emit("__connected", msg.data);
116
153
  }
154
+ if (msg.event === "password_wrap") applyPasswordWrap(msg.data);
117
155
  this.emit(msg.event, msg.data);
118
156
  }
119
157