@cjhyy/code-shell-server 0.9.51 → 0.10.1

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 (93) hide show
  1. package/dist/account/cli.d.ts +23 -0
  2. package/dist/account/cli.js +249 -0
  3. package/dist/account/cloud-contract.d.ts +39 -0
  4. package/dist/account/cloud-contract.js +134 -0
  5. package/dist/account/cloud-types.d.ts +47 -0
  6. package/dist/account/cloud-types.js +1 -0
  7. package/dist/account/feature-service.d.ts +47 -0
  8. package/dist/account/feature-service.js +184 -0
  9. package/dist/account/http.d.ts +17 -0
  10. package/dist/account/http.js +158 -0
  11. package/dist/account/links.d.ts +13 -0
  12. package/dist/account/links.js +137 -0
  13. package/dist/account/manager.d.ts +96 -0
  14. package/dist/account/manager.js +976 -0
  15. package/dist/account/native-lock.d.ts +6 -0
  16. package/dist/account/native-lock.js +26 -0
  17. package/dist/account/native-store.d.ts +32 -0
  18. package/dist/account/native-store.js +162 -0
  19. package/dist/account/profile.d.ts +4 -0
  20. package/dist/account/profile.js +46 -0
  21. package/dist/account/store.d.ts +34 -0
  22. package/dist/account/store.js +118 -0
  23. package/dist/account/types.d.ts +138 -0
  24. package/dist/account/types.js +2 -0
  25. package/dist/desktop-web/http-api.d.ts +4 -0
  26. package/dist/desktop-web/http-api.js +12 -4
  27. package/dist/environment-identity.d.ts +1 -1
  28. package/dist/environment-identity.js +2 -1
  29. package/dist/headless-worker-env.d.ts +2 -0
  30. package/dist/headless-worker-env.js +15 -0
  31. package/dist/hub/approval-lease.js +6 -1
  32. package/dist/hub/configuration.d.ts +11 -0
  33. package/dist/hub/configuration.js +34 -1
  34. package/dist/hub/model-parameter-metadata.d.ts +26 -0
  35. package/dist/hub/model-parameter-metadata.js +92 -0
  36. package/dist/hub/run-replay.js +1 -1
  37. package/dist/hub/session-management.js +1 -1
  38. package/dist/index.account.d.ts +12 -0
  39. package/dist/index.account.js +12 -0
  40. package/dist/index.mobile-remote.d.ts +2 -0
  41. package/dist/index.mobile-remote.js +1 -0
  42. package/dist/index.worker.d.ts +1 -0
  43. package/dist/index.worker.js +1 -0
  44. package/dist/mobile-remote/account-relay-access.d.ts +59 -0
  45. package/dist/mobile-remote/account-relay-access.js +174 -0
  46. package/dist/mobile-remote/approval-clock.d.ts +3 -0
  47. package/dist/mobile-remote/approval-clock.js +15 -0
  48. package/dist/mobile-remote/codex-room-agent.d.ts +6 -0
  49. package/dist/mobile-remote/codex-room-agent.js +17 -1
  50. package/dist/mobile-remote/mobile-chat-turn.d.ts +4 -0
  51. package/dist/mobile-remote/mobile-chat-turn.js +3 -0
  52. package/dist/mobile-remote/mobile-client-event-validator.js +47 -2
  53. package/dist/mobile-remote/mobile-upload-service.d.ts +9 -0
  54. package/dist/mobile-remote/mobile-upload-service.js +25 -0
  55. package/dist/mobile-remote/remote-host-manager.d.ts +21 -4
  56. package/dist/mobile-remote/remote-host-manager.js +142 -25
  57. package/dist/mobile-remote/resident-agent.d.ts +14 -0
  58. package/dist/mobile-remote/resident-agent.js +38 -11
  59. package/dist/mobile-remote/room-history.d.ts +7 -0
  60. package/dist/mobile-remote/room-history.js +56 -1
  61. package/dist/mobile-remote/room-manager.d.ts +216 -9
  62. package/dist/mobile-remote/room-manager.js +700 -65
  63. package/dist/mobile-remote/room-process-stop.d.ts +15 -0
  64. package/dist/mobile-remote/room-process-stop.js +41 -0
  65. package/dist/mobile-remote/trusted-device-store.d.ts +12 -0
  66. package/dist/mobile-remote/trusted-device-store.js +54 -1
  67. package/dist/mobile-remote/turn-queue.d.ts +183 -0
  68. package/dist/mobile-remote/turn-queue.js +422 -0
  69. package/dist/mobile-remote/types.d.ts +1 -1
  70. package/dist/panels/agent-task-host.js +6 -12
  71. package/dist/panels/agent-task-service.d.ts +2 -0
  72. package/dist/panels/agent-task-service.js +7 -0
  73. package/dist/panels/browser-capture.d.ts +1 -15
  74. package/dist/panels/browser-capture.js +1 -35
  75. package/dist/panels/directory-picker.d.ts +48 -0
  76. package/dist/panels/directory-picker.js +247 -0
  77. package/dist/panels/http.d.ts +15 -15
  78. package/dist/panels/management-http.d.ts +9 -9
  79. package/dist/panels/process-service.d.ts +5 -4
  80. package/dist/panels/process-service.js +54 -16
  81. package/dist/panels/runtime.js +148 -30
  82. package/dist/panels/tool-jobs-storage.d.ts +4 -0
  83. package/dist/panels/tool-jobs-storage.js +111 -34
  84. package/dist/panels/types.d.ts +1 -97
  85. package/dist/remote-relay/connector.d.ts +8 -0
  86. package/dist/remote-relay/connector.js +29 -2
  87. package/dist/remote-relay/protocol.d.ts +21 -0
  88. package/dist/remote-relay/protocol.js +31 -0
  89. package/dist/serve/cli.js +6 -0
  90. package/dist/serve/headless-server.js +24 -19
  91. package/dist/sessions-service.d.ts +4 -0
  92. package/dist/sessions-service.js +14 -1
  93. package/package.json +10 -6
