@huaqiu/dsh-auth 0.3.16 → 0.3.18

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 CHANGED
@@ -797,9 +797,10 @@ window.__ModuleLoader__.load({
797
797
  },
798
798
  logout: async () => {
799
799
  if (hostMode) {
800
- hostSession = null;
801
- storage.clear();
802
- emit(null);
800
+ await transport.pushLogout();
801
+ hostSessionLoaded = false;
802
+ await resolveHost();
803
+ emit(hostSession);
803
804
  return;
804
805
  }
805
806
  storage.clear();
@@ -854,6 +855,7 @@ window.__ModuleLoader__.load({
854
855
  * late host handover still flips the gate.
855
856
  */
856
857
  async refreshHost() {
858
+ const prevToken = hostSession?.token ?? storage.get()?.token ?? null;
857
859
  let mode = false;
858
860
  try {
859
861
  mode = await transport.fetchHostMode();
@@ -863,7 +865,8 @@ window.__ModuleLoader__.load({
863
865
  hostMode = mode;
864
866
  hostSessionLoaded = false;
865
867
  await resolveHost();
866
- emit(hostSession ?? storage.get());
868
+ const next = hostSession ?? storage.get();
869
+ if (next?.token !== prevToken) emit(next);
867
870
  return hostMode;
868
871
  },
869
872
  /**
@@ -1505,7 +1508,7 @@ window.__ModuleLoader__.load({
1505
1508
  palette,
1506
1509
  onSelect: () => {
1507
1510
  setMenuOpen(false);
1508
- auth.logout();
1511
+ auth.logout().catch(() => {});
1509
1512
  }
1510
1513
  })
1511
1514
  ]
@@ -1553,6 +1556,15 @@ window.__ModuleLoader__.load({
1553
1556
  * service, the sidebar login entrypoint and the webServer credential channel.
1554
1557
  */
1555
1558
  const AUTH_TOOL_NAMES = [];
1559
+ /**
1560
+ * How often the browser re-pulls the host-owned session in host mode.
1561
+ *
1562
+ * KiCad's HQ auth token watcher polls the credential file every 3s and
1563
+ * publishes an `auth_state_changed` event on change, so 5s bounds the
1564
+ * end-to-end lag of an EDA-side login/logout reaching this UI at well under
1565
+ * ten seconds while staying negligible against two loopback requests.
1566
+ */
1567
+ const HOST_RESYNC_INTERVAL_MS = 5e3;
1556
1568
  function apply(ctx) {
1557
1569
  const client = createAuthClient({
1558
1570
  storage: createAuthStorage(localStorage),
@@ -1574,6 +1586,7 @@ window.__ModuleLoader__.load({
1574
1586
  }));
1575
1587
  const sync = () => {
1576
1588
  client.syncNow();
1589
+ if (client.auth.isHostMode()) client.refreshHost().catch(() => void 0);
1577
1590
  };
1578
1591
  window.addEventListener("focus", sync);
1579
1592
  document.addEventListener("visibilitychange", sync);
@@ -1599,6 +1612,19 @@ window.__ModuleLoader__.load({
1599
1612
  const slots = ctx.slots;
1600
1613
  client.refreshHost().catch(() => void 0).then(() => {
1601
1614
  if (disposed) return;
1615
+ if (client.auth.isHostMode()) {
1616
+ let polling = false;
1617
+ const timer = window.setInterval(() => {
1618
+ if (polling) return;
1619
+ polling = true;
1620
+ client.refreshHost().catch(() => void 0).finally(() => {
1621
+ polling = false;
1622
+ });
1623
+ }, HOST_RESYNC_INTERVAL_MS);
1624
+ disposers.push(() => {
1625
+ window.clearInterval(timer);
1626
+ });
1627
+ }
1602
1628
  if (slots && typeof slots.inject === "function" && typeof slots.register === "function") {
1603
1629
  for (const toolName of AUTH_TOOL_NAMES) disposers.push(slots.inject("tool.call.toolview", () => slots.register({
1604
1630
  name: "tool.call.toolview",
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["listeners","useSyncExternalStore","useMemo","unsubscribe","useMemo","useSyncExternalStore","useRef","memo","useState","memo","useSyncExternalStore","useRef","createPortal"],"sources":["../src/client/storage.ts","../src/client/transport.ts","../src/client/lib.ts","../src/client/ui/common.tsx","../src/client/ui-env.ts","../src/client/i18n.ts","../src/client/ui/login-dialog.ts","../src/client/client.ts","../src/client/auth-state.ts","../src/client/ui/needs-auth-toolview.tsx","../src/client/ui/hq-icon.tsx","../src/client/ui/sidebar-action.tsx","../src/client/index.tsx"],"sourcesContent":["/**\n * localStorage-backed credential cache (client side). Survives reload, which\n * is what makes the fingerprint silent-login restore (acceptance group D) work.\n */\nimport type { AuthTokenPayload } from './lib.js'\n\nexport const DEFAULT_STORAGE_KEY = 'huaqiu.dsh.auth'\n\nexport interface AuthStorage {\n get(): AuthTokenPayload | null\n set(info: AuthTokenPayload): void\n clear(): void\n}\n\nexport function createAuthStorage(\n storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>,\n key: string = DEFAULT_STORAGE_KEY,\n): AuthStorage {\n return {\n get() {\n const raw = storage.getItem(key)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as AuthTokenPayload\n if (!parsed || typeof parsed.token !== 'string' || typeof parsed.id !== 'string') return null\n // Parity with auth.eda.cn's 5-day token window.\n if (parsed.expiresAt !== undefined && parsed.expiresAt * 1000 <= Date.now()) {\n storage.removeItem(key)\n return null\n }\n return parsed\n } catch {\n return null\n }\n },\n set(info) {\n storage.setItem(key, JSON.stringify(info))\n },\n clear() {\n storage.removeItem(key)\n },\n }\n}\n","/**\n * Browser→node credential transport over the plugin-owned webServer routes\n * (same-origin; no CORS, no external dependency). This is the chosen Phase 0A\n * browser→host channel — `apiProxy`'s dispatch table is closed, so a\n * plugin-owned `webServer` route is the smallest supported extension point.\n */\nimport type { AuthTokenPayload } from './lib.js'\n\nexport interface AuthTransport {\n pushSession(info: AuthTokenPayload): Promise<void>\n pushLogout(): Promise<void>\n /**\n * Whether the plugin runs under an HQ Edge host. In host mode hq-edge already\n * holds the operator credential (EDA hands it over on launch), so the\n * browser half's own login UI (sidebar entrypoint) is suppressed.\n */\n fetchHostMode(): Promise<boolean>\n /**\n * Fetch the node half's authoritative session. The node resolves host-mode\n * credentials first (host session → pushed browser session → persisted\n * file), so in host mode this is how the browser half learns the host-owned\n * credential without ever opening the auth.eda.cn iframe.\n */\n fetchSession(): Promise<{ authenticated: boolean; user: HostSessionUser | null }>\n /**\n * Ask the node half to trigger the EDA login dialog through the host.\n * Standalone (no host) must NOT call this — login is the browser iframe\n * there. In host mode the node blocks until the dialog is completed (the\n * host route waits for AuthStateChanged), then the caller re-fetches the\n * session to pick up the fresh credential.\n */\n triggerLogin(): Promise<void>\n /**\n * Fetch the rich eda.cn profile (nickname + headimage) for the current\n * token. The node half resolves the token (host → pushed → persisted) and\n * calls eda.cn; used by the sidebar in host mode when the host session\n * carries only {token, userId} — e.g. host mode under kicad.\n */\n fetchUserInfo(): Promise<EdaUserProfile | null>\n}\n\n/** Minimal session user shape returned by the node `/session` route. */\nexport interface HostSessionUser {\n id?: string | number\n token?: string\n nickname?: string\n expiresAt?: number\n}\n\n/** Rich eda.cn profile served by the node `/user-info` route. */\nexport interface EdaUserProfile {\n nickname?: string\n headimage?: string\n phone?: string\n username?: string\n}\n\nexport function createWebServerAuthTransport(\n base: string = '/api/v1/huaqiu/auth',\n doFetch: typeof fetch = globalThis.fetch.bind(globalThis),\n): AuthTransport {\n return {\n async pushSession(info) {\n const res = await doFetch(`${base}/session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n token: info.token,\n userId: info.id,\n ...(info.nickname !== undefined ? { nickname: info.nickname } : {}),\n ...(info.expiresAt !== undefined ? { expiresAt: info.expiresAt } : {}),\n }),\n })\n if (!res.ok) throw new Error(`auth push failed: HTTP ${res.status}`)\n },\n async pushLogout() {\n const res = await doFetch(`${base}/logout`, { method: 'POST' })\n if (!res.ok) throw new Error(`auth logout push failed: HTTP ${res.status}`)\n },\n async fetchHostMode() {\n try {\n const res = await doFetch(`${base}/config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return false\n const body = await res.json() as { hostMode?: unknown }\n return body.hostMode === true\n } catch {\n // Offline/same-origin failure: fall back to standalone (show the login\n // entrypoint) rather than hiding it — a login UI is never a security\n // regression, but a missing one in standalone would lock the user out.\n return false\n }\n },\n async fetchSession() {\n const res = await doFetch(`${base}/session`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return { authenticated: false, user: null }\n const body = await res.json() as { authenticated?: unknown; user?: unknown }\n const user = body.user && typeof body.user === 'object' ? body.user as HostSessionUser : null\n return { authenticated: body.authenticated === true, user }\n },\n async triggerLogin() {\n const res = await doFetch(`${base}/login`, {\n method: 'POST',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`auth login trigger failed: HTTP ${res.status}`)\n },\n async fetchUserInfo() {\n try {\n const res = await doFetch(`${base}/user-info`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return null\n const body = await res.json() as { userInfo?: EdaUserProfile | null }\n return body.userInfo ?? null\n } catch {\n return null\n }\n },\n }\n}\n","/**\n * Pure browser message parsing for auth.eda.cn postMessage envelopes.\n *\n * auth.eda.cn posts `JSON.stringify({ category: 1, data: { type, data } })`\n * to the parent window with `targetOrigin: '*'`.\n *\n * SECURITY NOTE (offline deployment): the DSH harness runs fully offline /\n * local (127.0.0.1), so there is no public attack surface of a malicious\n * website posting a forged token at us. The origin gate is therefore dropped\n * by design — webviews may even report an opaque origin (\"null\") for the\n * embedded auth.eda.cn iframe, which would otherwise reject legitimate login\n * messages. What remains is the ENVELOPE validation in `parseAuthMessage`\n * (category 1 + well-formed token/userId), which keeps unrelated window\n * messages from ever corrupting the credential cache.\n *\n * Envelope types (see `/Users/admin/code/eda-cn-login/lib/kicadTools.ts`):\n * { category: 1, data: { type: 'update_access_token', data: { userId, token, expires_at, ... } } }\n * { category: 1, data: { type: 'logout', data: null } }\n * { category: 1, data: { type: 'close_dialog', data: null } }\n */\n\nexport const AUTH_ORIGIN = 'https://auth.eda.cn'\n\n/**「Go to profile」destination: the eda.cn account page. */\nexport const PROFILE_URL = 'https://www.eda.cn/account/profile'\n\n/**\n * Build the「Go to profile」URL: the eda.cn account page WITH the access token\n * in the query, mirroring `hq-eda-ai`'s `UserMenu`\n * (`/account/profile?token=…&phone=…`).\n *\n * The token is always attached: eda.cn consumes it to establish the session\n * and strips it from the address bar / history itself, so there is nothing to\n * leak beyond the target site. `encodeURIComponent` is required (not cosmetic):\n * tokens are base64-ish and may contain `+`, `/` or `=`, and a raw `+` in a\n * query string decodes to a space, which would corrupt the credential.\n */\nexport function buildProfileUrl(options: { token: string; phone?: string | number }): string {\n const phone = options.phone === undefined || options.phone === null ? '' : String(options.phone)\n return `${PROFILE_URL}?token=${encodeURIComponent(options.token)}&phone=${encodeURIComponent(phone)}`\n}\n\n/**\n * Contract version of the auth.eda.cn embed, shared with the web app\n * (`hq-eda-ai` LoginDialog) so both send the same cache-busting `v=`.\n */\nexport const AUTH_IFRAME_VERSION = '20260409'\n\n/** UI language of the auth.eda.cn embed. */\nexport type AuthLocale = 'zh' | 'en'\n\n/** Color scheme of the auth.eda.cn embed (its own vocabulary: light | dark). */\nexport type AuthTheme = 'light' | 'dark'\n\n/**\n * auth.eda.cn's own language ids, keyed by our locale id.\n *\n * The embed reads `?locale=`, NOT `lang`: `eda-cn-login/app/layout.tsx` reads\n * `urlParams.get('locale')` and `components/ui/LanguageContext.tsx`\n * (`getLangFromUrl`) only accepts the ids in `locales/index.ts` — `cn` and\n * `en` (`zh` / `zh_CN` are aliased to `cn` there, but we send the canonical\n * id outright).\n *\n * NOTE: `hq-eda-ai`'s `LoginDialog.tsx` sends `lang=zh`, which the embed\n * IGNORES, so its login card always falls back to whatever the browser asks\n * for. We send `locale` (what is actually read) and keep `lang` alongside it\n * for parity with the web app and forward compatibility.\n */\nexport const AUTH_LOCALE_ID: Record<AuthLocale, string> = { zh: 'cn', en: 'en' }\n\n/**\n * Options for the auth.eda.cn overlay iframe opened by `auth.login()`.\n *\n * The embed has TWO rendering modes, and which one is right depends on the\n * surface that hosts the iframe:\n *\n * - **Transparent card mode** (default, no `fill`): the embedded page sets\n * `html[data-iframe-mode=\"true\"]` and the root paints\n * `background: transparent` (see `eda-cn-login/app/page.tsx` — the wrapper\n * only gets the `bg-transparent` class when `fill !== 'full'`). The host\n * then paints a card around the iframe (e.g. the login dialog's backdrop\n * + centered card) so Blink's white `BaseBackgroundColor()` canvas never\n * shows. This is the right mode when the iframe sits inside a host-painted\n * card with its own visual edge — e.g. the sidebar-triggered login dialog.\n *\n * - **Fill mode** (`fill: 'full'`): the embed's `DialogContent` becomes\n * `w-full h-full max-w-none max-h-none left-0 top-0 rounded-none border-none`\n * (see `eda-cn-login/components/LoginDialog.tsx` — `fillFull` branch at\n * line 61) and the wrapper drops `bg-transparent` so the page paints its\n * own `bg-background` edge-to-edge. This is the right mode when the iframe\n * fills its host container (e.g. the toolview card) and there is no\n * surrounding card to mask the embed's rounded corners or transparent\n * 20px grid strips.\n *\n * The `lang` and `theme` params follow the host UI in both modes.\n */\nexport interface LoginOptions {\n /** Ask auth.eda.cn to self-close on an outside click (default `true`). */\n closeOnOutsideClick?: boolean\n /** Embed UI language (default `zh`). */\n lang?: AuthLocale\n /** Embed color scheme (default `light`). */\n theme?: AuthTheme\n /**\n * Set to `'full'` to make the embed fill its iframe viewport edge-to-edge\n * (no rounded corners, no transparent grid strips, embed paints its own\n * `bg-background`). Omit for the transparent card mode described above.\n * `true` is accepted as a shorthand for `'full'`.\n */\n fill?: 'full' | 'transparent' | true\n}\n\nexport interface AuthTokenPayload {\n id: string\n token: string\n nickname?: string\n /** User avatar URL (`headimage` in the auth.eda.cn payload). */\n avatar?: string\n /** Bound mobile number; forwarded to the eda.cn profile page as `phone=`. */\n phone?: string\n /** unix seconds; undefined = no expiry */\n expiresAt?: number\n}\n\n/**\n * Build the auth.eda.cn embed URL.\n *\n * The URL switches between two rendering modes based on `options.fill`:\n * - `fill: 'full'` (or `true`) → `fill=full` is sent; the embed's\n * `DialogContent` becomes `w-full h-full … rounded-none` and the wrapper\n * drops `bg-transparent`, so the embed fills the iframe viewport with\n * its own `bg-background`. Use this when the iframe is the surface (e.g.\n * the toolview card).\n * - any other value (including unset) → no `fill` is sent; the embed stays\n * in transparent card mode. The host is responsible for painting a card\n * around the iframe so Blink's white base canvas never reaches the user.\n */\nexport function buildLoginUrl(options: LoginOptions & { baseUrl?: string } = {}): string {\n const url = new URL(options.baseUrl ?? `${AUTH_ORIGIN}/`)\n url.searchParams.set('v', AUTH_IFRAME_VERSION)\n if (options.closeOnOutsideClick !== false) url.searchParams.set('clickOutsideToClose', 'true')\n if (options.fill === 'full' || options.fill === true) url.searchParams.set('fill', 'full')\n url.searchParams.set('transparent', 'true')\n const lang = options.lang ?? 'zh'\n // `locale` is the param auth.eda.cn reads; `lang` keeps parity with\n // hq-eda-ai's LoginDialog (see AUTH_LOCALE_ID).\n url.searchParams.set('locale', AUTH_LOCALE_ID[lang])\n url.searchParams.set('lang', lang)\n url.searchParams.set('theme', options.theme ?? 'light')\n return url.toString()\n}\n\nexport type ParsedAuthMessage =\n | { kind: 'token'; info: AuthTokenPayload }\n | { kind: 'logout' }\n | { kind: 'close' }\n\n/** Structural event (origin + data) so tests don't need a real MessageEvent. */\nexport interface AuthMessageEventLike {\n origin: string\n data: unknown\n /** The posting window; retained for completeness (origin is not gated). */\n source?: unknown\n}\n\ninterface RawEnvelope {\n category?: unknown\n data?: { type?: unknown; data?: unknown }\n}\n\n/** Coerce an id field (string or number, as auth.eda.cn sends) to a string. */\nfunction stringifyId(value: unknown): string | null {\n if (typeof value === 'string' && value.length > 0) return value\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return null\n}\n\nexport function parseAuthMessage(raw: unknown): ParsedAuthMessage | null {\n let envelope: RawEnvelope | null = null\n if (typeof raw === 'string') {\n try {\n envelope = JSON.parse(raw) as RawEnvelope\n } catch {\n return null\n }\n } else if (raw !== null && typeof raw === 'object') {\n envelope = raw as RawEnvelope\n }\n if (!envelope || envelope.category !== 1) return null\n const data = envelope.data\n if (!data || typeof data !== 'object') return null\n\n switch (data.type) {\n case 'update_access_token': {\n const d = data.data\n if (!d || typeof d !== 'object') return null\n const record = d as Record<string, unknown>\n const token = typeof record.token === 'string' && record.token.length > 0 ? record.token : null\n // auth.eda.cn sends userId/id as NUMBERS (e.g. 6215935) — coerce to string.\n const id = stringifyId(record.userId) ?? stringifyId(record.id)\n if (!token || !id) return null\n const nickname = typeof record.nickname === 'string' && record.nickname.length > 0 ? record.nickname : undefined\n // auth.eda.cn sends the avatar as `headimage`; `avatar` accepted as alias.\n const avatar = typeof record.headimage === 'string' && record.headimage.length > 0\n ? record.headimage\n : typeof record.avatar === 'string' && record.avatar.length > 0 ? record.avatar : undefined\n // Phone may arrive as a string or a number (mirrors `stringifyId`).\n const phone = stringifyId(record.phone) ?? undefined\n const expiresAt = typeof record.expires_at === 'number' ? record.expires_at : undefined\n return {\n kind: 'token',\n info: {\n id,\n token,\n ...(nickname !== undefined ? { nickname } : {}),\n ...(avatar !== undefined ? { avatar } : {}),\n ...(phone !== undefined ? { phone } : {}),\n ...(expiresAt !== undefined ? { expiresAt } : {}),\n },\n }\n }\n case 'logout':\n return { kind: 'logout' }\n case 'close_dialog':\n return { kind: 'close' }\n default:\n return null\n }\n}\n\n/** Origin-agnostic envelope parsing. The ONLY entry point for window message events. */\nexport function handleAuthMessage(event: AuthMessageEventLike): ParsedAuthMessage | null {\n return parseAuthMessage(event.data)\n}\n","/**\n * Login-state + result rendering helpers shared by the client React cards.\n *\n * Every style is a FUNCTION of the active color scheme: the cards are inline\n * styled (the client bundle ships no CSS file), so light/dark support has to\n * be expressed in JS. Colors prefer DSH's `--dsw-alias-*` tokens and fall back\n * to an explicit per-scheme value (see `sidebar-action.tsx`).\n */\nimport type { CSSProperties, ReactNode } from 'react'\nimport type { Translate } from '../i18n.js'\n\n/** Tool result content block (subset of DSH `ContentBlock`). */\ninterface ContentBlockLike {\n type?: string\n text?: string\n}\n\n/** Structural tool-call block subset (we only read settled text content). */\nexport interface ToolBlockLike {\n content?: readonly ContentBlockLike[]\n}\n\n/** Best-effort JSON.parse of the tool's text output blocks. */\nexport function parseToolResult(block: ToolBlockLike | undefined): Record<string, unknown> | null {\n if (!block || !Array.isArray(block.content)) return null\n const text = block.content\n .filter((c): c is ContentBlockLike => !!c && c.type === 'text' && typeof c.text === 'string')\n .map((c) => c.text as string)\n .join('')\n if (!text) return null\n try {\n const parsed = JSON.parse(text) as unknown\n return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null\n } catch {\n return null\n }\n}\n\n/** True when the parsed result is the auth-gate signal. */\nexport function isNeedsAuthResult(result: Record<string, unknown> | null): result is Record<string, unknown> & { status: 'needs_auth' } {\n return !!result && result.status === 'needs_auth'\n}\n\nexport const AUTH_ORIGIN = 'https://auth.eda.cn'\n\n/** Card colors for one color scheme (DSH token first, explicit fallback second). */\nexport interface CardPalette {\n surface: string\n border: string\n text: string\n muted: string\n success: string\n danger: string\n}\n\nexport const LIGHT_CARD_PALETTE: CardPalette = {\n surface: 'var(--dsw-alias-bg-layer-1, #ffffff)',\n border: 'var(--dsw-alias-border-l1, #e4e7ec)',\n text: 'var(--dsw-alias-label-primary, inherit)',\n muted: 'var(--dsw-alias-label-secondary, #5b6472)',\n success: 'var(--dsw-alias-state-success-primary, #1677ff)',\n danger: 'var(--dsw-alias-state-error-primary, #d4380d)',\n}\n\nexport const DARK_CARD_PALETTE: CardPalette = {\n surface: 'var(--dsw-alias-bg-layer-1, #20242c)',\n border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.14))',\n text: 'var(--dsw-alias-label-primary, #e6eaf0)',\n muted: 'var(--dsw-alias-label-secondary, #8b95a5)',\n success: 'var(--dsw-alias-state-success-primary, #4cc38a)',\n danger: 'var(--dsw-alias-state-error-primary, #ff7875)',\n}\n\nexport function cardPalette(dark: boolean): CardPalette {\n return dark ? DARK_CARD_PALETTE : LIGHT_CARD_PALETTE\n}\n\nexport function cardStyle(palette: CardPalette): CSSProperties {\n return {\n border: `1px solid ${palette.border}`,\n borderRadius: 10,\n padding: '12px 14px',\n margin: '4px 0',\n background: palette.surface,\n color: palette.text,\n fontFamily: 'inherit',\n }\n}\n\nexport const TITLE_STYLE: CSSProperties = {\n fontSize: 14,\n fontWeight: 600,\n margin: '0 0 6px',\n}\n\nexport const STATUS_STYLE: CSSProperties = {\n fontSize: 13,\n margin: '0 0 10px',\n lineHeight: 1.5,\n}\n\n/**\n * Iframe height for both the dialog and the toolview card. Tuned to the\n * auth.eda.cn login form's actual painted height (≈390px at 768px width,\n * measured with a magenta iframe element background so the embedded doc's\n * transparent top/bottom strips are obvious). The dialog and toolview both\n * use this same number for consistency.\n *\n * Note: the auth.eda.cn page wrapper is `grid-rows-[20px_1fr_20px]`, so the\n * embedded doc always leaves two 20px transparent strips above and below the\n * form — they are NOT additional empty space we can shave off; they are\n * always there in the embed's own layout. The 30px buffer above the 390px\n * content (→ 440) gives the form room to grow slightly on error states\n * without immediately overflowing.\n */\nexport const LOGIN_IFRAME_HEIGHT = 440\n\n/**\n * The embedded login iframe: painted with the same surface as the wrapping\n * card so the login box blends in both schemes.\n *\n * Why not `background: transparent`? Blink's `BaseBackgroundColor()` falls\n * back to WHITE whenever the embedded doc's root element has a transparent\n * background (and auth.eda.cn's `data-iframe-mode` page is exactly that).\n * That white canvas shows through wherever the document doesn't paint, which\n * reads as a glaring white \"frame\" around the login card in dark mode. Light\n * mode hid the bug because the white canvas happened to match the light\n * host. Painting the iframe ELEMENT with the card's surface (DSH alias\n * `--dsw-alias-bg-layer-1` with a per-scheme fallback) puts a dark sheet in\n * dark mode and a light sheet in light mode, so the login card sits on a\n * surface that blends with the host in both schemes.\n */\nexport function iframeStyle(palette: CardPalette): CSSProperties {\n return {\n width: '100%',\n height: LOGIN_IFRAME_HEIGHT,\n border: `1px solid ${palette.border}`,\n borderRadius: 8,\n background: palette.surface,\n display: 'block',\n }\n}\n\nexport function StatusLine({\n authenticated,\n nickname,\n palette,\n t,\n}: {\n authenticated: boolean\n nickname?: string\n palette: CardPalette\n t: Translate\n}): ReactNode {\n if (authenticated) {\n return (\n <p style={{ ...STATUS_STYLE, color: palette.success }}>\n {t('card.loggedIn', {\n nickname: nickname ? t('card.nicknameSep', { nickname }) : '',\n })}\n </p>\n )\n }\n return (\n <p style={{ ...STATUS_STYLE, color: palette.danger }}>\n {t('card.loggedOut')}\n </p>\n )\n}\n","/**\n * Host theme + locale sensing for the client UI.\n *\n * The DSH slot system injects React components with PROPS, not the cordis ctx,\n * so the cards cannot reach `ctx.theme` / `ctx.locale` the way a plugin body\n * can. Both services do, however, publish their state into the DOM, and that\n * is what this module reads:\n *\n * - THEME — `ui-layout`'s presenter switches `body[data-ds-dark-theme]` from\n * the resolved snapshot (`packages/client/ui-layout/src/client/theme-presenter.ts`,\n * `DARK_ATTRIBUTE`), so the attribute's presence IS the dark palette. Same\n * signal the sibling packages already use\n * (`dsh-tool-schematic-gen/src/client/theme.ts`). `prefers-color-scheme` is\n * deliberately NOT consulted: DSH resolves `system` itself, and an OS-dark /\n * DSH-light combination would then be misdetected.\n * - LOCALE — `dsh-client-locale` writes `<html lang>` on every locale change\n * (`syncDocumentLanguage`: `zh-CN` | `en`). Falling back to the browser's\n * own `navigator.languages` keeps the UI usable on hosts without that\n * plugin. Chinese is the last resort because this is a Chinese-first app\n * (and `hq-eda-ai` defaults to zh: `languageMap[lang] || \"zh\"`).\n *\n * Both are exposed as `useSyncExternalStore` snapshots so every mounted card\n * re-renders together when the user flips theme or language.\n */\nimport { useSyncExternalStore } from 'react'\nimport type { AuthLocale, AuthTheme } from './lib.js'\n\n/** DSH's dark-palette marker, written by ui-layout's theme presenter. */\nexport const DARK_ATTRIBUTE = 'data-ds-dark-theme'\n\nfunction isDarkDocument(): boolean {\n if (typeof document === 'undefined') return false\n if (document.body?.hasAttribute(DARK_ATTRIBUTE)) return true\n // Fallbacks for hosts that mark the scheme on <html> instead of <body>.\n const root = document.documentElement\n if (!root) return false\n const dataTheme = root.getAttribute('data-theme')\n if (dataTheme !== null) return dataTheme.toLowerCase() === 'dark'\n return root.classList.contains('dark')\n}\n\n/** `zh-CN`, `zh-Hans`, `en-GB`, … → our locale id (`undefined` = unknown). */\nfunction localeFromTag(tag: string | null | undefined): AuthLocale | undefined {\n if (!tag) return undefined\n const primary = tag.toLowerCase().split('-')[0]\n return primary === 'zh' || primary === 'en' ? primary : undefined\n}\n\nfunction detectLocale(): AuthLocale {\n if (typeof document !== 'undefined') {\n const fromDocument = localeFromTag(document.documentElement?.getAttribute('lang'))\n if (fromDocument) return fromDocument\n }\n if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {\n // `window` is the browser test: Node exposes a global `navigator`\n // reporting the machine's own language, which would otherwise decide the\n // locale for non-browser runs (same guard DSH's locale plugin uses).\n for (const tag of [...(navigator.languages ?? []), navigator.language]) {\n const match = localeFromTag(tag)\n if (match) return match\n }\n }\n return 'zh'\n}\n\nlet dark = isDarkDocument()\nlet locale = detectLocale()\nconst listeners = new Set<() => void>()\nlet darkObserver: MutationObserver | null = null\nlet localeObserver: MutationObserver | null = null\n\nfunction notify(): void {\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n /* one crashing subscriber must not strand the rest on a stale value */\n }\n }\n}\n\n/** Re-read the DOM and notify only what actually changed. */\nexport function syncUiEnv(): void {\n let changed = false\n const nextDark = isDarkDocument()\n if (nextDark !== dark) {\n dark = nextDark\n changed = true\n }\n const nextLocale = detectLocale()\n if (nextLocale !== locale) {\n locale = nextLocale\n changed = true\n }\n if (changed) notify()\n}\n\n/** Start observing (idempotent; also re-reads so no change is missed). */\nfunction watch(): void {\n if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return\n if (!darkObserver && document.body) {\n darkObserver = new MutationObserver(syncUiEnv)\n darkObserver.observe(document.body, { attributes: true, attributeFilter: [DARK_ATTRIBUTE] })\n }\n if (!localeObserver && document.documentElement) {\n localeObserver = new MutationObserver(syncUiEnv)\n localeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['lang', 'data-theme', 'class'],\n })\n }\n syncUiEnv()\n}\n\nfunction subscribe(callback: () => void): () => void {\n watch()\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n}\n\n/**\n * Imperative subscription for non-React consumers (e.g. the login dialog's\n * backdrop/card DOM). The callback fires on every theme or locale flip.\n */\nexport function subscribeUiEnv(callback: () => void): () => void {\n return subscribe(callback)\n}\n\nconst getDark = (): boolean => dark\nconst getLocale = (): AuthLocale => locale\n\n/**\n * Synchronous read of the current dark-palette state. Safe outside React\n * (the auth client uses it when appending the overlay iframe, before any\n * component has a chance to subscribe).\n */\nexport function getCurrentDark(): boolean {\n return dark\n}\n\n/** Synchronous read of the current host UI locale. */\nexport function getCurrentLocale(): AuthLocale {\n return locale\n}\n\n/** Synchronous read of the current host surface color (matches the palette). */\nexport function getCurrentSurfaceColor(): string {\n return dark ? 'var(--dsw-alias-bg-layer-1, #20242c)' : 'var(--dsw-alias-bg-layer-1, #ffffff)'\n}\n\n/** `true` while the host renders the dark palette. */\nexport function useIsDark(): boolean {\n return useSyncExternalStore(subscribe, getDark, getDark)\n}\n\n/** The host UI language. */\nexport function useLocale(): AuthLocale {\n return useSyncExternalStore(subscribe, getLocale, getLocale)\n}\n\n/** The host color scheme in auth.eda.cn's own vocabulary. */\nexport function useColorScheme(): AuthTheme {\n return useIsDark() ? 'dark' : 'light'\n}\n\n/** Release the observers (called from `apply()`'s disposer). */\nexport function disposeUiEnv(): void {\n darkObserver?.disconnect()\n localeObserver?.disconnect()\n darkObserver = null\n localeObserver = null\n listeners.clear()\n}\n","/**\n * zh / en copy for every user-visible string of the auth UI (sidebar trigger,\n * account menu, login tool card).\n *\n * Kept self-contained rather than registered into DSH's `ctx.locale`\n * namespace — same call the sibling packages made\n * (`dsh-tool-symbol-footprint/src/client/i18n.ts`) — because the slot system\n * hands components props, not ctx, and a missing namespace would leave the UI\n * blank. `en` is typed as `Record<AuthCopyKey, string>`, so a key added to one\n * language without the other is a COMPILE error (bilingual balance enforced at\n * build time, mirroring DSH's own locale registry).\n *\n * The en「Go to profile」/「Log out」wording is the one the sidebar spec asks\n * for; the zh side follows `hq-eda-ai`'s `locales/cn.ts` (个人中心 / 退出登录).\n */\nimport { useMemo } from 'react'\nimport type { AuthLocale } from './lib.js'\nimport { useLocale } from './ui-env.js'\n\nconst zh = {\n 'sidebar.login': '华秋EDA AI登录',\n 'sidebar.loginTitle': '登录华秋 EDA AI(eda.cn)账号',\n 'sidebar.accountTitle': '华秋 EDA AI 账号',\n 'sidebar.account': '华秋EDA AI · 已登录',\n\n 'menu.profile': '个人中心',\n 'menu.logout': '退出登录',\n\n 'card.title': '华秋 EDA AI(eda.cn)登录',\n 'card.desc': '工具「{tool}」需要登录华秋 EDA AI 账号才能继续。请在下方的登录框完成登录(或点击左侧「华秋EDA AI登录」按钮);登录完成后,回复助手「已登录,请重试」,助手会自动重新调用该工具。',\n 'card.loggedIn': '✓ 已登录{nickname} —— 现在可以回复助手「已登录,请重试」,助手会重新调用工具。',\n 'card.loggedOut': '未登录 —— 请在上方登录华秋 EDA AI(eda.cn)账号,或点击左侧「华秋EDA AI登录」按钮;登录完成后让助手重试。',\n 'card.tool': '工具:{tool}',\n 'card.empty': '(无输出)',\n // Substituted into `{nickname}` by `card.loggedIn`. zh uses a full-width\n // colon, en a half-width one plus a space; hardcoding ':' made the English\n // card read \"Logged in:John\".\n 'card.nicknameSep': ':{nickname}',\n\n 'dialog.close': '关闭',\n} as const\n\n/** Every key of the zh dictionary — the contract both languages satisfy. */\nexport type AuthCopyKey = keyof typeof zh\n\nconst en: Record<AuthCopyKey, string> = {\n 'sidebar.login': 'Huaqiu EDA AI login',\n 'sidebar.loginTitle': 'Sign in to your Huaqiu EDA AI (eda.cn) account',\n 'sidebar.accountTitle': 'Huaqiu EDA AI account',\n 'sidebar.account': 'Huaqiu EDA AI · signed in',\n\n 'menu.profile': 'Go to profile',\n 'menu.logout': 'Log out',\n\n 'card.title': 'Huaqiu EDA AI (eda.cn) login',\n // The reply phrase used to be hardcoded to the Chinese \"已登录,请重试\" even\n // in these English strings, telling an English-speaking user to type Chinese.\n 'card.desc': 'Tool \"{tool}\" needs a Huaqiu EDA AI account. Complete the login below (or use the Huaqiu EDA AI button in the sidebar), then reply \"I have logged in, please retry\" so the assistant can retry the tool.',\n 'card.loggedIn': '✓ Logged in{nickname} — reply \"I have logged in, please retry\" and the assistant will retry the tool.',\n 'card.loggedOut': 'Not logged in — sign in above, or use the Huaqiu EDA AI button in the sidebar, then ask the assistant to retry.',\n 'card.tool': 'Tool: {tool}',\n 'card.empty': '(no output)',\n 'card.nicknameSep': ': {nickname}',\n\n 'dialog.close': 'Close',\n}\n\nconst COPY: Record<AuthLocale, Record<AuthCopyKey, string>> = { zh, en }\n\n/** Every copy key, in declaration order (used to assert bilingual balance). */\nexport const AUTH_COPY_KEYS = Object.keys(zh) as AuthCopyKey[]\n\nexport type Translate = (key: AuthCopyKey, params?: Record<string, unknown>) => string\n\n/**\n * Look a key up, interpolating `{name}` placeholders.\n *\n * Chain: active locale → zh (the source of truth) → the key itself, so a\n * missing translation stays VISIBLE instead of blanking the UI.\n */\nexport function translate(locale: AuthLocale, key: AuthCopyKey, params?: Record<string, unknown>): string {\n const template = COPY[locale]?.[key] ?? COPY.zh[key] ?? key\n if (!params) return template\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n name in params ? String(params[name]) : match)\n}\n\n/** Translate bound to one locale (stable for the lifetime of that locale). */\nexport function createT(locale: AuthLocale): Translate {\n return (key, params) => translate(locale, key, params)\n}\n\n/** Translate bound to the host UI language, re-created when it changes. */\nexport function useT(): Translate {\n const locale = useLocale()\n return useMemo(() => createT(locale), [locale])\n}\n","/**\n * The sidebar login dialog — a real modal (backdrop + centered card + iframe).\n *\n * WHY A DIALOG AND NOT A FULL-VIEWPORT IFRAME\n *\n * The embed (`auth.eda.cn`) reads only two URL params: `fill` and\n * `clickOutsideToClose`. With `fill !== 'full'` it sets\n * `data-iframe-mode=\"true\"` on `<html>` and the CSS rule\n * `html[data-iframe-mode=true], html[data-iframe-mode=true] body { background: 0 0 !important }`\n * makes its root transparent — but the page wrapper still uses a\n * `grid-rows-[20px_1fr_20px]` layout, so the 20px strips above/below the\n * card are empty and show through to whatever is behind the iframe. Behind\n * the iframe element, with no background set, Blink falls back to a WHITE\n * base background canvas. That white is what was reading as a \"white frame\n * around the login card in dark mode\" (and was invisibly there in light\n * mode, blending with the white host).\n *\n * The two ways out:\n * 1. Paint the iframe element with a color → in a full-viewport iframe\n * that blanks the whole app with that color (light surface = white\n * blocks the light host; dark surface = dark blocks the dark host).\n * 2. Make the iframe CARD-SIZED and put it inside a host-painted card,\n * so the iframe's background can be `transparent` and the card's\n * surface shows through wherever the embedded doc is transparent.\n * This is the pattern the toolview card already uses\n * (`needs-auth-toolview.tsx`) and the pattern `hq-eda-ai`'s\n * `LoginDialog.tsx` uses.\n *\n * We use (2): a fixed full-viewport backdrop (semi-transparent black) +\n * centered card (host surface bg) + the iframe (transparent inner doc,\n * card surface as element bg so Blink's white canvas never reaches the\n * user). Click on the backdrop, the × button, Escape, or the auth embed's\n * own `close_dialog` postMessage closes the dialog.\n */\nimport { LOGIN_IFRAME_HEIGHT } from './common.js'\nimport { buildLoginUrl, type AuthLocale, type AuthTheme } from '../lib.js'\nimport { translate } from '../i18n.js'\nimport { getCurrentLocale, getCurrentSurfaceColor, subscribeUiEnv, syncUiEnv } from '../ui-env.js'\n\n/** Aria / data attribute names — stable so tests and CSS can target them. */\nexport const DIALOG_OVERLAY_ATTR = 'data-hq-auth-dialog'\nexport const DIALOG_CARD_ATTR = 'data-hq-auth-dialog-card'\nexport const DIALOG_IFRAME_ATTR = 'data-hq-auth-dialog-iframe'\nexport const DIALOG_CLOSE_ATTR = 'data-hq-auth-dialog-close'\n\n/**\n * Iframe height. Tuned to the auth.eda.cn login form's actual painted height\n * (≈390px at 768px width, measured with a magenta iframe element background\n * so the embedded doc's transparent 20px top/bottom strips are obvious). The\n * shared constant lives in `./common.jsx` so the dialog and the toolview\n * card stay in lock-step.\n */\nconst IFRAME_HEIGHT = LOGIN_IFRAME_HEIGHT\nconst CARD_MAX_WIDTH = 768\n\nlet container: HTMLDivElement | null = null\nlet unsubscribe: (() => void) | null = null\nlet onCloseRequested: (() => void) | null = null\n\n/**\n * Open the login dialog. Idempotent: a second call while open is a no-op\n * (mirrors the client-side `if (iframe) return` guard). `onClose` fires\n * whenever the dialog closes for ANY reason (backdrop click, Escape, close\n * button, postMessage, programmatic close) — the auth client uses it to\n * unblock its own `isOpen` state.\n */\nexport function openLoginDialog(options: { lang?: AuthLocale; theme?: AuthTheme } = {}, onClose?: () => void): void {\n if (container) return\n // The ui-env module reads the DOM once at import time. Re-read here so\n // the dialog picks up the current theme even if no React component has\n // subscribed yet (e.g. when the sidebar opens the dialog before any\n // card has mounted), or when a test sets the attribute after import.\n syncUiEnv()\n const locale = options.lang ?? getCurrentLocale()\n\n const root = document.createElement('div')\n root.setAttribute(DIALOG_OVERLAY_ATTR, '')\n // Backdrop: dim the host without blanking it. Theme-agnostic — rgba black\n // works on both light and dark hosts.\n root.style.cssText = [\n 'position:fixed',\n 'inset:0',\n 'width:100vw',\n 'height:100vh',\n 'border:0',\n 'z-index:2147483647',\n 'background:rgba(0, 0, 0, 0.55)',\n 'display:flex',\n 'align-items:center',\n 'justify-content:center',\n 'box-sizing:border-box',\n ].join(';')\n\n const card = document.createElement('div')\n card.setAttribute(DIALOG_CARD_ATTR, '')\n const applyCardColors = (): void => {\n const surface = getCurrentSurfaceColor()\n // No card border: a 1px border on each side would shrink the iframe's\n // viewport to 766px on a 768px card, missing the embed's `md:grid-cols`\n // (Tailwind `md` = 768px) threshold by 2px and silently falling back to\n // a single-column layout. The card's box-shadow already gives the card\n // a clear visual edge against the dimmed host.\n card.style.cssText = [\n `width:min(100vw, ${CARD_MAX_WIDTH}px)`,\n // Card height = iframe height exactly. A taller card would leave a\n // strip of card surface below the form (the flex column has only one\n // child, the iframe, so any extra height piles up at the bottom).\n `height:min(90vh, ${IFRAME_HEIGHT}px)`,\n 'border-radius:12px',\n 'box-shadow:0 24px 48px rgba(0, 0, 0, 0.32)',\n 'position:relative',\n 'box-sizing:border-box',\n `background:${surface}`,\n 'display:flex',\n 'flex-direction:column',\n ].join(';')\n }\n applyCardColors()\n\n const closeButton = document.createElement('button')\n closeButton.setAttribute(DIALOG_CLOSE_ATTR, '')\n closeButton.type = 'button'\n closeButton.setAttribute('aria-label', translate(locale, 'dialog.close'))\n closeButton.title = translate(locale, 'dialog.close')\n closeButton.textContent = '×'\n closeButton.style.cssText = [\n 'position:absolute',\n 'top:6px',\n 'right:10px',\n 'width:28px',\n 'height:28px',\n 'border:0',\n 'background:transparent',\n 'color:var(--dsw-alias-label-secondary, #5b6472)',\n 'font-size:22px',\n 'line-height:1',\n 'cursor:pointer',\n 'border-radius:6px',\n 'padding:0',\n ].join(';')\n closeButton.addEventListener('click', closeLoginDialog)\n\n const iframe = document.createElement('iframe')\n iframe.setAttribute(DIALOG_IFRAME_ATTR, '')\n iframe.src = buildLoginUrl({ lang: options.lang, theme: options.theme })\n iframe.title = translate(locale, 'card.title')\n iframe.allow = 'clipboard-write'\n // The iframe element background = the card surface. That is what masks\n // Blink's white base canvas in the embedded doc's transparent strips\n // (see file header). The embedded doc itself is transparent because\n // buildLoginUrl never sends `fill=full`.\n iframe.style.cssText = [\n 'width:100%',\n `height:${IFRAME_HEIGHT}px`,\n 'border:0',\n 'border-radius:8px',\n `background:${getCurrentSurfaceColor()}`,\n 'display:block',\n 'flex:0 0 auto',\n ].join(';')\n\n card.appendChild(closeButton)\n card.appendChild(iframe)\n root.appendChild(card)\n document.body.appendChild(root)\n\n // Backdrop click: close ONLY when the click lands on the backdrop itself,\n // not on the card. The card stops propagation in its own click handler.\n root.addEventListener('mousedown', backdropMouseDown)\n card.addEventListener('mousedown', stopPropagation)\n document.addEventListener('keydown', onKeyDown)\n\n // React to theme/locale flips so the card surface + iframe background\n // track the host (a mid-session switch while the dialog is open would\n // otherwise leave a stale-colored card).\n unsubscribe = subscribeUiEnv(() => {\n if (!container) return\n applyCardColors()\n iframe.style.background = getCurrentSurfaceColor()\n closeButton.title = translate(getCurrentLocale(), 'dialog.close')\n closeButton.setAttribute('aria-label', closeButton.title)\n })\n\n container = root\n onCloseRequested = onClose ?? null\n}\n\n/** Programmatic close (used by the auth client after a successful login). */\nexport function closeLoginDialog(): void {\n if (!container) return\n container.remove()\n container = null\n document.removeEventListener('keydown', onKeyDown)\n unsubscribe?.()\n unsubscribe = null\n const cb = onCloseRequested\n onCloseRequested = null\n cb?.()\n}\n\n/** True while the dialog is mounted. */\nexport function isLoginDialogOpen(): boolean {\n return container !== null\n}\n\nfunction backdropMouseDown(event: MouseEvent): void {\n if (event.target === container) closeLoginDialog()\n}\n\nfunction stopPropagation(event: MouseEvent): void {\n event.stopPropagation()\n}\n\nfunction onKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n event.stopPropagation()\n closeLoginDialog()\n }\n}\n","/**\n * Auth client core — the Phase 0A POC logic, factored as a testable factory.\n * `apply()` in index.ts wires this to the real window/document/localStorage.\n */\nimport {\n AUTH_ORIGIN,\n buildLoginUrl,\n handleAuthMessage,\n type AuthMessageEventLike,\n type AuthTokenPayload,\n type LoginOptions,\n} from './lib.js'\nimport { closeLoginDialog, isLoginDialogOpen, openLoginDialog } from './ui/login-dialog.js'\nimport type { AuthStorage } from './storage.js'\nimport type { AuthTransport } from './transport.js'\n\nexport interface AuthClientDeps {\n storage: AuthStorage\n transport: AuthTransport\n /** Strict origin gate; defaults to auth.eda.cn. */\n trustedOrigin?: string\n loginUrl?: string\n windowLike: Pick<Window, 'addEventListener' | 'removeEventListener'>\n documentLike: Pick<Document, 'createElement' | 'body'>\n}\n\nexport interface AuthClient {\n auth: {\n isAuthenticated(): boolean\n /** HQ Edge host mode: EDA owns the credential; login triggers the EDA dialog. */\n isHostMode(): boolean\n getAccessToken(): Promise<string | null>\n getUserInfo(): Promise<AuthTokenPayload | null>\n /**\n * Rich eda.cn profile (nickname + headimage) for the current token. The\n * node half resolves the token and calls eda.cn — used by the sidebar in\n * host mode when the host session carries only {token, userId} (e.g.\n * kicad). Null on failure; the UI degrades to the HQ icon.\n */\n getUserProfile(): Promise<{ nickname?: string; headimage?: string } | null>\n login(options?: LoginOptions): Promise<void>\n logout(): Promise<void>\n onAuthStateChanged(listener: (info: AuthTokenPayload | null) => void): () => void\n }\n /** Route window 'message' events here. Exposed for direct testing. */\n handleMessageEvent(event: AuthMessageEventLike): void\n /** Re-push persisted credentials on boot (acceptance group D). */\n restore(): Promise<void>\n /** Re-push persisted credentials on demand (heals a reset/absent node half). */\n syncNow(): Promise<void>\n /**\n * Resolve host mode from the node half and, when active, adopt the host-owned\n * session as the browser credential. Resolves `hostMode`; the resolved\n * session is emitted through `onAuthStateChanged` so apps and the HIT cards\n * flip to authenticated without ever opening the auth.eda.cn iframe.\n */\n refreshHost(): Promise<boolean>\n /** Browser→node transport (used to read host mode before UI registration). */\n transport: AuthTransport\n dispose(): void\n}\n\nexport function createAuthClient(deps: AuthClientDeps): AuthClient {\n const trustedOrigin = deps.trustedOrigin ?? AUTH_ORIGIN\n const loginUrl = deps.loginUrl ?? `${AUTH_ORIGIN}/`\n const { storage, transport } = deps\n const listeners = new Set<(info: AuthTokenPayload | null) => void>()\n\n // Host-mode credential state. In host mode hq-edge owns the session (EDA\n // hands the operator token over on launch) — the node half resolves it and\n // the browser adopts it through `/session`, so the auth gate, the HIT cards\n // and the sidebar all read authenticated without an iframe login.\n let hostMode = false\n let hostSession: AuthTokenPayload | null = null\n let hostSessionLoaded = false\n\n const emit = (info: AuthTokenPayload | null): void => {\n for (const listener of listeners) listener(info)\n }\n const closeIframe = (): void => {\n // The dialog module owns the DOM. Tear it down on any close path\n // (token success, logout, close_dialog postMessage, dispose).\n if (isLoginDialogOpen()) closeLoginDialog()\n }\n /**\n * Open the login dialog (backdrop + centered card + auth.eda.cn iframe).\n *\n * The dialog is ALWAYS transparent (no `transparent` option exists): the\n * embedded doc sets its own root to `background: transparent` (we never\n * send `fill=full`), and the iframe sits inside a host-painted card so\n * Blink's white base canvas never reaches the user. See the long header\n * in `ui/login-dialog.ts` for the full why.\n */\n const openIframe = (options: LoginOptions = {}): void => {\n if (isLoginDialogOpen()) return\n const baseUrl = loginUrl\n openLoginDialog(\n {\n ...(options.lang ? { lang: options.lang } : {}),\n ...(options.theme ? { theme: options.theme } : {}),\n },\n () => {\n // Re-render safety: nothing to do here — the auth client's own state\n // is just the dialog-open boolean, which `isLoginDialogOpen()` reads\n // directly from the dialog module.\n void baseUrl\n },\n )\n }\n\n /**\n * Adopt the host-owned session as the browser credential (host mode only).\n * Kept synchronous for `isAuthenticated()`'s first-paint fast path — callers\n * that need the resolved payload use `getUserInfo()` / `getAccessToken()`,\n * which await host resolution before returning.\n */\n const resolveHost = async (): Promise<void> => {\n hostSessionLoaded = true\n if (!hostMode) {\n hostSession = null\n return\n }\n try {\n const session = await transport.fetchSession()\n hostSession = session.authenticated ? normalizeHostUserPayload(session.user) : null\n } catch {\n // Host unreachable: stay logged out rather than inventing a session.\n hostSession = null\n }\n }\n\n const auth = {\n // Host mode is STRICT (mirrors the node half): hq-edge is the single\n // source of truth, so a pushed/persisted credential left behind by an\n // earlier standalone login must never report the operator as logged in\n // while the host has no session — that mismatch is exactly how the HIT\n // card reads \"logged in\" while the tool still gates on needs_auth.\n isAuthenticated: (): boolean => (hostMode ? hostSession !== null : storage.get() !== null),\n isHostMode: (): boolean => hostMode,\n getAccessToken: async (): Promise<string | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n return hostMode\n ? (hostSession?.token ?? null)\n : (hostSession?.token ?? storage.get()?.token ?? null)\n },\n getUserInfo: async (): Promise<AuthTokenPayload | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n return hostMode ? hostSession : (hostSession ?? storage.get())\n },\n getUserProfile: async (): Promise<{ nickname?: string; headimage?: string } | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n if (!(await auth.getAccessToken())) return null\n return transport.fetchUserInfo()\n },\n login: async (options?: LoginOptions): Promise<void> => {\n // Host mode: hq-edge owns the session — ask the host (EDA) to open its\n // own login dialog instead of the auth.eda.cn iframe. The node route\n // blocks until the dialog completes; then re-resolve and emit (only on\n // a real change, so a redundant trigger never re-renders every card).\n if (hostMode) {\n const prevToken = hostSession?.token\n await transport.triggerLogin()\n hostSessionLoaded = false\n await resolveHost()\n if (hostSession?.token !== prevToken) emit(hostSession)\n return\n }\n openIframe(options ?? {})\n },\n logout: async (): Promise<void> => {\n if (hostMode) {\n // Host-owned session: nothing to revoke browser-side beyond our cache.\n hostSession = null\n storage.clear()\n emit(null)\n return\n }\n storage.clear()\n try {\n await transport.pushLogout()\n } catch {\n /* node may be absent — local state is still cleared */\n }\n emit(null)\n closeIframe()\n },\n onAuthStateChanged: (listener: (info: AuthTokenPayload | null) => void): (() => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n }\n\n const handleMessageEvent = (event: AuthMessageEventLike): void => {\n // Offline deployment: no origin gate (see lib.ts). Envelope validation is\n // the only gate, so unrelated window messages can never corrupt state.\n const msg = handleAuthMessage(event)\n if (!msg) return\n if (msg.kind === 'token') {\n hostSession = null\n storage.set(msg.info)\n void transport.pushSession(msg.info).catch(() => { /* node push is best-effort; syncNow heals */ })\n emit(msg.info)\n closeIframe()\n } else if (msg.kind === 'logout') {\n hostSession = null\n storage.clear()\n emit(null)\n void transport.pushLogout().catch(() => { /* local state already cleared */ })\n closeIframe()\n } else if (msg.kind === 'close') {\n closeIframe()\n }\n }\n\n const onWindowMessage = (event: MessageEvent): void => {\n handleMessageEvent({ origin: event.origin, data: event.data })\n }\n\n deps.windowLike.addEventListener('message', onWindowMessage)\n\n const restore = async (): Promise<void> => {\n const restored = storage.get()\n if (restored) {\n try {\n await transport.pushSession(restored)\n } catch {\n /* boot push is best-effort; syncNow heals */\n }\n }\n }\n\n return {\n auth,\n handleMessageEvent,\n restore,\n /**\n * Resolve host mode + host session from the node half and emit the result.\n * Called at apply() boot; also re-runnable on demand (e.g. focus) so a\n * late host handover still flips the gate.\n */\n async refreshHost(): Promise<boolean> {\n let mode = false\n try {\n mode = await transport.fetchHostMode()\n } catch {\n mode = false\n }\n hostMode = mode\n hostSessionLoaded = false\n await resolveHost()\n emit(hostSession ?? storage.get())\n return hostMode\n },\n /**\n * Browser→node transport. Exposed so the client entry can read host mode\n * (whether an HQ Edge host supplies the credential and the login UI should\n * be suppressed) before registering the sidebar entrypoint.\n */\n transport,\n /**\n * Re-push the persisted credential to the node half. Healing path: the\n * node keeps auth in memory, so a `dsh web` restart (or a failed first\n * push) drops it while the browser still has the token. Callers re-sync on\n * focus / visibilitychange / login-card mount so the tool gate reflects\n * the actual browser login without requiring a page reload.\n */\n async syncNow(): Promise<void> {\n const info = storage.get()\n if (!info) return\n try {\n await transport.pushSession(info)\n } catch {\n /* sync is best-effort; a later focus event retries */\n }\n },\n dispose() {\n deps.windowLike.removeEventListener('message', onWindowMessage)\n closeIframe()\n hostMode = false\n hostSession = null\n hostSessionLoaded = false\n void trustedOrigin // referenced for clarity: the auth iframe URL comes from it\n },\n }\n}\n\n/** Map the node `/session` user into the client payload shape. */\nfunction normalizeHostUserPayload(\n user: { id?: string | number; token?: string; nickname?: string; expiresAt?: number } | null,\n): AuthTokenPayload | null {\n if (!user) return null\n const token = typeof user.token === 'string' && user.token.length > 0 ? user.token : null\n const id = typeof user.id === 'string' && user.id.length > 0\n ? user.id\n : typeof user.id === 'number' && Number.isFinite(user.id) ? String(user.id)\n : null\n if (!token || !id) return null\n return {\n id,\n token,\n ...(user.nickname !== undefined && typeof user.nickname === 'string' ? { nickname: user.nickname } : {}),\n ...(user.expiresAt !== undefined && typeof user.expiresAt === 'number' ? { expiresAt: user.expiresAt } : {}),\n }\n}\n","/**\n * Module-level auth state store shared by the client React components.\n *\n * The DSH slot system injects React components with props, not the cordis ctx,\n * so the components read login state through this tiny external store\n * (`useSyncExternalStore`), fed by the singleton `huaqiuAuth` client service\n * created in `apply()`. When the user logs in (in the sidebar overlay or an\n * embedded card iframe), `onAuthStateChanged` fires and every mounted card /\n * sidebar button re-renders.\n */\nimport type { AuthClient } from './client.js'\n\nexport interface AuthState {\n authenticated: boolean\n nickname?: string\n /** Avatar URL for the sidebar trigger; absent → the HQ icon is shown. */\n avatar?: string\n /**\n * Access token, needed by「Go to profile」(eda.cn takes it from the query\n * and hides it itself). Kept in the store, never rendered or logged.\n */\n token?: string\n /** Bound mobile number, forwarded to the profile page as `phone=`. */\n phone?: string\n /**\n * Rich eda.cn profile fetched in host mode (host session carries only\n * {token, userId}): nickname + headimage, merged above the host session.\n */\n profile?: { nickname?: string; headimage?: string }\n}\n\n/** Snapshot for one credential payload (`null` = logged out). */\nfunction stateOf(info: AuthTokenPayloadLike | null): AuthState {\n if (!info) return { authenticated: false }\n return {\n authenticated: true,\n ...(info.nickname ? { nickname: info.nickname } : {}),\n ...(info.avatar ? { avatar: info.avatar } : {}),\n ...(info.token ? { token: info.token } : {}),\n ...(info.phone ? { phone: info.phone } : {}),\n }\n}\n\ntype AuthTokenPayloadLike = { nickname?: string; avatar?: string; token?: string; phone?: string } | null\n\nlet auth: AuthClient['auth'] | null = null\nlet state: AuthState = { authenticated: false }\nconst listeners = new Set<() => void>()\nlet unsubscribe: (() => void) | null = null\nlet syncNow: (() => void) | null = null\n\nfunction setState(next: AuthState): void {\n state = next\n for (const l of listeners) l()\n}\n\n/** Attach the singleton auth capability and push the initial snapshot. */\nexport function registerAuth(a: AuthClient['auth']): void {\n auth = a\n unsubscribe = a.onAuthStateChanged((info) => {\n setState(stateOf(info))\n })\n void a.getUserInfo()\n .then((info) => setState(stateOf(info)))\n .catch(() => setState({ authenticated: false }))\n}\n\n/** The live auth capability (for login()/logout() from components). */\nexport function getAuth(): AuthClient['auth'] | null {\n return auth\n}\n\n/** Current snapshot, for `useSyncExternalStore`'s getSnapshot. */\nexport function getAuthState(): AuthState {\n return state\n}\n\n/** Subscribe, for `useSyncExternalStore`'s subscribe. */\nexport function subscribeAuth(callback: () => void): () => void {\n listeners.add(callback)\n return () => listeners.delete(callback)\n}\n\n/**\n * Merge the rich eda.cn profile (nickname + headimage) into the store —\n * sidebar components call this after `auth.getUserProfile()` in host mode.\n * The trigger then renders the real name/photo instead of the bare HQ icon.\n */\nexport function setProfile(profile: { nickname?: string; headimage?: string } | null | undefined): void {\n setState({ ...state, profile: profile ?? undefined })\n}\n\n/** Current profile (nickname + headimage) resolved from the host, if any. */\nexport function getProfile(): { nickname?: string; headimage?: string } | undefined {\n return state.profile\n}\n\n/** Register the node re-sync hook (wired in apply(); called by the login card on mount). */\nexport function registerAuthSync(fn: () => void): void {\n syncNow = fn\n}\n\n/**\n * Lazy host-identity getter (edge-bridge browser half), registered by\n * `apply()` via `ctx.get('hqEdge')`. The sidebar needs it to decide whether\n * the host already exposes its own login surface: hq-eda does (native login\n * button + status badge), so the sidebar trigger is hidden there; other hosts\n * (kicad, generic) have no login entrypoint of their own, so the trigger\n * stays. Registered lazily so plugin load order never matters.\n */\nexport interface HqEdgeContextLike {\n context?: {\n getTargetHost?(): string\n }\n}\nlet hqEdgeGetter: (() => HqEdgeContextLike | undefined) | null = null\nexport function registerHqEdgeGetter(get: () => HqEdgeContextLike | undefined): void {\n hqEdgeGetter = get\n}\nexport function getHqEdge(): HqEdgeContextLike | undefined {\n return hqEdgeGetter ? hqEdgeGetter() : undefined\n}\n\n/** Re-push the persisted credential to the node half, if one is available. */\nexport function syncAuthNow(): void {\n syncNow?.()\n}\n\nexport function disposeAuth(): void {\n unsubscribe?.()\n unsubscribe = null\n auth = null\n syncNow = null\n hqEdgeGetter = null\n listeners.clear()\n state = { authenticated: false }\n}\n","/**\n * Keyed `tool.call.toolview` renderer for the Huaqiu EDA tools.\n *\n * When a Huaqiu tool returns `status: \"needs_auth\"`, this card renders the\n * login human-in-the-loop step: an embedded auth.eda.cn login iframe plus a\n * live login-state line. The singleton auth client's `message` listener\n * already receives the postMessage from this same-origin iframe, caches the\n * credential and pushes it to the node service, so after the user logs in the\n * card flips to「已登录」and the model can retry the tool.\n *\n * The embed is the second of the two FULL login surfaces (the other is the\n * sidebar overlay): it uses the same `buildLoginUrl()` contract — always\n * transparent — and passes the host's language and color scheme.\n *\n * For any other result it renders a faithful JSON fallback (the generic row\n * that this keyed entry replaces), so nothing is lost for successful calls.\n */\nimport { memo, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'\nimport { getAuthState, subscribeAuth, syncAuthNow } from '../auth-state.js'\nimport { buildLoginUrl } from '../lib.js'\nimport { useIsDark, useLocale } from '../ui-env.js'\nimport { useT } from '../i18n.js'\nimport {\n cardPalette,\n cardStyle,\n iframeStyle,\n isNeedsAuthResult,\n parseToolResult,\n StatusLine,\n TITLE_STYLE,\n type ToolBlockLike,\n} from './common.jsx'\n\nexport interface NeedsAuthToolViewProps {\n toolName: string\n block?: ToolBlockLike\n}\n\nconst DESC_STYLE = {\n fontSize: 13,\n margin: '0 0 10px',\n lineHeight: 1.5,\n} as const\n\nfunction JsonFallback({ toolName, block }: { toolName: string; block?: ToolBlockLike }): React.JSX.Element {\n const result = useMemo(() => parseToolResult(block), [block])\n const dark = useIsDark()\n const t = useT()\n const palette = cardPalette(dark)\n return (\n <div style={cardStyle(palette)}>\n <p style={TITLE_STYLE}>{t('card.tool', { tool: toolName })}</p>\n <pre style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 320, overflow: 'auto' }}>\n {result ? JSON.stringify(result, null, 2) : t('card.empty')}\n </pre>\n </div>\n )\n}\n\nfunction LoginCard({ toolName }: { toolName: string }): React.JSX.Element {\n const authState = useSyncExternalStore(subscribeAuth, getAuthState)\n const iframeRef = useRef<HTMLIFrameElement | null>(null)\n const dark = useIsDark()\n const locale = useLocale()\n const t = useT()\n const palette = cardPalette(dark)\n // Healing: if the browser already holds a token (e.g. the node half was\n // reset by a server restart), push it again the moment the login card\n // mounts so the tool gate flips to authenticated without a manual re-login.\n useEffect(() => {\n syncAuthNow()\n }, [toolName])\n\n // Toolview is the second of the two FULL login surfaces (the other is the\n // sidebar-triggered login dialog). Unlike the dialog — which sits inside a\n // host-painted card with its own visual edge and therefore wants the embed\n // in TRANSPARENT card mode — the toolview card IS the surface: the iframe\n // fills it edge-to-edge. So we pass `fill: 'full'` and let the embed paint\n // its own `bg-background` (dark in dark theme, light in light theme). This\n // eliminates the white gaps that the transparent mode's 20px grid strips\n // would otherwise leave above and below the form.\n const src = useMemo(\n () => buildLoginUrl({ fill: 'full', lang: locale, theme: dark ? 'dark' : 'light' }),\n [locale, dark],\n )\n // Force a full iframe remount when theme/locale flips. Chrome's\n // `iframe.src =` update keeps the old embed loaded and ignores the new\n // `fill`/`theme` params (the embed is a single Next.js page that reads\n // params once on mount); only a remount picks them up.\n const remountKey = `${locale}|${dark ? 'd' : 'l'}`\n\n return (\n <div style={cardStyle(palette)}>\n <p style={TITLE_STYLE}>{t('card.title')}</p>\n <p style={{ ...DESC_STYLE, color: palette.muted }}>\n {t('card.desc', { tool: toolName })}\n </p>\n <StatusLine authenticated={authState.authenticated} nickname={authState.nickname} palette={palette} t={t} />\n <iframe\n key={remountKey}\n ref={iframeRef}\n src={src}\n title={t('card.title')}\n style={iframeStyle(palette)}\n allow=\"clipboard-write\"\n />\n </div>\n )\n}\n\nexport const HuaqiuToolView = memo(function HuaqiuToolView(props: NeedsAuthToolViewProps): React.JSX.Element {\n const { toolName, block } = props\n const result = useMemo(() => parseToolResult(block), [block])\n if (isNeedsAuthResult(result)) {\n return <LoginCard toolName={toolName} />\n }\n return <JsonFallback toolName={toolName} block={block} />\n})\n","/**\n * The Huaqiu (华秋) mark, used as the sidebar auth trigger's DEFAULT icon\n * (mirrors `HQ_ICON` in `hq-eda-ai/apps/web/src/components/ui/icons.tsx`).\n *\n * Plain inline SVG: the DSH client bundle ships as a classic script with no\n * Tailwind, so the Next.js wrapper (div + utility classes) is dropped and the\n * 40×40 viewBox paths are kept verbatim.\n */\nexport interface HqIconProps {\n size?: number\n /** Brand blue by default; the paths are monochrome so one fill covers all. */\n color?: string\n title?: string\n}\n\nexport function HQ_ICON({ size = 24, color = '#1a81c4', title }: HqIconProps): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 40 40\"\n width={size}\n height={size}\n role={title ? 'img' : undefined}\n aria-hidden={title ? undefined : true}\n focusable=\"false\"\n style={{ display: 'block', flex: '0 0 auto' }}\n >\n {title ? <title>{title}</title> : null}\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M29.71,30a2.75,2.75,0,1,0,2.75,2.74A2.74,2.74,0,0,0,29.71,30Z\"\n />\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M26.59,10.49H13.41a5.93,5.93,0,0,0-5.91,5.9V29.58a5.93,5.93,0,0,0,5.91,5.91H26.85a4,4,0,0,1-1.13-2.78,4.43,4.43,0,0,1,.1-.9H13.41a2.23,2.23,0,0,1-2.22-2.22V16.39a2.23,2.23,0,0,1,2.22-2.21H26.59a2.23,2.23,0,0,1,2.22,2.21V28.81a4.43,4.43,0,0,1,.9-.1,4,4,0,0,1,2.78,1.13,2.26,2.26,0,0,0,0-.26V16.39A5.93,5.93,0,0,0,26.59,10.49Z\"\n />\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M26.38,27.52V18.46a1.85,1.85,0,0,0-1.85-1.85h0a1.84,1.84,0,0,0-1.84,1.85v2.68H17.31V18.46a1.84,1.84,0,0,0-1.84-1.85h0a1.85,1.85,0,0,0-1.85,1.85v9.06a1.85,1.85,0,0,0,1.85,1.85h0a1.84,1.84,0,0,0,1.84-1.85V24.83h5.38v2.69a1.84,1.84,0,0,0,1.84,1.85h0A1.85,1.85,0,0,0,26.38,27.52Z\"\n />\n <circle fill={color} cx=\"20\" cy=\"5.04\" r=\"2.86\" />\n <rect fill={color} x=\"19\" y=\"5.04\" width=\"2\" height=\"6.7\" />\n <path fill={color} d=\"M6.37,17.71a4.89,4.89,0,0,0,0,9.78Z\" />\n <path fill={color} d=\"M33.63,17.71a4.89,4.89,0,1,1,0,9.78Z\" />\n </svg>\n )\n}\n","/**\n * `sidebar.footer.action` entry: the Huaqiu EDA account trigger at the bottom\n * of the DSH sidebar (beside Settings).\n *\n * - Not logged in: shows the HQ icon and opens the login dialog through\n * `auth.login({ lang, theme })` — a real modal (backdrop + centered card +\n * auth.eda.cn iframe) that is ALWAYS TRANSPARENT in the embed itself\n * (`fill=full` is never sent, see `lib.ts#buildLoginUrl`), and the card\n * surface masks Blink's white base canvas so the login card floats over\n * the dimmed app in both light and dark themes. `lang`/`theme` follow the\n * host UI. Click on the backdrop, the × button, or Escape closes it, and\n * auth.eda.cn's own `close_dialog` postMessage closes it as well.\n * - Logged in: the trigger becomes the user's AVATAR (`headimage` from the\n * auth.eda.cn payload, HQ icon while it is missing/fails to load) and a click\n * opens a context menu with「Go to profile」(the eda.cn account page, with\n * the access token) and「Log out」— the same shape as `hq-eda-ai`'s\n * `UserMenu`, portalled to `document.body` with fixed positioning so the\n * sidebar's `overflow: hidden` can never clip it.\n *\n * THEMING: colors prefer DSH's `--dsw-alias-*` tokens (so a custom host theme\n * is honored) and fall back to an explicit light/dark pair chosen from\n * `useIsDark()`; the two paths cannot disagree, because ui-layout's presenter\n * writes `body[data-ds-dark-theme]` from the very snapshot that installs those\n * tokens.\n */\nimport { memo, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type CSSProperties } from 'react'\nimport { createPortal } from 'react-dom'\nimport { getAuth, getAuthState, getHqEdge, setProfile, subscribeAuth } from '../auth-state.js'\nimport { buildProfileUrl } from '../lib.js'\nimport { useIsDark, useLocale } from '../ui-env.js'\nimport { useT } from '../i18n.js'\nimport { HQ_ICON } from './hq-icon.jsx'\n\nexport interface SidebarFooterActionOwnerProps {\n wide?: boolean\n}\n\nconst AVATAR_SIZE = 26\nconst ICON_SIZE = 22\n\n/** One color scheme's menu colors (DSH token first, explicit fallback second). */\ninterface Palette {\n surface: string\n border: string\n text: string\n muted: string\n hover: string\n danger: string\n dangerHover: string\n avatarBg: string\n shadow: string\n}\n\nconst LIGHT_PALETTE: Palette = {\n surface: 'var(--dsw-alias-bg-overlay, #ffffff)',\n border: 'var(--dsw-alias-border-l1, #e4e7ec)',\n text: 'var(--dsw-alias-label-primary, #3a4356)',\n muted: 'var(--dsw-alias-label-secondary, #8a94a6)',\n hover: 'var(--dsw-alias-interactive-bg-hover, #f5f7fa)',\n danger: 'var(--dsw-alias-state-error-primary, #d4380d)',\n dangerHover: 'rgba(216, 56, 13, 0.08)',\n avatarBg: 'var(--dsw-alias-bg-layer-2, #eef2f7)',\n shadow: '0 12px 32px rgba(15, 23, 42, 0.16)',\n}\n\nconst DARK_PALETTE: Palette = {\n surface: 'var(--dsw-alias-bg-overlay, #20242c)',\n border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.14))',\n text: 'var(--dsw-alias-label-primary, #e6eaf0)',\n muted: 'var(--dsw-alias-label-secondary, #8b95a5)',\n hover: 'var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))',\n danger: 'var(--dsw-alias-state-error-primary, #ff7875)',\n dangerHover: 'rgba(255, 120, 117, 0.14)',\n avatarBg: 'var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.10))',\n shadow: '0 12px 32px rgba(0, 0, 0, 0.46)',\n}\n\nconst TRIGGER_BASE: CSSProperties = {\n width: '100%',\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n padding: '8px 12px',\n border: 'none',\n borderRadius: 8,\n background: 'transparent',\n fontSize: 13,\n fontWeight: 500,\n cursor: 'pointer',\n textAlign: 'left',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n}\n\nconst MENU_BASE: CSSProperties = {\n position: 'fixed',\n zIndex: 2147483000,\n minWidth: 184,\n padding: 6,\n borderWidth: 1,\n borderStyle: 'solid',\n borderRadius: 12,\n fontFamily: 'inherit',\n fontSize: 13,\n}\n\nconst MENU_HEADER_BASE: CSSProperties = {\n padding: '6px 10px 8px',\n fontSize: 12,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n}\n\nconst MENU_ITEM_BASE: CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n width: '100%',\n padding: '8px 10px',\n border: 'none',\n borderRadius: 8,\n background: 'transparent',\n font: 'inherit',\n fontSize: 13,\n textAlign: 'left',\n cursor: 'pointer',\n}\n\n/**\n * One menu row. Hover is tracked in state: the client bundle ships no CSS\n * file, so inline styles cannot express `:hover`.\n */\nfunction MenuItem({\n label,\n icon,\n danger,\n palette,\n onSelect,\n}: {\n label: string\n icon: React.JSX.Element\n danger?: boolean\n palette: Palette\n onSelect: () => void\n}): React.JSX.Element {\n const [hovered, setHovered] = useState(false)\n return (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={{\n ...MENU_ITEM_BASE,\n color: danger ? palette.danger : palette.text,\n background: hovered ? (danger ? palette.dangerHover : palette.hover) : 'transparent',\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n onClick={onSelect}\n >\n {icon}\n <span>{label}</span>\n </button>\n )\n}\n\nfunction UserIcon(): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={15}\n height={15}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n style={{ flex: '0 0 auto' }}\n >\n <path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\" />\n <circle cx=\"12\" cy=\"7\" r=\"4\" />\n </svg>\n )\n}\n\nfunction LogoutIcon(): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={15}\n height={15}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n style={{ flex: '0 0 auto' }}\n >\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\nexport const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ wide }: SidebarFooterActionOwnerProps): React.JSX.Element | null {\n const authState = useSyncExternalStore(subscribeAuth, getAuthState)\n const auth = getAuth()\n const dark = useIsDark()\n const locale = useLocale()\n const t = useT()\n const [menuOpen, setMenuOpen] = useState(false)\n const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null)\n const [avatarBroken, setAvatarBroken] = useState(false)\n const [hovered, setHovered] = useState(false)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n\n const palette = dark ? DARK_PALETTE : LIGHT_PALETTE\n const authenticated = authState.authenticated\n const showLabel = wide !== false\n\n // Hide the trigger only when an HQ Edge host that ALREADY exposes its own\n // login surface is present: hq-eda has a native login button + status badge,\n // so this entrypoint would be redundant there. Every other configuration\n // (standalone DSH, kicad/generic hosts) has no login entrypoint of its own,\n // so the sidebar trigger is the only way in and must render.\n const hostMode = auth?.isHostMode?.() ?? false\n const targetHost = hostMode ? (getHqEdge()?.context?.getTargetHost?.() ?? '') : ''\n if (hostMode && targetHost === 'hq-eda') return null\n\n // Host sessions (kicad/generic) carry only {token, userId} — no nickname,\n // no avatar. Fetch the rich eda.cn profile (name + photo) so the trigger\n // renders the real account instead of a bare HQ icon. Standalone DSH already\n // has both from the auth.eda.cn payload, so this is host-only.\n const profile = authState.profile\n useEffect(() => {\n if (!hostMode || targetHost === 'hq-eda') return\n if (!authenticated) {\n setProfile(null)\n return\n }\n let cancelled = false\n auth?.getUserProfile?.()\n .then((info) => {\n if (cancelled) return\n setProfile(info)\n })\n .catch(() => { /* UI degrades to the HQ icon */ })\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hostMode, targetHost, authenticated])\n\n const displayName = profile?.nickname ?? authState.nickname\n const displayAvatar = profile?.headimage ?? authState.avatar\n const avatar = authenticated && !avatarBroken ? displayAvatar : undefined\n\n // A new avatar URL is a fresh chance to render it.\n useEffect(() => {\n setAvatarBroken(false)\n }, [displayAvatar])\n\n // Logging out (from anywhere: menu, another tab surface, node invalidation)\n // must never leave an orphan menu pointing at a signed-out account.\n useEffect(() => {\n if (!authenticated) setMenuOpen(false)\n }, [authenticated])\n\n // Anchor the portalled menu to the trigger before paint: the sidebar footer\n // sits at the bottom edge, so the menu grows UPWARD from the trigger's top.\n useLayoutEffect(() => {\n if (!menuOpen || !triggerRef.current) return\n const rect = triggerRef.current.getBoundingClientRect()\n setMenuStyle({\n ...MENU_BASE,\n background: palette.surface,\n borderColor: palette.border,\n color: palette.text,\n boxShadow: palette.shadow,\n left: Math.max(8, Math.round(rect.left)),\n bottom: Math.max(8, Math.round(window.innerHeight - rect.top + 8)),\n ...(wide ? { width: Math.round(rect.width) } : {}),\n })\n }, [menuOpen, wide, avatar, palette])\n\n // Close on: outside click, Escape, resize or scroll (the anchor moved).\n useEffect(() => {\n if (!menuOpen) return\n const onPointerDown = (event: MouseEvent): void => {\n const target = event.target as Node\n if (triggerRef.current?.contains(target)) return\n if (menuRef.current?.contains(target)) return\n setMenuOpen(false)\n }\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') setMenuOpen(false)\n }\n const dismiss = (): void => setMenuOpen(false)\n document.addEventListener('mousedown', onPointerDown)\n document.addEventListener('keydown', onKeyDown)\n window.addEventListener('resize', dismiss)\n window.addEventListener('scroll', dismiss, true)\n return () => {\n document.removeEventListener('mousedown', onPointerDown)\n document.removeEventListener('keydown', onKeyDown)\n window.removeEventListener('resize', dismiss)\n window.removeEventListener('scroll', dismiss, true)\n }\n }, [menuOpen])\n\n if (!auth) return null\n\n /**\n *「Go to profile」always carries the token, so eda.cn can establish the\n * session in the opened tab (it hides the token itself — see\n * `lib.ts#buildProfileUrl`). The snapshot normally has it; fall back to the\n * client so a stale snapshot can never open an unauthenticated tab.\n */\n const openProfile = (): void => {\n setMenuOpen(false)\n void (async () => {\n const info = authState.token\n ? { token: authState.token, phone: authState.phone }\n : await auth.getUserInfo()\n .then((i) => (i ? { token: i.token, phone: i.phone } : null))\n .catch(() => null)\n if (!info?.token) return\n window.open(buildProfileUrl(info), '_blank', 'noopener,noreferrer')\n })()\n }\n\n const label = authenticated\n ? (displayName ?? t('sidebar.account'))\n : t('sidebar.login')\n\n const title = authenticated ? t('sidebar.accountTitle') : t('sidebar.loginTitle')\n const triggerBackground = menuOpen || hovered ? palette.hover : 'transparent'\n\n return (\n <div style={{ position: 'relative', width: '100%' }}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n onClick={() => {\n if (!authenticated) {\n // Always-transparent embed in the host's language and color scheme;\n // `closeOnOutsideClick` defaults to true. The login dialog owns\n // the DOM (backdrop + card + iframe) and closes itself on\n // backdrop click, Escape, the × button, or the embed's\n // `close_dialog` postMessage.\n void auth.login({ lang: locale, theme: dark ? 'dark' : 'light' })\n return\n }\n setMenuOpen((open) => !open)\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n style={{\n ...TRIGGER_BASE,\n color: palette.text,\n padding: wide ? '8px 12px' : '8px 6px',\n background: triggerBackground,\n }}\n title={title}\n >\n {avatar ? (\n <span\n style={{\n flex: '0 0 auto',\n width: AVATAR_SIZE,\n height: AVATAR_SIZE,\n borderRadius: '50%',\n overflow: 'hidden',\n background: palette.avatarBg,\n display: 'block',\n }}\n >\n <img\n src={avatar}\n alt=\"\"\n width={AVATAR_SIZE}\n height={AVATAR_SIZE}\n onError={() => setAvatarBroken(true)}\n style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}\n />\n </span>\n ) : (\n <HQ_ICON size={ICON_SIZE} />\n )}\n {showLabel ? (\n <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>\n ) : null}\n </button>\n\n {menuOpen && menuStyle\n ? createPortal(\n <div ref={menuRef} role=\"menu\" style={menuStyle}>\n {displayName ? (\n <div style={{ ...MENU_HEADER_BASE, color: palette.muted }} title={displayName}>{displayName}</div>\n ) : null}\n <MenuItem\n label={t('menu.profile')}\n icon={<UserIcon />}\n palette={palette}\n onSelect={openProfile}\n />\n <MenuItem\n label={t('menu.logout')}\n icon={<LogoutIcon />}\n danger\n palette={palette}\n onSelect={() => {\n setMenuOpen(false)\n void auth.logout()\n }}\n />\n </div>,\n document.body,\n )\n : null}\n </div>\n )\n})\n","/**\n * `@huaqiu/dsh-auth` — browser half (the Phase 0A POC).\n *\n * Opens the auth.eda.cn login page in an overlay iframe, STRICTLY validates\n * the postMessage origin, caches credentials in localStorage (reload restore),\n * and pushes them to the node half over the plugin-owned webServer routes.\n * Provides the client-side `huaqiuAuth` service mirroring the node surface.\n *\n * On top of the credential flow it wires the two UI surfaces the login UX\n * needs:\n * - `sidebar.footer.action` — a persistent 华秋EDA login entrypoint at the\n * bottom of the sidebar (login/logout, live state).\n * - `tool.call.toolview` (keyed per Huaqiu tool) — when a node tool returns\n * `status: \"needs_auth\"` the tool card becomes the login HIT: an embedded\n * auth.eda.cn iframe + login-state line, so login is a step of the\n * conversation instead of a dead error the agent has to relay.\n */\nimport { createAuthStorage } from './storage.js'\nimport { createWebServerAuthTransport } from './transport.js'\nimport { createAuthClient, type AuthClient } from './client.js'\nimport { disposeAuth, registerAuth, registerAuthSync, registerHqEdgeGetter } from './auth-state.js'\nimport { HuaqiuToolView } from './ui/needs-auth-toolview.jsx'\nimport { HuaqiuAuthSidebarAction } from './ui/sidebar-action.jsx'\nimport { disposeUiEnv } from './ui-env.js'\n\n/**\n * Client cordis inject: REAL service names only (the loader maps these to\n * `ctx.inject([...])` dependencies). The `slots` registry service comes from\n * `@deepseek-ai/dsh-client-ui-slots`; it is required to register the toolview\n * and sidebar entries. The PACKAGE-level `dsh.client.inject` in package.json\n * (graph ordering) stays as-is and is NOT this export.\n */\nexport const inject: string[] = ['slots']\n\n/**\n * Huaqiu tools that still surface the auth login card via this plugin.\n *\n * Currently EMPTY: all five Huaqiu tools now own their keyed HIT cards in\n * their own plugins (`@huaqiu/dsh-tool-symbol-footprint` for the three\n * symbol/footprint generators, `@huaqiu/dsh-tool-schematic-gen` for the two\n * schematic/system generators), and each renders its own inline login card for\n * `needs_auth`. Keeping the toolview keys here would double-register the same\n * `tool.call.toolview` slot with an ambiguous winner.\n *\n * The auth plugin remains the credential owner: the `huaqiuAuth` client\n * service, the sidebar login entrypoint and the webServer credential channel.\n */\nexport const AUTH_TOOL_NAMES: readonly string[] = []\n\n/** Minimal structural client context (dsh-client-runtime provides this). */\nexport interface ClientContext {\n provide?(name: string, value: unknown): () => void\n get?<T>(name: string): T | undefined\n slots?: {\n inject(key: string, callback: () => () => void): () => void\n register(spec: { name: string; key?: string; id?: string }, component: unknown): unknown\n }\n}\n\nexport function apply(ctx: ClientContext): () => void {\n const client: AuthClient = createAuthClient({\n storage: createAuthStorage(localStorage),\n transport: createWebServerAuthTransport(),\n windowLike: window,\n documentLike: document,\n })\n\n // The sidebar needs the host identity (targetHost) to decide whether the\n // host already has its own login surface (hq-eda hides the trigger; kicad /\n // generic keep it). Lazy so edge-bridge load order never matters.\n registerHqEdgeGetter(() => ctx.get?.('hqEdge'))\n\n const disposers: Array<() => void> = []\n let disposed = false\n const disposeProvide = ctx.provide?.('huaqiuAuth', { auth: client.auth })\n registerAuth(client.auth)\n registerAuthSync(() => { void client.syncNow() })\n void client.restore()\n disposers.push(client.auth.onAuthStateChanged((info) => {\n void client.syncNow()\n }))\n\n // Healing: the node half keeps auth in memory, so a server restart drops it\n // while the browser still holds the token. Re-sync whenever the tab regains\n // focus/visibility so the tool gate flips back to authenticated without a\n // reload.\n const sync = (): void => { void client.syncNow() }\n window.addEventListener('focus', sync)\n document.addEventListener('visibilitychange', sync)\n disposers.push(() => {\n window.removeEventListener('focus', sync)\n document.removeEventListener('visibilitychange', sync)\n })\n\n /**\n * In HQ Edge host mode (config.hqEdgeBaseUrl set on the node half), EDA\n * launches hq-edge WITH the operator credential, so hq-edge — not this\n * plugin — owns authentication for the session. `refreshHost()` resolves\n * that host mode and adopts the host-owned session as the browser\n * credential, so the auth gate / HIT cards read authenticated immediately.\n *\n * The sidebar entrypoint is registered UNCONDITIONALLY and decides its own\n * visibility: it hides itself only when the host is hq-eda (which has a\n * native login button + status badge). Standalone DSH and other hosts\n * (kicad, generic — no login surface of their own) keep the trigger. The\n * mode is read from the node half over the plugin-owned webServer route\n * (async), so the component re-renders when the host session lands\n * (auth-state emits on adopt).\n */\n const slots = ctx.slots\n void client.refreshHost().catch(() => undefined).then(() => {\n if (disposed) return\n if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function') {\n for (const toolName of AUTH_TOOL_NAMES) {\n disposers.push(slots.inject('tool.call.toolview', () => slots.register({ name: 'tool.call.toolview', key: toolName }, HuaqiuToolView) as () => void))\n }\n disposers.push(slots.inject('sidebar.footer.action', () => slots.register({ name: 'sidebar.footer.action', id: 'huaqiu-auth' }, HuaqiuAuthSidebarAction) as () => void))\n }\n })\n\n return () => {\n disposed = true\n for (const dispose of disposers) {\n try {\n dispose()\n } catch {\n /* already disposed */\n }\n }\n disposeProvide?.()\n client.dispose()\n disposeAuth()\n disposeUiEnv()\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["listeners","useSyncExternalStore","useMemo","unsubscribe","useMemo","useSyncExternalStore","useRef","memo","useState","memo","useSyncExternalStore","useRef","createPortal"],"sources":["../src/client/storage.ts","../src/client/transport.ts","../src/client/lib.ts","../src/client/ui/common.tsx","../src/client/ui-env.ts","../src/client/i18n.ts","../src/client/ui/login-dialog.ts","../src/client/client.ts","../src/client/auth-state.ts","../src/client/ui/needs-auth-toolview.tsx","../src/client/ui/hq-icon.tsx","../src/client/ui/sidebar-action.tsx","../src/client/index.tsx"],"sourcesContent":["/**\n * localStorage-backed credential cache (client side). Survives reload, which\n * is what makes the fingerprint silent-login restore (acceptance group D) work.\n */\nimport type { AuthTokenPayload } from './lib.js'\n\nexport const DEFAULT_STORAGE_KEY = 'huaqiu.dsh.auth'\n\nexport interface AuthStorage {\n get(): AuthTokenPayload | null\n set(info: AuthTokenPayload): void\n clear(): void\n}\n\nexport function createAuthStorage(\n storage: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>,\n key: string = DEFAULT_STORAGE_KEY,\n): AuthStorage {\n return {\n get() {\n const raw = storage.getItem(key)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as AuthTokenPayload\n if (!parsed || typeof parsed.token !== 'string' || typeof parsed.id !== 'string') return null\n // Parity with auth.eda.cn's 5-day token window.\n if (parsed.expiresAt !== undefined && parsed.expiresAt * 1000 <= Date.now()) {\n storage.removeItem(key)\n return null\n }\n return parsed\n } catch {\n return null\n }\n },\n set(info) {\n storage.setItem(key, JSON.stringify(info))\n },\n clear() {\n storage.removeItem(key)\n },\n }\n}\n","/**\n * Browser→node credential transport over the plugin-owned webServer routes\n * (same-origin; no CORS, no external dependency). This is the chosen Phase 0A\n * browser→host channel — `apiProxy`'s dispatch table is closed, so a\n * plugin-owned `webServer` route is the smallest supported extension point.\n */\nimport type { AuthTokenPayload } from './lib.js'\n\nexport interface AuthTransport {\n pushSession(info: AuthTokenPayload): Promise<void>\n pushLogout(): Promise<void>\n /**\n * Whether the plugin runs under an HQ Edge host. In host mode hq-edge already\n * holds the operator credential (EDA hands it over on launch), so the\n * browser half's own login UI (sidebar entrypoint) is suppressed.\n */\n fetchHostMode(): Promise<boolean>\n /**\n * Fetch the node half's authoritative session. The node resolves host-mode\n * credentials first (host session → pushed browser session → persisted\n * file), so in host mode this is how the browser half learns the host-owned\n * credential without ever opening the auth.eda.cn iframe.\n */\n fetchSession(): Promise<{ authenticated: boolean; user: HostSessionUser | null }>\n /**\n * Ask the node half to trigger the EDA login dialog through the host.\n * Standalone (no host) must NOT call this — login is the browser iframe\n * there. In host mode the node blocks until the dialog is completed (the\n * host route waits for AuthStateChanged), then the caller re-fetches the\n * session to pick up the fresh credential.\n */\n triggerLogin(): Promise<void>\n /**\n * Fetch the rich eda.cn profile (nickname + headimage) for the current\n * token. The node half resolves the token (host → pushed → persisted) and\n * calls eda.cn; used by the sidebar in host mode when the host session\n * carries only {token, userId} — e.g. host mode under kicad.\n */\n fetchUserInfo(): Promise<EdaUserProfile | null>\n}\n\n/** Minimal session user shape returned by the node `/session` route. */\nexport interface HostSessionUser {\n id?: string | number\n token?: string\n nickname?: string\n expiresAt?: number\n}\n\n/** Rich eda.cn profile served by the node `/user-info` route. */\nexport interface EdaUserProfile {\n nickname?: string\n headimage?: string\n phone?: string\n username?: string\n}\n\nexport function createWebServerAuthTransport(\n base: string = '/api/v1/huaqiu/auth',\n doFetch: typeof fetch = globalThis.fetch.bind(globalThis),\n): AuthTransport {\n return {\n async pushSession(info) {\n const res = await doFetch(`${base}/session`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n token: info.token,\n userId: info.id,\n ...(info.nickname !== undefined ? { nickname: info.nickname } : {}),\n ...(info.expiresAt !== undefined ? { expiresAt: info.expiresAt } : {}),\n }),\n })\n if (!res.ok) throw new Error(`auth push failed: HTTP ${res.status}`)\n },\n async pushLogout() {\n const res = await doFetch(`${base}/logout`, { method: 'POST' })\n if (!res.ok) throw new Error(`auth logout push failed: HTTP ${res.status}`)\n },\n async fetchHostMode() {\n try {\n const res = await doFetch(`${base}/config`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return false\n const body = await res.json() as { hostMode?: unknown }\n return body.hostMode === true\n } catch {\n // Offline/same-origin failure: fall back to standalone (show the login\n // entrypoint) rather than hiding it — a login UI is never a security\n // regression, but a missing one in standalone would lock the user out.\n return false\n }\n },\n async fetchSession() {\n const res = await doFetch(`${base}/session`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return { authenticated: false, user: null }\n const body = await res.json() as { authenticated?: unknown; user?: unknown }\n const user = body.user && typeof body.user === 'object' ? body.user as HostSessionUser : null\n return { authenticated: body.authenticated === true, user }\n },\n async triggerLogin() {\n const res = await doFetch(`${base}/login`, {\n method: 'POST',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) throw new Error(`auth login trigger failed: HTTP ${res.status}`)\n },\n async fetchUserInfo() {\n try {\n const res = await doFetch(`${base}/user-info`, {\n method: 'GET',\n headers: { accept: 'application/json' },\n })\n if (!res.ok) return null\n const body = await res.json() as { userInfo?: EdaUserProfile | null }\n return body.userInfo ?? null\n } catch {\n return null\n }\n },\n }\n}\n","/**\n * Pure browser message parsing for auth.eda.cn postMessage envelopes.\n *\n * auth.eda.cn posts `JSON.stringify({ category: 1, data: { type, data } })`\n * to the parent window with `targetOrigin: '*'`.\n *\n * SECURITY NOTE (offline deployment): the DSH harness runs fully offline /\n * local (127.0.0.1), so there is no public attack surface of a malicious\n * website posting a forged token at us. The origin gate is therefore dropped\n * by design — webviews may even report an opaque origin (\"null\") for the\n * embedded auth.eda.cn iframe, which would otherwise reject legitimate login\n * messages. What remains is the ENVELOPE validation in `parseAuthMessage`\n * (category 1 + well-formed token/userId), which keeps unrelated window\n * messages from ever corrupting the credential cache.\n *\n * Envelope types (see `/Users/admin/code/eda-cn-login/lib/kicadTools.ts`):\n * { category: 1, data: { type: 'update_access_token', data: { userId, token, expires_at, ... } } }\n * { category: 1, data: { type: 'logout', data: null } }\n * { category: 1, data: { type: 'close_dialog', data: null } }\n */\n\nexport const AUTH_ORIGIN = 'https://auth.eda.cn'\n\n/**「Go to profile」destination: the eda.cn account page. */\nexport const PROFILE_URL = 'https://www.eda.cn/account/profile'\n\n/**\n * Build the「Go to profile」URL: the eda.cn account page WITH the access token\n * in the query, mirroring `hq-eda-ai`'s `UserMenu`\n * (`/account/profile?token=…&phone=…`).\n *\n * The token is always attached: eda.cn consumes it to establish the session\n * and strips it from the address bar / history itself, so there is nothing to\n * leak beyond the target site. `encodeURIComponent` is required (not cosmetic):\n * tokens are base64-ish and may contain `+`, `/` or `=`, and a raw `+` in a\n * query string decodes to a space, which would corrupt the credential.\n */\nexport function buildProfileUrl(options: { token: string; phone?: string | number }): string {\n const phone = options.phone === undefined || options.phone === null ? '' : String(options.phone)\n return `${PROFILE_URL}?token=${encodeURIComponent(options.token)}&phone=${encodeURIComponent(phone)}`\n}\n\n/**\n * Contract version of the auth.eda.cn embed, shared with the web app\n * (`hq-eda-ai` LoginDialog) so both send the same cache-busting `v=`.\n */\nexport const AUTH_IFRAME_VERSION = '20260409'\n\n/** UI language of the auth.eda.cn embed. */\nexport type AuthLocale = 'zh' | 'en'\n\n/** Color scheme of the auth.eda.cn embed (its own vocabulary: light | dark). */\nexport type AuthTheme = 'light' | 'dark'\n\n/**\n * auth.eda.cn's own language ids, keyed by our locale id.\n *\n * The embed reads `?locale=`, NOT `lang`: `eda-cn-login/app/layout.tsx` reads\n * `urlParams.get('locale')` and `components/ui/LanguageContext.tsx`\n * (`getLangFromUrl`) only accepts the ids in `locales/index.ts` — `cn` and\n * `en` (`zh` / `zh_CN` are aliased to `cn` there, but we send the canonical\n * id outright).\n *\n * NOTE: `hq-eda-ai`'s `LoginDialog.tsx` sends `lang=zh`, which the embed\n * IGNORES, so its login card always falls back to whatever the browser asks\n * for. We send `locale` (what is actually read) and keep `lang` alongside it\n * for parity with the web app and forward compatibility.\n */\nexport const AUTH_LOCALE_ID: Record<AuthLocale, string> = { zh: 'cn', en: 'en' }\n\n/**\n * Options for the auth.eda.cn overlay iframe opened by `auth.login()`.\n *\n * The embed has TWO rendering modes, and which one is right depends on the\n * surface that hosts the iframe:\n *\n * - **Transparent card mode** (default, no `fill`): the embedded page sets\n * `html[data-iframe-mode=\"true\"]` and the root paints\n * `background: transparent` (see `eda-cn-login/app/page.tsx` — the wrapper\n * only gets the `bg-transparent` class when `fill !== 'full'`). The host\n * then paints a card around the iframe (e.g. the login dialog's backdrop\n * + centered card) so Blink's white `BaseBackgroundColor()` canvas never\n * shows. This is the right mode when the iframe sits inside a host-painted\n * card with its own visual edge — e.g. the sidebar-triggered login dialog.\n *\n * - **Fill mode** (`fill: 'full'`): the embed's `DialogContent` becomes\n * `w-full h-full max-w-none max-h-none left-0 top-0 rounded-none border-none`\n * (see `eda-cn-login/components/LoginDialog.tsx` — `fillFull` branch at\n * line 61) and the wrapper drops `bg-transparent` so the page paints its\n * own `bg-background` edge-to-edge. This is the right mode when the iframe\n * fills its host container (e.g. the toolview card) and there is no\n * surrounding card to mask the embed's rounded corners or transparent\n * 20px grid strips.\n *\n * The `lang` and `theme` params follow the host UI in both modes.\n */\nexport interface LoginOptions {\n /** Ask auth.eda.cn to self-close on an outside click (default `true`). */\n closeOnOutsideClick?: boolean\n /** Embed UI language (default `zh`). */\n lang?: AuthLocale\n /** Embed color scheme (default `light`). */\n theme?: AuthTheme\n /**\n * Set to `'full'` to make the embed fill its iframe viewport edge-to-edge\n * (no rounded corners, no transparent grid strips, embed paints its own\n * `bg-background`). Omit for the transparent card mode described above.\n * `true` is accepted as a shorthand for `'full'`.\n */\n fill?: 'full' | 'transparent' | true\n}\n\nexport interface AuthTokenPayload {\n id: string\n token: string\n nickname?: string\n /** User avatar URL (`headimage` in the auth.eda.cn payload). */\n avatar?: string\n /** Bound mobile number; forwarded to the eda.cn profile page as `phone=`. */\n phone?: string\n /** unix seconds; undefined = no expiry */\n expiresAt?: number\n}\n\n/**\n * Build the auth.eda.cn embed URL.\n *\n * The URL switches between two rendering modes based on `options.fill`:\n * - `fill: 'full'` (or `true`) → `fill=full` is sent; the embed's\n * `DialogContent` becomes `w-full h-full … rounded-none` and the wrapper\n * drops `bg-transparent`, so the embed fills the iframe viewport with\n * its own `bg-background`. Use this when the iframe is the surface (e.g.\n * the toolview card).\n * - any other value (including unset) → no `fill` is sent; the embed stays\n * in transparent card mode. The host is responsible for painting a card\n * around the iframe so Blink's white base canvas never reaches the user.\n */\nexport function buildLoginUrl(options: LoginOptions & { baseUrl?: string } = {}): string {\n const url = new URL(options.baseUrl ?? `${AUTH_ORIGIN}/`)\n url.searchParams.set('v', AUTH_IFRAME_VERSION)\n if (options.closeOnOutsideClick !== false) url.searchParams.set('clickOutsideToClose', 'true')\n if (options.fill === 'full' || options.fill === true) url.searchParams.set('fill', 'full')\n url.searchParams.set('transparent', 'true')\n const lang = options.lang ?? 'zh'\n // `locale` is the param auth.eda.cn reads; `lang` keeps parity with\n // hq-eda-ai's LoginDialog (see AUTH_LOCALE_ID).\n url.searchParams.set('locale', AUTH_LOCALE_ID[lang])\n url.searchParams.set('lang', lang)\n url.searchParams.set('theme', options.theme ?? 'light')\n return url.toString()\n}\n\nexport type ParsedAuthMessage =\n | { kind: 'token'; info: AuthTokenPayload }\n | { kind: 'logout' }\n | { kind: 'close' }\n\n/** Structural event (origin + data) so tests don't need a real MessageEvent. */\nexport interface AuthMessageEventLike {\n origin: string\n data: unknown\n /** The posting window; retained for completeness (origin is not gated). */\n source?: unknown\n}\n\ninterface RawEnvelope {\n category?: unknown\n data?: { type?: unknown; data?: unknown }\n}\n\n/** Coerce an id field (string or number, as auth.eda.cn sends) to a string. */\nfunction stringifyId(value: unknown): string | null {\n if (typeof value === 'string' && value.length > 0) return value\n if (typeof value === 'number' && Number.isFinite(value)) return String(value)\n return null\n}\n\nexport function parseAuthMessage(raw: unknown): ParsedAuthMessage | null {\n let envelope: RawEnvelope | null = null\n if (typeof raw === 'string') {\n try {\n envelope = JSON.parse(raw) as RawEnvelope\n } catch {\n return null\n }\n } else if (raw !== null && typeof raw === 'object') {\n envelope = raw as RawEnvelope\n }\n if (!envelope || envelope.category !== 1) return null\n const data = envelope.data\n if (!data || typeof data !== 'object') return null\n\n switch (data.type) {\n case 'update_access_token': {\n const d = data.data\n if (!d || typeof d !== 'object') return null\n const record = d as Record<string, unknown>\n const token = typeof record.token === 'string' && record.token.length > 0 ? record.token : null\n // auth.eda.cn sends userId/id as NUMBERS (e.g. 6215935) — coerce to string.\n const id = stringifyId(record.userId) ?? stringifyId(record.id)\n if (!token || !id) return null\n const nickname = typeof record.nickname === 'string' && record.nickname.length > 0 ? record.nickname : undefined\n // auth.eda.cn sends the avatar as `headimage`; `avatar` accepted as alias.\n const avatar = typeof record.headimage === 'string' && record.headimage.length > 0\n ? record.headimage\n : typeof record.avatar === 'string' && record.avatar.length > 0 ? record.avatar : undefined\n // Phone may arrive as a string or a number (mirrors `stringifyId`).\n const phone = stringifyId(record.phone) ?? undefined\n const expiresAt = typeof record.expires_at === 'number' ? record.expires_at : undefined\n return {\n kind: 'token',\n info: {\n id,\n token,\n ...(nickname !== undefined ? { nickname } : {}),\n ...(avatar !== undefined ? { avatar } : {}),\n ...(phone !== undefined ? { phone } : {}),\n ...(expiresAt !== undefined ? { expiresAt } : {}),\n },\n }\n }\n case 'logout':\n return { kind: 'logout' }\n case 'close_dialog':\n return { kind: 'close' }\n default:\n return null\n }\n}\n\n/** Origin-agnostic envelope parsing. The ONLY entry point for window message events. */\nexport function handleAuthMessage(event: AuthMessageEventLike): ParsedAuthMessage | null {\n return parseAuthMessage(event.data)\n}\n","/**\n * Login-state + result rendering helpers shared by the client React cards.\n *\n * Every style is a FUNCTION of the active color scheme: the cards are inline\n * styled (the client bundle ships no CSS file), so light/dark support has to\n * be expressed in JS. Colors prefer DSH's `--dsw-alias-*` tokens and fall back\n * to an explicit per-scheme value (see `sidebar-action.tsx`).\n */\nimport type { CSSProperties, ReactNode } from 'react'\nimport type { Translate } from '../i18n.js'\n\n/** Tool result content block (subset of DSH `ContentBlock`). */\ninterface ContentBlockLike {\n type?: string\n text?: string\n}\n\n/** Structural tool-call block subset (we only read settled text content). */\nexport interface ToolBlockLike {\n content?: readonly ContentBlockLike[]\n}\n\n/** Best-effort JSON.parse of the tool's text output blocks. */\nexport function parseToolResult(block: ToolBlockLike | undefined): Record<string, unknown> | null {\n if (!block || !Array.isArray(block.content)) return null\n const text = block.content\n .filter((c): c is ContentBlockLike => !!c && c.type === 'text' && typeof c.text === 'string')\n .map((c) => c.text as string)\n .join('')\n if (!text) return null\n try {\n const parsed = JSON.parse(text) as unknown\n return parsed && typeof parsed === 'object' ? (parsed as Record<string, unknown>) : null\n } catch {\n return null\n }\n}\n\n/** True when the parsed result is the auth-gate signal. */\nexport function isNeedsAuthResult(result: Record<string, unknown> | null): result is Record<string, unknown> & { status: 'needs_auth' } {\n return !!result && result.status === 'needs_auth'\n}\n\nexport const AUTH_ORIGIN = 'https://auth.eda.cn'\n\n/** Card colors for one color scheme (DSH token first, explicit fallback second). */\nexport interface CardPalette {\n surface: string\n border: string\n text: string\n muted: string\n success: string\n danger: string\n}\n\nexport const LIGHT_CARD_PALETTE: CardPalette = {\n surface: 'var(--dsw-alias-bg-layer-1, #ffffff)',\n border: 'var(--dsw-alias-border-l1, #e4e7ec)',\n text: 'var(--dsw-alias-label-primary, inherit)',\n muted: 'var(--dsw-alias-label-secondary, #5b6472)',\n success: 'var(--dsw-alias-state-success-primary, #1677ff)',\n danger: 'var(--dsw-alias-state-error-primary, #d4380d)',\n}\n\nexport const DARK_CARD_PALETTE: CardPalette = {\n surface: 'var(--dsw-alias-bg-layer-1, #20242c)',\n border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.14))',\n text: 'var(--dsw-alias-label-primary, #e6eaf0)',\n muted: 'var(--dsw-alias-label-secondary, #8b95a5)',\n success: 'var(--dsw-alias-state-success-primary, #4cc38a)',\n danger: 'var(--dsw-alias-state-error-primary, #ff7875)',\n}\n\nexport function cardPalette(dark: boolean): CardPalette {\n return dark ? DARK_CARD_PALETTE : LIGHT_CARD_PALETTE\n}\n\nexport function cardStyle(palette: CardPalette): CSSProperties {\n return {\n border: `1px solid ${palette.border}`,\n borderRadius: 10,\n padding: '12px 14px',\n margin: '4px 0',\n background: palette.surface,\n color: palette.text,\n fontFamily: 'inherit',\n }\n}\n\nexport const TITLE_STYLE: CSSProperties = {\n fontSize: 14,\n fontWeight: 600,\n margin: '0 0 6px',\n}\n\nexport const STATUS_STYLE: CSSProperties = {\n fontSize: 13,\n margin: '0 0 10px',\n lineHeight: 1.5,\n}\n\n/**\n * Iframe height for both the dialog and the toolview card. Tuned to the\n * auth.eda.cn login form's actual painted height (≈390px at 768px width,\n * measured with a magenta iframe element background so the embedded doc's\n * transparent top/bottom strips are obvious). The dialog and toolview both\n * use this same number for consistency.\n *\n * Note: the auth.eda.cn page wrapper is `grid-rows-[20px_1fr_20px]`, so the\n * embedded doc always leaves two 20px transparent strips above and below the\n * form — they are NOT additional empty space we can shave off; they are\n * always there in the embed's own layout. The 30px buffer above the 390px\n * content (→ 440) gives the form room to grow slightly on error states\n * without immediately overflowing.\n */\nexport const LOGIN_IFRAME_HEIGHT = 440\n\n/**\n * The embedded login iframe: painted with the same surface as the wrapping\n * card so the login box blends in both schemes.\n *\n * Why not `background: transparent`? Blink's `BaseBackgroundColor()` falls\n * back to WHITE whenever the embedded doc's root element has a transparent\n * background (and auth.eda.cn's `data-iframe-mode` page is exactly that).\n * That white canvas shows through wherever the document doesn't paint, which\n * reads as a glaring white \"frame\" around the login card in dark mode. Light\n * mode hid the bug because the white canvas happened to match the light\n * host. Painting the iframe ELEMENT with the card's surface (DSH alias\n * `--dsw-alias-bg-layer-1` with a per-scheme fallback) puts a dark sheet in\n * dark mode and a light sheet in light mode, so the login card sits on a\n * surface that blends with the host in both schemes.\n */\nexport function iframeStyle(palette: CardPalette): CSSProperties {\n return {\n width: '100%',\n height: LOGIN_IFRAME_HEIGHT,\n border: `1px solid ${palette.border}`,\n borderRadius: 8,\n background: palette.surface,\n display: 'block',\n }\n}\n\nexport function StatusLine({\n authenticated,\n nickname,\n palette,\n t,\n}: {\n authenticated: boolean\n nickname?: string\n palette: CardPalette\n t: Translate\n}): ReactNode {\n if (authenticated) {\n return (\n <p style={{ ...STATUS_STYLE, color: palette.success }}>\n {t('card.loggedIn', {\n nickname: nickname ? t('card.nicknameSep', { nickname }) : '',\n })}\n </p>\n )\n }\n return (\n <p style={{ ...STATUS_STYLE, color: palette.danger }}>\n {t('card.loggedOut')}\n </p>\n )\n}\n","/**\n * Host theme + locale sensing for the client UI.\n *\n * The DSH slot system injects React components with PROPS, not the cordis ctx,\n * so the cards cannot reach `ctx.theme` / `ctx.locale` the way a plugin body\n * can. Both services do, however, publish their state into the DOM, and that\n * is what this module reads:\n *\n * - THEME — `ui-layout`'s presenter switches `body[data-ds-dark-theme]` from\n * the resolved snapshot (`packages/client/ui-layout/src/client/theme-presenter.ts`,\n * `DARK_ATTRIBUTE`), so the attribute's presence IS the dark palette. Same\n * signal the sibling packages already use\n * (`dsh-tool-schematic-gen/src/client/theme.ts`). `prefers-color-scheme` is\n * deliberately NOT consulted: DSH resolves `system` itself, and an OS-dark /\n * DSH-light combination would then be misdetected.\n * - LOCALE — `dsh-client-locale` writes `<html lang>` on every locale change\n * (`syncDocumentLanguage`: `zh-CN` | `en`). Falling back to the browser's\n * own `navigator.languages` keeps the UI usable on hosts without that\n * plugin. Chinese is the last resort because this is a Chinese-first app\n * (and `hq-eda-ai` defaults to zh: `languageMap[lang] || \"zh\"`).\n *\n * Both are exposed as `useSyncExternalStore` snapshots so every mounted card\n * re-renders together when the user flips theme or language.\n */\nimport { useSyncExternalStore } from 'react'\nimport type { AuthLocale, AuthTheme } from './lib.js'\n\n/** DSH's dark-palette marker, written by ui-layout's theme presenter. */\nexport const DARK_ATTRIBUTE = 'data-ds-dark-theme'\n\nfunction isDarkDocument(): boolean {\n if (typeof document === 'undefined') return false\n if (document.body?.hasAttribute(DARK_ATTRIBUTE)) return true\n // Fallbacks for hosts that mark the scheme on <html> instead of <body>.\n const root = document.documentElement\n if (!root) return false\n const dataTheme = root.getAttribute('data-theme')\n if (dataTheme !== null) return dataTheme.toLowerCase() === 'dark'\n return root.classList.contains('dark')\n}\n\n/** `zh-CN`, `zh-Hans`, `en-GB`, … → our locale id (`undefined` = unknown). */\nfunction localeFromTag(tag: string | null | undefined): AuthLocale | undefined {\n if (!tag) return undefined\n const primary = tag.toLowerCase().split('-')[0]\n return primary === 'zh' || primary === 'en' ? primary : undefined\n}\n\nfunction detectLocale(): AuthLocale {\n if (typeof document !== 'undefined') {\n const fromDocument = localeFromTag(document.documentElement?.getAttribute('lang'))\n if (fromDocument) return fromDocument\n }\n if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {\n // `window` is the browser test: Node exposes a global `navigator`\n // reporting the machine's own language, which would otherwise decide the\n // locale for non-browser runs (same guard DSH's locale plugin uses).\n for (const tag of [...(navigator.languages ?? []), navigator.language]) {\n const match = localeFromTag(tag)\n if (match) return match\n }\n }\n return 'zh'\n}\n\nlet dark = isDarkDocument()\nlet locale = detectLocale()\nconst listeners = new Set<() => void>()\nlet darkObserver: MutationObserver | null = null\nlet localeObserver: MutationObserver | null = null\n\nfunction notify(): void {\n for (const listener of [...listeners]) {\n try {\n listener()\n } catch {\n /* one crashing subscriber must not strand the rest on a stale value */\n }\n }\n}\n\n/** Re-read the DOM and notify only what actually changed. */\nexport function syncUiEnv(): void {\n let changed = false\n const nextDark = isDarkDocument()\n if (nextDark !== dark) {\n dark = nextDark\n changed = true\n }\n const nextLocale = detectLocale()\n if (nextLocale !== locale) {\n locale = nextLocale\n changed = true\n }\n if (changed) notify()\n}\n\n/** Start observing (idempotent; also re-reads so no change is missed). */\nfunction watch(): void {\n if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return\n if (!darkObserver && document.body) {\n darkObserver = new MutationObserver(syncUiEnv)\n darkObserver.observe(document.body, { attributes: true, attributeFilter: [DARK_ATTRIBUTE] })\n }\n if (!localeObserver && document.documentElement) {\n localeObserver = new MutationObserver(syncUiEnv)\n localeObserver.observe(document.documentElement, {\n attributes: true,\n attributeFilter: ['lang', 'data-theme', 'class'],\n })\n }\n syncUiEnv()\n}\n\nfunction subscribe(callback: () => void): () => void {\n watch()\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n}\n\n/**\n * Imperative subscription for non-React consumers (e.g. the login dialog's\n * backdrop/card DOM). The callback fires on every theme or locale flip.\n */\nexport function subscribeUiEnv(callback: () => void): () => void {\n return subscribe(callback)\n}\n\nconst getDark = (): boolean => dark\nconst getLocale = (): AuthLocale => locale\n\n/**\n * Synchronous read of the current dark-palette state. Safe outside React\n * (the auth client uses it when appending the overlay iframe, before any\n * component has a chance to subscribe).\n */\nexport function getCurrentDark(): boolean {\n return dark\n}\n\n/** Synchronous read of the current host UI locale. */\nexport function getCurrentLocale(): AuthLocale {\n return locale\n}\n\n/** Synchronous read of the current host surface color (matches the palette). */\nexport function getCurrentSurfaceColor(): string {\n return dark ? 'var(--dsw-alias-bg-layer-1, #20242c)' : 'var(--dsw-alias-bg-layer-1, #ffffff)'\n}\n\n/** `true` while the host renders the dark palette. */\nexport function useIsDark(): boolean {\n return useSyncExternalStore(subscribe, getDark, getDark)\n}\n\n/** The host UI language. */\nexport function useLocale(): AuthLocale {\n return useSyncExternalStore(subscribe, getLocale, getLocale)\n}\n\n/** The host color scheme in auth.eda.cn's own vocabulary. */\nexport function useColorScheme(): AuthTheme {\n return useIsDark() ? 'dark' : 'light'\n}\n\n/** Release the observers (called from `apply()`'s disposer). */\nexport function disposeUiEnv(): void {\n darkObserver?.disconnect()\n localeObserver?.disconnect()\n darkObserver = null\n localeObserver = null\n listeners.clear()\n}\n","/**\n * zh / en copy for every user-visible string of the auth UI (sidebar trigger,\n * account menu, login tool card).\n *\n * Kept self-contained rather than registered into DSH's `ctx.locale`\n * namespace — same call the sibling packages made\n * (`dsh-tool-symbol-footprint/src/client/i18n.ts`) — because the slot system\n * hands components props, not ctx, and a missing namespace would leave the UI\n * blank. `en` is typed as `Record<AuthCopyKey, string>`, so a key added to one\n * language without the other is a COMPILE error (bilingual balance enforced at\n * build time, mirroring DSH's own locale registry).\n *\n * The en「Go to profile」/「Log out」wording is the one the sidebar spec asks\n * for; the zh side follows `hq-eda-ai`'s `locales/cn.ts` (个人中心 / 退出登录).\n */\nimport { useMemo } from 'react'\nimport type { AuthLocale } from './lib.js'\nimport { useLocale } from './ui-env.js'\n\nconst zh = {\n 'sidebar.login': '华秋EDA AI登录',\n 'sidebar.loginTitle': '登录华秋 EDA AI(eda.cn)账号',\n 'sidebar.accountTitle': '华秋 EDA AI 账号',\n 'sidebar.account': '华秋EDA AI · 已登录',\n\n 'menu.profile': '个人中心',\n 'menu.logout': '退出登录',\n\n 'card.title': '华秋 EDA AI(eda.cn)登录',\n 'card.desc': '工具「{tool}」需要登录华秋 EDA AI 账号才能继续。请在下方的登录框完成登录(或点击左侧「华秋EDA AI登录」按钮);登录完成后,回复助手「已登录,请重试」,助手会自动重新调用该工具。',\n 'card.loggedIn': '✓ 已登录{nickname} —— 现在可以回复助手「已登录,请重试」,助手会重新调用工具。',\n 'card.loggedOut': '未登录 —— 请在上方登录华秋 EDA AI(eda.cn)账号,或点击左侧「华秋EDA AI登录」按钮;登录完成后让助手重试。',\n 'card.tool': '工具:{tool}',\n 'card.empty': '(无输出)',\n // Substituted into `{nickname}` by `card.loggedIn`. zh uses a full-width\n // colon, en a half-width one plus a space; hardcoding ':' made the English\n // card read \"Logged in:John\".\n 'card.nicknameSep': ':{nickname}',\n\n 'dialog.close': '关闭',\n} as const\n\n/** Every key of the zh dictionary — the contract both languages satisfy. */\nexport type AuthCopyKey = keyof typeof zh\n\nconst en: Record<AuthCopyKey, string> = {\n 'sidebar.login': 'Huaqiu EDA AI login',\n 'sidebar.loginTitle': 'Sign in to your Huaqiu EDA AI (eda.cn) account',\n 'sidebar.accountTitle': 'Huaqiu EDA AI account',\n 'sidebar.account': 'Huaqiu EDA AI · signed in',\n\n 'menu.profile': 'Go to profile',\n 'menu.logout': 'Log out',\n\n 'card.title': 'Huaqiu EDA AI (eda.cn) login',\n // The reply phrase used to be hardcoded to the Chinese \"已登录,请重试\" even\n // in these English strings, telling an English-speaking user to type Chinese.\n 'card.desc': 'Tool \"{tool}\" needs a Huaqiu EDA AI account. Complete the login below (or use the Huaqiu EDA AI button in the sidebar), then reply \"I have logged in, please retry\" so the assistant can retry the tool.',\n 'card.loggedIn': '✓ Logged in{nickname} — reply \"I have logged in, please retry\" and the assistant will retry the tool.',\n 'card.loggedOut': 'Not logged in — sign in above, or use the Huaqiu EDA AI button in the sidebar, then ask the assistant to retry.',\n 'card.tool': 'Tool: {tool}',\n 'card.empty': '(no output)',\n 'card.nicknameSep': ': {nickname}',\n\n 'dialog.close': 'Close',\n}\n\nconst COPY: Record<AuthLocale, Record<AuthCopyKey, string>> = { zh, en }\n\n/** Every copy key, in declaration order (used to assert bilingual balance). */\nexport const AUTH_COPY_KEYS = Object.keys(zh) as AuthCopyKey[]\n\nexport type Translate = (key: AuthCopyKey, params?: Record<string, unknown>) => string\n\n/**\n * Look a key up, interpolating `{name}` placeholders.\n *\n * Chain: active locale → zh (the source of truth) → the key itself, so a\n * missing translation stays VISIBLE instead of blanking the UI.\n */\nexport function translate(locale: AuthLocale, key: AuthCopyKey, params?: Record<string, unknown>): string {\n const template = COPY[locale]?.[key] ?? COPY.zh[key] ?? key\n if (!params) return template\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n name in params ? String(params[name]) : match)\n}\n\n/** Translate bound to one locale (stable for the lifetime of that locale). */\nexport function createT(locale: AuthLocale): Translate {\n return (key, params) => translate(locale, key, params)\n}\n\n/** Translate bound to the host UI language, re-created when it changes. */\nexport function useT(): Translate {\n const locale = useLocale()\n return useMemo(() => createT(locale), [locale])\n}\n","/**\n * The sidebar login dialog — a real modal (backdrop + centered card + iframe).\n *\n * WHY A DIALOG AND NOT A FULL-VIEWPORT IFRAME\n *\n * The embed (`auth.eda.cn`) reads only two URL params: `fill` and\n * `clickOutsideToClose`. With `fill !== 'full'` it sets\n * `data-iframe-mode=\"true\"` on `<html>` and the CSS rule\n * `html[data-iframe-mode=true], html[data-iframe-mode=true] body { background: 0 0 !important }`\n * makes its root transparent — but the page wrapper still uses a\n * `grid-rows-[20px_1fr_20px]` layout, so the 20px strips above/below the\n * card are empty and show through to whatever is behind the iframe. Behind\n * the iframe element, with no background set, Blink falls back to a WHITE\n * base background canvas. That white is what was reading as a \"white frame\n * around the login card in dark mode\" (and was invisibly there in light\n * mode, blending with the white host).\n *\n * The two ways out:\n * 1. Paint the iframe element with a color → in a full-viewport iframe\n * that blanks the whole app with that color (light surface = white\n * blocks the light host; dark surface = dark blocks the dark host).\n * 2. Make the iframe CARD-SIZED and put it inside a host-painted card,\n * so the iframe's background can be `transparent` and the card's\n * surface shows through wherever the embedded doc is transparent.\n * This is the pattern the toolview card already uses\n * (`needs-auth-toolview.tsx`) and the pattern `hq-eda-ai`'s\n * `LoginDialog.tsx` uses.\n *\n * We use (2): a fixed full-viewport backdrop (semi-transparent black) +\n * centered card (host surface bg) + the iframe (transparent inner doc,\n * card surface as element bg so Blink's white canvas never reaches the\n * user). Click on the backdrop, the × button, Escape, or the auth embed's\n * own `close_dialog` postMessage closes the dialog.\n */\nimport { LOGIN_IFRAME_HEIGHT } from './common.js'\nimport { buildLoginUrl, type AuthLocale, type AuthTheme } from '../lib.js'\nimport { translate } from '../i18n.js'\nimport { getCurrentLocale, getCurrentSurfaceColor, subscribeUiEnv, syncUiEnv } from '../ui-env.js'\n\n/** Aria / data attribute names — stable so tests and CSS can target them. */\nexport const DIALOG_OVERLAY_ATTR = 'data-hq-auth-dialog'\nexport const DIALOG_CARD_ATTR = 'data-hq-auth-dialog-card'\nexport const DIALOG_IFRAME_ATTR = 'data-hq-auth-dialog-iframe'\nexport const DIALOG_CLOSE_ATTR = 'data-hq-auth-dialog-close'\n\n/**\n * Iframe height. Tuned to the auth.eda.cn login form's actual painted height\n * (≈390px at 768px width, measured with a magenta iframe element background\n * so the embedded doc's transparent 20px top/bottom strips are obvious). The\n * shared constant lives in `./common.jsx` so the dialog and the toolview\n * card stay in lock-step.\n */\nconst IFRAME_HEIGHT = LOGIN_IFRAME_HEIGHT\nconst CARD_MAX_WIDTH = 768\n\nlet container: HTMLDivElement | null = null\nlet unsubscribe: (() => void) | null = null\nlet onCloseRequested: (() => void) | null = null\n\n/**\n * Open the login dialog. Idempotent: a second call while open is a no-op\n * (mirrors the client-side `if (iframe) return` guard). `onClose` fires\n * whenever the dialog closes for ANY reason (backdrop click, Escape, close\n * button, postMessage, programmatic close) — the auth client uses it to\n * unblock its own `isOpen` state.\n */\nexport function openLoginDialog(options: { lang?: AuthLocale; theme?: AuthTheme } = {}, onClose?: () => void): void {\n if (container) return\n // The ui-env module reads the DOM once at import time. Re-read here so\n // the dialog picks up the current theme even if no React component has\n // subscribed yet (e.g. when the sidebar opens the dialog before any\n // card has mounted), or when a test sets the attribute after import.\n syncUiEnv()\n const locale = options.lang ?? getCurrentLocale()\n\n const root = document.createElement('div')\n root.setAttribute(DIALOG_OVERLAY_ATTR, '')\n // Backdrop: dim the host without blanking it. Theme-agnostic — rgba black\n // works on both light and dark hosts.\n root.style.cssText = [\n 'position:fixed',\n 'inset:0',\n 'width:100vw',\n 'height:100vh',\n 'border:0',\n 'z-index:2147483647',\n 'background:rgba(0, 0, 0, 0.55)',\n 'display:flex',\n 'align-items:center',\n 'justify-content:center',\n 'box-sizing:border-box',\n ].join(';')\n\n const card = document.createElement('div')\n card.setAttribute(DIALOG_CARD_ATTR, '')\n const applyCardColors = (): void => {\n const surface = getCurrentSurfaceColor()\n // No card border: a 1px border on each side would shrink the iframe's\n // viewport to 766px on a 768px card, missing the embed's `md:grid-cols`\n // (Tailwind `md` = 768px) threshold by 2px and silently falling back to\n // a single-column layout. The card's box-shadow already gives the card\n // a clear visual edge against the dimmed host.\n card.style.cssText = [\n `width:min(100vw, ${CARD_MAX_WIDTH}px)`,\n // Card height = iframe height exactly. A taller card would leave a\n // strip of card surface below the form (the flex column has only one\n // child, the iframe, so any extra height piles up at the bottom).\n `height:min(90vh, ${IFRAME_HEIGHT}px)`,\n 'border-radius:12px',\n 'box-shadow:0 24px 48px rgba(0, 0, 0, 0.32)',\n 'position:relative',\n 'box-sizing:border-box',\n `background:${surface}`,\n 'display:flex',\n 'flex-direction:column',\n ].join(';')\n }\n applyCardColors()\n\n const closeButton = document.createElement('button')\n closeButton.setAttribute(DIALOG_CLOSE_ATTR, '')\n closeButton.type = 'button'\n closeButton.setAttribute('aria-label', translate(locale, 'dialog.close'))\n closeButton.title = translate(locale, 'dialog.close')\n closeButton.textContent = '×'\n closeButton.style.cssText = [\n 'position:absolute',\n 'top:6px',\n 'right:10px',\n 'width:28px',\n 'height:28px',\n 'border:0',\n 'background:transparent',\n 'color:var(--dsw-alias-label-secondary, #5b6472)',\n 'font-size:22px',\n 'line-height:1',\n 'cursor:pointer',\n 'border-radius:6px',\n 'padding:0',\n ].join(';')\n closeButton.addEventListener('click', closeLoginDialog)\n\n const iframe = document.createElement('iframe')\n iframe.setAttribute(DIALOG_IFRAME_ATTR, '')\n iframe.src = buildLoginUrl({ lang: options.lang, theme: options.theme })\n iframe.title = translate(locale, 'card.title')\n iframe.allow = 'clipboard-write'\n // The iframe element background = the card surface. That is what masks\n // Blink's white base canvas in the embedded doc's transparent strips\n // (see file header). The embedded doc itself is transparent because\n // buildLoginUrl never sends `fill=full`.\n iframe.style.cssText = [\n 'width:100%',\n `height:${IFRAME_HEIGHT}px`,\n 'border:0',\n 'border-radius:8px',\n `background:${getCurrentSurfaceColor()}`,\n 'display:block',\n 'flex:0 0 auto',\n ].join(';')\n\n card.appendChild(closeButton)\n card.appendChild(iframe)\n root.appendChild(card)\n document.body.appendChild(root)\n\n // Backdrop click: close ONLY when the click lands on the backdrop itself,\n // not on the card. The card stops propagation in its own click handler.\n root.addEventListener('mousedown', backdropMouseDown)\n card.addEventListener('mousedown', stopPropagation)\n document.addEventListener('keydown', onKeyDown)\n\n // React to theme/locale flips so the card surface + iframe background\n // track the host (a mid-session switch while the dialog is open would\n // otherwise leave a stale-colored card).\n unsubscribe = subscribeUiEnv(() => {\n if (!container) return\n applyCardColors()\n iframe.style.background = getCurrentSurfaceColor()\n closeButton.title = translate(getCurrentLocale(), 'dialog.close')\n closeButton.setAttribute('aria-label', closeButton.title)\n })\n\n container = root\n onCloseRequested = onClose ?? null\n}\n\n/** Programmatic close (used by the auth client after a successful login). */\nexport function closeLoginDialog(): void {\n if (!container) return\n container.remove()\n container = null\n document.removeEventListener('keydown', onKeyDown)\n unsubscribe?.()\n unsubscribe = null\n const cb = onCloseRequested\n onCloseRequested = null\n cb?.()\n}\n\n/** True while the dialog is mounted. */\nexport function isLoginDialogOpen(): boolean {\n return container !== null\n}\n\nfunction backdropMouseDown(event: MouseEvent): void {\n if (event.target === container) closeLoginDialog()\n}\n\nfunction stopPropagation(event: MouseEvent): void {\n event.stopPropagation()\n}\n\nfunction onKeyDown(event: KeyboardEvent): void {\n if (event.key === 'Escape') {\n event.stopPropagation()\n closeLoginDialog()\n }\n}\n","/**\n * Auth client core — the Phase 0A POC logic, factored as a testable factory.\n * `apply()` in index.ts wires this to the real window/document/localStorage.\n */\nimport {\n AUTH_ORIGIN,\n buildLoginUrl,\n handleAuthMessage,\n type AuthMessageEventLike,\n type AuthTokenPayload,\n type LoginOptions,\n} from './lib.js'\nimport { closeLoginDialog, isLoginDialogOpen, openLoginDialog } from './ui/login-dialog.js'\nimport type { AuthStorage } from './storage.js'\nimport type { AuthTransport } from './transport.js'\n\nexport interface AuthClientDeps {\n storage: AuthStorage\n transport: AuthTransport\n /** Strict origin gate; defaults to auth.eda.cn. */\n trustedOrigin?: string\n loginUrl?: string\n windowLike: Pick<Window, 'addEventListener' | 'removeEventListener'>\n documentLike: Pick<Document, 'createElement' | 'body'>\n}\n\nexport interface AuthClient {\n auth: {\n isAuthenticated(): boolean\n /** HQ Edge host mode: EDA owns the credential; login triggers the EDA dialog. */\n isHostMode(): boolean\n getAccessToken(): Promise<string | null>\n getUserInfo(): Promise<AuthTokenPayload | null>\n /**\n * Rich eda.cn profile (nickname + headimage) for the current token. The\n * node half resolves the token and calls eda.cn — used by the sidebar in\n * host mode when the host session carries only {token, userId} (e.g.\n * kicad). Null on failure; the UI degrades to the HQ icon.\n */\n getUserProfile(): Promise<{ nickname?: string; headimage?: string } | null>\n login(options?: LoginOptions): Promise<void>\n /**\n * Request logout. In host mode this asks the EDA host (through the node\n * half → hq-edge → TriggerLogout) to log out; it never declares the\n * operator logged out locally. Throws when the request is rejected, so\n * the UI keeps showing the authenticated state.\n */\n logout(): Promise<void>\n onAuthStateChanged(listener: (info: AuthTokenPayload | null) => void): () => void\n }\n /** Route window 'message' events here. Exposed for direct testing. */\n handleMessageEvent(event: AuthMessageEventLike): void\n /** Re-push persisted credentials on boot (acceptance group D). */\n restore(): Promise<void>\n /** Re-push persisted credentials on demand (heals a reset/absent node half). */\n syncNow(): Promise<void>\n /**\n * Resolve host mode from the node half and, when active, adopt the host-owned\n * session as the browser credential. Resolves `hostMode`; the resolved\n * session is emitted through `onAuthStateChanged` so apps and the HIT cards\n * flip to authenticated without ever opening the auth.eda.cn iframe.\n */\n refreshHost(): Promise<boolean>\n /** Browser→node transport (used to read host mode before UI registration). */\n transport: AuthTransport\n dispose(): void\n}\n\nexport function createAuthClient(deps: AuthClientDeps): AuthClient {\n const trustedOrigin = deps.trustedOrigin ?? AUTH_ORIGIN\n const loginUrl = deps.loginUrl ?? `${AUTH_ORIGIN}/`\n const { storage, transport } = deps\n const listeners = new Set<(info: AuthTokenPayload | null) => void>()\n\n // Host-mode credential state. In host mode hq-edge owns the session (EDA\n // hands the operator token over on launch) — the node half resolves it and\n // the browser adopts it through `/session`, so the auth gate, the HIT cards\n // and the sidebar all read authenticated without an iframe login.\n let hostMode = false\n let hostSession: AuthTokenPayload | null = null\n let hostSessionLoaded = false\n\n const emit = (info: AuthTokenPayload | null): void => {\n for (const listener of listeners) listener(info)\n }\n const closeIframe = (): void => {\n // The dialog module owns the DOM. Tear it down on any close path\n // (token success, logout, close_dialog postMessage, dispose).\n if (isLoginDialogOpen()) closeLoginDialog()\n }\n /**\n * Open the login dialog (backdrop + centered card + auth.eda.cn iframe).\n *\n * The dialog is ALWAYS transparent (no `transparent` option exists): the\n * embedded doc sets its own root to `background: transparent` (we never\n * send `fill=full`), and the iframe sits inside a host-painted card so\n * Blink's white base canvas never reaches the user. See the long header\n * in `ui/login-dialog.ts` for the full why.\n */\n const openIframe = (options: LoginOptions = {}): void => {\n if (isLoginDialogOpen()) return\n const baseUrl = loginUrl\n openLoginDialog(\n {\n ...(options.lang ? { lang: options.lang } : {}),\n ...(options.theme ? { theme: options.theme } : {}),\n },\n () => {\n // Re-render safety: nothing to do here — the auth client's own state\n // is just the dialog-open boolean, which `isLoginDialogOpen()` reads\n // directly from the dialog module.\n void baseUrl\n },\n )\n }\n\n /**\n * Adopt the host-owned session as the browser credential (host mode only).\n * Kept synchronous for `isAuthenticated()`'s first-paint fast path — callers\n * that need the resolved payload use `getUserInfo()` / `getAccessToken()`,\n * which await host resolution before returning.\n */\n const resolveHost = async (): Promise<void> => {\n hostSessionLoaded = true\n if (!hostMode) {\n hostSession = null\n return\n }\n try {\n const session = await transport.fetchSession()\n hostSession = session.authenticated ? normalizeHostUserPayload(session.user) : null\n } catch {\n // Host unreachable: stay logged out rather than inventing a session.\n hostSession = null\n }\n }\n\n const auth = {\n // Host mode is STRICT (mirrors the node half): hq-edge is the single\n // source of truth, so a pushed/persisted credential left behind by an\n // earlier standalone login must never report the operator as logged in\n // while the host has no session — that mismatch is exactly how the HIT\n // card reads \"logged in\" while the tool still gates on needs_auth.\n isAuthenticated: (): boolean => (hostMode ? hostSession !== null : storage.get() !== null),\n isHostMode: (): boolean => hostMode,\n getAccessToken: async (): Promise<string | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n return hostMode\n ? (hostSession?.token ?? null)\n : (hostSession?.token ?? storage.get()?.token ?? null)\n },\n getUserInfo: async (): Promise<AuthTokenPayload | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n return hostMode ? hostSession : (hostSession ?? storage.get())\n },\n getUserProfile: async (): Promise<{ nickname?: string; headimage?: string } | null> => {\n if (hostMode && !hostSessionLoaded) await resolveHost()\n if (!(await auth.getAccessToken())) return null\n return transport.fetchUserInfo()\n },\n login: async (options?: LoginOptions): Promise<void> => {\n // Host mode: hq-edge owns the session — ask the host (EDA) to open its\n // own login dialog instead of the auth.eda.cn iframe. The node route\n // blocks until the dialog completes; then re-resolve and emit (only on\n // a real change, so a redundant trigger never re-renders every card).\n if (hostMode) {\n const prevToken = hostSession?.token\n await transport.triggerLogin()\n hostSessionLoaded = false\n await resolveHost()\n if (hostSession?.token !== prevToken) emit(hostSession)\n return\n }\n openIframe(options ?? {})\n },\n logout: async (): Promise<void> => {\n if (hostMode) {\n // Host-owned session: ask the EDA host to log out (node half →\n // hq-edge → AuthService.TriggerLogout). Success only means the\n // request was accepted, so re-resolve the host session and emit the\n // HOST's verdict — never a locally invented \"logged out\". A rejected\n // request throws and leaves `hostSession` (the real state) intact.\n await transport.pushLogout()\n hostSessionLoaded = false\n await resolveHost()\n emit(hostSession)\n return\n }\n storage.clear()\n try {\n await transport.pushLogout()\n } catch {\n /* node may be absent — local state is still cleared */\n }\n emit(null)\n closeIframe()\n },\n onAuthStateChanged: (listener: (info: AuthTokenPayload | null) => void): (() => void) => {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n }\n\n const handleMessageEvent = (event: AuthMessageEventLike): void => {\n // Offline deployment: no origin gate (see lib.ts). Envelope validation is\n // the only gate, so unrelated window messages can never corrupt state.\n const msg = handleAuthMessage(event)\n if (!msg) return\n if (msg.kind === 'token') {\n hostSession = null\n storage.set(msg.info)\n void transport.pushSession(msg.info).catch(() => { /* node push is best-effort; syncNow heals */ })\n emit(msg.info)\n closeIframe()\n } else if (msg.kind === 'logout') {\n hostSession = null\n storage.clear()\n emit(null)\n void transport.pushLogout().catch(() => { /* local state already cleared */ })\n closeIframe()\n } else if (msg.kind === 'close') {\n closeIframe()\n }\n }\n\n const onWindowMessage = (event: MessageEvent): void => {\n handleMessageEvent({ origin: event.origin, data: event.data })\n }\n\n deps.windowLike.addEventListener('message', onWindowMessage)\n\n const restore = async (): Promise<void> => {\n const restored = storage.get()\n if (restored) {\n try {\n await transport.pushSession(restored)\n } catch {\n /* boot push is best-effort; syncNow heals */\n }\n }\n }\n\n return {\n auth,\n handleMessageEvent,\n restore,\n /**\n * Resolve host mode + host session from the node half and emit the result.\n * Called at apply() boot; also re-runnable on demand (e.g. focus) so a\n * late host handover still flips the gate.\n */\n async refreshHost(): Promise<boolean> {\n // Remember what callers currently believe, so we only notify on a real\n // change. This routine is re-run periodically in host mode (there is no\n // push channel into the webview), and re-emitting an identical payload\n // would re-render every mounted card and HIT for nothing.\n const prevToken = hostSession?.token ?? storage.get()?.token ?? null\n let mode = false\n try {\n mode = await transport.fetchHostMode()\n } catch {\n mode = false\n }\n hostMode = mode\n hostSessionLoaded = false\n await resolveHost()\n const next = hostSession ?? storage.get()\n if (next?.token !== prevToken) emit(next)\n return hostMode\n },\n /**\n * Browser→node transport. Exposed so the client entry can read host mode\n * (whether an HQ Edge host supplies the credential and the login UI should\n * be suppressed) before registering the sidebar entrypoint.\n */\n transport,\n /**\n * Re-push the persisted credential to the node half. Healing path: the\n * node keeps auth in memory, so a `dsh web` restart (or a failed first\n * push) drops it while the browser still has the token. Callers re-sync on\n * focus / visibilitychange / login-card mount so the tool gate reflects\n * the actual browser login without requiring a page reload.\n */\n async syncNow(): Promise<void> {\n const info = storage.get()\n if (!info) return\n try {\n await transport.pushSession(info)\n } catch {\n /* sync is best-effort; a later focus event retries */\n }\n },\n dispose() {\n deps.windowLike.removeEventListener('message', onWindowMessage)\n closeIframe()\n hostMode = false\n hostSession = null\n hostSessionLoaded = false\n void trustedOrigin // referenced for clarity: the auth iframe URL comes from it\n },\n }\n}\n\n/** Map the node `/session` user into the client payload shape. */\nfunction normalizeHostUserPayload(\n user: { id?: string | number; token?: string; nickname?: string; expiresAt?: number } | null,\n): AuthTokenPayload | null {\n if (!user) return null\n const token = typeof user.token === 'string' && user.token.length > 0 ? user.token : null\n const id = typeof user.id === 'string' && user.id.length > 0\n ? user.id\n : typeof user.id === 'number' && Number.isFinite(user.id) ? String(user.id)\n : null\n if (!token || !id) return null\n return {\n id,\n token,\n ...(user.nickname !== undefined && typeof user.nickname === 'string' ? { nickname: user.nickname } : {}),\n ...(user.expiresAt !== undefined && typeof user.expiresAt === 'number' ? { expiresAt: user.expiresAt } : {}),\n }\n}\n","/**\n * Module-level auth state store shared by the client React components.\n *\n * The DSH slot system injects React components with props, not the cordis ctx,\n * so the components read login state through this tiny external store\n * (`useSyncExternalStore`), fed by the singleton `huaqiuAuth` client service\n * created in `apply()`. When the user logs in (in the sidebar overlay or an\n * embedded card iframe), `onAuthStateChanged` fires and every mounted card /\n * sidebar button re-renders.\n */\nimport type { AuthClient } from './client.js'\n\nexport interface AuthState {\n authenticated: boolean\n nickname?: string\n /** Avatar URL for the sidebar trigger; absent → the HQ icon is shown. */\n avatar?: string\n /**\n * Access token, needed by「Go to profile」(eda.cn takes it from the query\n * and hides it itself). Kept in the store, never rendered or logged.\n */\n token?: string\n /** Bound mobile number, forwarded to the profile page as `phone=`. */\n phone?: string\n /**\n * Rich eda.cn profile fetched in host mode (host session carries only\n * {token, userId}): nickname + headimage, merged above the host session.\n */\n profile?: { nickname?: string; headimage?: string }\n}\n\n/** Snapshot for one credential payload (`null` = logged out). */\nfunction stateOf(info: AuthTokenPayloadLike | null): AuthState {\n if (!info) return { authenticated: false }\n return {\n authenticated: true,\n ...(info.nickname ? { nickname: info.nickname } : {}),\n ...(info.avatar ? { avatar: info.avatar } : {}),\n ...(info.token ? { token: info.token } : {}),\n ...(info.phone ? { phone: info.phone } : {}),\n }\n}\n\ntype AuthTokenPayloadLike = { nickname?: string; avatar?: string; token?: string; phone?: string } | null\n\nlet auth: AuthClient['auth'] | null = null\nlet state: AuthState = { authenticated: false }\nconst listeners = new Set<() => void>()\nlet unsubscribe: (() => void) | null = null\nlet syncNow: (() => void) | null = null\n\nfunction setState(next: AuthState): void {\n state = next\n for (const l of listeners) l()\n}\n\n/** Attach the singleton auth capability and push the initial snapshot. */\nexport function registerAuth(a: AuthClient['auth']): void {\n auth = a\n unsubscribe = a.onAuthStateChanged((info) => {\n setState(stateOf(info))\n })\n void a.getUserInfo()\n .then((info) => setState(stateOf(info)))\n .catch(() => setState({ authenticated: false }))\n}\n\n/** The live auth capability (for login()/logout() from components). */\nexport function getAuth(): AuthClient['auth'] | null {\n return auth\n}\n\n/** Current snapshot, for `useSyncExternalStore`'s getSnapshot. */\nexport function getAuthState(): AuthState {\n return state\n}\n\n/** Subscribe, for `useSyncExternalStore`'s subscribe. */\nexport function subscribeAuth(callback: () => void): () => void {\n listeners.add(callback)\n return () => listeners.delete(callback)\n}\n\n/**\n * Merge the rich eda.cn profile (nickname + headimage) into the store —\n * sidebar components call this after `auth.getUserProfile()` in host mode.\n * The trigger then renders the real name/photo instead of the bare HQ icon.\n */\nexport function setProfile(profile: { nickname?: string; headimage?: string } | null | undefined): void {\n setState({ ...state, profile: profile ?? undefined })\n}\n\n/** Current profile (nickname + headimage) resolved from the host, if any. */\nexport function getProfile(): { nickname?: string; headimage?: string } | undefined {\n return state.profile\n}\n\n/** Register the node re-sync hook (wired in apply(); called by the login card on mount). */\nexport function registerAuthSync(fn: () => void): void {\n syncNow = fn\n}\n\n/**\n * Lazy host-identity getter (edge-bridge browser half), registered by\n * `apply()` via `ctx.get('hqEdge')`. The sidebar needs it to decide whether\n * the host already exposes its own login surface: hq-eda does (native login\n * button + status badge), so the sidebar trigger is hidden there; other hosts\n * (kicad, generic) have no login entrypoint of their own, so the trigger\n * stays. Registered lazily so plugin load order never matters.\n */\nexport interface HqEdgeContextLike {\n context?: {\n getTargetHost?(): string\n }\n}\nlet hqEdgeGetter: (() => HqEdgeContextLike | undefined) | null = null\nexport function registerHqEdgeGetter(get: () => HqEdgeContextLike | undefined): void {\n hqEdgeGetter = get\n}\nexport function getHqEdge(): HqEdgeContextLike | undefined {\n return hqEdgeGetter ? hqEdgeGetter() : undefined\n}\n\n/** Re-push the persisted credential to the node half, if one is available. */\nexport function syncAuthNow(): void {\n syncNow?.()\n}\n\nexport function disposeAuth(): void {\n unsubscribe?.()\n unsubscribe = null\n auth = null\n syncNow = null\n hqEdgeGetter = null\n listeners.clear()\n state = { authenticated: false }\n}\n","/**\n * Keyed `tool.call.toolview` renderer for the Huaqiu EDA tools.\n *\n * When a Huaqiu tool returns `status: \"needs_auth\"`, this card renders the\n * login human-in-the-loop step: an embedded auth.eda.cn login iframe plus a\n * live login-state line. The singleton auth client's `message` listener\n * already receives the postMessage from this same-origin iframe, caches the\n * credential and pushes it to the node service, so after the user logs in the\n * card flips to「已登录」and the model can retry the tool.\n *\n * The embed is the second of the two FULL login surfaces (the other is the\n * sidebar overlay): it uses the same `buildLoginUrl()` contract — always\n * transparent — and passes the host's language and color scheme.\n *\n * For any other result it renders a faithful JSON fallback (the generic row\n * that this keyed entry replaces), so nothing is lost for successful calls.\n */\nimport { memo, useEffect, useMemo, useRef, useSyncExternalStore } from 'react'\nimport { getAuthState, subscribeAuth, syncAuthNow } from '../auth-state.js'\nimport { buildLoginUrl } from '../lib.js'\nimport { useIsDark, useLocale } from '../ui-env.js'\nimport { useT } from '../i18n.js'\nimport {\n cardPalette,\n cardStyle,\n iframeStyle,\n isNeedsAuthResult,\n parseToolResult,\n StatusLine,\n TITLE_STYLE,\n type ToolBlockLike,\n} from './common.jsx'\n\nexport interface NeedsAuthToolViewProps {\n toolName: string\n block?: ToolBlockLike\n}\n\nconst DESC_STYLE = {\n fontSize: 13,\n margin: '0 0 10px',\n lineHeight: 1.5,\n} as const\n\nfunction JsonFallback({ toolName, block }: { toolName: string; block?: ToolBlockLike }): React.JSX.Element {\n const result = useMemo(() => parseToolResult(block), [block])\n const dark = useIsDark()\n const t = useT()\n const palette = cardPalette(dark)\n return (\n <div style={cardStyle(palette)}>\n <p style={TITLE_STYLE}>{t('card.tool', { tool: toolName })}</p>\n <pre style={{ margin: 0, fontSize: 12, whiteSpace: 'pre-wrap', wordBreak: 'break-word', maxHeight: 320, overflow: 'auto' }}>\n {result ? JSON.stringify(result, null, 2) : t('card.empty')}\n </pre>\n </div>\n )\n}\n\nfunction LoginCard({ toolName }: { toolName: string }): React.JSX.Element {\n const authState = useSyncExternalStore(subscribeAuth, getAuthState)\n const iframeRef = useRef<HTMLIFrameElement | null>(null)\n const dark = useIsDark()\n const locale = useLocale()\n const t = useT()\n const palette = cardPalette(dark)\n // Healing: if the browser already holds a token (e.g. the node half was\n // reset by a server restart), push it again the moment the login card\n // mounts so the tool gate flips to authenticated without a manual re-login.\n useEffect(() => {\n syncAuthNow()\n }, [toolName])\n\n // Toolview is the second of the two FULL login surfaces (the other is the\n // sidebar-triggered login dialog). Unlike the dialog — which sits inside a\n // host-painted card with its own visual edge and therefore wants the embed\n // in TRANSPARENT card mode — the toolview card IS the surface: the iframe\n // fills it edge-to-edge. So we pass `fill: 'full'` and let the embed paint\n // its own `bg-background` (dark in dark theme, light in light theme). This\n // eliminates the white gaps that the transparent mode's 20px grid strips\n // would otherwise leave above and below the form.\n const src = useMemo(\n () => buildLoginUrl({ fill: 'full', lang: locale, theme: dark ? 'dark' : 'light' }),\n [locale, dark],\n )\n // Force a full iframe remount when theme/locale flips. Chrome's\n // `iframe.src =` update keeps the old embed loaded and ignores the new\n // `fill`/`theme` params (the embed is a single Next.js page that reads\n // params once on mount); only a remount picks them up.\n const remountKey = `${locale}|${dark ? 'd' : 'l'}`\n\n return (\n <div style={cardStyle(palette)}>\n <p style={TITLE_STYLE}>{t('card.title')}</p>\n <p style={{ ...DESC_STYLE, color: palette.muted }}>\n {t('card.desc', { tool: toolName })}\n </p>\n <StatusLine authenticated={authState.authenticated} nickname={authState.nickname} palette={palette} t={t} />\n <iframe\n key={remountKey}\n ref={iframeRef}\n src={src}\n title={t('card.title')}\n style={iframeStyle(palette)}\n allow=\"clipboard-write\"\n />\n </div>\n )\n}\n\nexport const HuaqiuToolView = memo(function HuaqiuToolView(props: NeedsAuthToolViewProps): React.JSX.Element {\n const { toolName, block } = props\n const result = useMemo(() => parseToolResult(block), [block])\n if (isNeedsAuthResult(result)) {\n return <LoginCard toolName={toolName} />\n }\n return <JsonFallback toolName={toolName} block={block} />\n})\n","/**\n * The Huaqiu (华秋) mark, used as the sidebar auth trigger's DEFAULT icon\n * (mirrors `HQ_ICON` in `hq-eda-ai/apps/web/src/components/ui/icons.tsx`).\n *\n * Plain inline SVG: the DSH client bundle ships as a classic script with no\n * Tailwind, so the Next.js wrapper (div + utility classes) is dropped and the\n * 40×40 viewBox paths are kept verbatim.\n */\nexport interface HqIconProps {\n size?: number\n /** Brand blue by default; the paths are monochrome so one fill covers all. */\n color?: string\n title?: string\n}\n\nexport function HQ_ICON({ size = 24, color = '#1a81c4', title }: HqIconProps): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n viewBox=\"0 0 40 40\"\n width={size}\n height={size}\n role={title ? 'img' : undefined}\n aria-hidden={title ? undefined : true}\n focusable=\"false\"\n style={{ display: 'block', flex: '0 0 auto' }}\n >\n {title ? <title>{title}</title> : null}\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M29.71,30a2.75,2.75,0,1,0,2.75,2.74A2.74,2.74,0,0,0,29.71,30Z\"\n />\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M26.59,10.49H13.41a5.93,5.93,0,0,0-5.91,5.9V29.58a5.93,5.93,0,0,0,5.91,5.91H26.85a4,4,0,0,1-1.13-2.78,4.43,4.43,0,0,1,.1-.9H13.41a2.23,2.23,0,0,1-2.22-2.22V16.39a2.23,2.23,0,0,1,2.22-2.21H26.59a2.23,2.23,0,0,1,2.22,2.21V28.81a4.43,4.43,0,0,1,.9-.1,4,4,0,0,1,2.78,1.13,2.26,2.26,0,0,0,0-.26V16.39A5.93,5.93,0,0,0,26.59,10.49Z\"\n />\n <path\n fill={color}\n fillRule=\"evenodd\"\n d=\"M26.38,27.52V18.46a1.85,1.85,0,0,0-1.85-1.85h0a1.84,1.84,0,0,0-1.84,1.85v2.68H17.31V18.46a1.84,1.84,0,0,0-1.84-1.85h0a1.85,1.85,0,0,0-1.85,1.85v9.06a1.85,1.85,0,0,0,1.85,1.85h0a1.84,1.84,0,0,0,1.84-1.85V24.83h5.38v2.69a1.84,1.84,0,0,0,1.84,1.85h0A1.85,1.85,0,0,0,26.38,27.52Z\"\n />\n <circle fill={color} cx=\"20\" cy=\"5.04\" r=\"2.86\" />\n <rect fill={color} x=\"19\" y=\"5.04\" width=\"2\" height=\"6.7\" />\n <path fill={color} d=\"M6.37,17.71a4.89,4.89,0,0,0,0,9.78Z\" />\n <path fill={color} d=\"M33.63,17.71a4.89,4.89,0,1,1,0,9.78Z\" />\n </svg>\n )\n}\n","/**\n * `sidebar.footer.action` entry: the Huaqiu EDA account trigger at the bottom\n * of the DSH sidebar (beside Settings).\n *\n * - Not logged in: shows the HQ icon and opens the login dialog through\n * `auth.login({ lang, theme })` — a real modal (backdrop + centered card +\n * auth.eda.cn iframe) that is ALWAYS TRANSPARENT in the embed itself\n * (`fill=full` is never sent, see `lib.ts#buildLoginUrl`), and the card\n * surface masks Blink's white base canvas so the login card floats over\n * the dimmed app in both light and dark themes. `lang`/`theme` follow the\n * host UI. Click on the backdrop, the × button, or Escape closes it, and\n * auth.eda.cn's own `close_dialog` postMessage closes it as well.\n * - Logged in: the trigger becomes the user's AVATAR (`headimage` from the\n * auth.eda.cn payload, HQ icon while it is missing/fails to load) and a click\n * opens a context menu with「Go to profile」(the eda.cn account page, with\n * the access token) and「Log out」— the same shape as `hq-eda-ai`'s\n * `UserMenu`, portalled to `document.body` with fixed positioning so the\n * sidebar's `overflow: hidden` can never clip it.\n *\n * THEMING: colors prefer DSH's `--dsw-alias-*` tokens (so a custom host theme\n * is honored) and fall back to an explicit light/dark pair chosen from\n * `useIsDark()`; the two paths cannot disagree, because ui-layout's presenter\n * writes `body[data-ds-dark-theme]` from the very snapshot that installs those\n * tokens.\n */\nimport { memo, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type CSSProperties } from 'react'\nimport { createPortal } from 'react-dom'\nimport { getAuth, getAuthState, getHqEdge, setProfile, subscribeAuth } from '../auth-state.js'\nimport { buildProfileUrl } from '../lib.js'\nimport { useIsDark, useLocale } from '../ui-env.js'\nimport { useT } from '../i18n.js'\nimport { HQ_ICON } from './hq-icon.jsx'\n\nexport interface SidebarFooterActionOwnerProps {\n wide?: boolean\n}\n\nconst AVATAR_SIZE = 26\nconst ICON_SIZE = 22\n\n/** One color scheme's menu colors (DSH token first, explicit fallback second). */\ninterface Palette {\n surface: string\n border: string\n text: string\n muted: string\n hover: string\n danger: string\n dangerHover: string\n avatarBg: string\n shadow: string\n}\n\nconst LIGHT_PALETTE: Palette = {\n surface: 'var(--dsw-alias-bg-overlay, #ffffff)',\n border: 'var(--dsw-alias-border-l1, #e4e7ec)',\n text: 'var(--dsw-alias-label-primary, #3a4356)',\n muted: 'var(--dsw-alias-label-secondary, #8a94a6)',\n hover: 'var(--dsw-alias-interactive-bg-hover, #f5f7fa)',\n danger: 'var(--dsw-alias-state-error-primary, #d4380d)',\n dangerHover: 'rgba(216, 56, 13, 0.08)',\n avatarBg: 'var(--dsw-alias-bg-layer-2, #eef2f7)',\n shadow: '0 12px 32px rgba(15, 23, 42, 0.16)',\n}\n\nconst DARK_PALETTE: Palette = {\n surface: 'var(--dsw-alias-bg-overlay, #20242c)',\n border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.14))',\n text: 'var(--dsw-alias-label-primary, #e6eaf0)',\n muted: 'var(--dsw-alias-label-secondary, #8b95a5)',\n hover: 'var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))',\n danger: 'var(--dsw-alias-state-error-primary, #ff7875)',\n dangerHover: 'rgba(255, 120, 117, 0.14)',\n avatarBg: 'var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.10))',\n shadow: '0 12px 32px rgba(0, 0, 0, 0.46)',\n}\n\nconst TRIGGER_BASE: CSSProperties = {\n width: '100%',\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n padding: '8px 12px',\n border: 'none',\n borderRadius: 8,\n background: 'transparent',\n fontSize: 13,\n fontWeight: 500,\n cursor: 'pointer',\n textAlign: 'left',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n}\n\nconst MENU_BASE: CSSProperties = {\n position: 'fixed',\n zIndex: 2147483000,\n minWidth: 184,\n padding: 6,\n borderWidth: 1,\n borderStyle: 'solid',\n borderRadius: 12,\n fontFamily: 'inherit',\n fontSize: 13,\n}\n\nconst MENU_HEADER_BASE: CSSProperties = {\n padding: '6px 10px 8px',\n fontSize: 12,\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n whiteSpace: 'nowrap',\n}\n\nconst MENU_ITEM_BASE: CSSProperties = {\n display: 'flex',\n alignItems: 'center',\n gap: 8,\n width: '100%',\n padding: '8px 10px',\n border: 'none',\n borderRadius: 8,\n background: 'transparent',\n font: 'inherit',\n fontSize: 13,\n textAlign: 'left',\n cursor: 'pointer',\n}\n\n/**\n * One menu row. Hover is tracked in state: the client bundle ships no CSS\n * file, so inline styles cannot express `:hover`.\n */\nfunction MenuItem({\n label,\n icon,\n danger,\n palette,\n onSelect,\n}: {\n label: string\n icon: React.JSX.Element\n danger?: boolean\n palette: Palette\n onSelect: () => void\n}): React.JSX.Element {\n const [hovered, setHovered] = useState(false)\n return (\n <button\n type=\"button\"\n role=\"menuitem\"\n style={{\n ...MENU_ITEM_BASE,\n color: danger ? palette.danger : palette.text,\n background: hovered ? (danger ? palette.dangerHover : palette.hover) : 'transparent',\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n onClick={onSelect}\n >\n {icon}\n <span>{label}</span>\n </button>\n )\n}\n\nfunction UserIcon(): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={15}\n height={15}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n style={{ flex: '0 0 auto' }}\n >\n <path d=\"M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2\" />\n <circle cx=\"12\" cy=\"7\" r=\"4\" />\n </svg>\n )\n}\n\nfunction LogoutIcon(): React.JSX.Element {\n return (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width={15}\n height={15}\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={2}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden\n style={{ flex: '0 0 auto' }}\n >\n <path d=\"M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4\" />\n <polyline points=\"16 17 21 12 16 7\" />\n <line x1=\"21\" x2=\"9\" y1=\"12\" y2=\"12\" />\n </svg>\n )\n}\n\nexport const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ wide }: SidebarFooterActionOwnerProps): React.JSX.Element | null {\n const authState = useSyncExternalStore(subscribeAuth, getAuthState)\n const auth = getAuth()\n const dark = useIsDark()\n const locale = useLocale()\n const t = useT()\n const [menuOpen, setMenuOpen] = useState(false)\n const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null)\n const [avatarBroken, setAvatarBroken] = useState(false)\n const [hovered, setHovered] = useState(false)\n const triggerRef = useRef<HTMLButtonElement | null>(null)\n const menuRef = useRef<HTMLDivElement | null>(null)\n\n const palette = dark ? DARK_PALETTE : LIGHT_PALETTE\n const authenticated = authState.authenticated\n const showLabel = wide !== false\n\n // Hide the trigger only when an HQ Edge host that ALREADY exposes its own\n // login surface is present: hq-eda has a native login button + status badge,\n // so this entrypoint would be redundant there. Every other configuration\n // (standalone DSH, kicad/generic hosts) has no login entrypoint of its own,\n // so the sidebar trigger is the only way in and must render.\n const hostMode = auth?.isHostMode?.() ?? false\n const targetHost = hostMode ? (getHqEdge()?.context?.getTargetHost?.() ?? '') : ''\n if (hostMode && targetHost === 'hq-eda') return null\n\n // Host sessions (kicad/generic) carry only {token, userId} — no nickname,\n // no avatar. Fetch the rich eda.cn profile (name + photo) so the trigger\n // renders the real account instead of a bare HQ icon. Standalone DSH already\n // has both from the auth.eda.cn payload, so this is host-only.\n const profile = authState.profile\n useEffect(() => {\n if (!hostMode || targetHost === 'hq-eda') return\n if (!authenticated) {\n setProfile(null)\n return\n }\n let cancelled = false\n auth?.getUserProfile?.()\n .then((info) => {\n if (cancelled) return\n setProfile(info)\n })\n .catch(() => { /* UI degrades to the HQ icon */ })\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hostMode, targetHost, authenticated])\n\n const displayName = profile?.nickname ?? authState.nickname\n const displayAvatar = profile?.headimage ?? authState.avatar\n const avatar = authenticated && !avatarBroken ? displayAvatar : undefined\n\n // A new avatar URL is a fresh chance to render it.\n useEffect(() => {\n setAvatarBroken(false)\n }, [displayAvatar])\n\n // Logging out (from anywhere: menu, another tab surface, node invalidation)\n // must never leave an orphan menu pointing at a signed-out account.\n useEffect(() => {\n if (!authenticated) setMenuOpen(false)\n }, [authenticated])\n\n // Anchor the portalled menu to the trigger before paint: the sidebar footer\n // sits at the bottom edge, so the menu grows UPWARD from the trigger's top.\n useLayoutEffect(() => {\n if (!menuOpen || !triggerRef.current) return\n const rect = triggerRef.current.getBoundingClientRect()\n setMenuStyle({\n ...MENU_BASE,\n background: palette.surface,\n borderColor: palette.border,\n color: palette.text,\n boxShadow: palette.shadow,\n left: Math.max(8, Math.round(rect.left)),\n bottom: Math.max(8, Math.round(window.innerHeight - rect.top + 8)),\n ...(wide ? { width: Math.round(rect.width) } : {}),\n })\n }, [menuOpen, wide, avatar, palette])\n\n // Close on: outside click, Escape, resize or scroll (the anchor moved).\n useEffect(() => {\n if (!menuOpen) return\n const onPointerDown = (event: MouseEvent): void => {\n const target = event.target as Node\n if (triggerRef.current?.contains(target)) return\n if (menuRef.current?.contains(target)) return\n setMenuOpen(false)\n }\n const onKeyDown = (event: KeyboardEvent): void => {\n if (event.key === 'Escape') setMenuOpen(false)\n }\n const dismiss = (): void => setMenuOpen(false)\n document.addEventListener('mousedown', onPointerDown)\n document.addEventListener('keydown', onKeyDown)\n window.addEventListener('resize', dismiss)\n window.addEventListener('scroll', dismiss, true)\n return () => {\n document.removeEventListener('mousedown', onPointerDown)\n document.removeEventListener('keydown', onKeyDown)\n window.removeEventListener('resize', dismiss)\n window.removeEventListener('scroll', dismiss, true)\n }\n }, [menuOpen])\n\n if (!auth) return null\n\n /**\n *「Go to profile」always carries the token, so eda.cn can establish the\n * session in the opened tab (it hides the token itself — see\n * `lib.ts#buildProfileUrl`). The snapshot normally has it; fall back to the\n * client so a stale snapshot can never open an unauthenticated tab.\n */\n const openProfile = (): void => {\n setMenuOpen(false)\n void (async () => {\n const info = authState.token\n ? { token: authState.token, phone: authState.phone }\n : await auth.getUserInfo()\n .then((i) => (i ? { token: i.token, phone: i.phone } : null))\n .catch(() => null)\n if (!info?.token) return\n window.open(buildProfileUrl(info), '_blank', 'noopener,noreferrer')\n })()\n }\n\n const label = authenticated\n ? (displayName ?? t('sidebar.account'))\n : t('sidebar.login')\n\n const title = authenticated ? t('sidebar.accountTitle') : t('sidebar.loginTitle')\n const triggerBackground = menuOpen || hovered ? palette.hover : 'transparent'\n\n return (\n <div style={{ position: 'relative', width: '100%' }}>\n <button\n ref={triggerRef}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-expanded={menuOpen}\n onClick={() => {\n if (!authenticated) {\n // Always-transparent embed in the host's language and color scheme;\n // `closeOnOutsideClick` defaults to true. The login dialog owns\n // the DOM (backdrop + card + iframe) and closes itself on\n // backdrop click, Escape, the × button, or the embed's\n // `close_dialog` postMessage.\n void auth.login({ lang: locale, theme: dark ? 'dark' : 'light' })\n return\n }\n setMenuOpen((open) => !open)\n }}\n onMouseEnter={() => setHovered(true)}\n onMouseLeave={() => setHovered(false)}\n style={{\n ...TRIGGER_BASE,\n color: palette.text,\n padding: wide ? '8px 12px' : '8px 6px',\n background: triggerBackground,\n }}\n title={title}\n >\n {avatar ? (\n <span\n style={{\n flex: '0 0 auto',\n width: AVATAR_SIZE,\n height: AVATAR_SIZE,\n borderRadius: '50%',\n overflow: 'hidden',\n background: palette.avatarBg,\n display: 'block',\n }}\n >\n <img\n src={avatar}\n alt=\"\"\n width={AVATAR_SIZE}\n height={AVATAR_SIZE}\n onError={() => setAvatarBroken(true)}\n style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}\n />\n </span>\n ) : (\n <HQ_ICON size={ICON_SIZE} />\n )}\n {showLabel ? (\n <span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>{label}</span>\n ) : null}\n </button>\n\n {menuOpen && menuStyle\n ? createPortal(\n <div ref={menuRef} role=\"menu\" style={menuStyle}>\n {displayName ? (\n <div style={{ ...MENU_HEADER_BASE, color: palette.muted }} title={displayName}>{displayName}</div>\n ) : null}\n <MenuItem\n label={t('menu.profile')}\n icon={<UserIcon />}\n palette={palette}\n onSelect={openProfile}\n />\n <MenuItem\n label={t('menu.logout')}\n icon={<LogoutIcon />}\n danger\n palette={palette}\n onSelect={() => {\n setMenuOpen(false)\n // Host mode: a rejected logout request must leave the UI\n // authenticated — `auth.logout()` only REQUESTS the logout,\n // the state comes from the host. Swallow the rejection so it\n // never becomes an unhandled promise error.\n void auth.logout().catch(() => { /* host refused — stay authenticated */ })\n }}\n />\n </div>,\n document.body,\n )\n : null}\n </div>\n )\n})\n","/**\n * `@huaqiu/dsh-auth` — browser half (the Phase 0A POC).\n *\n * Opens the auth.eda.cn login page in an overlay iframe, STRICTLY validates\n * the postMessage origin, caches credentials in localStorage (reload restore),\n * and pushes them to the node half over the plugin-owned webServer routes.\n * Provides the client-side `huaqiuAuth` service mirroring the node surface.\n *\n * On top of the credential flow it wires the two UI surfaces the login UX\n * needs:\n * - `sidebar.footer.action` — a persistent 华秋EDA login entrypoint at the\n * bottom of the sidebar (login/logout, live state).\n * - `tool.call.toolview` (keyed per Huaqiu tool) — when a node tool returns\n * `status: \"needs_auth\"` the tool card becomes the login HIT: an embedded\n * auth.eda.cn iframe + login-state line, so login is a step of the\n * conversation instead of a dead error the agent has to relay.\n */\nimport { createAuthStorage } from './storage.js'\nimport { createWebServerAuthTransport } from './transport.js'\nimport { createAuthClient, type AuthClient } from './client.js'\nimport { disposeAuth, registerAuth, registerAuthSync, registerHqEdgeGetter } from './auth-state.js'\nimport { HuaqiuToolView } from './ui/needs-auth-toolview.jsx'\nimport { HuaqiuAuthSidebarAction } from './ui/sidebar-action.jsx'\nimport { disposeUiEnv } from './ui-env.js'\n\n/**\n * Client cordis inject: REAL service names only (the loader maps these to\n * `ctx.inject([...])` dependencies). The `slots` registry service comes from\n * `@deepseek-ai/dsh-client-ui-slots`; it is required to register the toolview\n * and sidebar entries. The PACKAGE-level `dsh.client.inject` in package.json\n * (graph ordering) stays as-is and is NOT this export.\n */\nexport const inject: string[] = ['slots']\n\n/**\n * Huaqiu tools that still surface the auth login card via this plugin.\n *\n * Currently EMPTY: all five Huaqiu tools now own their keyed HIT cards in\n * their own plugins (`@huaqiu/dsh-tool-symbol-footprint` for the three\n * symbol/footprint generators, `@huaqiu/dsh-tool-schematic-gen` for the two\n * schematic/system generators), and each renders its own inline login card for\n * `needs_auth`. Keeping the toolview keys here would double-register the same\n * `tool.call.toolview` slot with an ambiguous winner.\n *\n * The auth plugin remains the credential owner: the `huaqiuAuth` client\n * service, the sidebar login entrypoint and the webServer credential channel.\n */\nexport const AUTH_TOOL_NAMES: readonly string[] = []\n\n/**\n * How often the browser re-pulls the host-owned session in host mode.\n *\n * KiCad's HQ auth token watcher polls the credential file every 3s and\n * publishes an `auth_state_changed` event on change, so 5s bounds the\n * end-to-end lag of an EDA-side login/logout reaching this UI at well under\n * ten seconds while staying negligible against two loopback requests.\n */\nconst HOST_RESYNC_INTERVAL_MS = 5000\n\n/** Minimal structural client context (dsh-client-runtime provides this). */\nexport interface ClientContext {\n provide?(name: string, value: unknown): () => void\n get?<T>(name: string): T | undefined\n slots?: {\n inject(key: string, callback: () => () => void): () => void\n register(spec: { name: string; key?: string; id?: string }, component: unknown): unknown\n }\n}\n\nexport function apply(ctx: ClientContext): () => void {\n const client: AuthClient = createAuthClient({\n storage: createAuthStorage(localStorage),\n transport: createWebServerAuthTransport(),\n windowLike: window,\n documentLike: document,\n })\n\n // The sidebar needs the host identity (targetHost) to decide whether the\n // host already has its own login surface (hq-eda hides the trigger; kicad /\n // generic keep it). Lazy so edge-bridge load order never matters.\n registerHqEdgeGetter(() => ctx.get?.('hqEdge'))\n\n const disposers: Array<() => void> = []\n let disposed = false\n const disposeProvide = ctx.provide?.('huaqiuAuth', { auth: client.auth })\n registerAuth(client.auth)\n registerAuthSync(() => { void client.syncNow() })\n void client.restore()\n disposers.push(client.auth.onAuthStateChanged((info) => {\n void client.syncNow()\n }))\n\n // Healing: the node half keeps auth in memory, so a server restart drops it\n // while the browser still holds the token. Re-sync whenever the tab regains\n // focus/visibility so the tool gate flips back to authenticated without a\n // reload. In host mode there is nothing stored locally to push, so instead\n // re-pull the host-owned session — an EDA-side login/logout performed while\n // this tab was hidden can only be discovered by asking.\n const sync = (): void => {\n void client.syncNow()\n if (client.auth.isHostMode()) void client.refreshHost().catch(() => undefined)\n }\n window.addEventListener('focus', sync)\n document.addEventListener('visibilitychange', sync)\n disposers.push(() => {\n window.removeEventListener('focus', sync)\n document.removeEventListener('visibilitychange', sync)\n })\n\n /**\n * In HQ Edge host mode (config.hqEdgeBaseUrl set on the node half), EDA\n * launches hq-edge WITH the operator credential, so hq-edge — not this\n * plugin — owns authentication for the session. `refreshHost()` resolves\n * that host mode and adopts the host-owned session as the browser\n * credential, so the auth gate / HIT cards read authenticated immediately.\n *\n * The sidebar entrypoint is registered UNCONDITIONALLY and decides its own\n * visibility: it hides itself only when the host is hq-eda (which has a\n * native login button + status badge). Standalone DSH and other hosts\n * (kicad, generic — no login surface of their own) keep the trigger. The\n * mode is read from the node half over the plugin-owned webServer route\n * (async), so the component re-renders when the host session lands\n * (auth-state emits on adopt).\n */\n const slots = ctx.slots\n void client.refreshHost().catch(() => undefined).then(() => {\n if (disposed) return\n\n // Host-mode credential watch.\n //\n // In host mode the credential is owned by EDA (KiCad), not by this plugin:\n // the operator can log in or out from EDA's own dialog, from another\n // webview (online library), or through hq-edge — none of which can reach\n // into this webview, because the DSH panel is deliberately not part of the\n // copilot WEBVIEW_CONTROLLER broadcast set (it has no JS bridge at all).\n // So the host session must be re-pulled to notice changes; otherwise the\n // UI stays stale until KiCad is restarted and the page re-loads.\n //\n // `refreshHost()` only emits when the token actually changed, so this poll\n // costs two loopback requests per tick and no re-render when nothing moved.\n if (client.auth.isHostMode()) {\n let polling = false\n const timer = window.setInterval(() => {\n if (polling) return\n polling = true\n void client.refreshHost()\n .catch(() => undefined)\n .finally(() => { polling = false })\n }, HOST_RESYNC_INTERVAL_MS)\n disposers.push(() => { window.clearInterval(timer) })\n }\n\n if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function') {\n for (const toolName of AUTH_TOOL_NAMES) {\n disposers.push(slots.inject('tool.call.toolview', () => slots.register({ name: 'tool.call.toolview', key: toolName }, HuaqiuToolView) as () => void))\n }\n disposers.push(slots.inject('sidebar.footer.action', () => slots.register({ name: 'sidebar.footer.action', id: 'huaqiu-auth' }, HuaqiuAuthSidebarAction) as () => void))\n }\n })\n\n return () => {\n disposed = true\n for (const dispose of disposers) {\n try {\n dispose()\n } catch {\n /* already disposed */\n }\n }\n disposeProvide?.()\n client.dispose()\n disposeAuth()\n disposeUiEnv()\n }\n}\n"],"mappings":"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"}
package/lib/index.d.mts CHANGED
@@ -29,6 +29,8 @@ interface HuaqiuAuthConfig {
29
29
  hostAuthPath?: string;
30
30
  /** Host endpoint that triggers the EDA login dialog; default "/api/v1/auth/login". */
31
31
  hostLoginPath?: string;
32
+ /** Host endpoint that requests EDA logout; default "/api/v1/auth/logout". */
33
+ hostLogoutPath?: string;
32
34
  /** Seconds a host session is reused before re-fetching. Default 300. */
33
35
  hostSessionTtlSeconds?: number;
34
36
  /** Seconds remote token-validation results are cached. Default 60. */
@@ -94,8 +96,17 @@ interface HuaqiuAuthApi {
94
96
  isAuthenticated(): Promise<boolean>;
95
97
  getAccessToken(): Promise<string | null>;
96
98
  getUserInfo(): Promise<HuaqiuUserInfo | null>;
97
- /** Node-side no-op: login always happens in the browser. */
99
+ /** Node-side no-op in standalone: login always happens in the browser. */
98
100
  login(): Promise<void>;
101
+ /**
102
+ * Request logout.
103
+ *
104
+ * In host mode this asks the EDA host (through hq-edge) to log out — it is
105
+ * a REQUEST, never a local declaration. The authoritative state keeps coming
106
+ * from the host, so on success we only drop the cached host session and let
107
+ * the next `resolve()` re-ask hq-edge. A failed request throws and leaves
108
+ * the current authentication state untouched.
109
+ */
99
110
  logout(): Promise<void>;
100
111
  /**
101
112
  * Single authoritative validation path (spec §7). Works identically for
@@ -136,6 +147,7 @@ declare class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
136
147
  private readonly validator;
137
148
  private readonly doFetch;
138
149
  private readonly hostLoginPath;
150
+ private readonly hostLogoutPath;
139
151
  /** Bound on the host login RPC (user must complete the EDA dialog). */
140
152
  private readonly loginWaitMs;
141
153
  /**
package/lib/index.mjs CHANGED
@@ -38,6 +38,7 @@ function resolveHostConfig(config, env = process.env) {
38
38
  const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? "";
39
39
  const hostAuthPath = config?.hostAuthPath ?? env.HQ_EDGE_AUTH_PATH ?? "/api/v1/auth/token";
40
40
  const hostLoginPath = config?.hostLoginPath ?? env.HQ_EDGE_LOGIN_PATH ?? "/api/v1/auth/login";
41
+ const hostLogoutPath = config?.hostLogoutPath ?? env.HQ_EDGE_LOGOUT_PATH ?? "/api/v1/auth/logout";
41
42
  const ttlRaw = config?.hostSessionTtlSeconds ?? env.HQ_EDGE_HOST_TTL_SECONDS;
42
43
  let ttl = 300;
43
44
  if (typeof ttlRaw === "number" && Number.isFinite(ttlRaw) && ttlRaw > 0) ttl = ttlRaw;
@@ -56,6 +57,7 @@ function resolveHostConfig(config, env = process.env) {
56
57
  hqEdgeBaseUrl: baseUrl,
57
58
  hostAuthPath,
58
59
  hostLoginPath,
60
+ hostLogoutPath,
59
61
  hostSessionTtlSeconds: ttl,
60
62
  validationTtlSeconds
61
63
  };
@@ -323,6 +325,12 @@ var TokenValidator = class {
323
325
  const PERSIST_FILE = "session.json";
324
326
  const PERSIST_DIR = () => dshHomePath("auth");
325
327
  /**
328
+ * Bounded wait for the host to confirm a logout. Shorter than the login wait
329
+ * (which must cover a human completing the EDA dialog): logout is a host-side
330
+ * operation, so a confirmation that takes longer than this means it failed.
331
+ */
332
+ const LOGOUT_WAIT_MS = 3e4;
333
+ /**
326
334
  * Returns the persisted session, or null if absent/unreadable. Best-effort: a
327
335
  * corrupt or unreadable file is treated as "no session" rather than thrown.
328
336
  */
@@ -369,6 +377,7 @@ var InMemoryHuaqiuAuthService = class {
369
377
  validator;
370
378
  doFetch;
371
379
  hostLoginPath;
380
+ hostLogoutPath;
372
381
  /** Bound on the host login RPC (user must complete the EDA dialog). */
373
382
  loginWaitMs;
374
383
  /**
@@ -389,6 +398,7 @@ var InMemoryHuaqiuAuthService = class {
389
398
  this.doFetch = opts?.fetchImpl ?? globalThis.fetch.bind(globalThis);
390
399
  this.host = new HostSessionResolver(resolved.hqEdgeBaseUrl ?? "", resolved.hostAuthPath ?? "/api/v1/auth/token", (resolved.hostSessionTtlSeconds ?? 300) * 1e3, this.doFetch);
391
400
  this.hostLoginPath = resolved.hostLoginPath ?? "/api/v1/auth/login";
401
+ this.hostLogoutPath = resolved.hostLogoutPath ?? "/api/v1/auth/logout";
392
402
  this.loginWaitMs = 3e5;
393
403
  this.validator = new TokenValidator({
394
404
  ttlMs: (resolved.validationTtlSeconds ?? 60) * 1e3,
@@ -424,7 +434,22 @@ var InMemoryHuaqiuAuthService = class {
424
434
  this.validator.invalidate();
425
435
  this.emit();
426
436
  },
427
- logout: async () => this.invalidate(),
437
+ logout: async () => {
438
+ if (!this.host.enabled) {
439
+ this.invalidate();
440
+ return;
441
+ }
442
+ const res = await this.doFetch(`${this.host.baseUrl}${this.hostLogoutPath}`, {
443
+ method: "POST",
444
+ headers: { accept: "application/json" },
445
+ signal: AbortSignal.timeout(LOGOUT_WAIT_MS)
446
+ });
447
+ if (!res.ok) throw new Error(`host logout not completed: HTTP ${res.status}`);
448
+ this.host.clear();
449
+ this.stale = true;
450
+ this.validator.invalidate();
451
+ this.emit();
452
+ },
428
453
  validate: () => this.validateInternal(),
429
454
  invalidate: () => this.markStale(),
430
455
  onAuthStateChanged: (listener) => this.on(listener)
@@ -656,7 +681,7 @@ function createAuthHandler(service) {
656
681
  return;
657
682
  }
658
683
  if (req.method === "POST" && pathname === `/api/v1/huaqiu/auth/logout`) {
659
- service.invalidate();
684
+ await service.auth.logout();
660
685
  sendJson(res, 200, { ok: true });
661
686
  return;
662
687
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-auth",
3
- "version": "0.3.16",
3
+ "version": "0.3.18",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -32,7 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.0",
35
- "@huaqiu/dsh-plugin-log": "0.3.16"
35
+ "@huaqiu/dsh-plugin-log": "0.3.18"
36
36
  },
37
37
  "files": [
38
38
  "lib",
@@ -39,6 +39,12 @@ export interface AuthClient {
39
39
  */
40
40
  getUserProfile(): Promise<{ nickname?: string; headimage?: string } | null>
41
41
  login(options?: LoginOptions): Promise<void>
42
+ /**
43
+ * Request logout. In host mode this asks the EDA host (through the node
44
+ * half → hq-edge → TriggerLogout) to log out; it never declares the
45
+ * operator logged out locally. Throws when the request is rejected, so
46
+ * the UI keeps showing the authenticated state.
47
+ */
42
48
  logout(): Promise<void>
43
49
  onAuthStateChanged(listener: (info: AuthTokenPayload | null) => void): () => void
44
50
  }
@@ -169,10 +175,15 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
169
175
  },
170
176
  logout: async (): Promise<void> => {
171
177
  if (hostMode) {
172
- // Host-owned session: nothing to revoke browser-side beyond our cache.
173
- hostSession = null
174
- storage.clear()
175
- emit(null)
178
+ // Host-owned session: ask the EDA host to log out (node half →
179
+ // hq-edge → AuthService.TriggerLogout). Success only means the
180
+ // request was accepted, so re-resolve the host session and emit the
181
+ // HOST's verdict — never a locally invented "logged out". A rejected
182
+ // request throws and leaves `hostSession` (the real state) intact.
183
+ await transport.pushLogout()
184
+ hostSessionLoaded = false
185
+ await resolveHost()
186
+ emit(hostSession)
176
187
  return
177
188
  }
178
189
  storage.clear()
@@ -239,6 +250,11 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
239
250
  * late host handover still flips the gate.
240
251
  */
241
252
  async refreshHost(): Promise<boolean> {
253
+ // Remember what callers currently believe, so we only notify on a real
254
+ // change. This routine is re-run periodically in host mode (there is no
255
+ // push channel into the webview), and re-emitting an identical payload
256
+ // would re-render every mounted card and HIT for nothing.
257
+ const prevToken = hostSession?.token ?? storage.get()?.token ?? null
242
258
  let mode = false
243
259
  try {
244
260
  mode = await transport.fetchHostMode()
@@ -248,7 +264,8 @@ export function createAuthClient(deps: AuthClientDeps): AuthClient {
248
264
  hostMode = mode
249
265
  hostSessionLoaded = false
250
266
  await resolveHost()
251
- emit(hostSession ?? storage.get())
267
+ const next = hostSession ?? storage.get()
268
+ if (next?.token !== prevToken) emit(next)
252
269
  return hostMode
253
270
  },
254
271
  /**
@@ -47,6 +47,16 @@ export const inject: string[] = ['slots']
47
47
  */
48
48
  export const AUTH_TOOL_NAMES: readonly string[] = []
49
49
 
50
+ /**
51
+ * How often the browser re-pulls the host-owned session in host mode.
52
+ *
53
+ * KiCad's HQ auth token watcher polls the credential file every 3s and
54
+ * publishes an `auth_state_changed` event on change, so 5s bounds the
55
+ * end-to-end lag of an EDA-side login/logout reaching this UI at well under
56
+ * ten seconds while staying negligible against two loopback requests.
57
+ */
58
+ const HOST_RESYNC_INTERVAL_MS = 5000
59
+
50
60
  /** Minimal structural client context (dsh-client-runtime provides this). */
51
61
  export interface ClientContext {
52
62
  provide?(name: string, value: unknown): () => void
@@ -83,8 +93,13 @@ export function apply(ctx: ClientContext): () => void {
83
93
  // Healing: the node half keeps auth in memory, so a server restart drops it
84
94
  // while the browser still holds the token. Re-sync whenever the tab regains
85
95
  // focus/visibility so the tool gate flips back to authenticated without a
86
- // reload.
87
- const sync = (): void => { void client.syncNow() }
96
+ // reload. In host mode there is nothing stored locally to push, so instead
97
+ // re-pull the host-owned session — an EDA-side login/logout performed while
98
+ // this tab was hidden can only be discovered by asking.
99
+ const sync = (): void => {
100
+ void client.syncNow()
101
+ if (client.auth.isHostMode()) void client.refreshHost().catch(() => undefined)
102
+ }
88
103
  window.addEventListener('focus', sync)
89
104
  document.addEventListener('visibilitychange', sync)
90
105
  disposers.push(() => {
@@ -110,6 +125,31 @@ export function apply(ctx: ClientContext): () => void {
110
125
  const slots = ctx.slots
111
126
  void client.refreshHost().catch(() => undefined).then(() => {
112
127
  if (disposed) return
128
+
129
+ // Host-mode credential watch.
130
+ //
131
+ // In host mode the credential is owned by EDA (KiCad), not by this plugin:
132
+ // the operator can log in or out from EDA's own dialog, from another
133
+ // webview (online library), or through hq-edge — none of which can reach
134
+ // into this webview, because the DSH panel is deliberately not part of the
135
+ // copilot WEBVIEW_CONTROLLER broadcast set (it has no JS bridge at all).
136
+ // So the host session must be re-pulled to notice changes; otherwise the
137
+ // UI stays stale until KiCad is restarted and the page re-loads.
138
+ //
139
+ // `refreshHost()` only emits when the token actually changed, so this poll
140
+ // costs two loopback requests per tick and no re-render when nothing moved.
141
+ if (client.auth.isHostMode()) {
142
+ let polling = false
143
+ const timer = window.setInterval(() => {
144
+ if (polling) return
145
+ polling = true
146
+ void client.refreshHost()
147
+ .catch(() => undefined)
148
+ .finally(() => { polling = false })
149
+ }, HOST_RESYNC_INTERVAL_MS)
150
+ disposers.push(() => { window.clearInterval(timer) })
151
+ }
152
+
113
153
  if (slots && typeof slots.inject === 'function' && typeof slots.register === 'function') {
114
154
  for (const toolName of AUTH_TOOL_NAMES) {
115
155
  disposers.push(slots.inject('tool.call.toolview', () => slots.register({ name: 'tool.call.toolview', key: toolName }, HuaqiuToolView) as () => void))
@@ -417,7 +417,11 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
417
417
  palette={palette}
418
418
  onSelect={() => {
419
419
  setMenuOpen(false)
420
- void auth.logout()
420
+ // Host mode: a rejected logout request must leave the UI
421
+ // authenticated — `auth.logout()` only REQUESTS the logout,
422
+ // the state comes from the host. Swallow the rejection so it
423
+ // never becomes an unhandled promise error.
424
+ void auth.logout().catch(() => { /* host refused — stay authenticated */ })
421
425
  }}
422
426
  />
423
427
  </div>,
package/src/host.ts CHANGED
@@ -39,6 +39,8 @@ export interface HuaqiuAuthConfig {
39
39
  hostAuthPath?: string
40
40
  /** Host endpoint that triggers the EDA login dialog; default "/api/v1/auth/login". */
41
41
  hostLoginPath?: string
42
+ /** Host endpoint that requests EDA logout; default "/api/v1/auth/logout". */
43
+ hostLogoutPath?: string
42
44
  /** Seconds a host session is reused before re-fetching. Default 300. */
43
45
  hostSessionTtlSeconds?: number
44
46
  /** Seconds remote token-validation results are cached. Default 60. */
@@ -47,6 +49,7 @@ export interface HuaqiuAuthConfig {
47
49
 
48
50
  export const DEFAULT_HOST_AUTH_PATH = '/api/v1/auth/token'
49
51
  export const DEFAULT_HOST_LOGIN_PATH = '/api/v1/auth/login'
52
+ export const DEFAULT_HOST_LOGOUT_PATH = '/api/v1/auth/logout'
50
53
  export const DEFAULT_HOST_TTL_SECONDS = 300
51
54
  export const DEFAULT_VALIDATION_TTL_SECONDS = 60
52
55
 
@@ -68,6 +71,9 @@ export function resolveHostConfig(
68
71
  const hostLoginPath = config?.hostLoginPath
69
72
  ?? env.HQ_EDGE_LOGIN_PATH
70
73
  ?? DEFAULT_HOST_LOGIN_PATH
74
+ const hostLogoutPath = config?.hostLogoutPath
75
+ ?? env.HQ_EDGE_LOGOUT_PATH
76
+ ?? DEFAULT_HOST_LOGOUT_PATH
71
77
  const ttlRaw = config?.hostSessionTtlSeconds ?? env.HQ_EDGE_HOST_TTL_SECONDS
72
78
  let ttl = DEFAULT_HOST_TTL_SECONDS
73
79
  if (typeof ttlRaw === 'number' && Number.isFinite(ttlRaw) && ttlRaw > 0) {
@@ -88,6 +94,7 @@ export function resolveHostConfig(
88
94
  hqEdgeBaseUrl: baseUrl,
89
95
  hostAuthPath,
90
96
  hostLoginPath,
97
+ hostLogoutPath,
91
98
  hostSessionTtlSeconds: ttl,
92
99
  validationTtlSeconds,
93
100
  }
package/src/routes.ts CHANGED
@@ -130,7 +130,11 @@ export function createAuthHandler(service: HuaqiuAuthService): AuthHandler {
130
130
  }
131
131
 
132
132
  if (req.method === 'POST' && pathname === `${AUTH_ROUTE_PREFIX}/logout`) {
133
- service.invalidate()
133
+ // Forward through `auth.logout()` rather than invalidating locally: in
134
+ // host mode the EDA host owns the session, so only it can log the
135
+ // operator out. A rejected request surfaces as 500 and the browser
136
+ // half keeps its current (authenticated) state.
137
+ await service.auth.logout()
134
138
  sendJson(res, 200, { ok: true })
135
139
  return
136
140
  }
package/src/service.ts CHANGED
@@ -51,8 +51,17 @@ export interface HuaqiuAuthApi {
51
51
  isAuthenticated(): Promise<boolean>
52
52
  getAccessToken(): Promise<string | null>
53
53
  getUserInfo(): Promise<HuaqiuUserInfo | null>
54
- /** Node-side no-op: login always happens in the browser. */
54
+ /** Node-side no-op in standalone: login always happens in the browser. */
55
55
  login(): Promise<void>
56
+ /**
57
+ * Request logout.
58
+ *
59
+ * In host mode this asks the EDA host (through hq-edge) to log out — it is
60
+ * a REQUEST, never a local declaration. The authoritative state keeps coming
61
+ * from the host, so on success we only drop the cached host session and let
62
+ * the next `resolve()` re-ask hq-edge. A failed request throws and leaves
63
+ * the current authentication state untouched.
64
+ */
56
65
  logout(): Promise<void>
57
66
  /**
58
67
  * Single authoritative validation path (spec §7). Works identically for
@@ -91,6 +100,13 @@ export interface HuaqiuAuthService {
91
100
  const PERSIST_FILE = 'session.json'
92
101
  const PERSIST_DIR = () => dshHomePath('auth')
93
102
 
103
+ /**
104
+ * Bounded wait for the host to confirm a logout. Shorter than the login wait
105
+ * (which must cover a human completing the EDA dialog): logout is a host-side
106
+ * operation, so a confirmation that takes longer than this means it failed.
107
+ */
108
+ const LOGOUT_WAIT_MS = 30_000
109
+
94
110
  /**
95
111
  * Returns the persisted session, or null if absent/unreadable. Best-effort: a
96
112
  * corrupt or unreadable file is treated as "no session" rather than thrown.
@@ -149,6 +165,7 @@ export class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
149
165
  private readonly validator: TokenValidator
150
166
  private readonly doFetch: typeof fetch
151
167
  private readonly hostLoginPath: string
168
+ private readonly hostLogoutPath: string
152
169
  /** Bound on the host login RPC (user must complete the EDA dialog). */
153
170
  private readonly loginWaitMs: number
154
171
  /**
@@ -178,6 +195,7 @@ export class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
178
195
  this.doFetch,
179
196
  )
180
197
  this.hostLoginPath = resolved.hostLoginPath ?? '/api/v1/auth/login'
198
+ this.hostLogoutPath = resolved.hostLogoutPath ?? '/api/v1/auth/logout'
181
199
  this.loginWaitMs = 5 * 60_000
182
200
  this.validator = new TokenValidator({
183
201
  ttlMs: (resolved.validationTtlSeconds ?? 60) * 1000,
@@ -231,7 +249,31 @@ export class InMemoryHuaqiuAuthService implements HuaqiuAuthService {
231
249
  this.validator.invalidate()
232
250
  this.emit()
233
251
  },
234
- logout: async () => this.invalidate(),
252
+ logout: async () => {
253
+ // Standalone: the browser owns the credential — drop it locally.
254
+ if (!this.host.enabled) {
255
+ this.invalidate()
256
+ return
257
+ }
258
+ // Host mode: the EDA host (KiCad) owns the session, so logout is a
259
+ // REQUEST forwarded through hq-edge (→ AuthService.TriggerLogout).
260
+ // Success only means the host accepted/performed it; the authoritative
261
+ // state is whatever the host reports next, so we drop the cached host
262
+ // session and let the following resolve() re-ask hq-edge instead of
263
+ // declaring the operator logged out ourselves.
264
+ const res = await this.doFetch(`${this.host.baseUrl}${this.hostLogoutPath}`, {
265
+ method: 'POST',
266
+ headers: { accept: 'application/json' },
267
+ signal: AbortSignal.timeout(LOGOUT_WAIT_MS),
268
+ })
269
+ if (!res.ok) {
270
+ throw new Error(`host logout not completed: HTTP ${res.status}`)
271
+ }
272
+ this.host.clear()
273
+ this.stale = true
274
+ this.validator.invalidate()
275
+ this.emit()
276
+ },
235
277
  validate: () => this.validateInternal(),
236
278
  invalidate: () => this.markStale(),
237
279
  onAuthStateChanged: (listener) => this.on(listener),