@ysgroup/core 0.1.10 → 0.1.12

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.10",
3
+ "version": "0.1.12",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
package/src/index.ts CHANGED
@@ -2,4 +2,18 @@
2
2
 
3
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,162 @@
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
+ ])) as CryptoKeyPair;
83
+ const recipient = await crypto.subtle.importKey(
84
+ "raw",
85
+ asArrayBuffer(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(
98
+ "raw",
99
+ asArrayBuffer(shared),
100
+ "HKDF",
101
+ false,
102
+ ["deriveBits"],
103
+ );
104
+ const aesRaw = await crypto.subtle.deriveBits(
105
+ {
106
+ name: "HKDF",
107
+ hash: "SHA-256",
108
+ salt: asArrayBuffer(SALT),
109
+ info: asArrayBuffer(INFO),
110
+ },
111
+ hkdfKey,
112
+ 256,
113
+ );
114
+ const aesKey = await crypto.subtle.importKey(
115
+ "raw",
116
+ aesRaw,
117
+ { name: "AES-GCM" },
118
+ false,
119
+ ["encrypt"],
120
+ );
121
+ const aad = new TextEncoder().encode(`${PREFIX}${kid}`);
122
+ const packedCt = new Uint8Array(
123
+ await crypto.subtle.encrypt(
124
+ {
125
+ name: "AES-GCM",
126
+ iv: asArrayBuffer(NONCE),
127
+ additionalData: aad,
128
+ tagLength: 128,
129
+ },
130
+ aesKey,
131
+ new TextEncoder().encode(plaintext),
132
+ ),
133
+ );
134
+ const ephPk = new Uint8Array(
135
+ await crypto.subtle.exportKey("raw", ephemeral.publicKey),
136
+ );
137
+ const packed = new Uint8Array(ephPk.byteLength + packedCt.byteLength);
138
+ packed.set(ephPk, 0);
139
+ packed.set(packedCt, ephPk.byteLength);
140
+ return `${PREFIX}${kid}.${b64uEncode(packed)}`;
141
+ }
142
+
143
+ function asArrayBuffer(bytes: Uint8Array): ArrayBuffer {
144
+ return bytes.buffer.slice(
145
+ bytes.byteOffset,
146
+ bytes.byteOffset + bytes.byteLength,
147
+ ) as ArrayBuffer;
148
+ }
149
+
150
+ function b64uEncode(raw: Uint8Array): string {
151
+ let bin = "";
152
+ for (const byte of raw) bin += String.fromCharCode(byte);
153
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
154
+ }
155
+
156
+ function b64uDecode(text: string): Uint8Array {
157
+ const pad = "=".repeat((4 - (text.length % 4)) % 4);
158
+ const bin = atob(text.replace(/-/g, "+").replace(/_/g, "/") + pad);
159
+ const out = new Uint8Array(bin.length);
160
+ for (let i = 0; i < bin.length; i += 1) out[i] = bin.charCodeAt(i);
161
+ return out;
162
+ }
package/src/rest.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /* @ysgroup/core —— 管理接口客户端(支持 HTTP 或统一 WS RPC 传输)。 */
2
2
 
3
+ import { sealLoginPassword } from "./password";
3
4
  import { YsRpcError } from "./ws";
4
5
 
5
6
  export type YsAdminRpcCall = (data: {
@@ -47,7 +48,11 @@ export class YsRest {
47
48
  private rpcCall?: YsAdminRpcCall,
48
49
  ) {}
49
50
 
50
- 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> {
51
56
  if (this.rpcCall) {
52
57
  try {
53
58
  return (await this.rpcCall({
@@ -109,7 +114,9 @@ export class YsRest {
109
114
  body: form,
110
115
  });
111
116
  if (!resp.ok) {
112
- const parsed = await resp.json().catch(() => ({ detail: resp.statusText }));
117
+ const parsed = await resp
118
+ .json()
119
+ .catch(() => ({ detail: resp.statusText }));
113
120
  throw new YsRestError(resp.status, parsed.detail ?? parsed);
114
121
  }
115
122
  return (await resp.json()) as T;
@@ -122,16 +129,25 @@ export class YsRest {
122
129
  list<T = Record<string, unknown>>(table: string) {
123
130
  return this.request<T[]>("GET", `/api/tables/${table}`);
124
131
  }
125
- 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
+ ) {
126
136
  return this.request<T>("PUT", `/api/tables/${table}`, doc);
127
137
  }
128
138
  remove(table: string, id: string) {
129
- return this.request<{ deleted: string }>("DELETE", `/api/tables/${table}/${id}`);
139
+ return this.request<{ deleted: string }>(
140
+ "DELETE",
141
+ `/api/tables/${table}/${id}`,
142
+ );
130
143
  }
131
144
  deleteImpact(table: string, id: string) {
132
- 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
+ );
133
149
  }
134
- revealCredentials(
150
+ async revealCredentials(
135
151
  table: string,
136
152
  id: string,
137
153
  body: {
@@ -143,9 +159,12 @@ export class YsRest {
143
159
  return this.request<{
144
160
  credentials: Record<string, RevealedCredential>;
145
161
  expires_in: number;
146
- }>("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
+ });
147
166
  }
148
- updateCredentials(
167
+ async updateCredentials(
149
168
  table: string,
150
169
  id: string,
151
170
  body: {
@@ -154,13 +173,23 @@ export class YsRest {
154
173
  reason: string;
155
174
  },
156
175
  ) {
157
- 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
+ );
158
184
  }
159
185
  listApps() {
160
186
  return this.request<Record<string, unknown>[]>("GET", "/api/apps");
161
187
  }
162
188
  listAppReferences() {
163
- 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
+ );
164
193
  }
165
194
  createApp(body: Record<string, unknown>) {
166
195
  return this.request<Record<string, unknown>>("POST", "/api/apps", body);
@@ -172,54 +201,92 @@ export class YsRest {
172
201
  return this.request("POST", `/api/apps/${appId}/grants`, grants);
173
202
  }
174
203
  updateApp(appId: string, body: Record<string, unknown>) {
175
- 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
+ );
176
209
  }
177
210
  rotateSecret(appId: string) {
178
- 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
+ );
179
216
  }
180
217
  disableUser(userId: string) {
181
218
  return this.request("POST", `/api/users/${userId}/disable`, {});
182
219
  }
183
220
  issueSso(appName: string) {
184
- return this.request<{ code: string; expires_in: number }>("POST", "/api/sso/issue", {
185
- app_name: appName,
186
- });
221
+ return this.request<{ code: string; expires_in: number }>(
222
+ "POST",
223
+ "/api/sso/issue",
224
+ {
225
+ app_name: appName,
226
+ },
227
+ );
187
228
  }
188
229
  portalApps() {
189
230
  return this.request<Record<string, unknown>[]>("GET", "/api/portal/apps");
190
231
  }
191
- createUser(body: Record<string, unknown>) {
192
- 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
+ });
193
241
  }
194
242
  updateUser(id: string, body: Record<string, unknown>) {
195
- 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
+ );
196
248
  }
197
249
  setUserEnabled(id: string, enabled: boolean) {
198
- 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
+ );
199
255
  }
200
- resetUserPassword(id: string, newPassword: string, reason: string) {
256
+ async resetUserPassword(id: string, newPassword: string, reason: string) {
201
257
  return this.request("POST", `/api/users/${id}/reset-password`, {
202
- new_password: newPassword,
258
+ new_password: await sealLoginPassword(newPassword),
203
259
  reason,
204
260
  });
205
261
  }
206
- changeMyPassword(currentPassword: string, newPassword: string) {
262
+ async changeMyPassword(currentPassword: string, newPassword: string) {
207
263
  return this.request("POST", "/api/me/change-password", {
208
- current_password: currentPassword,
209
- new_password: newPassword,
264
+ current_password: await sealLoginPassword(currentPassword),
265
+ new_password: await sealLoginPassword(newPassword),
210
266
  });
211
267
  }
212
268
  removeUser(id: string) {
213
269
  return this.request("DELETE", `/api/users/${id}`);
214
270
  }
215
271
  getUserApps(id: string) {
216
- 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
+ );
217
276
  }
218
- setUserApps(id: string, grants: { app_id: string; admin: boolean }[], reason = "") {
219
- return this.request<{ grants: { app_id: string; admin: boolean }[] }>("PUT", `/api/users/${id}/apps`, {
220
- grants,
221
- reason,
222
- });
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
+ );
223
290
  }
224
291
  uploadOpenAccountConfirmation(ownerId: string, file: File) {
225
292
  return this.upload<{
@@ -230,21 +297,49 @@ export class YsRest {
230
297
  }>(`/api/owners/${ownerId}/open-account-confirmation`, file);
231
298
  }
232
299
  upsertMountExchangeAccount(body: Record<string, unknown>) {
233
- 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
+ );
234
305
  }
235
306
  setProjectOwners(projectId: string, ownerIds: string[]) {
236
- return this.request<{ owner_ids: string[] }>("PUT", `/api/projects/${projectId}/owners`, {
237
- owner_ids: ownerIds,
238
- });
307
+ return this.request<{ owner_ids: string[] }>(
308
+ "PUT",
309
+ `/api/projects/${projectId}/owners`,
310
+ {
311
+ owner_ids: ownerIds,
312
+ },
313
+ );
239
314
  }
240
- getRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies") {
241
- 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
+ );
242
324
  }
243
- setRelations(entity: "owners" | "accounts", id: string, relation: "apps" | "strategies", ids: string[], reason = "") {
244
- 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
+ );
245
337
  }
246
338
  listOwnerFiles(ownerId: string) {
247
- 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
+ );
248
343
  }