@@ -0,0 +1,976 @@
1
+ import { readAccountFeatures } from "@cjhyy/code-shell-web/account";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+ import { cloudManagementTicket, cloudOrigin, cloudId, } from "./cloud-contract.js";
4
+ import { isRelayOrigin, isRelayToken } from "../remote-relay/protocol.js";
5
+ import { accountLinksRequest, accountLinkView, accountLinkRevocation } from "./links.js";
6
+ import { accountProfile, accountProfileRequest } from "./profile.js";
7
+ import { validCloudAccountGrant } from "./store.js";
8
+ import { CloudAccountHttpError, createCloudAccountTransport, } from "./http.js";
9
+ function cancelPrepared(prepared) {
10
+ try {
11
+ prepared?.cancel();
12
+ }
13
+ catch {
14
+ /* Link cleanup cannot undo an account login. */
15
+ }
16
+ }
17
+ function savedGrant(value, origin) {
18
+ return {
19
+ origin,
20
+ accessToken: value.accessToken,
21
+ refreshToken: value.refreshToken,
22
+ accessTokenExpiresAt: value.accessTokenExpiresAt,
23
+ refreshTokenExpiresAt: value.refreshTokenExpiresAt,
24
+ sessionId: value.sessionId,
25
+ account: {
26
+ id: value.account.id,
27
+ username: value.account.username,
28
+ ...(value.account.displayName === undefined
29
+ ? {}
30
+ : { displayName: value.account.displayName }),
31
+ },
32
+ ...(value.kind === undefined ? {} : { kind: value.kind }),
33
+ ...(value.audience === undefined ? {} : { audience: value.audience }),
34
+ ...(value.hostId === undefined ? {} : { hostId: value.hostId }),
35
+ };
36
+ }
37
+ function linkContinuation(value, authorizationUrl) {
38
+ if (!value || typeof value !== "object")
39
+ return;
40
+ const result = value;
41
+ if (result.state !== "ready" || typeof result.authorizationUrl !== "string")
42
+ return;
43
+ try {
44
+ const expected = new URL(authorizationUrl);
45
+ const url = new URL(result.authorizationUrl);
46
+ if (url.origin !== expected.origin ||
47
+ url.protocol !== "https:" ||
48
+ url.username ||
49
+ url.password ||
50
+ url.hash ||
51
+ url.pathname !== "/oauth/account-link/claim" ||
52
+ [...url.searchParams.keys()].length !== 1 ||
53
+ !/^[A-Za-z0-9_-]{43}$/.test(url.searchParams.get("ticket") ?? "") ||
54
+ result.authorizationUrl.length > 2048)
55
+ return;
56
+ return url.href;
57
+ }
58
+ catch {
59
+ return;
60
+ }
61
+ }
62
+ function requireOrigin(origin) {
63
+ if (!isRelayOrigin(origin) || origin.length > 2048)
64
+ throw new Error("请输入完整的 HTTPS 云服务地址。");
65
+ }
66
+ function wait(ms, signal) {
67
+ return new Promise((resolve, reject) => {
68
+ signal.throwIfAborted();
69
+ const abort = () => {
70
+ clearTimeout(timer);
71
+ reject(new Error("云账号操作已取消。"));
72
+ };
73
+ const timer = setTimeout(() => {
74
+ signal.removeEventListener("abort", abort);
75
+ resolve();
76
+ }, ms);
77
+ signal.addEventListener("abort", abort, { once: true });
78
+ });
79
+ }
80
+ /** Optional cloud identity. Construction/status never initiate network activity. */
81
+ export class CloudAccountManager {
82
+ deps;
83
+ loaded = false;
84
+ disposed = false;
85
+ statusListeners = new Set();
86
+ saved;
87
+ state = "signed-out";
88
+ generation = 0;
89
+ operation = new AbortController();
90
+ refresh;
91
+ refreshResponse;
92
+ revocationResponses = new WeakSet();
93
+ request;
94
+ githubConnection;
95
+ pendingGitHubLink;
96
+ profileRevision = 0;
97
+ profileRequestRevision = 0;
98
+ profileUpdate;
99
+ activation;
100
+ expiryTimer;
101
+ constructor(deps) {
102
+ this.deps = deps;
103
+ this.request = deps.request ?? createCloudAccountTransport();
104
+ }
105
+ now() {
106
+ return this.deps.now?.() ?? Date.now();
107
+ }
108
+ load() {
109
+ if (this.loaded)
110
+ return;
111
+ this.loaded = true;
112
+ try {
113
+ this.saved = this.deps.store.load();
114
+ this.state = this.saved ? "signed-in" : "signed-out";
115
+ }
116
+ catch {
117
+ this.state = "storage-error";
118
+ }
119
+ }
120
+ status() {
121
+ this.load();
122
+ return {
123
+ state: this.state,
124
+ ...(this.saved ? { origin: this.saved.origin, account: { ...this.saved.account } } : {}),
125
+ ...(this.githubConnection ? { githubConnection: { ...this.githubConnection } } : {}),
126
+ };
127
+ }
128
+ onStatus(listener) {
129
+ this.statusListeners.add(listener);
130
+ return () => {
131
+ this.statusListeners.delete(listener);
132
+ };
133
+ }
134
+ /** Cancel this host's work without revoking a saved account session. */
135
+ dispose() {
136
+ this.disposed = true;
137
+ this.invalidate();
138
+ this.operation.abort();
139
+ this.statusListeners.clear();
140
+ }
141
+ async overview(input, authorized = () => true, signal) {
142
+ const lease = this.captureRootAccount(input.origin, input.accountId, authorized);
143
+ const requestSignal = signal ? AbortSignal.any([lease.signal, signal]) : lease.signal;
144
+ requestSignal.throwIfAborted();
145
+ const token = await this.getCredential(input.origin, input.accountId, requestSignal);
146
+ lease.current();
147
+ const raw = await this.request({
148
+ origin: input.origin,
149
+ path: "/api/v1/account/overview",
150
+ method: "GET",
151
+ token,
152
+ signal: requestSignal,
153
+ });
154
+ lease.current();
155
+ requestSignal.throwIfAborted();
156
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
157
+ throw new Error("云服务返回了无效的账号概况。");
158
+ const value = raw;
159
+ const account = accountProfile(value, input.accountId);
160
+ return {
161
+ account,
162
+ features: readAccountFeatures(value),
163
+ };
164
+ }
165
+ /** Public service discovery never reads or changes this device's account authority. */
166
+ async capabilities(origin, signal) {
167
+ requireOrigin(origin);
168
+ if (this.disposed)
169
+ throw new Error("云账号服务已关闭。");
170
+ const value = (await this.request({
171
+ origin,
172
+ path: "/api/v1/account/status",
173
+ method: "GET",
174
+ signal,
175
+ }));
176
+ if (!value ||
177
+ typeof value !== "object" ||
178
+ Array.isArray(value) ||
179
+ typeof value.registrationAllowed !== "boolean" ||
180
+ typeof value.githubEnabled !== "boolean" ||
181
+ (value.browserLoginEnabled !== undefined && typeof value.browserLoginEnabled !== "boolean") ||
182
+ (value.githubLinkEnabled !== undefined && typeof value.githubLinkEnabled !== "boolean") ||
183
+ (value.linkManagementEnabled !== undefined &&
184
+ typeof value.linkManagementEnabled !== "boolean") ||
185
+ (value.profileEnabled !== undefined && typeof value.profileEnabled !== "boolean"))
186
+ throw new Error("云服务返回了无效的账号能力信息。");
187
+ return {
188
+ features: readAccountFeatures(value),
189
+ registrationAllowed: value.registrationAllowed,
190
+ githubEnabled: value.githubEnabled,
191
+ ...(value.browserLoginEnabled === undefined
192
+ ? {}
193
+ : { browserLoginEnabled: value.browserLoginEnabled }),
194
+ ...(value.githubLinkEnabled === undefined
195
+ ? {}
196
+ : { githubLinkEnabled: value.githubLinkEnabled }),
197
+ ...(value.linkManagementEnabled === undefined
198
+ ? {}
199
+ : { linkManagementEnabled: value.linkManagementEnabled }),
200
+ ...(value.profileEnabled === undefined ? {} : { profileEnabled: value.profileEnabled }),
201
+ ...(this.relayCapability(value.relay, origin)
202
+ ? { relay: this.relayCapability(value.relay, origin) }
203
+ : {}),
204
+ };
205
+ }
206
+ relayCapability(value, origin) {
207
+ if (value === undefined)
208
+ return;
209
+ if (!value || typeof value !== "object" || typeof value.enabled !== "boolean")
210
+ throw new Error("云服务返回了无效的中继能力信息。");
211
+ if (!value.enabled)
212
+ return { enabled: false };
213
+ if (value.directoryOrigin !== origin ||
214
+ value.protocolVersion !== 1 ||
215
+ value.accountHandoffVersion !== 1)
216
+ throw new Error("此服务的设备协作需要升级,或目录地址不属于当前账号服务。");
217
+ return {
218
+ enabled: true,
219
+ directoryOrigin: origin,
220
+ protocolVersion: 1,
221
+ accountHandoffVersion: 1,
222
+ };
223
+ }
224
+ armExpiry() {
225
+ clearTimeout(this.expiryTimer);
226
+ const saved = this.saved;
227
+ if (!saved)
228
+ return;
229
+ const delay = Math.min(2_147_483_647, Math.max(1, saved.refreshTokenExpiresAt - this.now()));
230
+ this.expiryTimer = setTimeout(() => {
231
+ if (this.saved?.sessionId !== saved.sessionId)
232
+ return;
233
+ if (saved.refreshTokenExpiresAt <= this.now())
234
+ void this.logout().catch(() => { });
235
+ else
236
+ this.armExpiry();
237
+ }, delay);
238
+ this.expiryTimer.unref?.();
239
+ }
240
+ async activate(authorized) {
241
+ const saved = this.saved;
242
+ if (!saved || !this.deps.activateRelay)
243
+ return;
244
+ if (this.activation?.sessionId === saved.sessionId)
245
+ return this.activation.pending;
246
+ const generation = this.generation;
247
+ const current = () => {
248
+ try {
249
+ this.current(generation, authorized);
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ return this.state === "signed-in" && this.saved?.sessionId === saved.sessionId;
255
+ };
256
+ const pending = (async () => {
257
+ try {
258
+ await this.deps.activateRelay({ origin: saved.origin, accountId: saved.account.id }, current);
259
+ }
260
+ catch {
261
+ // Device collaboration reports its own unavailable state; account login remains successful.
262
+ }
263
+ })();
264
+ this.activation = { sessionId: saved.sessionId, pending };
265
+ await pending;
266
+ if (this.activation?.pending === pending)
267
+ this.activation = undefined;
268
+ }
269
+ /** The application explicitly restores saved authority once its main window is ready. */
270
+ async resume(authorized = () => true) {
271
+ this.load();
272
+ const saved = this.saved;
273
+ if (!saved || this.state !== "signed-in" || !authorized())
274
+ return this.status();
275
+ await this.getCredential(saved.origin, saved.account.id);
276
+ if (this.saved?.sessionId !== saved.sessionId || !authorized())
277
+ return this.status();
278
+ this.armExpiry();
279
+ await this.activate(authorized);
280
+ return this.status();
281
+ }
282
+ publish(state) {
283
+ this.state = state;
284
+ try {
285
+ this.deps.changed(this.status());
286
+ }
287
+ catch {
288
+ // A closing renderer cannot keep cloud authority alive.
289
+ }
290
+ for (const listener of this.statusListeners) {
291
+ try {
292
+ listener(this.status());
293
+ }
294
+ catch {
295
+ /* Observers cannot change authority. */
296
+ }
297
+ }
298
+ }
299
+ invalidate() {
300
+ this.generation++;
301
+ this.activation = undefined;
302
+ clearTimeout(this.expiryTimer);
303
+ this.operation.abort();
304
+ cancelPrepared(this.pendingGitHubLink);
305
+ this.pendingGitHubLink = undefined;
306
+ if (this.githubConnection?.state === "pending")
307
+ this.githubConnection = { state: "skipped" };
308
+ this.operation = new AbortController();
309
+ this.refresh = undefined;
310
+ this.refreshResponse = undefined;
311
+ this.profileUpdate = undefined;
312
+ }
313
+ current(generation, authorized) {
314
+ if (this.disposed ||
315
+ generation !== this.generation ||
316
+ this.operation.signal.aborted ||
317
+ !authorized())
318
+ throw new Error("云账号操作已取消。");
319
+ }
320
+ async retire(saved) {
321
+ if (saved)
322
+ await this.deps.retireRelay?.({ origin: saved.origin, accountId: saved.account.id });
323
+ }
324
+ validGrant(value, origin) {
325
+ if (!validCloudAccountGrant(value) ||
326
+ value.kind === "device" ||
327
+ (value.audience !== undefined && value.audience !== origin) ||
328
+ value.accessTokenExpiresAt <= this.now() ||
329
+ value.refreshTokenExpiresAt <= this.now())
330
+ throw new Error("云服务返回了无效的账号授权。");
331
+ }
332
+ async complete(value, origin, generation, authorized) {
333
+ this.validGrant(value, origin);
334
+ try {
335
+ this.current(generation, authorized);
336
+ }
337
+ catch (error) {
338
+ void this.revoke({ ...value, origin });
339
+ throw error;
340
+ }
341
+ const previous = this.saved;
342
+ const next = savedGrant(value, origin);
343
+ try {
344
+ this.deps.store.save(next);
345
+ }
346
+ catch (error) {
347
+ void this.revoke(next);
348
+ throw error;
349
+ }
350
+ this.saved = next;
351
+ this.armExpiry();
352
+ this.publish("signed-in");
353
+ if (previous && previous.sessionId !== next.sessionId)
354
+ await this.revoke(previous);
355
+ await this.activate(authorized);
356
+ return this.status();
357
+ }
358
+ async revoke(saved, inFlightRefresh) {
359
+ // A captured refresh can extend the session beyond the saved grant's expiry.
360
+ if (!inFlightRefresh && saved.refreshTokenExpiresAt <= this.now())
361
+ return true;
362
+ try {
363
+ let token = saved.accessToken;
364
+ if (inFlightRefresh || saved.accessTokenExpiresAt <= this.now()) {
365
+ const refreshed = await (inFlightRefresh ??
366
+ this.request({
367
+ origin: saved.origin,
368
+ path: "/api/v1/account/refresh",
369
+ body: { refreshToken: saved.refreshToken },
370
+ }));
371
+ this.validGrant(refreshed, saved.origin);
372
+ if (refreshed.sessionId !== saved.sessionId || refreshed.account.id !== saved.account.id)
373
+ return false;
374
+ token = refreshed.accessToken;
375
+ }
376
+ const value = await this.request({
377
+ origin: saved.origin,
378
+ path: "/api/v1/account/logout",
379
+ token,
380
+ body: {},
381
+ });
382
+ return (!!value &&
383
+ typeof value === "object" &&
384
+ !Array.isArray(value) &&
385
+ value.ok === true);
386
+ }
387
+ catch {
388
+ return false;
389
+ }
390
+ }
391
+ async prepare(origin) {
392
+ requireOrigin(origin);
393
+ this.load();
394
+ this.deps.store.preflight();
395
+ this.invalidate();
396
+ this.githubConnection = undefined;
397
+ const generation = this.generation;
398
+ const signal = this.operation.signal;
399
+ this.publish("signing-in");
400
+ try {
401
+ await this.retire(this.saved);
402
+ this.current(generation, () => true);
403
+ return { generation, signal };
404
+ }
405
+ catch (error) {
406
+ if (generation === this.generation)
407
+ this.publish(this.saved ? "signed-in" : "signed-out");
408
+ throw error;
409
+ }
410
+ }
411
+ async signIn(kind, input, authorized = () => true) {
412
+ if (!input ||
413
+ typeof input.username !== "string" ||
414
+ !/^[a-z0-9][a-z0-9._-]{2,63}$/i.test(input.username.trim()) ||
415
+ input.username.length > 128 ||
416
+ typeof input.password !== "string" ||
417
+ input.password.length < 12 ||
418
+ input.password.length > 1024 ||
419
+ (input.deviceName !== undefined &&
420
+ (typeof input.deviceName !== "string" || input.deviceName.length > 100)))
421
+ throw new Error("请填写账号名称和至少 12 位密码。");
422
+ if (!authorized())
423
+ throw new Error("云账号操作已取消。");
424
+ const { generation, signal } = await this.prepare(input.origin);
425
+ try {
426
+ const grant = await this.request({
427
+ origin: input.origin,
428
+ path: `/api/v1/account/${kind}`,
429
+ body: {
430
+ username: input.username.trim(),
431
+ password: input.password,
432
+ ...(input.deviceName ? { deviceName: input.deviceName } : {}),
433
+ },
434
+ signal,
435
+ });
436
+ return await this.complete(grant, input.origin, generation, authorized);
437
+ }
438
+ finally {
439
+ if (generation === this.generation && this.state === "signing-in")
440
+ this.publish(this.saved ? "signed-in" : "signed-out");
441
+ }
442
+ }
443
+ /** The website confirms identity; only this Host can redeem and persist its own session. */
444
+ async signInWithBrowser(input, authorized = () => true) {
445
+ if (!input ||
446
+ typeof input !== "object" ||
447
+ Object.keys(input).some((key) => key !== "origin" && key !== "deviceName") ||
448
+ (input.deviceName !== undefined &&
449
+ (typeof input.deviceName !== "string" || input.deviceName.length > 100)))
450
+ throw new Error("网页登录请求无效。");
451
+ if (!authorized())
452
+ throw new Error("云账号操作已取消。");
453
+ const { generation, signal } = await this.prepare(input.origin);
454
+ const codeVerifier = randomBytes(32).toString("base64url");
455
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
456
+ let receipt;
457
+ try {
458
+ const capabilities = await this.capabilities(input.origin, signal);
459
+ this.current(generation, authorized);
460
+ if (capabilities.browserLoginEnabled !== true)
461
+ throw new Error("此账号服务尚未启用网页登录,请更新服务或使用其他登录方式。");
462
+ const flow = (await this.request({
463
+ origin: input.origin,
464
+ path: "/api/v1/account/browser/start",
465
+ body: { codeChallenge, ...(input.deviceName ? { deviceName: input.deviceName } : {}) },
466
+ signal,
467
+ }));
468
+ // Capture a valid receipt before checking navigation so even malformed replies can be cancelled.
469
+ if (isRelayToken(flow?.receipt))
470
+ receipt = flow.receipt;
471
+ let url;
472
+ try {
473
+ if (typeof flow?.authorizationUrl === "string" && flow.authorizationUrl.length <= 2048)
474
+ url = new URL(flow.authorizationUrl);
475
+ }
476
+ catch {
477
+ /* Invalid service navigation is reported without echoing its contents. */
478
+ }
479
+ if (!url ||
480
+ url.origin !== input.origin ||
481
+ url.pathname !== "/desktop-login" ||
482
+ url.username ||
483
+ url.password ||
484
+ url.hash ||
485
+ [...url.searchParams.keys()].length !== 1 ||
486
+ !isRelayToken(url.searchParams.get("request")) ||
487
+ url.searchParams.get("request") === receipt ||
488
+ !receipt ||
489
+ !Number.isSafeInteger(flow.expiresAt) ||
490
+ flow.expiresAt <= this.now() ||
491
+ flow.expiresAt > this.now() + 15 * 60_000)
492
+ throw new Error("网页登录地址无效,请重新发起登录。");
493
+ this.current(generation, authorized);
494
+ await this.deps.openExternal(url.href);
495
+ const expiresAt = flow.expiresAt;
496
+ while (this.now() < expiresAt) {
497
+ this.current(generation, authorized);
498
+ const result = await this.request({
499
+ origin: input.origin,
500
+ path: "/api/v1/account/browser/poll",
501
+ body: { receipt, codeVerifier },
502
+ signal,
503
+ });
504
+ if (result?.status !== "pending")
505
+ return await this.complete(result, input.origin, generation, authorized);
506
+ await (this.deps.sleep ?? wait)(Math.min(10_000, Math.max(1000, typeof flow.pollIntervalMs === "number" && Number.isFinite(flow.pollIntervalMs)
507
+ ? flow.pollIntervalMs
508
+ : 1000)), signal);
509
+ }
510
+ throw new Error("网页登录已超时,请重新发起登录。");
511
+ }
512
+ finally {
513
+ if (receipt)
514
+ void this.request({
515
+ origin: input.origin,
516
+ path: "/api/v1/account/browser/cancel",
517
+ body: { receipt, codeVerifier },
518
+ }).catch(() => { });
519
+ if (generation === this.generation && this.state === "signing-in")
520
+ this.publish(this.saved ? "signed-in" : "signed-out");
521
+ }
522
+ }
523
+ async linkGitHub(authorized = () => true) {
524
+ this.load();
525
+ const saved = this.saved;
526
+ if (!saved || this.state !== "signed-in" || !authorized())
527
+ throw new Error("请先登录云账号。");
528
+ const token = await this.getCredential(saved.origin, saved.account.id);
529
+ if (!this.saved || this.saved.sessionId !== saved.sessionId || !authorized())
530
+ throw new Error("云账号操作已取消。");
531
+ return this.github({ origin: saved.origin }, authorized, { saved: this.saved, token });
532
+ }
533
+ /** Every read/revoke is bound to the reviewed identity and the current local account incarnation. */
534
+ async manageLinks(raw, authorized = () => true) {
535
+ const input = accountLinksRequest(raw);
536
+ this.load();
537
+ const saved = this.saved;
538
+ const generation = this.generation;
539
+ const signal = this.operation.signal;
540
+ const current = () => {
541
+ this.current(generation, authorized);
542
+ const actual = this.saved;
543
+ if (!saved ||
544
+ !actual ||
545
+ this.state !== "signed-in" ||
546
+ actual.sessionId !== saved.sessionId ||
547
+ actual.origin !== input.origin ||
548
+ actual.account.id !== input.accountId ||
549
+ actual.kind === "device" ||
550
+ (actual.audience !== undefined && actual.audience !== input.origin))
551
+ throw new Error("请先登录此云服务的账号。");
552
+ };
553
+ current();
554
+ const token = await this.getCredential(input.origin, input.accountId, signal);
555
+ current();
556
+ const value = await this.request({
557
+ origin: input.origin,
558
+ path: input.action === "list"
559
+ ? "/api/v1/account/links"
560
+ : `/api/v1/account/links/${input.kind}/${input.id}`,
561
+ method: input.action === "list" ? "GET" : "DELETE",
562
+ token,
563
+ signal,
564
+ });
565
+ current();
566
+ return input.action === "list"
567
+ ? { action: "list", view: accountLinkView(value) }
568
+ : accountLinkRevocation(value, input);
569
+ }
570
+ async profile(raw, authorized = () => true) {
571
+ const input = accountProfileRequest(raw);
572
+ this.load();
573
+ if (this.profileUpdate)
574
+ throw new Error("账号资料正在保存,请稍后重试。");
575
+ const saved = this.saved;
576
+ const generation = this.generation;
577
+ const signal = this.operation.signal;
578
+ const revision = ++this.profileRequestRevision;
579
+ const current = () => {
580
+ this.current(generation, authorized);
581
+ if (!saved ||
582
+ !this.saved ||
583
+ this.state !== "signed-in" ||
584
+ this.saved.sessionId !== saved.sessionId ||
585
+ this.saved.origin !== input.origin ||
586
+ this.saved.account.id !== input.accountId ||
587
+ revision !== this.profileRequestRevision)
588
+ throw new Error("云账号操作已取消。");
589
+ };
590
+ current();
591
+ const update = input.action === "update" ? {} : undefined;
592
+ if (update)
593
+ this.profileUpdate = update;
594
+ try {
595
+ const token = await this.getCredential(input.origin, input.accountId, signal);
596
+ current();
597
+ const value = await this.request({
598
+ origin: input.origin,
599
+ path: "/api/v1/account/profile",
600
+ method: input.action === "read" ? "GET" : "PATCH",
601
+ token,
602
+ signal,
603
+ ...(input.action === "read"
604
+ ? {}
605
+ : {
606
+ body: {
607
+ ...(input.displayName === undefined ? {} : { displayName: input.displayName }),
608
+ ...(input.username === undefined ? {} : { username: input.username }),
609
+ },
610
+ }),
611
+ });
612
+ current();
613
+ const account = accountProfile(value, input.accountId);
614
+ const next = { ...this.saved, account };
615
+ try {
616
+ this.deps.store.save(next);
617
+ }
618
+ catch {
619
+ throw new Error("账号资料同步后无法保存到本机,请重试。");
620
+ }
621
+ this.profileRevision++;
622
+ this.saved = next;
623
+ this.publish("signed-in");
624
+ return this.status();
625
+ }
626
+ finally {
627
+ if (update && this.profileUpdate === update)
628
+ this.profileUpdate = undefined;
629
+ }
630
+ }
631
+ signInWithGitHub(input, authorized = () => true) {
632
+ return this.github(input, authorized);
633
+ }
634
+ async github(input, authorized, link) {
635
+ if (!input || !authorized())
636
+ throw new Error("云账号操作已取消。");
637
+ if ((input.connectGitHub !== undefined && typeof input.connectGitHub !== "boolean") ||
638
+ (input.deviceName !== undefined &&
639
+ (typeof input.deviceName !== "string" || input.deviceName.length > 100)))
640
+ throw new Error("GitHub 登录请求无效。");
641
+ requireOrigin(input.origin);
642
+ if (link)
643
+ this.invalidate();
644
+ const { generation, signal } = link
645
+ ? { generation: this.generation, signal: this.operation.signal }
646
+ : await this.prepare(input.origin);
647
+ const codeVerifier = randomBytes(32).toString("base64url");
648
+ const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
649
+ let receipt;
650
+ let prepared;
651
+ try {
652
+ const status = (await this.request({
653
+ origin: input.origin,
654
+ path: "/api/v1/account/github/status",
655
+ method: "GET",
656
+ signal,
657
+ }));
658
+ this.current(generation, authorized);
659
+ if (status?.enabled !== true)
660
+ throw new Error("此云服务尚未配置 GitHub 登录。");
661
+ if (!link && input.connectGitHub === true && status.linkEnabled === true) {
662
+ try {
663
+ prepared = await this.deps.prepareGitHubLink?.(signal, authorized);
664
+ this.current(generation, authorized);
665
+ if (prepared) {
666
+ const url = new URL(prepared.authorizationUrl);
667
+ if (url.protocol !== "https:" ||
668
+ url.username ||
669
+ url.password ||
670
+ url.hash ||
671
+ url.pathname !== "/oauth/authorize" ||
672
+ prepared.authorizationUrl.length > 16384)
673
+ throw new Error("GitHub 数据连接地址无效。");
674
+ }
675
+ }
676
+ catch {
677
+ cancelPrepared(prepared);
678
+ prepared = undefined;
679
+ this.current(generation, authorized);
680
+ }
681
+ }
682
+ const flow = (await this.request({
683
+ origin: input.origin,
684
+ path: "/api/v1/account/github/start",
685
+ body: {
686
+ codeChallenge,
687
+ ...(link ? { mode: "link" } : {}),
688
+ ...(prepared ? { linkAuthorization: prepared.authorizationUrl } : {}),
689
+ },
690
+ ...(link ? { token: link.token } : {}),
691
+ signal,
692
+ }));
693
+ const url = new URL(flow.authorizationUrl);
694
+ const scopes = url.searchParams.get("scope")?.split(" ").filter(Boolean) ?? [];
695
+ if (url.origin !== "https://github.com" ||
696
+ url.pathname !== "/login/oauth/authorize" ||
697
+ url.username ||
698
+ url.password ||
699
+ url.hash ||
700
+ scopes.some((scope) => scope !== "read:user" && !(prepared && scope === "repo")) ||
701
+ url.searchParams.get("code_challenge") !== codeChallenge ||
702
+ url.searchParams.get("code_challenge_method") !== "S256" ||
703
+ !isRelayToken(flow.receipt) ||
704
+ !Number.isSafeInteger(flow.expiresAt) ||
705
+ flow.expiresAt <= this.now() ||
706
+ flow.expiresAt > this.now() + 15 * 60_000)
707
+ throw new Error("GitHub 登录地址无效。");
708
+ receipt = flow.receipt;
709
+ this.current(generation, authorized);
710
+ await this.deps.openExternal(url.href);
711
+ while (this.now() < flow.expiresAt) {
712
+ this.current(generation, authorized);
713
+ const result = await this.request({
714
+ origin: input.origin,
715
+ path: "/api/v1/account/github/poll",
716
+ body: {
717
+ receipt,
718
+ codeVerifier,
719
+ ...(input.deviceName ? { deviceName: input.deviceName } : {}),
720
+ },
721
+ signal,
722
+ });
723
+ if (result?.status !== "pending") {
724
+ if (link) {
725
+ this.current(generation, authorized);
726
+ const linked = result;
727
+ if (linked?.linked !== true ||
728
+ linked.account?.id !== link.saved.account.id ||
729
+ this.saved?.sessionId !== link.saved.sessionId)
730
+ throw new Error("GitHub 关联未完成。");
731
+ return this.status();
732
+ }
733
+ await this.complete(result, input.origin, generation, authorized);
734
+ this.current(generation, authorized);
735
+ if (input.connectGitHub === true) {
736
+ const continuation = prepared &&
737
+ linkContinuation(result.githubLink, prepared.authorizationUrl);
738
+ if (prepared && continuation) {
739
+ this.continueGitHubLink(prepared, continuation, generation, authorized);
740
+ prepared = undefined;
741
+ }
742
+ else {
743
+ this.githubConnection = { state: "failed" };
744
+ this.publish("signed-in");
745
+ }
746
+ }
747
+ else if (input.connectGitHub === false) {
748
+ this.githubConnection = { state: "skipped" };
749
+ this.publish("signed-in");
750
+ }
751
+ return this.status();
752
+ }
753
+ await (this.deps.sleep ?? wait)(Math.min(10_000, Math.max(1000, flow.pollIntervalMs || 1000)), signal);
754
+ }
755
+ throw new Error("GitHub 登录已超时,请重试。");
756
+ }
757
+ finally {
758
+ cancelPrepared(prepared);
759
+ if (receipt)
760
+ void this.request({
761
+ origin: input.origin,
762
+ path: "/api/v1/account/github/cancel",
763
+ body: { receipt, codeVerifier },
764
+ }).catch(() => { });
765
+ if (generation === this.generation && this.state === "signing-in")
766
+ this.publish(this.saved ? "signed-in" : "signed-out");
767
+ }
768
+ }
769
+ continueGitHubLink(prepared, url, generation, authorized) {
770
+ const sessionId = this.saved?.sessionId;
771
+ this.pendingGitHubLink = prepared;
772
+ this.githubConnection = { state: "pending" };
773
+ this.publish("signed-in");
774
+ void Promise.resolve()
775
+ .then(() => {
776
+ this.current(generation, authorized);
777
+ return prepared.continue(url);
778
+ })
779
+ .then((result) => {
780
+ this.current(generation, authorized);
781
+ if (this.saved?.sessionId !== sessionId)
782
+ return;
783
+ this.githubConnection = { state: result === "connected" ? "connected" : "skipped" };
784
+ this.publish("signed-in");
785
+ })
786
+ .catch(() => {
787
+ if (generation === this.generation && this.saved?.sessionId === sessionId) {
788
+ let allowed = false;
789
+ try {
790
+ allowed = authorized();
791
+ }
792
+ catch {
793
+ /* A closed owner cannot receive continuation status. */
794
+ }
795
+ this.githubConnection = { state: allowed ? "failed" : "skipped" };
796
+ this.publish("signed-in");
797
+ }
798
+ })
799
+ .finally(() => {
800
+ cancelPrepared(prepared);
801
+ if (this.pendingGitHubLink === prepared)
802
+ this.pendingGitHubLink = undefined;
803
+ });
804
+ }
805
+ cancelSignIn() {
806
+ this.load();
807
+ this.invalidate();
808
+ this.publish(this.saved ? "signed-in" : "signed-out");
809
+ }
810
+ async logout() {
811
+ this.load();
812
+ const previous = this.saved;
813
+ const inFlightRefresh = this.refreshResponse;
814
+ if (inFlightRefresh)
815
+ this.revocationResponses.add(inFlightRefresh);
816
+ this.invalidate();
817
+ this.saved = undefined;
818
+ this.githubConnection = undefined;
819
+ let storageError = false;
820
+ try {
821
+ this.deps.store.forget();
822
+ this.publish("signed-out");
823
+ }
824
+ catch {
825
+ storageError = true;
826
+ this.publish("storage-error");
827
+ }
828
+ let retireError;
829
+ try {
830
+ await this.retire(previous);
831
+ }
832
+ catch (error) {
833
+ retireError = error;
834
+ }
835
+ const confirmed = previous ? await this.revoke(previous, inFlightRefresh) : true;
836
+ if (retireError)
837
+ throw retireError;
838
+ if (storageError)
839
+ throw new Error("无法移除账号登记,请检查文件权限后重试。");
840
+ return { state: "signed-out", revocation: confirmed ? "confirmed" : "unconfirmed" };
841
+ }
842
+ /** Main-only root authority lease; refresh preserves it, logout/relogin invalidates it. */
843
+ captureRootAccount(origin, accountId, authorized) {
844
+ requireOrigin(origin);
845
+ this.load();
846
+ const saved = this.saved;
847
+ const generation = this.generation;
848
+ const signal = this.operation.signal;
849
+ const current = () => {
850
+ this.current(generation, authorized);
851
+ const actual = this.saved;
852
+ if (!saved ||
853
+ !actual ||
854
+ this.state !== "signed-in" ||
855
+ actual.sessionId !== saved.sessionId ||
856
+ actual.origin !== origin ||
857
+ actual.account.id !== accountId ||
858
+ actual.refreshTokenExpiresAt <= this.now() ||
859
+ actual.kind === "device" ||
860
+ (actual.audience !== undefined && actual.audience !== origin))
861
+ throw new Error("请先登录此云服务的账号。");
862
+ };
863
+ current();
864
+ return {
865
+ signal,
866
+ current,
867
+ ticket: async (targetOrigin, installationId) => {
868
+ cloudOrigin(targetOrigin);
869
+ cloudId(installationId);
870
+ current();
871
+ const token = await this.getCredential(origin, accountId, signal);
872
+ current();
873
+ const value = await this.request({
874
+ origin,
875
+ path: "/api/v1/account/cloud-management/tickets",
876
+ method: "POST",
877
+ token,
878
+ body: { cloudOrigin: targetOrigin, installationId },
879
+ signal,
880
+ });
881
+ current();
882
+ return cloudManagementTicket(value, { cloudOrigin: targetOrigin, accountOrigin: origin, installationId }, this.now());
883
+ },
884
+ };
885
+ }
886
+ /** Returns credentials only to main callers, for one exact service/account. */
887
+ async getCredential(origin, accountId, signal) {
888
+ requireOrigin(origin);
889
+ this.load();
890
+ const saved = this.saved;
891
+ if (this.state !== "signed-in" ||
892
+ !saved ||
893
+ saved.origin !== origin ||
894
+ (accountId !== undefined && accountId !== saved.account.id))
895
+ throw new Error("请先登录此云服务的账号。");
896
+ signal?.throwIfAborted();
897
+ if (saved.refreshTokenExpiresAt <= this.now()) {
898
+ await this.logout();
899
+ throw new Error("云账号授权已过期,请重新登录。");
900
+ }
901
+ if (saved.accessTokenExpiresAt > this.now() + 30_000)
902
+ return saved.accessToken;
903
+ if (!this.refresh) {
904
+ const generation = this.generation;
905
+ const profileRevision = this.profileRevision;
906
+ const operationSignal = this.operation.signal;
907
+ // A logout can await this one response to revoke the rotated token without
908
+ // persisting it. Client cancellation fences use, not the bounded request.
909
+ const response = this.request({
910
+ origin,
911
+ path: "/api/v1/account/refresh",
912
+ body: { refreshToken: saved.refreshToken },
913
+ });
914
+ this.refreshResponse = response;
915
+ const pending = (async () => {
916
+ try {
917
+ const grant = await response;
918
+ this.validGrant(grant, origin);
919
+ try {
920
+ this.current(generation, () => this.saved?.sessionId === saved.sessionId &&
921
+ this.saved?.account.id === saved.account.id &&
922
+ !operationSignal.aborted);
923
+ }
924
+ catch (error) {
925
+ if (!this.revocationResponses.has(response))
926
+ void this.revoke({ ...grant, origin });
927
+ throw error;
928
+ }
929
+ if (grant.account.id !== saved.account.id || grant.sessionId !== saved.sessionId)
930
+ throw new Error("云账号授权发生变化,请重新登录。");
931
+ const next = savedGrant(grant, origin);
932
+ // A refresh issued before a profile update may carry its older label.
933
+ if (profileRevision !== this.profileRevision && this.saved?.sessionId === saved.sessionId)
934
+ next.account = { ...this.saved.account };
935
+ try {
936
+ this.deps.store.save(next);
937
+ }
938
+ catch (error) {
939
+ this.saved = undefined;
940
+ this.publish("storage-error");
941
+ try {
942
+ await this.retire(saved);
943
+ }
944
+ finally {
945
+ void this.revoke(next);
946
+ }
947
+ throw error;
948
+ }
949
+ this.saved = next;
950
+ this.armExpiry();
951
+ this.publish("signed-in");
952
+ return next.accessToken;
953
+ }
954
+ catch (error) {
955
+ if (generation === this.generation &&
956
+ error instanceof CloudAccountHttpError &&
957
+ (error.status === 401 || error.status === 403))
958
+ await this.logout();
959
+ throw error;
960
+ }
961
+ })();
962
+ this.refresh = pending;
963
+ void pending
964
+ .finally(() => {
965
+ if (this.refresh === pending)
966
+ this.refresh = undefined;
967
+ if (this.refreshResponse === response)
968
+ this.refreshResponse = undefined;
969
+ })
970
+ .catch(() => { });
971
+ }
972
+ const token = await this.refresh;
973
+ signal?.throwIfAborted();
974
+ return token;
975
+ }
976
+ }