@huaqiu/dsh-auth 0.3.9 → 0.3.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/lib/client.js +15 -1
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +46 -2
- package/lib/index.mjs +211 -19
- package/package.json +3 -2
- package/src/client/client.ts +12 -2
- package/src/client/transport.ts +15 -0
- package/src/host.ts +105 -12
- package/src/index.ts +15 -0
- package/src/routes.ts +19 -0
- package/src/service.ts +118 -8
package/lib/index.d.mts
CHANGED
|
@@ -27,6 +27,8 @@ interface HuaqiuAuthConfig {
|
|
|
27
27
|
hqEdgeBaseUrl?: string;
|
|
28
28
|
/** Path on the host; default "/api/v1/auth/token". */
|
|
29
29
|
hostAuthPath?: string;
|
|
30
|
+
/** Host endpoint that triggers the EDA login dialog; default "/api/v1/auth/login". */
|
|
31
|
+
hostLoginPath?: string;
|
|
30
32
|
/** Seconds a host session is reused before re-fetching. Default 300. */
|
|
31
33
|
hostSessionTtlSeconds?: number;
|
|
32
34
|
/** Seconds remote token-validation results are cached. Default 60. */
|
|
@@ -132,12 +134,20 @@ declare class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
|
|
|
132
134
|
private listeners;
|
|
133
135
|
private readonly host;
|
|
134
136
|
private readonly validator;
|
|
137
|
+
private readonly doFetch;
|
|
138
|
+
private readonly hostLoginPath;
|
|
139
|
+
/** Bound on the host login RPC (user must complete the EDA dialog). */
|
|
140
|
+
private readonly loginWaitMs;
|
|
135
141
|
/**
|
|
136
142
|
* True once the current credential has been rejected (API 401) or explicitly
|
|
137
143
|
* invalidated. Keeps the credential for recovery but makes `isAuthenticated()`
|
|
138
144
|
* short-circuit to false and forces a fresh remote validation next time.
|
|
139
145
|
*/
|
|
140
146
|
private stale;
|
|
147
|
+
/** Last host credential version seen; see `observeHostVersion`. */
|
|
148
|
+
private lastHostVersion;
|
|
149
|
+
private _log;
|
|
150
|
+
private get log();
|
|
141
151
|
constructor(config?: Partial<HuaqiuAuthConfig> | null, opts?: {
|
|
142
152
|
fetchImpl?: typeof fetch;
|
|
143
153
|
});
|
|
@@ -153,11 +163,45 @@ declare class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
|
|
|
153
163
|
* standalone (auth.eda.cn) login. Otherwise a stale `~/.dsh/auth/session.json`
|
|
154
164
|
* would let the DSH plugins keep calling the backend while hq-edge itself has
|
|
155
165
|
* no auth info. The standalone fallbacks only apply when no host is configured.
|
|
166
|
+
*
|
|
167
|
+
* A host credential the host itself reports as `authenticated: false` counts
|
|
168
|
+
* as "no credential" — otherwise `/session` would hand the browser half a user
|
|
169
|
+
* object its own `authenticated` flag contradicts, and the browser half (which
|
|
170
|
+
* adopts the session only when `authenticated` is true) would silently drop
|
|
171
|
+
* it. That mismatch is exactly how "the token reached HQ Edge but never
|
|
172
|
+
* reached the plugin" presents.
|
|
156
173
|
*/
|
|
157
174
|
private resolve;
|
|
158
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Track the host's credential version so a replaced credential re-arms the
|
|
177
|
+
* session after a 401.
|
|
178
|
+
*
|
|
179
|
+
* `invalidate()` (called when a backend answers 401) must keep the session
|
|
180
|
+
* down — but only for *that* credential. HQ Edge bumps `version` whenever EDA
|
|
181
|
+
* replaces or clears its credential, so a new version means the operator has
|
|
182
|
+
* a new token and the previous rejection no longer applies.
|
|
183
|
+
*/
|
|
184
|
+
private observeHostVersion;
|
|
185
|
+
/**
|
|
186
|
+
* Spec §7: resolve → local expiry → remote validation → update state.
|
|
187
|
+
*
|
|
188
|
+
* In HOST mode there is no remote validation: the host is the authority. It
|
|
189
|
+
* received the credential from EDA and publishes its own `authenticated`
|
|
190
|
+
* verdict, so probing `www.eda.cn/api/token/validate` here is not just
|
|
191
|
+
* redundant — it is wrong. That endpoint does not know the credential class
|
|
192
|
+
* EDA hands to HQ Edge (it answers `result: false`) and is unreachable in
|
|
193
|
+
* offline deployments, so gating on it made every host-mode session read as
|
|
194
|
+
* unauthenticated even while hq-edge reported `authenticated: true`.
|
|
195
|
+
*/
|
|
159
196
|
private validateInternal;
|
|
160
|
-
/** Validation-scoped invalidation: keep the credential, drop cached validity.
|
|
197
|
+
/** Validation-scoped invalidation: keep the credential, drop cached validity.
|
|
198
|
+
*
|
|
199
|
+
* In host mode the host cache *is* the cached validity — the validator only
|
|
200
|
+
* confirmed what the host already said — so the next call must re-fetch:
|
|
201
|
+
* otherwise a 401-driven `auth.invalidate()` would keep stale=true latched
|
|
202
|
+
* for the full `hostSessionTtlSeconds` (default 300 s), and an HQ-Edge
|
|
203
|
+
* credential replace in between would be invisible until the cache expired.
|
|
204
|
+
*/
|
|
161
205
|
private markStale;
|
|
162
206
|
setCredentials(info: HuaqiuUserInfo): void;
|
|
163
207
|
invalidate(): void;
|
package/lib/index.mjs
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
|
+
import { getLogger } from "@huaqiu/dsh-plugin-log";
|
|
1
2
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
5
|
+
//#region src/host.ts
|
|
6
|
+
/**
|
|
7
|
+
* HQ Edge host mode for `dsh-auth`.
|
|
8
|
+
*
|
|
9
|
+
* In standalone DSH, credentials arrive only when the user logs in through the
|
|
10
|
+
* browser (auth.eda.cn iframe → postMessage → `webServer` route →
|
|
11
|
+
* `setCredentials`). That means the node half is empty until a browser tab
|
|
12
|
+
* focuses the card, and every tool returns `needs_auth` in the gap — including
|
|
13
|
+
* across a process restart (the in-memory cache is gone).
|
|
14
|
+
*
|
|
15
|
+
* Host mode removes that gap. When HQ Edge is the host, it already holds the
|
|
16
|
+
* operator-supplied token + user id, and exposes them on a loopback route. The
|
|
17
|
+
* node half fetches them on boot and is authoritative immediately, without
|
|
18
|
+
* waiting for a browser. The browser half is untouched — host mode is *mode*,
|
|
19
|
+
* not API (spec §5/§14).
|
|
20
|
+
*
|
|
21
|
+
* Resolution order inside `getUserInfo()` (spec §6.2):
|
|
22
|
+
* 1. host session — HQ Edge configured → fetch + cache (TTL)
|
|
23
|
+
* 2. pushed session — what the browser half sent (today's behaviour)
|
|
24
|
+
* 3. persisted file — node-side `~/.dsh/auth/session.json`, written on every set
|
|
25
|
+
* 4. null → tools return needs_auth
|
|
26
|
+
*/
|
|
27
|
+
let _log = null;
|
|
28
|
+
function log$1() {
|
|
29
|
+
if (_log === null) _log = getLogger("dsh-auth");
|
|
30
|
+
return _log;
|
|
31
|
+
}
|
|
4
32
|
/**
|
|
5
33
|
* Resolve the effective config: overlay `config` (highest) > env (safety net
|
|
6
34
|
* for non-supervisor installs) > defaults. Centralised here so only `dsh-auth`
|
|
@@ -9,6 +37,7 @@ import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
|
9
37
|
function resolveHostConfig(config, env = process.env) {
|
|
10
38
|
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
|
|
11
39
|
const hostAuthPath = config?.hostAuthPath ?? env.HQ_EDGE_AUTH_PATH ?? "/api/v1/auth/token";
|
|
40
|
+
const hostLoginPath = config?.hostLoginPath ?? env.HQ_EDGE_LOGIN_PATH ?? "/api/v1/auth/login";
|
|
12
41
|
const ttlRaw = config?.hostSessionTtlSeconds ?? env.HQ_EDGE_HOST_TTL_SECONDS;
|
|
13
42
|
let ttl = 300;
|
|
14
43
|
if (typeof ttlRaw === "number" && Number.isFinite(ttlRaw) && ttlRaw > 0) ttl = ttlRaw;
|
|
@@ -26,13 +55,21 @@ function resolveHostConfig(config, env = process.env) {
|
|
|
26
55
|
return {
|
|
27
56
|
hqEdgeBaseUrl: baseUrl,
|
|
28
57
|
hostAuthPath,
|
|
58
|
+
hostLoginPath,
|
|
29
59
|
hostSessionTtlSeconds: ttl,
|
|
30
60
|
validationTtlSeconds
|
|
31
61
|
};
|
|
32
62
|
}
|
|
33
63
|
/**
|
|
34
|
-
* Fetches and caches the host (HQ Edge) session. The cache is memory-only with
|
|
35
|
-
* TTL; the loopback GET is cheap
|
|
64
|
+
* Fetches and caches the host (HQ Edge) session. The cache is memory-only with
|
|
65
|
+
* a TTL; the loopback GET is cheap, so we never persist the credential.
|
|
66
|
+
*
|
|
67
|
+
* Caching rule — **only a usable session is cached**. A host that is up but has
|
|
68
|
+
* no credential yet (EDA not logged in, or the login dialog still open) must
|
|
69
|
+
* NOT be cached: the operator may complete the login a second later, and a
|
|
70
|
+
* cached negative would pin every tool to `needs_auth` for the whole TTL. The
|
|
71
|
+
* old code cached whatever it got, so a DSH that booted before EDA
|
|
72
|
+
* authenticated stayed "logged out" for up to `hostSessionTtlSeconds` (300s).
|
|
36
73
|
*/
|
|
37
74
|
var HostSessionResolver = class {
|
|
38
75
|
baseUrl;
|
|
@@ -54,22 +91,45 @@ var HostSessionResolver = class {
|
|
|
54
91
|
if (!this.enabled) return null;
|
|
55
92
|
const now = Date.now();
|
|
56
93
|
if (this.cache !== null && now - this.cache.fetchedAt < this.ttlMs) return this.cache.info;
|
|
94
|
+
const url = `${this.baseUrl}${this.path}`;
|
|
95
|
+
let info = null;
|
|
96
|
+
let status = null;
|
|
57
97
|
try {
|
|
58
|
-
const res = await this.doFetch(
|
|
98
|
+
const res = await this.doFetch(url, {
|
|
59
99
|
method: "GET",
|
|
60
100
|
headers: { accept: "application/json" }
|
|
61
101
|
});
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
102
|
+
status = res.status;
|
|
103
|
+
if (res.ok) info = normalizeHostUser(await res.json());
|
|
104
|
+
} catch (err) {
|
|
105
|
+
log$1().warn("host session fetch failed", {
|
|
106
|
+
url,
|
|
107
|
+
error: String(err)
|
|
108
|
+
});
|
|
109
|
+
info = null;
|
|
110
|
+
}
|
|
111
|
+
if (info !== null && info.authenticated) {
|
|
65
112
|
this.cache = {
|
|
66
113
|
info,
|
|
67
114
|
fetchedAt: now
|
|
68
115
|
};
|
|
116
|
+
log$1().debug("host session resolved", {
|
|
117
|
+
url,
|
|
118
|
+
status,
|
|
119
|
+
userId: info.id,
|
|
120
|
+
version: info.version ?? null,
|
|
121
|
+
authenticated: info.authenticated
|
|
122
|
+
});
|
|
69
123
|
return info;
|
|
70
|
-
} catch {
|
|
71
|
-
return this.cache?.info ?? null;
|
|
72
124
|
}
|
|
125
|
+
this.cache = null;
|
|
126
|
+
log$1().info("no usable host session", {
|
|
127
|
+
url,
|
|
128
|
+
status,
|
|
129
|
+
parsed: info !== null,
|
|
130
|
+
hostAuthenticated: info?.authenticated ?? null
|
|
131
|
+
});
|
|
132
|
+
return null;
|
|
73
133
|
}
|
|
74
134
|
/** Drop the cached value so the next `resolve()` re-fetches (reactive invalidation). */
|
|
75
135
|
clear() {
|
|
@@ -81,16 +141,30 @@ function asId(raw) {
|
|
|
81
141
|
if (typeof raw === "number" && Number.isFinite(raw)) return String(raw);
|
|
82
142
|
return null;
|
|
83
143
|
}
|
|
84
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Parse the host route payload into a credential, tolerating key-name drift.
|
|
146
|
+
*
|
|
147
|
+
* HQ Edge answers `GET /api/v1/auth/token` with
|
|
148
|
+
* `{ authenticated, token, userId, version }`. The `authenticated` flag is the
|
|
149
|
+
* host's own verdict and is carried through verbatim; hosts that predate it
|
|
150
|
+
* (or a minimal `{ token, userId }` stub) omit the key, in which case the
|
|
151
|
+
* presence of a usable token+id is taken as the old contract was: authenticated.
|
|
152
|
+
*/
|
|
85
153
|
function normalizeHostUser(data) {
|
|
86
154
|
const token = typeof data.token === "string" && data.token.length > 0 ? data.token : null;
|
|
87
155
|
const id = asId(data.userId) ?? asId(data.id) ?? asId(data.user_id);
|
|
88
156
|
if (!token || !id) return null;
|
|
89
157
|
const nickname = typeof data.nickname === "string" && data.nickname.length > 0 ? data.nickname : void 0;
|
|
158
|
+
const authenticated = data.authenticated === void 0 ? true : data.authenticated === true;
|
|
159
|
+
const version = typeof data.version === "number" && Number.isFinite(data.version) ? data.version : void 0;
|
|
160
|
+
const expiresAt = typeof data.expiresAt === "number" && Number.isFinite(data.expiresAt) ? data.expiresAt : void 0;
|
|
90
161
|
return {
|
|
91
162
|
id,
|
|
92
163
|
token,
|
|
93
|
-
|
|
164
|
+
authenticated,
|
|
165
|
+
...nickname ? { nickname } : {},
|
|
166
|
+
...version !== void 0 ? { version } : {},
|
|
167
|
+
...expiresAt !== void 0 ? { expiresAt } : {}
|
|
94
168
|
};
|
|
95
169
|
}
|
|
96
170
|
/**
|
|
@@ -293,31 +367,63 @@ var InMemoryHuaqiuAuthService = class {
|
|
|
293
367
|
listeners = /* @__PURE__ */ new Set();
|
|
294
368
|
host;
|
|
295
369
|
validator;
|
|
370
|
+
doFetch;
|
|
371
|
+
hostLoginPath;
|
|
372
|
+
/** Bound on the host login RPC (user must complete the EDA dialog). */
|
|
373
|
+
loginWaitMs;
|
|
296
374
|
/**
|
|
297
375
|
* True once the current credential has been rejected (API 401) or explicitly
|
|
298
376
|
* invalidated. Keeps the credential for recovery but makes `isAuthenticated()`
|
|
299
377
|
* short-circuit to false and forces a fresh remote validation next time.
|
|
300
378
|
*/
|
|
301
379
|
stale = false;
|
|
380
|
+
/** Last host credential version seen; see `observeHostVersion`. */
|
|
381
|
+
lastHostVersion = null;
|
|
382
|
+
_log = null;
|
|
383
|
+
get log() {
|
|
384
|
+
if (this._log === null) this._log = getLogger("dsh-auth");
|
|
385
|
+
return this._log;
|
|
386
|
+
}
|
|
302
387
|
constructor(config, opts) {
|
|
303
388
|
const resolved = resolveHostConfig(config);
|
|
304
|
-
this.
|
|
389
|
+
this.doFetch = opts?.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
390
|
+
this.host = new HostSessionResolver(resolved.hqEdgeBaseUrl ?? "", resolved.hostAuthPath ?? "/api/v1/auth/token", (resolved.hostSessionTtlSeconds ?? 300) * 1e3, this.doFetch);
|
|
391
|
+
this.hostLoginPath = resolved.hostLoginPath ?? "/api/v1/auth/login";
|
|
392
|
+
this.loginWaitMs = 3e5;
|
|
305
393
|
this.validator = new TokenValidator({
|
|
306
394
|
ttlMs: (resolved.validationTtlSeconds ?? 60) * 1e3,
|
|
307
|
-
fetchImpl:
|
|
395
|
+
fetchImpl: this.doFetch
|
|
308
396
|
});
|
|
309
397
|
this.hostMode = this.host.enabled;
|
|
398
|
+
this.log.info("auth service started", {
|
|
399
|
+
hostMode: this.hostMode,
|
|
400
|
+
hostAuthPath: resolved.hostAuthPath,
|
|
401
|
+
hostSessionTtlSeconds: resolved.hostSessionTtlSeconds
|
|
402
|
+
});
|
|
310
403
|
}
|
|
311
404
|
/** Host mode is active iff a host base URL was configured (see HostSessionResolver.enabled). */
|
|
312
405
|
hostMode;
|
|
313
406
|
auth = {
|
|
314
407
|
isAuthenticated: async () => {
|
|
408
|
+
if (await this.resolve() === null) return false;
|
|
315
409
|
if (this.stale) return false;
|
|
316
410
|
return (await this.validateInternal()).status === "valid";
|
|
317
411
|
},
|
|
318
412
|
getAccessToken: async () => (await this.resolve())?.token ?? null,
|
|
319
413
|
getUserInfo: async () => this.resolve(),
|
|
320
|
-
login: async () => {
|
|
414
|
+
login: async () => {
|
|
415
|
+
if (!this.host.enabled) return;
|
|
416
|
+
const res = await this.doFetch(`${this.host.baseUrl}${this.hostLoginPath}`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: { accept: "application/json" },
|
|
419
|
+
signal: AbortSignal.timeout(this.loginWaitMs)
|
|
420
|
+
});
|
|
421
|
+
if (!res.ok) throw new Error(`host login not completed: HTTP ${res.status}`);
|
|
422
|
+
this.host.clear();
|
|
423
|
+
this.stale = false;
|
|
424
|
+
this.validator.invalidate();
|
|
425
|
+
this.emit();
|
|
426
|
+
},
|
|
321
427
|
logout: async () => this.invalidate(),
|
|
322
428
|
validate: () => this.validateInternal(),
|
|
323
429
|
invalidate: () => this.markStale(),
|
|
@@ -332,26 +438,83 @@ var InMemoryHuaqiuAuthService = class {
|
|
|
332
438
|
* standalone (auth.eda.cn) login. Otherwise a stale `~/.dsh/auth/session.json`
|
|
333
439
|
* would let the DSH plugins keep calling the backend while hq-edge itself has
|
|
334
440
|
* no auth info. The standalone fallbacks only apply when no host is configured.
|
|
441
|
+
*
|
|
442
|
+
* A host credential the host itself reports as `authenticated: false` counts
|
|
443
|
+
* as "no credential" — otherwise `/session` would hand the browser half a user
|
|
444
|
+
* object its own `authenticated` flag contradicts, and the browser half (which
|
|
445
|
+
* adopts the session only when `authenticated` is true) would silently drop
|
|
446
|
+
* it. That mismatch is exactly how "the token reached HQ Edge but never
|
|
447
|
+
* reached the plugin" presents.
|
|
335
448
|
*/
|
|
336
449
|
async resolve() {
|
|
337
450
|
if (this.host.enabled) {
|
|
338
451
|
const host = await this.host.resolve();
|
|
339
|
-
if (host
|
|
340
|
-
|
|
452
|
+
if (!host || !host.authenticated) {
|
|
453
|
+
this.log.debug("host session unusable", {
|
|
454
|
+
hostReachable: host !== null,
|
|
455
|
+
hostAuthenticated: host?.authenticated ?? null
|
|
456
|
+
});
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
this.observeHostVersion(host.version);
|
|
460
|
+
return toUserInfo(host);
|
|
341
461
|
}
|
|
342
462
|
if (this.current) return this.current;
|
|
343
463
|
return readPersisted();
|
|
344
464
|
}
|
|
345
|
-
/**
|
|
465
|
+
/**
|
|
466
|
+
* Track the host's credential version so a replaced credential re-arms the
|
|
467
|
+
* session after a 401.
|
|
468
|
+
*
|
|
469
|
+
* `invalidate()` (called when a backend answers 401) must keep the session
|
|
470
|
+
* down — but only for *that* credential. HQ Edge bumps `version` whenever EDA
|
|
471
|
+
* replaces or clears its credential, so a new version means the operator has
|
|
472
|
+
* a new token and the previous rejection no longer applies.
|
|
473
|
+
*/
|
|
474
|
+
observeHostVersion(version) {
|
|
475
|
+
if (typeof version !== "number" || !Number.isFinite(version)) return;
|
|
476
|
+
if (this.lastHostVersion !== null && version !== this.lastHostVersion) {
|
|
477
|
+
this.stale = false;
|
|
478
|
+
this.validator.invalidate();
|
|
479
|
+
this.log.info("host credential replaced — re-arming session", { version });
|
|
480
|
+
}
|
|
481
|
+
this.lastHostVersion = version;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Spec §7: resolve → local expiry → remote validation → update state.
|
|
485
|
+
*
|
|
486
|
+
* In HOST mode there is no remote validation: the host is the authority. It
|
|
487
|
+
* received the credential from EDA and publishes its own `authenticated`
|
|
488
|
+
* verdict, so probing `www.eda.cn/api/token/validate` here is not just
|
|
489
|
+
* redundant — it is wrong. That endpoint does not know the credential class
|
|
490
|
+
* EDA hands to HQ Edge (it answers `result: false`) and is unreachable in
|
|
491
|
+
* offline deployments, so gating on it made every host-mode session read as
|
|
492
|
+
* unauthenticated even while hq-edge reported `authenticated: true`.
|
|
493
|
+
*/
|
|
346
494
|
async validateInternal() {
|
|
347
495
|
const info = await this.resolve();
|
|
348
496
|
if (!info) {
|
|
349
|
-
this.stale = true;
|
|
497
|
+
if (!this.host.enabled) this.stale = true;
|
|
350
498
|
return {
|
|
351
499
|
status: "invalid",
|
|
352
500
|
reason: "invalid"
|
|
353
501
|
};
|
|
354
502
|
}
|
|
503
|
+
if (this.host.enabled) {
|
|
504
|
+
if (this.validator.isLocallyExpired(info.expiresAt)) {
|
|
505
|
+
this.validator.invalidate(info.token);
|
|
506
|
+
this.stale = true;
|
|
507
|
+
return {
|
|
508
|
+
status: "invalid",
|
|
509
|
+
reason: "expired"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
status: "valid",
|
|
514
|
+
userId: info.id,
|
|
515
|
+
...info.expiresAt !== void 0 ? { expiresAt: info.expiresAt } : {}
|
|
516
|
+
};
|
|
517
|
+
}
|
|
355
518
|
if (this.validator.isLocallyExpired(info.expiresAt)) {
|
|
356
519
|
this.validator.invalidate(info.token);
|
|
357
520
|
this.stale = true;
|
|
@@ -365,10 +528,18 @@ var InMemoryHuaqiuAuthService = class {
|
|
|
365
528
|
else if (result.status === "invalid") this.stale = true;
|
|
366
529
|
return result;
|
|
367
530
|
}
|
|
368
|
-
/** Validation-scoped invalidation: keep the credential, drop cached validity.
|
|
531
|
+
/** Validation-scoped invalidation: keep the credential, drop cached validity.
|
|
532
|
+
*
|
|
533
|
+
* In host mode the host cache *is* the cached validity — the validator only
|
|
534
|
+
* confirmed what the host already said — so the next call must re-fetch:
|
|
535
|
+
* otherwise a 401-driven `auth.invalidate()` would keep stale=true latched
|
|
536
|
+
* for the full `hostSessionTtlSeconds` (default 300 s), and an HQ-Edge
|
|
537
|
+
* credential replace in between would be invisible until the cache expired.
|
|
538
|
+
*/
|
|
369
539
|
markStale() {
|
|
370
540
|
this.stale = true;
|
|
371
541
|
this.validator.invalidate();
|
|
542
|
+
if (this.host.enabled) this.host.clear();
|
|
372
543
|
}
|
|
373
544
|
setCredentials(info) {
|
|
374
545
|
this.current = info;
|
|
@@ -399,7 +570,8 @@ function toUserInfo(host) {
|
|
|
399
570
|
return {
|
|
400
571
|
id: host.id,
|
|
401
572
|
token: host.token,
|
|
402
|
-
...host.nickname !== void 0 ? { nickname: host.nickname } : {}
|
|
573
|
+
...host.nickname !== void 0 ? { nickname: host.nickname } : {},
|
|
574
|
+
...host.expiresAt !== void 0 ? { expiresAt: host.expiresAt } : {}
|
|
403
575
|
};
|
|
404
576
|
}
|
|
405
577
|
//#endregion
|
|
@@ -463,6 +635,16 @@ function createAuthHandler(service) {
|
|
|
463
635
|
sendJson(res, 200, { hostMode: service.hostMode });
|
|
464
636
|
return;
|
|
465
637
|
}
|
|
638
|
+
if (req.method === "POST" && pathname === `/api/v1/huaqiu/auth/login`) {
|
|
639
|
+
await service.auth.login();
|
|
640
|
+
const user = await service.auth.getUserInfo();
|
|
641
|
+
sendJson(res, 200, {
|
|
642
|
+
ok: true,
|
|
643
|
+
authenticated: user !== null,
|
|
644
|
+
user
|
|
645
|
+
});
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
466
648
|
sendJson(res, 404, { error: "not found" });
|
|
467
649
|
} catch (err) {
|
|
468
650
|
sendJson(res, 500, {
|
|
@@ -474,6 +656,7 @@ function createAuthHandler(service) {
|
|
|
474
656
|
}
|
|
475
657
|
//#endregion
|
|
476
658
|
//#region src/index.ts
|
|
659
|
+
const log = getLogger("dsh-auth");
|
|
477
660
|
const name = "@huaqiu/dsh-auth";
|
|
478
661
|
const inject = ["webServer"];
|
|
479
662
|
/**
|
|
@@ -485,6 +668,11 @@ const inject = ["webServer"];
|
|
|
485
668
|
* without a supervisor (spec §6.4).
|
|
486
669
|
*/
|
|
487
670
|
function apply(ctx, config) {
|
|
671
|
+
log.info("applying dsh-auth node half", {
|
|
672
|
+
hasConfig: config != null,
|
|
673
|
+
configKeys: config != null ? Object.keys(config) : [],
|
|
674
|
+
hqEdgeBaseUrl: config?.hqEdgeBaseUrl ?? null
|
|
675
|
+
});
|
|
488
676
|
const service = new InMemoryHuaqiuAuthService(config);
|
|
489
677
|
ctx.effect(() => ctx.provide("huaqiuAuth", service));
|
|
490
678
|
ctx.effect(() => ctx.webServer.register({
|
|
@@ -492,6 +680,10 @@ function apply(ctx, config) {
|
|
|
492
680
|
path: AUTH_ROUTE_PREFIX,
|
|
493
681
|
handler: createAuthHandler(service)
|
|
494
682
|
}));
|
|
683
|
+
log.info("dsh-auth node half ready", {
|
|
684
|
+
routes: AUTH_ROUTE_PREFIX,
|
|
685
|
+
hostMode: service.hostMode
|
|
686
|
+
});
|
|
495
687
|
}
|
|
496
688
|
//#endregion
|
|
497
689
|
export { AUTH_ROUTE_PREFIX, InMemoryHuaqiuAuthService, apply, createAuthHandler, inject, name };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huaqiu/dsh-auth",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./lib/index.mjs",
|
|
6
6
|
"types": "./lib/index.d.mts",
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.0"
|
|
34
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.0",
|
|
35
|
+
"@huaqiu/dsh-plugin-log": "0.3.11"
|
|
35
36
|
},
|
|
36
37
|
"files": [
|
|
37
38
|
"lib",
|
package/src/client/client.ts
CHANGED
|
@@ -131,8 +131,18 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
|
|
|
131
131
|
return hostSession ?? storage.get()
|
|
132
132
|
},
|
|
133
133
|
login: async (options?: LoginOptions): Promise<void> => {
|
|
134
|
-
// Host mode: hq-edge owns the session —
|
|
135
|
-
|
|
134
|
+
// Host mode: hq-edge owns the session — ask the host (EDA) to open its
|
|
135
|
+
// own login dialog instead of the auth.eda.cn iframe. The node route
|
|
136
|
+
// blocks until the dialog completes; then re-resolve and emit (only on
|
|
137
|
+
// a real change, so a redundant trigger never re-renders every card).
|
|
138
|
+
if (hostMode) {
|
|
139
|
+
const prevToken = hostSession?.token
|
|
140
|
+
await transport.triggerLogin()
|
|
141
|
+
hostSessionLoaded = false
|
|
142
|
+
await resolveHost()
|
|
143
|
+
if (hostSession?.token !== prevToken) emit(hostSession ?? storage.get())
|
|
144
|
+
return
|
|
145
|
+
}
|
|
136
146
|
openIframe(options ?? {})
|
|
137
147
|
},
|
|
138
148
|
logout: async (): Promise<void> => {
|
package/src/client/transport.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface AuthTransport {
|
|
|
22
22
|
* credential without ever opening the auth.eda.cn iframe.
|
|
23
23
|
*/
|
|
24
24
|
fetchSession(): Promise<{ authenticated: boolean; user: HostSessionUser | null }>
|
|
25
|
+
/**
|
|
26
|
+
* Ask the node half to trigger the EDA login dialog through the host.
|
|
27
|
+
* Standalone (no host) must NOT call this — login is the browser iframe
|
|
28
|
+
* there. In host mode the node blocks until the dialog is completed (the
|
|
29
|
+
* host route waits for AuthStateChanged), then the caller re-fetches the
|
|
30
|
+
* session to pick up the fresh credential.
|
|
31
|
+
*/
|
|
32
|
+
triggerLogin(): Promise<void>
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** Minimal session user shape returned by the node `/session` route. */
|
|
@@ -80,5 +88,12 @@ export function createWebServerAuthTransport(
|
|
|
80
88
|
const user = body.user && typeof body.user === 'object' ? body.user as HostSessionUser : null
|
|
81
89
|
return { authenticated: body.authenticated === true, user }
|
|
82
90
|
},
|
|
91
|
+
async triggerLogin() {
|
|
92
|
+
const res = await doFetch(`${base}/login`, {
|
|
93
|
+
method: 'POST',
|
|
94
|
+
headers: { accept: 'application/json' },
|
|
95
|
+
})
|
|
96
|
+
if (!res.ok) throw new Error(`auth login trigger failed: HTTP ${res.status}`)
|
|
97
|
+
},
|
|
83
98
|
}
|
|
84
99
|
}
|