@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 +1 -1
- package/src/index.ts +15 -1
- package/src/password.ts +162 -0
- package/src/rest.ts +138 -41
- package/src/store.ts +19 -7
- package/src/ws.ts +27 -6
package/package.json
CHANGED
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 {
|
|
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";
|
package/src/password.ts
ADDED
|
@@ -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>(
|
|
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
|
|
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>>(
|
|
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 }>(
|
|
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>(
|
|
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`,
|
|
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[] }>(
|
|
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>[]>(
|
|
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>>(
|
|
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 }>(
|
|
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 }>(
|
|
185
|
-
|
|
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
|
-
|
|
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>>(
|
|
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(
|
|
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 }[]>(
|
|
272
|
+
return this.request<{ app_id: string; admin: boolean }[]>(
|
|
273
|
+
"GET",
|
|
274
|
+
`/api/users/${id}/apps`,
|
|
275
|
+
);
|
|
217
276
|
}
|
|
218
|
-
setUserApps(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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>>(
|
|
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[] }>(
|
|
237
|
-
|
|
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(
|
|
241
|
-
|
|
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(
|
|
244
|
-
|
|
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>[]>(
|
|
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 = () =>
|
|
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)
|
|
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) =>
|
|
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(
|
|
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", {
|
|
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(
|
|
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 =
|
|
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<
|
|
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())
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|