@huaqiu/dsh-auth 0.1.2 → 0.2.0
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 +2 -1
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +69 -2
- package/lib/index.mjs +199 -6
- package/package.json +1 -1
- package/src/client/transport.ts +1 -0
- package/src/host.ts +17 -1
- package/src/routes.ts +11 -3
- package/src/service.ts +90 -5
- package/src/validation.ts +182 -0
package/lib/client.js
CHANGED
|
@@ -45,7 +45,8 @@ window.__ModuleLoader__.load({
|
|
|
45
45
|
body: JSON.stringify({
|
|
46
46
|
token: info.token,
|
|
47
47
|
userId: info.id,
|
|
48
|
-
...info.nickname !== void 0 ? { nickname: info.nickname } : {}
|
|
48
|
+
...info.nickname !== void 0 ? { nickname: info.nickname } : {},
|
|
49
|
+
...info.expiresAt !== void 0 ? { expiresAt: info.expiresAt } : {}
|
|
49
50
|
})
|
|
50
51
|
});
|
|
51
52
|
if (!res.ok) throw new Error(`auth push failed: HTTP ${res.status}`);
|
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\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 }),\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 }\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 getAccessToken(): Promise<string | null>\n getUserInfo(): Promise<AuthTokenPayload | 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 /** 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 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 const auth = {\n isAuthenticated: (): boolean => storage.get() !== null,\n getAccessToken: async (): Promise<string | null> => storage.get()?.token ?? null,\n getUserInfo: async (): Promise<AuthTokenPayload | null> => storage.get(),\n login: async (options?: LoginOptions): Promise<void> => openIframe(options ?? {}),\n logout: async (): Promise<void> => {\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 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 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 * 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 void trustedOrigin // referenced for clarity: the auth iframe URL comes from it\n },\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\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/** 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/** 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 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, 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 avatar = authenticated && !avatarBroken ? authState.avatar : undefined\n const showLabel = wide !== false\n\n // A new avatar URL is a fresh chance to render it.\n useEffect(() => {\n setAvatarBroken(false)\n }, [authState.avatar])\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 ? (authState.nickname ?? 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 {authState.nickname ? (\n <div style={{ ...MENU_HEADER_BASE, color: palette.muted }} title={authState.nickname}>{authState.nickname}</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 } 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 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 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. The auth plugin's own login\n * UI (the `sidebar.footer.action` entrypoint and the login toolviews) is\n * therefore suppressed: it would be redundant and confusing next to the\n * host-provided session. In standalone DSH (official integration) the\n * sidebar entrypoint stays — it is the only login surface there.\n *\n * The mode is read from the node half over the plugin-owned webServer route\n * (async), so registration is deferred until it answers; the returned\n * disposer still drains anything registered later.\n */\n const slots = ctx.slots\n void client.transport.fetchHostMode().then((hostMode) => {\n if (disposed || hostMode) 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\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 }\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 getAccessToken(): Promise<string | null>\n getUserInfo(): Promise<AuthTokenPayload | 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 /** 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 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 const auth = {\n isAuthenticated: (): boolean => storage.get() !== null,\n getAccessToken: async (): Promise<string | null> => storage.get()?.token ?? null,\n getUserInfo: async (): Promise<AuthTokenPayload | null> => storage.get(),\n login: async (options?: LoginOptions): Promise<void> => openIframe(options ?? {}),\n logout: async (): Promise<void> => {\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 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 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 * 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 void trustedOrigin // referenced for clarity: the auth iframe URL comes from it\n },\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\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/** 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/** 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 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, 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 avatar = authenticated && !avatarBroken ? authState.avatar : undefined\n const showLabel = wide !== false\n\n // A new avatar URL is a fresh chance to render it.\n useEffect(() => {\n setAvatarBroken(false)\n }, [authState.avatar])\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 ? (authState.nickname ?? 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 {authState.nickname ? (\n <div style={{ ...MENU_HEADER_BASE, color: palette.muted }} title={authState.nickname}>{authState.nickname}</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 } 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 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 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. The auth plugin's own login\n * UI (the `sidebar.footer.action` entrypoint and the login toolviews) is\n * therefore suppressed: it would be redundant and confusing next to the\n * host-provided session. In standalone DSH (official integration) the\n * sidebar entrypoint stays — it is the only login surface there.\n *\n * The mode is read from the node half over the plugin-owned webServer route\n * (async), so registration is deferred until it answers; the returned\n * disposer still drains anything registered later.\n */\n const slots = ctx.slots\n void client.transport.fetchHostMode().then((hostMode) => {\n if (disposed || hostMode) 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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"}
|
package/lib/index.d.mts
CHANGED
|
@@ -28,26 +28,93 @@ interface HuaqiuAuthConfig {
|
|
|
28
28
|
hostAuthPath?: string;
|
|
29
29
|
/** Seconds a host session is reused before re-fetching. Default 300. */
|
|
30
30
|
hostSessionTtlSeconds?: number;
|
|
31
|
+
/** Seconds remote token-validation results are cached. Default 60. */
|
|
32
|
+
validationTtlSeconds?: number;
|
|
31
33
|
}
|
|
32
34
|
//#endregion
|
|
35
|
+
//#region src/validation.d.ts
|
|
36
|
+
/**
|
|
37
|
+
* Token validation for `@huaqiu/dsh-auth`.
|
|
38
|
+
*
|
|
39
|
+
* Single authoritative validation path shared by standalone (auth.eda.cn) and
|
|
40
|
+
* HQ Edge host credentials. Uses the existing Huaqiu endpoint
|
|
41
|
+
*
|
|
42
|
+
* GET https://www.eda.cn/api/token/validate?token=<token>
|
|
43
|
+
* → { code, message, result: boolean }
|
|
44
|
+
*
|
|
45
|
+
* (the same endpoint consumed by `NextChat/app/auth/is_token_valid.ts`; probe:
|
|
46
|
+
* `curl "https://www.eda.cn/api/token/validate?token=__dummy__"` → 200
|
|
47
|
+
* `{"code":200000,"message":"success","result":false}`).
|
|
48
|
+
*
|
|
49
|
+
* Local expiry is a cheap pre-check only (never authoritative). Remote
|
|
50
|
+
* validation is authoritative, short-lived in-memory cached, and never
|
|
51
|
+
* persisted. Network / 5xx failures are reported as `unavailable` — they are
|
|
52
|
+
* NOT converted into "token invalid", so a transient network blip never forces
|
|
53
|
+
* the user to log in again.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Outcome of an authoritative token validation.
|
|
57
|
+
*
|
|
58
|
+
* - `valid` — the Huaqiu API accepted the token.
|
|
59
|
+
* - `invalid` — the token is definitively rejected/expired.
|
|
60
|
+
* - `unavailable` — validation could not be performed (network/5xx); the token
|
|
61
|
+
* is NOT declared invalid (spec §17/§18).
|
|
62
|
+
*/
|
|
63
|
+
type AuthValidationResult = {
|
|
64
|
+
status: 'valid';
|
|
65
|
+
userId?: string;
|
|
66
|
+
expiresAt?: number;
|
|
67
|
+
} | {
|
|
68
|
+
status: 'invalid';
|
|
69
|
+
reason: 'expired' | 'unauthorized' | 'forbidden' | 'invalid';
|
|
70
|
+
} | {
|
|
71
|
+
status: 'unavailable';
|
|
72
|
+
error: Error;
|
|
73
|
+
};
|
|
74
|
+
//#endregion
|
|
33
75
|
//#region src/service.d.ts
|
|
34
76
|
interface HuaqiuUserInfo {
|
|
35
77
|
id: string;
|
|
36
78
|
token: string;
|
|
37
79
|
nickname?: string;
|
|
80
|
+
/** Unix seconds; known for browser-pushed sessions (auth.eda.cn window). */
|
|
81
|
+
expiresAt?: number;
|
|
38
82
|
}
|
|
39
83
|
interface HuaqiuAuthApi {
|
|
40
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Authoritative async check: a credential exists AND is known to be valid
|
|
86
|
+
* (local expiry + cached remote validation). Never a mere token-presence
|
|
87
|
+
* check — a host token supplied by hq-edge is not assumed valid just because
|
|
88
|
+
* it exists (spec §10). Short-circuits to `false` after `invalidate()` until
|
|
89
|
+
* re-validated or a fresh credential arrives.
|
|
90
|
+
*/
|
|
91
|
+
isAuthenticated(): Promise<boolean>;
|
|
41
92
|
getAccessToken(): Promise<string | null>;
|
|
42
93
|
getUserInfo(): Promise<HuaqiuUserInfo | null>;
|
|
43
94
|
/** Node-side no-op: login always happens in the browser. */
|
|
44
95
|
login(): Promise<void>;
|
|
45
96
|
logout(): Promise<void>;
|
|
97
|
+
/**
|
|
98
|
+
* Single authoritative validation path (spec §7). Works identically for
|
|
99
|
+
* standalone and host credentials; never depends on hq-edge.
|
|
100
|
+
*/
|
|
101
|
+
validate(): Promise<AuthValidationResult>;
|
|
102
|
+
/**
|
|
103
|
+
* Mark the current credential's validation state stale without deleting the
|
|
104
|
+
* credential (kept for recovery). Next validation cannot reuse a previous
|
|
105
|
+
* "valid" result (spec §9/§11). Call this when an API request returns 401.
|
|
106
|
+
*/
|
|
107
|
+
invalidate(): void;
|
|
46
108
|
onAuthStateChanged(listener: (info: HuaqiuUserInfo | null) => void): () => void;
|
|
47
109
|
}
|
|
48
110
|
interface HuaqiuAuthService {
|
|
49
111
|
auth: HuaqiuAuthApi;
|
|
50
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Node-only setters used by the webServer route handlers.
|
|
114
|
+
* NOTE: `service.invalidate()` is the FULL reset (logout: drops the pushed
|
|
115
|
+
* credential, persisted file and host cache). The capability-level
|
|
116
|
+
* `auth.invalidate()` is validation-scoped and keeps the credential.
|
|
117
|
+
*/
|
|
51
118
|
setCredentials(info: HuaqiuUserInfo): void;
|
|
52
119
|
invalidate(): void;
|
|
53
120
|
/**
|