249
344
  async downloadFile(fileId: string) {
250
345
  if (this.rpcCall) {
@@ -291,7 +386,8 @@ function rpcError(error: unknown): YsRestError {
291
386
  function fileToBase64(file: File): Promise<string> {
292
387
  return new Promise((resolve, reject) => {
293
388
  const reader = new FileReader();
294
- reader.onerror = () => reject(reader.error ?? new Error("读取上传文件失败"));
389
+ reader.onerror = () =>
390
+ reject(reader.error ?? new Error("读取上传文件失败"));
295
391
  reader.onload = () => resolve(String(reader.result).split(",", 2)[1] ?? "");
296
392
  reader.readAsDataURL(file);
297
393
  });
@@ -300,7 +396,8 @@ function fileToBase64(file: File): Promise<string> {
300
396
  function base64ToBlob(value: string, contentType: string): Blob {
301
397
  const binary = atob(value);
302
398
  const bytes = new Uint8Array(binary.length);
303
- 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);
304
401
  return new Blob([bytes], { type: contentType });
305
402
  }
306
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,5 +1,7 @@
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;
@@ -20,7 +22,8 @@ export class YsRpcError extends Error {
20
22
  message: string,
21
23
  public requestId = "",
22
24
  ) {
23
- const display = code >= 500 && requestId ? `${message}(编号 ${requestId})` : message;
25
+ const display =
26
+ code >= 500 && requestId ? `${message}(编号 ${requestId})` : message;
24
27
  super(display);
25
28
  this.name = "YsRpcError";
26
29
  this.clientMessage = message;
@@ -33,7 +36,10 @@ export class YsWsClient {
33
36
  private ws: WebSocket | null = null;
34
37
  private seq = 0;
35
38
  private connectionId = "";
36
- 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
+ >();
37
43
  private handlers = new Map<string, EventHandler[]>();
38
44
  private reconnectDelay = 1000;
39
45
  private stopped = false;
@@ -101,11 +107,15 @@ export class YsWsClient {
101
107
  };
102
108
  ws.onmessage = (e) => this.onMessage(e);
103
109
  ws.onclose = () => {
104
- 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"));
105
112
  this.pending.clear();
106
113
  this.emit("__disconnected", null);
107
114
  if (!this.stopped) {
108
- setTimeout(() => this.connect(), this.reconnectDelay + Math.random() * 1000);
115
+ setTimeout(
116
+ () => this.connect(),
117
+ this.reconnectDelay + Math.random() * 1000,
118
+ );
109
119
  this.reconnectDelay = Math.min(this.reconnectDelay * 2, 30_000);
110
120
  }
111
121
  };
@@ -123,14 +133,25 @@ export class YsWsClient {
123
133
  if (p) {
124
134
  this.pending.delete(msg.reply_to);
125
135
  if ((msg.code ?? 0) === 0) p.resolve(msg.data);
126
- else p.reject(new YsRpcError(msg.code ?? 500, msg.message ?? "", msg.request_id ?? ""));
136
+ else
137
+ p.reject(
138
+ new YsRpcError(
139
+ msg.code ?? 500,
140
+ msg.message ?? "",
141
+ msg.request_id ?? "",
142
+ ),
143
+ );
127
144
  }
128
145
  return;
129
146
  }
130
147
  if (msg.event === "hello") {
131
- 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);
132
152
  this.emit("__connected", msg.data);
133
153
  }
154
+ if (msg.event === "password_wrap") applyPasswordWrap(msg.data);
134
155
  this.emit(msg.event, msg.data);
135
156
  }
136
157