@riceawa/dsh-lan-gateway 0.5.1 → 0.5.3

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.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["useState"],"sources":["../src/client/lan-gateway-card.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * The lan-gateway settings card shown in the official DSH Settings → Plugins\n * page (the `settings.plugin.item` slot).\n *\n * ModLens-style: the card carries NO injected services. It reads and writes\n * the loopback-only `/lan-gateway/config` host route (the browser never sees\n * the settings seam or any secret), so the client bundle's only dependency is\n * the `slots` service that every plugin already has.\n *\n * @module @riceawa/dsh-lan-gateway/client/card\n */\n\nimport { useEffect, useState, type ChangeEvent, type ReactNode } from 'react'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\n/**\n * The official Settings → Plugins page declares the `settings.plugin.item`\n * slot keyed by the settings namespace each card edits (newer DSH releases;\n * older releases dispatched it as a list slot by `id`). The published package\n * ships no `src/`, so the entry is re-declared here — the runtime slot is\n * real; this only restores the compile-time table.\n */\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** One plugin's card inside the plugin configuration section. */\n 'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: { children?: never } }\n }\n}\n\n/** Props the renderer binds for this card (unused — the card is self-loading). */\nexport type LanGatewayCardProps = PropsRuntime<'settings.plugin.item'>\n\n/** The wire shape of the `lan-gateway` config section. */\nexport interface LanGatewaySettings {\n enabled?: boolean\n gatewayPort?: number\n dshTargetPort?: number\n lanCidrs?: string[]\n lanPasswordless?: boolean\n cookieMaxAgeDays?: number\n tlsEnabled?: boolean\n tlsMode?: 'self-signed' | 'custom'\n tlsCertPath?: string\n tlsKeyPath?: string\n tlsSelfSignedHosts?: string\n tlsCertMaxAgeDays?: number\n allowInsecurePlaintext?: boolean\n trustedTerminator?: string\n}\n\n/** GET /lan-gateway/config response. */\ninterface RouteState {\n config: LanGatewaySettings\n running: boolean\n port: number\n tls: string\n lastError: string | null\n}\n\n/* ------------------------------------------------------------------ */\n/* Bilingual copy (ModLens-style: two small sets, picked by browser) */\n/* ------------------------------------------------------------------ */\n\ninterface Labels {\n title: string\n description: string\n unsaved: string\n save: string\n saving: string\n discard: string\n reset: string\n overridden: string\n readOnly: string\n saveFailed: string\n loadFailed: string\n emptyMeansClear: string\n running: string\n stopped: string\n tls: string\n lastError: string\n [key: `field.${string}`]: string\n [key: `hint.${string}`]: string\n}\n\nconst LABELS: Record<'zh' | 'en', Labels> = {\n zh: {\n title: 'LAN 网关',\n description: '远程访问开关、端口、TLS 证书、受信网段等网关设置',\n unsaved: '未保存',\n save: '保存',\n saving: '保存中…',\n discard: '放弃',\n reset: '重置',\n overridden: '已覆盖',\n readOnly: '网关设置当前不可用(读不到配置路由)。',\n saveFailed: '保存未生效,请检查输入后重试。',\n loadFailed: '加载网关配置失败。',\n emptyMeansClear: '留空 = 使用默认',\n running: '运行中',\n stopped: '已停止',\n tls: 'TLS',\n lastError: '上次错误',\n 'field.enabled': '启用网关',\n 'hint.enabled': '启动时监听 0.0.0.0 网关端口',\n 'field.gatewayPort': '网关端口',\n 'hint.gatewayPort': '绑定到 0.0.0.0 的监听端口(默认 3081)',\n 'field.dshTargetPort': 'dsh 目标端口',\n 'hint.dshTargetPort': '留空则自动跟随 dsh web 端口(默认 3080)',\n 'field.lanCidrs': '免密 LAN 网段',\n 'hint.lanCidrs': '逗号分隔的 CIDR,如 10.0.0.0/8, 192.168.0.0/16',\n 'field.lanPasswordless': 'LAN 免登录',\n 'hint.lanPasswordless': 'LAN/回环来源跳过网关登录页,但仍共用同一上游会话(需 dsh ≥ 0.1.2)',\n 'field.allowInsecurePlaintext': '允许明文 HTTP',\n 'hint.allowInsecurePlaintext': '危险:关闭 TLS 或受信终止代理时仍启动监听,密码与会话将以明文传输',\n 'field.trustedTerminator': '受信 TLS 终止代理',\n 'hint.trustedTerminator': '可选:声明前置代理标识,视为加密入口(如 nginx)。留空 = 未声明',\n 'field.cookieMaxAgeDays': '会话有效期(天)',\n 'hint.cookieMaxAgeDays': '登录 cookie 的存活天数(默认 7)',\n 'field.tlsEnabled': '启用 TLS(HTTPS)',\n 'hint.tlsEnabled': '以 HTTPS 提供网关服务',\n 'field.tlsMode': '证书来源',\n 'hint.tlsMode': 'self-signed = 自动生成自签名证书;custom = 使用自己的证书',\n 'field.tlsSelfSignedHosts': '自签名证书域名/IP',\n 'hint.tlsSelfSignedHosts': '逗号分隔,写入证书 SAN,如 localhost, 192.168.1.5',\n 'field.tlsCertPath': '证书文件路径(custom)',\n 'hint.tlsCertPath': 'PEM 格式证书(或证书链)的绝对路径',\n 'field.tlsKeyPath': '私钥文件路径(custom)',\n 'hint.tlsKeyPath': '与证书配套的 PEM 私钥绝对路径',\n 'field.tlsCertMaxAgeDays': '自签名证书有效期(天)',\n 'hint.tlsCertMaxAgeDays': '默认 825(约 27 个月)',\n },\n en: {\n title: 'LAN Gateway',\n description: 'Remote-access switch, port, TLS certificate, trusted CIDRs and more',\n unsaved: 'Unsaved',\n save: 'Save',\n saving: 'Saving…',\n discard: 'Discard',\n reset: 'Reset',\n overridden: 'overridden',\n readOnly: 'Gateway settings unavailable (config route unreachable).',\n saveFailed: 'The save did not land — check the inputs and retry.',\n loadFailed: 'Failed to load gateway configuration.',\n emptyMeansClear: 'Empty = default',\n running: 'Running',\n stopped: 'Stopped',\n tls: 'TLS',\n lastError: 'Last error',\n 'field.enabled': 'Enable gateway',\n 'hint.enabled': 'Listen on the gateway port at boot',\n 'field.gatewayPort': 'Gateway port',\n 'hint.gatewayPort': 'Port bound on 0.0.0.0 (default 3081)',\n 'field.dshTargetPort': 'dsh target port',\n 'hint.dshTargetPort': 'Leave empty to follow the dsh web port (default 3080)',\n 'field.lanCidrs': 'Password-free LAN CIDRs',\n 'hint.lanCidrs': 'Comma separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16',\n 'field.lanPasswordless': 'LAN skip login',\n 'hint.lanPasswordless': 'LAN/loopback sources skip the gateway login page but still ride one shared upstream session (needs dsh >= 0.1.2)',\n 'field.allowInsecurePlaintext': 'Allow plaintext HTTP',\n 'hint.allowInsecurePlaintext': 'Dangerous: start the listener even without TLS or a trusted terminator; passwords and sessions travel in clear',\n 'field.trustedTerminator': 'Trusted TLS terminator',\n 'hint.trustedTerminator': 'Optional identifier for a front proxy (e.g. nginx) treated as the encrypted ingress. Empty = none declared',\n 'field.cookieMaxAgeDays': 'Session lifetime (days)',\n 'hint.cookieMaxAgeDays': 'Login cookie lifetime (default 7)',\n 'field.tlsEnabled': 'Enable TLS (HTTPS)',\n 'hint.tlsEnabled': 'Serve the gateway over HTTPS',\n 'field.tlsMode': 'Certificate source',\n 'hint.tlsMode': 'self-signed = auto-generated certificate; custom = your own files',\n 'field.tlsSelfSignedHosts': 'Self-signed hosts (SANs)',\n 'hint.tlsSelfSignedHosts': 'Comma separated DNS/IP names, e.g. localhost, 192.168.1.5',\n 'field.tlsCertPath': 'Certificate path (custom)',\n 'hint.tlsCertPath': 'Absolute path to a PEM certificate (or chain)',\n 'field.tlsKeyPath': 'Private key path (custom)',\n 'hint.tlsKeyPath': 'Absolute path to the matching PEM private key',\n 'field.tlsCertMaxAgeDays': 'Self-signed validity (days)',\n 'hint.tlsCertMaxAgeDays': 'Default 825 (about 27 months)',\n },\n}\n\nfunction labels(): Labels {\n const lang = (typeof navigator !== 'undefined' ? navigator.language : 'en').toLowerCase()\n return lang.startsWith('zh') ? LABELS.zh : LABELS.en\n}\n\n/* ------------------------------------------------------------------ */\n/* Field model */\n/* ------------------------------------------------------------------ */\n\ntype FieldKind = 'boolean' | 'number' | 'text' | 'cidrs' | 'select'\n\ninterface FieldDef {\n field: keyof LanGatewaySettings\n kind: FieldKind\n optional?: boolean\n options?: readonly string[]\n}\n\nconst FIELDS: readonly FieldDef[] = [\n { field: 'enabled', kind: 'boolean' },\n { field: 'gatewayPort', kind: 'number' },\n { field: 'dshTargetPort', kind: 'number', optional: true },\n { field: 'lanCidrs', kind: 'cidrs' },\n { field: 'lanPasswordless', kind: 'boolean' },\n { field: 'cookieMaxAgeDays', kind: 'number' },\n { field: 'tlsEnabled', kind: 'boolean' },\n { field: 'tlsMode', kind: 'select', options: ['self-signed', 'custom'] },\n { field: 'tlsSelfSignedHosts', kind: 'text' },\n { field: 'tlsCertPath', kind: 'text', optional: true },\n { field: 'tlsKeyPath', kind: 'text', optional: true },\n { field: 'tlsCertMaxAgeDays', kind: 'number' },\n { field: 'allowInsecurePlaintext', kind: 'boolean' },\n { field: 'trustedTerminator', kind: 'text', optional: true },\n]\n\nfunction formatValue(def: FieldDef, value: unknown): string {\n switch (def.kind) {\n case 'boolean': return value === true ? 'true' : 'false'\n case 'number': return typeof value === 'number' ? String(value) : ''\n case 'cidrs': return Array.isArray(value) ? value.join(', ') : ''\n case 'select': return typeof value === 'string' ? value : (def.options?.[0] ?? '')\n case 'text': return typeof value === 'string' ? value : ''\n }\n}\n\ntype Write = { kind: 'set'; value: unknown } | { kind: 'clear' }\n\n/** Parse draft text into a value for the POST body; undefined blocks saving. */\nfunction parseValue(def: FieldDef, text: string): Write | undefined {\n const trimmed = text.trim()\n switch (def.kind) {\n case 'boolean':\n if (trimmed === 'true') return { kind: 'set', value: true }\n if (trimmed === 'false') return { kind: 'set', value: false }\n return undefined\n case 'number':\n if (trimmed === '') return def.optional ? { kind: 'clear' } : undefined\n if (!/^\\d+$/.test(trimmed)) return undefined\n return { kind: 'set', value: Number(trimmed) }\n case 'cidrs': {\n const cidrs = trimmed.split(',').map(s => s.trim()).filter(s => s !== '')\n return cidrs.length === 0 ? { kind: 'clear' } : { kind: 'set', value: cidrs }\n }\n case 'select':\n return def.options?.includes(trimmed) ? { kind: 'set', value: trimmed } : undefined\n case 'text':\n return trimmed === '' ? (def.optional ? { kind: 'clear' } : undefined) : { kind: 'set', value: trimmed }\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Card */\n/* ------------------------------------------------------------------ */\n\n/**\n * Render the LAN gateway card. Self-loading: fetches the config route on\n * mount, posts the edited config on save.\n * @param _props - unused; the card needs no injected face.\n * @returns the card, or nothing while the route is unreachable.\n */\nexport function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {\n const t = labels()\n const [open, setOpen] = useState(false)\n const [route, setRoute] = useState<RouteState | null>(null)\n const [loadFailed, setLoadFailed] = useState(false)\n const [drafts, setDrafts] = useState<Partial<Record<string, string>>>({})\n const [saving, setSaving] = useState(false)\n const [failed, setFailed] = useState<string | null>(null)\n\n useEffect(() => {\n let cancelled = false\n fetch('/lan-gateway/config')\n .then(async (response) => {\n if (cancelled) return\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n setRoute(await response.json() as RouteState)\n })\n .catch(() => {\n if (!cancelled) setLoadFailed(true)\n })\n return () => { cancelled = true }\n }, [])\n\n if (loadFailed) return null\n if (route === null) return null\n\n const { config } = route\n const draftOf = (field: keyof LanGatewaySettings): string =>\n drafts[field] ?? formatValue(FIELDS.find(f => f.field === field)!, config[field])\n\n const stage = (field: string, text: string): void => {\n setDrafts(prev => ({ ...prev, [field]: text }))\n setFailed(null)\n }\n\n const resetField = (def: FieldDef): void => {\n setDrafts(prev => {\n const next = { ...prev }\n delete next[def.field]\n return next\n })\n }\n\n const discard = (): void => {\n setDrafts({})\n setFailed(null)\n }\n\n const invalid = (): boolean =>\n Object.entries(drafts).some(([field, text]) => {\n const def = FIELDS.find(f => f.field === field)\n return def === undefined || parseValue(def, text ?? '') === undefined\n })\n\n const dirty = Object.keys(drafts).length > 0\n\n const save = async (): Promise<void> => {\n if (!dirty || saving || invalid()) return\n setSaving(true)\n setFailed(null)\n try {\n // Build the next full config: the loaded one with drafts applied.\n const next: Record<string, unknown> = {}\n for (const def of FIELDS) {\n const text = drafts[def.field] ?? formatValue(def, config[def.field])\n const write = parseValue(def, text)\n if (write === undefined) continue\n next[def.field] = write.kind === 'clear' ? null : write.value\n }\n const response = await fetch('/lan-gateway/config', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(next),\n })\n const body = await response.json().catch(() => ({})) as Partial<RouteState> & { error?: string }\n if (!response.ok) {\n setFailed(body.error ?? `HTTP ${response.status}`)\n return\n }\n if (body.config !== undefined) {\n setRoute({\n config: body.config,\n running: body.running ?? false,\n port: body.port ?? 0,\n tls: body.tls ?? '',\n lastError: body.lastError ?? null,\n })\n }\n setDrafts({})\n } catch {\n setFailed(t.saveFailed)\n } finally {\n setSaving(false)\n }\n }\n\n const renderControl = (def: FieldDef): ReactNode => {\n const field = def.field\n const label = t[`field.${field}`]\n const hint = t[`hint.${field}`]\n const text = draftOf(field)\n switch (def.kind) {\n case 'boolean':\n return (\n <div style={styles.field}>\n <label style={styles.checkRow}>\n <input\n type=\"checkbox\"\n checked={text === 'true'}\n disabled={saving}\n onChange={(e: ChangeEvent<HTMLInputElement>) =>\n stage(field, e.target.checked ? 'true' : 'false')}\n />\n <span style={styles.label}>{label}</span>\n <button\n type=\"button\"\n style={styles.reset}\n disabled={saving || !drafts[field]}\n onClick={() => resetField(def)}\n >\n {t.reset}\n </button>\n </label>\n <span style={styles.hint}>{hint}</span>\n </div>\n )\n case 'select':\n return (\n <div style={styles.field}>\n <label style={styles.label} htmlFor={`lan-gw-${field}`}>{label}</label>\n <select\n id={`lan-gw-${field}`}\n style={styles.input}\n value={text}\n disabled={saving}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => stage(field, e.target.value)}\n >\n {def.options?.map(option => <option key={option} value={option}>{option}</option>)}\n </select>\n <span style={styles.hint}>{hint}</span>\n <button\n type=\"button\"\n style={styles.reset}\n disabled={saving || !drafts[field]}\n onClick={() => resetField(def)}\n >\n {t.reset}\n </button>\n </div>\n )\n default:\n return (\n <div style={styles.field}>\n <label style={styles.label} htmlFor={`lan-gw-${field}`}>{label}</label>\n <input\n id={`lan-gw-${field}`}\n style={styles.input}\n type={def.kind === 'number' ? 'number' : 'text'}\n value={text}\n disabled={saving}\n placeholder={def.optional ? t.emptyMeansClear : undefined}\n onChange={(e: ChangeEvent<HTMLInputElement>) => stage(field, e.target.value)}\n />\n <span style={styles.hint}>{hint}</span>\n </div>\n )\n }\n }\n\n const statusLine = `${route.running ? t.running : t.stopped} · ${t.tls}: ${route.tls} · :${route.port}`\n\n return (\n <li style={open ? { ...styles.card, ...styles.cardOpen } : styles.card}>\n <button\n type=\"button\"\n style={styles.header}\n aria-expanded={open}\n onClick={() => { setOpen(!open) }}\n >\n <span style={styles.headerTop}>\n <span style={styles.name}>{t.title}</span>\n <span style={styles.status} title={statusLine}>{statusLine}</span>\n {dirty ? <span style={styles.pending}>{t.unsaved}</span> : null}\n <span style={open ? { ...styles.chevron, ...styles.chevronOpen } : styles.chevron}>{open ? '▾' : '▸'}</span>\n </span>\n <span style={styles.description}>{t.description}</span>\n </button>\n {open\n ? (\n <div style={styles.body}>\n {route.lastError ? <p style={styles.error} role=\"status\">{t.lastError}: {route.lastError}</p> : null}\n {FIELDS.map(def => <div key={def.field}>{renderControl(def)}</div>)}\n <div style={styles.footer}>\n {failed ? <p style={styles.error} role=\"status\">{failed}</p> : null}\n <button\n type=\"button\"\n style={styles.discard}\n disabled={!dirty || saving}\n onClick={discard}\n >\n {t.discard}\n </button>\n <button\n type=\"button\"\n style={styles.save}\n disabled={!dirty || invalid() || saving}\n onClick={() => { void save() }}\n >\n {saving ? t.saving : t.save}\n </button>\n </div>\n </div>\n )\n : null}\n </li>\n )\n}\n\n/* ------------------------------------------------------------------ */\n/* Styling — the official DSH theme tokens (light/dark aware), with */\n/* neutral fallbacks so the card never renders black-on-black or */\n/* white-on-white even if a token is missing. */\n/* ------------------------------------------------------------------ */\n\n/** Theme token with a fallback for token-less environments. */\nfunction tk(token: string, fallback: string): string {\n return `var(${token}, ${fallback})`\n}\n\nconst L = {\n border: tk('--dsw-alias-border-l2', 'rgba(127,127,127,0.35)'),\n bg: tk('--dsw-alias-bg-layer-3', 'transparent'),\n bgOpen: tk('--dsw-alias-bg-layer-2', 'transparent'),\n labelPrimary: tk('--dsw-alias-label-primary', 'inherit'),\n labelSecondary: tk('--dsw-alias-label-secondary', 'inherit'),\n labelTertiary: tk('--dsw-alias-label-tertiary', 'rgba(127,127,127,0.8)'),\n labelDimmed: tk('--dsw-alias-label-dimmed', 'rgba(127,127,127,0.6)'),\n error: tk('--dsw-alias-label-error', '#d1242f'),\n brand: tk('--dsw-alias-brand-primary', '#4f6ef7'),\n badgeBg: tk('--dsw-alias-bg-module-platform', 'rgba(127,127,127,0.14)'),\n}\n\nconst styles: Record<string, React.CSSProperties> = {\n card: {\n listStyle: 'none',\n border: `1px solid ${L.border}`,\n borderRadius: '12px',\n background: L.bg,\n transition: 'border-color .16s, background .16s',\n overflow: 'hidden',\n },\n cardOpen: {\n background: L.bgOpen,\n borderColor: L.labelDimmed,\n },\n header: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n gap: '6px',\n width: '100%',\n padding: '14px 16px',\n border: 0,\n background: 'none',\n font: 'inherit',\n color: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n },\n headerTop: { display: 'flex', alignItems: 'center', gap: '12px', width: '100%' },\n name: { flex: '1 1 auto', minWidth: 0, fontSize: '15px', fontWeight: 600, lineHeight: 1.4, color: L.labelPrimary },\n // The status carries a verbose TLS cert summary; cap it and ellipsize so it\n // can never swallow the row or squeeze the title (the old nowrap alone\n // caused the description to be pushed into a thin wrapping column).\n status: {\n flex: '0 1 auto',\n minWidth: 0,\n maxWidth: '60%',\n fontSize: '11px',\n lineHeight: 1.4,\n color: L.labelTertiary,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n },\n description: {\n display: 'block',\n fontSize: '13px',\n lineHeight: 1.5,\n color: L.labelTertiary,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n },\n pending: {\n flex: 'none',\n borderRadius: '999px',\n padding: '1px 8px',\n fontSize: '11px',\n lineHeight: '17px',\n fontWeight: 500,\n whiteSpace: 'nowrap',\n background: L.badgeBg,\n color: L.labelSecondary,\n },\n chevron: { flex: 'none', color: L.labelTertiary, fontSize: '12px', transition: 'transform .16s' },\n chevronOpen: { transform: 'rotate(180deg)' },\n body: {\n borderTop: `1px solid ${L.border}`,\n margin: '0 16px',\n paddingBottom: '8px',\n display: 'flex',\n flexDirection: 'column',\n },\n field: {\n display: 'flex',\n flexDirection: 'column',\n gap: '6px',\n padding: '12px 0',\n },\n label: { fontSize: '13px', fontWeight: 500, lineHeight: 1.5, color: L.labelPrimary },\n hint: { margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.labelTertiary },\n checkRow: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' },\n reset: {\n border: 'none',\n background: 'none',\n padding: 0,\n font: 'inherit',\n fontSize: '12px',\n lineHeight: 1.5,\n color: L.labelSecondary,\n cursor: 'pointer',\n alignSelf: 'flex-start',\n },\n input: {\n height: '34px',\n padding: '0 12px',\n border: `1px solid ${L.border}`,\n borderRadius: '8px',\n background: L.bg,\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n color: L.labelPrimary,\n },\n footer: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: '8px',\n padding: '12px 0 4px',\n borderTop: `1px solid ${L.border}`,\n },\n error: { flex: 1, minWidth: 0, margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.error },\n discard: {\n appearance: 'none',\n border: `1px solid ${L.border}`,\n borderRadius: '8px',\n padding: '5px 14px',\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n background: 'none',\n color: L.labelSecondary,\n cursor: 'pointer',\n },\n save: {\n appearance: 'none',\n border: '1px solid transparent',\n borderRadius: '8px',\n padding: '5px 14px',\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n background: L.labelPrimary,\n color: L.bg,\n cursor: 'pointer',\n },\n}\n","/**\n * @riceawa/dsh-lan-gateway — browser half.\n *\n * Two jobs:\n * 1. Insecure-origin UUID shim: the gateway can serve the GUI over plain HTTP\n * on LAN addresses, where browsers lack `crypto.randomUUID()`. This bundle\n * installs a getRandomValues-backed `randomUUID` on the Crypto prototype at\n * module scope. With TLS enabled the origin is secure and the shim is a\n * no-op.\n * 2. Settings card: registers the LAN gateway card into the official\n * Settings → Plugins page (`settings.plugin.item` slot), editing the\n * `lan-gateway` settings namespace so port, CIDRs, auth, and TLS are\n * adjustable from the GUI.\n */\n\n/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */\nfunction uuidFromRandomValues(): string {\n const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n view.setUint8(6, (view.getUint8(6) & 0x0f) | 0x40)\n view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80)\n const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`\n}\n\n/**\n * Install `randomUUID` on the browser Crypto prototype when the platform\n * lacks it. Idempotent; re-checks every call.\n * @returns `true` when the shim was installed by this call.\n */\nexport function installRandomUuidShim(): boolean {\n const cryptoObj = globalThis.crypto\n if (cryptoObj === undefined) return false\n if (typeof cryptoObj.randomUUID === 'function') return false\n if (typeof cryptoObj.getRandomValues !== 'function') return false\n try {\n // Browsers expose crypto.randomUUID through the Crypto prototype; patching\n // the prototype (not the instance) covers `crypto.randomUUID()` everywhere.\n const proto = Object.getPrototypeOf(cryptoObj) as { randomUUID?: unknown } | null\n if (proto !== null && typeof proto.randomUUID !== 'function') {\n Object.defineProperty(proto, 'randomUUID', {\n value: uuidFromRandomValues,\n writable: true,\n configurable: true,\n })\n return true\n }\n // Fallback: direct instance property (sandboxed/odd environments).\n Object.defineProperty(cryptoObj, 'randomUUID', {\n value: uuidFromRandomValues,\n writable: true,\n configurable: true,\n })\n return true\n } catch {\n return false\n }\n}\n\n// Module scope: the shim is live as soon as this bundle is evaluated, before\n// any RPC/session code runs. apply() re-runs it as a belt-and-braces re-check.\ninstallRandomUuidShim()\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport { LanGatewayCard } from './lan-gateway-card.tsx'\n\nexport const name = 'dsh-lan-gateway'\n\n/** Only the slots service: the card itself is self-loading (ModLens-style). */\nexport const inject = ['slots']\n\n/**\n * Mount the settings card and the UUID shim.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContext): void {\n installRandomUuidShim()\n\n // The card rides the official Plugins → Configurable tab. Like ModLens, it\n // registers with no inject face and fetches its own loopback config route,\n // so it has no settings/locale/connection service dependencies.\n //\n // The `settings.plugin.item` slot is keyed BY the settings namespace the\n // card edits (rc.8 contract): the configurable tab only dispatches entries\n // whose `options.key` is both present and served by the Host's settings\n // describe mirror. Registering with `id` alone throws\n // `keyed slot \"settings.plugin.item\" requires options.key` and the card\n // silently disappears from Settings → Plugins.\n //\n // `id`/`order` ride the legacy list-slot shape (older DSH versions\n // dispatched this slot by id): harmless metadata on the keyed slot, and\n // what keeps the card mounting if this plugin ever loads into an older\n // deployment. Spread from a typed constant so the keyed registration type\n // stays exact.\n const legacyListOptions = { id: 'lan-gateway', order: 30 } as const\n ctx.slots.inject('settings.plugin.item', function* () {\n yield ctx.slots.register({\n name: 'settings.plugin.item',\n key: 'lan-gateway',\n ...legacyListOptions,\n }, LanGatewayCard)\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","names":["useState"],"sources":["../src/client/lan-gateway-card.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * The lan-gateway settings card shown in the official DSH Settings → Plugins\n * page (the `settings.plugin.item` slot).\n *\n * ModLens-style: the card carries NO injected services. It reads and writes\n * the loopback-only `/lan-gateway/config` host route (the browser never sees\n * the settings seam or any secret), so the client bundle's only dependency is\n * the `slots` service that every plugin already has.\n *\n * @module @riceawa/dsh-lan-gateway/client/card\n */\n\nimport { useEffect, useState, type ChangeEvent, type ReactNode } from 'react'\nimport type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\n\n/**\n * The official Settings → Plugins page declares the `settings.plugin.item`\n * slot keyed by the settings namespace each card edits (newer DSH releases;\n * older releases dispatched it as a list slot by `id`). The published package\n * ships no `src/`, so the entry is re-declared here — the runtime slot is\n * real; this only restores the compile-time table.\n */\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface SlotMap {\n /** One plugin's card inside the plugin configuration section. */\n 'settings.plugin.item': { kind: 'keyed'; scope: 'root'; owner: { children?: never } }\n }\n}\n\n/** Props the renderer binds for this card (unused — the card is self-loading). */\nexport type LanGatewayCardProps = PropsRuntime<'settings.plugin.item'>\n\n/** The wire shape of the `lan-gateway` config section. */\nexport interface LanGatewaySettings {\n enabled?: boolean\n gatewayPort?: number\n dshTargetPort?: number\n lanCidrs?: string[]\n lanPasswordless?: boolean\n cookieMaxAgeDays?: number\n tlsEnabled?: boolean\n tlsMode?: 'self-signed' | 'custom'\n tlsCertPath?: string\n tlsKeyPath?: string\n tlsSelfSignedHosts?: string\n tlsCertMaxAgeDays?: number\n allowInsecurePlaintext?: boolean\n trustedTerminator?: string\n secureCookies?: boolean\n}\n\n/** GET /lan-gateway/config response. */\ninterface RouteState {\n config: LanGatewaySettings\n running: boolean\n port: number\n tls: string\n lastError: string | null\n}\n\n/* ------------------------------------------------------------------ */\n/* Bilingual copy (ModLens-style: two small sets, picked by browser) */\n/* ------------------------------------------------------------------ */\n\ninterface Labels {\n title: string\n description: string\n unsaved: string\n save: string\n saving: string\n discard: string\n reset: string\n overridden: string\n readOnly: string\n saveFailed: string\n loadFailed: string\n emptyMeansClear: string\n running: string\n stopped: string\n tls: string\n lastError: string\n [key: `field.${string}`]: string\n [key: `hint.${string}`]: string\n [key: `opt.${string}`]: string\n}\n\nconst LABELS: Record<'zh' | 'en', Labels> = {\n zh: {\n title: 'LAN 网关',\n description: '远程访问开关、端口、TLS 证书、受信网段等网关设置',\n unsaved: '未保存',\n save: '保存',\n saving: '保存中…',\n discard: '放弃',\n reset: '重置',\n overridden: '已覆盖',\n readOnly: '网关设置当前不可用(读不到配置路由)。',\n saveFailed: '保存未生效,请检查输入后重试。',\n loadFailed: '加载网关配置失败。',\n emptyMeansClear: '留空 = 使用默认',\n running: '运行中',\n stopped: '已停止',\n tls: 'TLS',\n lastError: '上次错误',\n 'field.enabled': '启用网关',\n 'hint.enabled': '启动时监听 0.0.0.0 网关端口',\n 'field.gatewayPort': '网关端口',\n 'hint.gatewayPort': '绑定到 0.0.0.0 的监听端口(默认 3081)',\n 'field.dshTargetPort': 'dsh 目标端口',\n 'hint.dshTargetPort': '留空则自动跟随 dsh web 端口(默认 3080)',\n 'field.lanCidrs': '免密 LAN 网段',\n 'hint.lanCidrs': '逗号分隔的 CIDR,如 10.0.0.0/8, 192.168.0.0/16',\n 'field.lanPasswordless': 'LAN 免登录',\n 'hint.lanPasswordless': 'LAN/回环来源跳过网关登录页,但仍共用同一上游会话(需 dsh ≥ 0.1.2)',\n 'field.allowInsecurePlaintext': '允许明文 HTTP',\n 'hint.allowInsecurePlaintext': '危险:关闭 TLS 或受信终止代理时仍启动监听,密码与会话将以明文传输',\n 'field.trustedTerminator': '受信 TLS 终止代理',\n 'hint.trustedTerminator': '可选:声明前置代理标识,视为加密入口(如 nginx)。留空 = 未声明',\n 'field.secureCookies': '会话 cookie 的 Secure 属性',\n 'hint.secureCookies': '自动 = TLS 或已声明受信终止代理时加 Secure。受信代理只做明文鉴权、浏览器走 http 访问时须设为 false,否则浏览器拒收 Secure cookie,登录会无限弹回登录页',\n 'opt.auto': '自动',\n 'opt.true': '始终 Secure',\n 'opt.false': '不加 Secure(明文浏览器入口)',\n 'field.cookieMaxAgeDays': '会话有效期(天)',\n 'hint.cookieMaxAgeDays': '登录 cookie 的存活天数(默认 7)',\n 'field.tlsEnabled': '启用 TLS(HTTPS)',\n 'hint.tlsEnabled': '以 HTTPS 提供网关服务',\n 'field.tlsMode': '证书来源',\n 'hint.tlsMode': 'self-signed = 自动生成自签名证书;custom = 使用自己的证书',\n 'field.tlsSelfSignedHosts': '自签名证书域名/IP',\n 'hint.tlsSelfSignedHosts': '逗号分隔,写入证书 SAN,如 localhost, 192.168.1.5',\n 'field.tlsCertPath': '证书文件路径(custom)',\n 'hint.tlsCertPath': 'PEM 格式证书(或证书链)的绝对路径',\n 'field.tlsKeyPath': '私钥文件路径(custom)',\n 'hint.tlsKeyPath': '与证书配套的 PEM 私钥绝对路径',\n 'field.tlsCertMaxAgeDays': '自签名证书有效期(天)',\n 'hint.tlsCertMaxAgeDays': '默认 825(约 27 个月)',\n },\n en: {\n title: 'LAN Gateway',\n description: 'Remote-access switch, port, TLS certificate, trusted CIDRs and more',\n unsaved: 'Unsaved',\n save: 'Save',\n saving: 'Saving…',\n discard: 'Discard',\n reset: 'Reset',\n overridden: 'overridden',\n readOnly: 'Gateway settings unavailable (config route unreachable).',\n saveFailed: 'The save did not land — check the inputs and retry.',\n loadFailed: 'Failed to load gateway configuration.',\n emptyMeansClear: 'Empty = default',\n running: 'Running',\n stopped: 'Stopped',\n tls: 'TLS',\n lastError: 'Last error',\n 'field.enabled': 'Enable gateway',\n 'hint.enabled': 'Listen on the gateway port at boot',\n 'field.gatewayPort': 'Gateway port',\n 'hint.gatewayPort': 'Port bound on 0.0.0.0 (default 3081)',\n 'field.dshTargetPort': 'dsh target port',\n 'hint.dshTargetPort': 'Leave empty to follow the dsh web port (default 3080)',\n 'field.lanCidrs': 'Password-free LAN CIDRs',\n 'hint.lanCidrs': 'Comma separated CIDRs, e.g. 10.0.0.0/8, 192.168.0.0/16',\n 'field.lanPasswordless': 'LAN skip login',\n 'hint.lanPasswordless': 'LAN/loopback sources skip the gateway login page but still ride one shared upstream session (needs dsh >= 0.1.2)',\n 'field.allowInsecurePlaintext': 'Allow plaintext HTTP',\n 'hint.allowInsecurePlaintext': 'Dangerous: start the listener even without TLS or a trusted terminator; passwords and sessions travel in clear',\n 'field.trustedTerminator': 'Trusted TLS terminator',\n 'hint.trustedTerminator': 'Optional identifier for a front proxy (e.g. nginx) treated as the encrypted ingress. Empty = none declared',\n 'field.secureCookies': 'Session cookie Secure attribute',\n 'hint.secureCookies': 'Auto = Secure when TLS or a trusted terminator is declared. Set false when the trusted proxy only authenticates over plaintext and browsers reach it over http — otherwise browsers drop the Secure cookie and every login bounces back to the login page',\n 'opt.auto': 'Auto',\n 'opt.true': 'Always Secure',\n 'opt.false': 'No Secure (plaintext browser ingress)',\n 'field.cookieMaxAgeDays': 'Session lifetime (days)',\n 'hint.cookieMaxAgeDays': 'Login cookie lifetime (default 7)',\n 'field.tlsEnabled': 'Enable TLS (HTTPS)',\n 'hint.tlsEnabled': 'Serve the gateway over HTTPS',\n 'field.tlsMode': 'Certificate source',\n 'hint.tlsMode': 'self-signed = auto-generated certificate; custom = your own files',\n 'field.tlsSelfSignedHosts': 'Self-signed hosts (SANs)',\n 'hint.tlsSelfSignedHosts': 'Comma separated DNS/IP names, e.g. localhost, 192.168.1.5',\n 'field.tlsCertPath': 'Certificate path (custom)',\n 'hint.tlsCertPath': 'Absolute path to a PEM certificate (or chain)',\n 'field.tlsKeyPath': 'Private key path (custom)',\n 'hint.tlsKeyPath': 'Absolute path to the matching PEM private key',\n 'field.tlsCertMaxAgeDays': 'Self-signed validity (days)',\n 'hint.tlsCertMaxAgeDays': 'Default 825 (about 27 months)',\n },\n}\n\nfunction labels(): Labels {\n const lang = (typeof navigator !== 'undefined' ? navigator.language : 'en').toLowerCase()\n return lang.startsWith('zh') ? LABELS.zh : LABELS.en\n}\n\n/* ------------------------------------------------------------------ */\n/* Field model */\n/* ------------------------------------------------------------------ */\n\ntype FieldKind = 'boolean' | 'number' | 'text' | 'cidrs' | 'select' | 'tristate'\n\ninterface FieldDef {\n field: keyof LanGatewaySettings\n kind: FieldKind\n optional?: boolean\n options?: readonly string[]\n}\n\n/**\n * The card's field table and its two value codecs are exported for tests: the\n * tri-state codec is the load-bearing part of the settings round-trip (an\n * unset value must stay distinguishable from an explicit false, or the\n * plaintext-proxy escape hatch silently reverts).\n */\nexport { TRISTATE_OPTIONS, FIELDS, formatValue, parseValue }\nexport type { FieldDef, Write }\n\n/** The three states of a tri-state field, in display order. */\nconst TRISTATE_OPTIONS = ['auto', 'true', 'false'] as const\n\nconst FIELDS: readonly FieldDef[] = [\n { field: 'enabled', kind: 'boolean' },\n { field: 'gatewayPort', kind: 'number' },\n { field: 'dshTargetPort', kind: 'number', optional: true },\n { field: 'lanCidrs', kind: 'cidrs' },\n { field: 'lanPasswordless', kind: 'boolean' },\n { field: 'cookieMaxAgeDays', kind: 'number' },\n { field: 'tlsEnabled', kind: 'boolean' },\n { field: 'tlsMode', kind: 'select', options: ['self-signed', 'custom'] },\n { field: 'tlsSelfSignedHosts', kind: 'text' },\n { field: 'tlsCertPath', kind: 'text', optional: true },\n { field: 'tlsKeyPath', kind: 'text', optional: true },\n { field: 'tlsCertMaxAgeDays', kind: 'number' },\n { field: 'allowInsecurePlaintext', kind: 'boolean' },\n { field: 'trustedTerminator', kind: 'text', optional: true },\n { field: 'secureCookies', kind: 'tristate' },\n]\n\nfunction formatValue(def: FieldDef, value: unknown): string {\n switch (def.kind) {\n case 'boolean': return value === true ? 'true' : 'false'\n case 'number': return typeof value === 'number' ? String(value) : ''\n case 'cidrs': return Array.isArray(value) ? value.join(', ') : ''\n case 'select': return typeof value === 'string' ? value : (def.options?.[0] ?? '')\n // Tri-state: an unset value is a distinct third state (\"auto\"), never \"false\".\n case 'tristate': return value === true ? 'true' : value === false ? 'false' : 'auto'\n case 'text': return typeof value === 'string' ? value : ''\n }\n}\n\ntype Write = { kind: 'set'; value: unknown } | { kind: 'clear' }\n\n/** Parse draft text into a value for the POST body; undefined blocks saving. */\nfunction parseValue(def: FieldDef, text: string): Write | undefined {\n const trimmed = text.trim()\n switch (def.kind) {\n case 'boolean':\n if (trimmed === 'true') return { kind: 'set', value: true }\n if (trimmed === 'false') return { kind: 'set', value: false }\n return undefined\n case 'number':\n if (trimmed === '') return def.optional ? { kind: 'clear' } : undefined\n if (!/^\\d+$/.test(trimmed)) return undefined\n return { kind: 'set', value: Number(trimmed) }\n case 'cidrs': {\n const cidrs = trimmed.split(',').map(s => s.trim()).filter(s => s !== '')\n return cidrs.length === 0 ? { kind: 'clear' } : { kind: 'set', value: cidrs }\n }\n case 'select':\n return def.options?.includes(trimmed) ? { kind: 'set', value: trimmed } : undefined\n case 'tristate':\n // 'auto' clears the key so it re-inherits the composition layer (and the\n // resolution rule), which is what an unset tri-state means.\n if (trimmed === 'auto') return { kind: 'clear' }\n if (trimmed === 'true') return { kind: 'set', value: true }\n if (trimmed === 'false') return { kind: 'set', value: false }\n return undefined\n case 'text':\n return trimmed === '' ? (def.optional ? { kind: 'clear' } : undefined) : { kind: 'set', value: trimmed }\n }\n}\n\n/* ------------------------------------------------------------------ */\n/* Card */\n/* ------------------------------------------------------------------ */\n\n/**\n * Render the LAN gateway card. Self-loading: fetches the config route on\n * mount, posts the edited config on save.\n * @param _props - unused; the card needs no injected face.\n * @returns the card, or nothing while the route is unreachable.\n */\nexport function LanGatewayCard(_props: LanGatewayCardProps): ReactNode {\n const t = labels()\n const [open, setOpen] = useState(false)\n const [route, setRoute] = useState<RouteState | null>(null)\n const [loadFailed, setLoadFailed] = useState(false)\n const [drafts, setDrafts] = useState<Partial<Record<string, string>>>({})\n const [saving, setSaving] = useState(false)\n const [failed, setFailed] = useState<string | null>(null)\n\n useEffect(() => {\n let cancelled = false\n fetch('/lan-gateway/config')\n .then(async (response) => {\n if (cancelled) return\n if (!response.ok) throw new Error(`HTTP ${response.status}`)\n setRoute(await response.json() as RouteState)\n })\n .catch(() => {\n if (!cancelled) setLoadFailed(true)\n })\n return () => { cancelled = true }\n }, [])\n\n if (loadFailed) return null\n if (route === null) return null\n\n const { config } = route\n const draftOf = (field: keyof LanGatewaySettings): string =>\n drafts[field] ?? formatValue(FIELDS.find(f => f.field === field)!, config[field])\n\n const stage = (field: string, text: string): void => {\n setDrafts(prev => ({ ...prev, [field]: text }))\n setFailed(null)\n }\n\n const resetField = (def: FieldDef): void => {\n setDrafts(prev => {\n const next = { ...prev }\n delete next[def.field]\n return next\n })\n }\n\n const discard = (): void => {\n setDrafts({})\n setFailed(null)\n }\n\n const invalid = (): boolean =>\n Object.entries(drafts).some(([field, text]) => {\n const def = FIELDS.find(f => f.field === field)\n return def === undefined || parseValue(def, text ?? '') === undefined\n })\n\n const dirty = Object.keys(drafts).length > 0\n\n const save = async (): Promise<void> => {\n if (!dirty || saving || invalid()) return\n setSaving(true)\n setFailed(null)\n try {\n // Build the next full config: the loaded one with drafts applied.\n const next: Record<string, unknown> = {}\n for (const def of FIELDS) {\n const text = drafts[def.field] ?? formatValue(def, config[def.field])\n const write = parseValue(def, text)\n if (write === undefined) continue\n next[def.field] = write.kind === 'clear' ? null : write.value\n }\n const response = await fetch('/lan-gateway/config', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(next),\n })\n const body = await response.json().catch(() => ({})) as Partial<RouteState> & { error?: string }\n if (!response.ok) {\n setFailed(body.error ?? `HTTP ${response.status}`)\n return\n }\n if (body.config !== undefined) {\n setRoute({\n config: body.config,\n running: body.running ?? false,\n port: body.port ?? 0,\n tls: body.tls ?? '',\n lastError: body.lastError ?? null,\n })\n }\n setDrafts({})\n } catch {\n setFailed(t.saveFailed)\n } finally {\n setSaving(false)\n }\n }\n\n const renderControl = (def: FieldDef): ReactNode => {\n const field = def.field\n const label = t[`field.${field}`]\n const hint = t[`hint.${field}`]\n const text = draftOf(field)\n switch (def.kind) {\n case 'boolean':\n return (\n <div style={styles.field}>\n <label style={styles.checkRow}>\n <input\n type=\"checkbox\"\n checked={text === 'true'}\n disabled={saving}\n onChange={(e: ChangeEvent<HTMLInputElement>) =>\n stage(field, e.target.checked ? 'true' : 'false')}\n />\n <span style={styles.label}>{label}</span>\n <button\n type=\"button\"\n style={styles.reset}\n disabled={saving || !drafts[field]}\n onClick={() => resetField(def)}\n >\n {t.reset}\n </button>\n </label>\n <span style={styles.hint}>{hint}</span>\n </div>\n )\n case 'select':\n case 'tristate': {\n // A tri-state renders as a three-way select because neither a checkbox\n // (cannot express \"unset\") nor a text field (cannot express false)\n // distinguishes auto from an explicit false.\n const options = def.kind === 'tristate' ? TRISTATE_OPTIONS : (def.options ?? [])\n return (\n <div style={styles.field}>\n <label style={styles.label} htmlFor={`lan-gw-${field}`}>{label}</label>\n <select\n id={`lan-gw-${field}`}\n style={styles.input}\n value={text}\n disabled={saving}\n onChange={(e: ChangeEvent<HTMLSelectElement>) => stage(field, e.target.value)}\n >\n {options.map(option => (\n <option key={option} value={option}>\n {def.kind === 'tristate' ? t[`opt.${option}`] : option}\n </option>\n ))}\n </select>\n <span style={styles.hint}>{hint}</span>\n <button\n type=\"button\"\n style={styles.reset}\n disabled={saving || !drafts[field]}\n onClick={() => resetField(def)}\n >\n {t.reset}\n </button>\n </div>\n )\n }\n default:\n return (\n <div style={styles.field}>\n <label style={styles.label} htmlFor={`lan-gw-${field}`}>{label}</label>\n <input\n id={`lan-gw-${field}`}\n style={styles.input}\n type={def.kind === 'number' ? 'number' : 'text'}\n value={text}\n disabled={saving}\n placeholder={def.optional ? t.emptyMeansClear : undefined}\n onChange={(e: ChangeEvent<HTMLInputElement>) => stage(field, e.target.value)}\n />\n <span style={styles.hint}>{hint}</span>\n </div>\n )\n }\n }\n\n const statusLine = `${route.running ? t.running : t.stopped} · ${t.tls}: ${route.tls} · :${route.port}`\n\n return (\n <li style={open ? { ...styles.card, ...styles.cardOpen } : styles.card}>\n <button\n type=\"button\"\n style={styles.header}\n aria-expanded={open}\n onClick={() => { setOpen(!open) }}\n >\n <span style={styles.headerTop}>\n <span style={styles.name}>{t.title}</span>\n <span style={styles.status} title={statusLine}>{statusLine}</span>\n {dirty ? <span style={styles.pending}>{t.unsaved}</span> : null}\n <span style={open ? { ...styles.chevron, ...styles.chevronOpen } : styles.chevron}>{open ? '▾' : '▸'}</span>\n </span>\n <span style={styles.description}>{t.description}</span>\n </button>\n {open\n ? (\n <div style={styles.body}>\n {route.lastError ? <p style={styles.error} role=\"status\">{t.lastError}: {route.lastError}</p> : null}\n {FIELDS.map(def => <div key={def.field}>{renderControl(def)}</div>)}\n <div style={styles.footer}>\n {failed ? <p style={styles.error} role=\"status\">{failed}</p> : null}\n <button\n type=\"button\"\n style={styles.discard}\n disabled={!dirty || saving}\n onClick={discard}\n >\n {t.discard}\n </button>\n <button\n type=\"button\"\n style={styles.save}\n disabled={!dirty || invalid() || saving}\n onClick={() => { void save() }}\n >\n {saving ? t.saving : t.save}\n </button>\n </div>\n </div>\n )\n : null}\n </li>\n )\n}\n\n/* ------------------------------------------------------------------ */\n/* Styling — the official DSH theme tokens (light/dark aware), with */\n/* neutral fallbacks so the card never renders black-on-black or */\n/* white-on-white even if a token is missing. */\n/* ------------------------------------------------------------------ */\n\n/** Theme token with a fallback for token-less environments. */\nfunction tk(token: string, fallback: string): string {\n return `var(${token}, ${fallback})`\n}\n\nconst L = {\n border: tk('--dsw-alias-border-l2', 'rgba(127,127,127,0.35)'),\n bg: tk('--dsw-alias-bg-layer-3', 'transparent'),\n bgOpen: tk('--dsw-alias-bg-layer-2', 'transparent'),\n labelPrimary: tk('--dsw-alias-label-primary', 'inherit'),\n labelSecondary: tk('--dsw-alias-label-secondary', 'inherit'),\n labelTertiary: tk('--dsw-alias-label-tertiary', 'rgba(127,127,127,0.8)'),\n labelDimmed: tk('--dsw-alias-label-dimmed', 'rgba(127,127,127,0.6)'),\n error: tk('--dsw-alias-label-error', '#d1242f'),\n brand: tk('--dsw-alias-brand-primary', '#4f6ef7'),\n badgeBg: tk('--dsw-alias-bg-module-platform', 'rgba(127,127,127,0.14)'),\n}\n\nconst styles: Record<string, React.CSSProperties> = {\n card: {\n listStyle: 'none',\n border: `1px solid ${L.border}`,\n borderRadius: '12px',\n background: L.bg,\n transition: 'border-color .16s, background .16s',\n overflow: 'hidden',\n },\n cardOpen: {\n background: L.bgOpen,\n borderColor: L.labelDimmed,\n },\n header: {\n display: 'flex',\n flexDirection: 'column',\n alignItems: 'stretch',\n gap: '6px',\n width: '100%',\n padding: '14px 16px',\n border: 0,\n background: 'none',\n font: 'inherit',\n color: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n },\n headerTop: { display: 'flex', alignItems: 'center', gap: '12px', width: '100%' },\n name: { flex: '1 1 auto', minWidth: 0, fontSize: '15px', fontWeight: 600, lineHeight: 1.4, color: L.labelPrimary },\n // The status carries a verbose TLS cert summary; cap it and ellipsize so it\n // can never swallow the row or squeeze the title (the old nowrap alone\n // caused the description to be pushed into a thin wrapping column).\n status: {\n flex: '0 1 auto',\n minWidth: 0,\n maxWidth: '60%',\n fontSize: '11px',\n lineHeight: 1.4,\n color: L.labelTertiary,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n },\n description: {\n display: 'block',\n fontSize: '13px',\n lineHeight: 1.5,\n color: L.labelTertiary,\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis',\n },\n pending: {\n flex: 'none',\n borderRadius: '999px',\n padding: '1px 8px',\n fontSize: '11px',\n lineHeight: '17px',\n fontWeight: 500,\n whiteSpace: 'nowrap',\n background: L.badgeBg,\n color: L.labelSecondary,\n },\n chevron: { flex: 'none', color: L.labelTertiary, fontSize: '12px', transition: 'transform .16s' },\n chevronOpen: { transform: 'rotate(180deg)' },\n body: {\n borderTop: `1px solid ${L.border}`,\n margin: '0 16px',\n paddingBottom: '8px',\n display: 'flex',\n flexDirection: 'column',\n },\n field: {\n display: 'flex',\n flexDirection: 'column',\n gap: '6px',\n padding: '12px 0',\n },\n label: { fontSize: '13px', fontWeight: 500, lineHeight: 1.5, color: L.labelPrimary },\n hint: { margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.labelTertiary },\n checkRow: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' },\n reset: {\n border: 'none',\n background: 'none',\n padding: 0,\n font: 'inherit',\n fontSize: '12px',\n lineHeight: 1.5,\n color: L.labelSecondary,\n cursor: 'pointer',\n alignSelf: 'flex-start',\n },\n input: {\n height: '34px',\n padding: '0 12px',\n border: `1px solid ${L.border}`,\n borderRadius: '8px',\n background: L.bg,\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n color: L.labelPrimary,\n },\n footer: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'flex-end',\n gap: '8px',\n padding: '12px 0 4px',\n borderTop: `1px solid ${L.border}`,\n },\n error: { flex: 1, minWidth: 0, margin: 0, fontSize: '12px', lineHeight: 1.5, color: L.error },\n discard: {\n appearance: 'none',\n border: `1px solid ${L.border}`,\n borderRadius: '8px',\n padding: '5px 14px',\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n background: 'none',\n color: L.labelSecondary,\n cursor: 'pointer',\n },\n save: {\n appearance: 'none',\n border: '1px solid transparent',\n borderRadius: '8px',\n padding: '5px 14px',\n font: 'inherit',\n fontSize: '13px',\n lineHeight: 1.5,\n background: L.labelPrimary,\n color: L.bg,\n cursor: 'pointer',\n },\n}\n","/**\n * @riceawa/dsh-lan-gateway — browser half.\n *\n * Two jobs:\n * 1. Insecure-origin UUID shim: the gateway can serve the GUI over plain HTTP\n * on LAN addresses, where browsers lack `crypto.randomUUID()`. This bundle\n * installs a getRandomValues-backed `randomUUID` on the Crypto prototype at\n * module scope. With TLS enabled the origin is secure and the shim is a\n * no-op.\n * 2. Settings card: registers the LAN gateway card into the official\n * Settings → Plugins page (`settings.plugin.item` slot), editing the\n * `lan-gateway` settings namespace so port, CIDRs, auth, and TLS are\n * adjustable from the GUI.\n */\n\n/** RFC 4122 v4 UUID from crypto.getRandomValues (available on insecure origins). */\nfunction uuidFromRandomValues(): string {\n const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16))\n const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n view.setUint8(6, (view.getUint8(6) & 0x0f) | 0x40)\n view.setUint8(8, (view.getUint8(8) & 0x3f) | 0x80)\n const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('')\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`\n}\n\n/**\n * Install `randomUUID` on the browser Crypto prototype when the platform\n * lacks it. Idempotent; re-checks every call.\n * @returns `true` when the shim was installed by this call.\n */\nexport function installRandomUuidShim(): boolean {\n const cryptoObj = globalThis.crypto\n if (cryptoObj === undefined) return false\n if (typeof cryptoObj.randomUUID === 'function') return false\n if (typeof cryptoObj.getRandomValues !== 'function') return false\n try {\n // Browsers expose crypto.randomUUID through the Crypto prototype; patching\n // the prototype (not the instance) covers `crypto.randomUUID()` everywhere.\n const proto = Object.getPrototypeOf(cryptoObj) as { randomUUID?: unknown } | null\n if (proto !== null && typeof proto.randomUUID !== 'function') {\n Object.defineProperty(proto, 'randomUUID', {\n value: uuidFromRandomValues,\n writable: true,\n configurable: true,\n })\n return true\n }\n // Fallback: direct instance property (sandboxed/odd environments).\n Object.defineProperty(cryptoObj, 'randomUUID', {\n value: uuidFromRandomValues,\n writable: true,\n configurable: true,\n })\n return true\n } catch {\n return false\n }\n}\n\n// Module scope: the shim is live as soon as this bundle is evaluated, before\n// any RPC/session code runs. apply() re-runs it as a belt-and-braces re-check.\ninstallRandomUuidShim()\n\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport { LanGatewayCard } from './lan-gateway-card.tsx'\n\nexport const name = 'dsh-lan-gateway'\n\n/** Only the slots service: the card itself is self-loading (ModLens-style). */\nexport const inject = ['slots']\n\n/**\n * Mount the settings card and the UUID shim.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContext): void {\n installRandomUuidShim()\n\n // The card rides the official Plugins → Configurable tab. Like ModLens, it\n // registers with no inject face and fetches its own loopback config route,\n // so it has no settings/locale/connection service dependencies.\n //\n // The `settings.plugin.item` slot is keyed BY the settings namespace the\n // card edits (rc.8 contract): the configurable tab only dispatches entries\n // whose `options.key` is both present and served by the Host's settings\n // describe mirror. Registering with `id` alone throws\n // `keyed slot \"settings.plugin.item\" requires options.key` and the card\n // silently disappears from Settings → Plugins.\n //\n // `id`/`order` ride the legacy list-slot shape (older DSH versions\n // dispatched this slot by id): harmless metadata on the keyed slot, and\n // what keeps the card mounting if this plugin ever loads into an older\n // deployment. Spread from a typed constant so the keyed registration type\n // stays exact.\n const legacyListOptions = { id: 'lan-gateway', order: 30 } as const\n ctx.slots.inject('settings.plugin.item', function* () {\n yield ctx.slots.register({\n name: 'settings.plugin.item',\n key: 'lan-gateway',\n ...legacyListOptions,\n }, LanGatewayCard)\n })\n}\n"],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -90,6 +90,14 @@ interface Config {
90
90
  * encrypted-ingress gate) without this listener sending HSTS.
91
91
  */
92
92
  trustedTerminator?: string;
93
+ /**
94
+ * Explicit override for the session cookie's `Secure` attribute. Unset =
95
+ * automatic: Secure when the gateway serves TLS itself or a
96
+ * `trustedTerminator` is declared. Set `false` when the trusted proxy fronts
97
+ * a plaintext browser ingress — browsers refuse to store a Secure cookie over
98
+ * plain HTTP, so every login would bounce straight back to `/__login`.
99
+ */
100
+ secureCookies?: boolean;
93
101
  }
94
102
  /** Schemastery configuration validated by the Loader. */
95
103
  declare const Config: z<Config>;
@@ -104,6 +112,18 @@ interface StartFacts {
104
112
  * migration at once. Exported for tests.
105
113
  */
106
114
  declare function gatewayStartProblems(cfg: Config, facts: StartFacts): string[];
115
+ /**
116
+ * Resolve the effective `Secure` attribute for the session cookie: an explicit
117
+ * `secureCookies` always wins; unset falls back to automatic — Secure when the
118
+ * gateway terminates TLS itself or a trusted terminator is declared. The
119
+ * override exists for a trusted proxy that authenticates users but speaks plain
120
+ * HTTP to browsers: `encryptedIngress` is a fair proxy for "a proxy is in front"
121
+ * but not for "the browser leg is encrypted", and a Secure cookie on a plain
122
+ * HTTP origin is silently dropped, looping the login.
123
+ *
124
+ * Exported for tests.
125
+ */
126
+ declare function resolveSecureCookies(cfg: Pick<Config, 'secureCookies' | 'tlsEnabled' | 'trustedTerminator'>): boolean;
107
127
  /**
108
128
  * Same-origin loopback fence for the native `/lan-gateway/config` route. The
109
129
  * gateway refuses to relay this prefix, so the only way in is the native
@@ -115,4 +135,4 @@ declare function gatewayStartProblems(cfg: Config, facts: StartFacts): string[];
115
135
  declare function isTrustedConfigRequest(req: IncomingMessage): boolean;
116
136
  declare function apply(ctx: Context, config: Config): void;
117
137
  //#endregion
118
- export { Config, GatewayController, StartFacts, ToolResult, UpstreamConnectionSurface, WebServerSurface, apply, gatewayStartProblems, inject, isTrustedConfigRequest, name };
138
+ export { Config, GatewayController, StartFacts, ToolResult, UpstreamConnectionSurface, WebServerSurface, apply, gatewayStartProblems, inject, isTrustedConfigRequest, name, resolveSecureCookies };
package/lib/index.js CHANGED
@@ -1155,6 +1155,22 @@ function nameValueOnly(setCookie) {
1155
1155
  const semi = setCookie.indexOf(";");
1156
1156
  return (semi === -1 ? setCookie : setCookie.slice(0, semi)).trim();
1157
1157
  }
1158
+ /** The cookie name of a `Set-Cookie` string (`''` when it is malformed). */
1159
+ function cookieNameOf(setCookie) {
1160
+ const eq = setCookie.indexOf("=");
1161
+ return eq === -1 ? "" : setCookie.slice(0, eq).trim();
1162
+ }
1163
+ /**
1164
+ * Whether a `Set-Cookie` string is the upstream browser-session cookie. The
1165
+ * name upstream mints is `dsh-auth-<base64url(sha256(authority))>`: the prefix
1166
+ * is followed by the authority hash, never by `=` itself, so the test is a
1167
+ * prefix plus at least one character — matching on `dsh-auth-=` finds nothing
1168
+ * and silently relays every request anonymously.
1169
+ */
1170
+ function isUpstreamSessionCookie(setCookie) {
1171
+ const name = cookieNameOf(setCookie);
1172
+ return name.startsWith(UPSTREAM_COOKIE_PREFIX) && name.length > 9;
1173
+ }
1158
1174
  /** Pull the Max-Age attribute (seconds) out of a Set-Cookie string, if any. */
1159
1175
  function maxAgeSeconds(setCookie) {
1160
1176
  const match = /\bMax-Age=(\d+)\b/i.exec(setCookie);
@@ -1167,12 +1183,13 @@ function maxAgeSeconds(setCookie) {
1167
1183
  * or no session cookie came back — e.g. an older base without browser
1168
1184
  * sessions).
1169
1185
  */
1170
- function exchange(url, authority, port) {
1186
+ function exchange(url, authority, port, log) {
1171
1187
  return new Promise((resolve) => {
1172
1188
  let target;
1173
1189
  try {
1174
1190
  target = new URL(url);
1175
1191
  } catch {
1192
+ log(`exchange: unparseable authenticatedUrl ${url}`);
1176
1193
  resolve(void 0);
1177
1194
  return;
1178
1195
  }
@@ -1189,23 +1206,34 @@ function exchange(url, authority, port) {
1189
1206
  const setCookies = response.headers["set-cookie"];
1190
1207
  response.resume();
1191
1208
  if (setCookies === void 0) {
1209
+ log(`exchange ${target.pathname} -> ${response.statusCode} (no set-cookie)`);
1192
1210
  resolve(void 0);
1193
1211
  return;
1194
1212
  }
1195
- const raw = (Array.isArray(setCookies) ? setCookies : [setCookies]).find((value) => value.startsWith(`${UPSTREAM_COOKIE_PREFIX}=`));
1213
+ const all = Array.isArray(setCookies) ? setCookies : [setCookies];
1214
+ const raw = all.find(isUpstreamSessionCookie);
1196
1215
  if (raw === void 0) {
1216
+ const names = all.map(cookieNameOf).filter((name) => name !== "");
1217
+ log(`exchange ${target.pathname} -> ${response.statusCode} (no ${UPSTREAM_COOKIE_PREFIX}* cookie; got: ${names.join(", ") || "none"})`);
1197
1218
  resolve(void 0);
1198
1219
  return;
1199
1220
  }
1200
1221
  const header = nameValueOnly(raw);
1201
1222
  const maxAge = maxAgeSeconds(raw);
1223
+ log(`exchange ${target.pathname} -> ${response.statusCode} (got ${cookieNameOf(raw)}, maxAge=${maxAge ?? "n/a"})`);
1202
1224
  resolve({
1203
1225
  header,
1204
1226
  expiresAt: Date.now() + (maxAge ?? 0) * 1e3
1205
1227
  });
1206
1228
  });
1207
- request.on("error", () => resolve(void 0));
1208
- request.setTimeout(5e3, () => request.destroy(/* @__PURE__ */ new Error("upstream-session exchange timeout")));
1229
+ request.on("error", (error) => {
1230
+ log(`exchange error: ${error.message}`);
1231
+ resolve(void 0);
1232
+ });
1233
+ request.setTimeout(5e3, () => {
1234
+ log("exchange timeout (5s)");
1235
+ request.destroy(/* @__PURE__ */ new Error("upstream-session exchange timeout"));
1236
+ });
1209
1237
  request.end();
1210
1238
  });
1211
1239
  }
@@ -1218,12 +1246,14 @@ var UpstreamSessionRelay = class {
1218
1246
  port;
1219
1247
  authority;
1220
1248
  authenticatedUrl;
1249
+ log;
1221
1250
  held;
1222
1251
  inflight;
1223
1252
  constructor(options) {
1224
1253
  this.port = options.port;
1225
1254
  this.authority = options.authority ?? `127.0.0.1:${options.port}`;
1226
1255
  this.authenticatedUrl = options.authenticatedUrl;
1256
+ this.log = options.log ?? (() => {});
1227
1257
  }
1228
1258
  /** Whether the held session is still comfortably inside its lifetime. */
1229
1259
  fresh() {
@@ -1235,6 +1265,7 @@ var UpstreamSessionRelay = class {
1235
1265
  return this.held?.header;
1236
1266
  }
1237
1267
  invalidate() {
1268
+ if (this.held !== void 0) this.log("invalidating held session (upstream rejected it)");
1238
1269
  this.held = void 0;
1239
1270
  }
1240
1271
  async cookie() {
@@ -1251,9 +1282,16 @@ var UpstreamSessionRelay = class {
1251
1282
  }
1252
1283
  async doExchange() {
1253
1284
  const url = this.authenticatedUrl();
1254
- if (url === void 0) return void 0;
1255
- const result = await exchange(url, this.authority, this.port);
1256
- if (result !== void 0) this.held = result;
1285
+ if (url === void 0) {
1286
+ this.log("authenticatedUrl() returned undefined; keeping current session");
1287
+ return this.held?.header;
1288
+ }
1289
+ this.log(`acquiring session from ${url}`);
1290
+ const result = await exchange(url, this.authority, this.port, this.log);
1291
+ if (result !== void 0) {
1292
+ this.held = result;
1293
+ this.log("session acquired and cached");
1294
+ } else this.log("exchange failed; keeping current session");
1257
1295
  return this.held?.header;
1258
1296
  }
1259
1297
  };
@@ -1294,7 +1332,8 @@ const Config = z.object({
1294
1332
  tlsSelfSignedHosts: z.string().default("localhost"),
1295
1333
  tlsCertMaxAgeDays: z.natural().min(1).max(3650).default(825),
1296
1334
  allowInsecurePlaintext: z.boolean().default(false),
1297
- trustedTerminator: z.string()
1335
+ trustedTerminator: z.string(),
1336
+ secureCookies: z.boolean()
1298
1337
  });
1299
1338
  /**
1300
1339
  * The fail-closed problems that prevent a config from enabling the listener.
@@ -1308,6 +1347,21 @@ function gatewayStartProblems(cfg, facts) {
1308
1347
  if (!(cfg.tlsEnabled || cfg.trustedTerminator !== void 0) && !cfg.allowInsecurePlaintext) problems.push("Refusing to serve over plaintext HTTP: enable TLS (tlsEnabled: true), declare a trusted TLS-terminating proxy (trustedTerminator), or set allowInsecurePlaintext: true to accept the plaintext exposure (passwords and sessions would travel in clear).");
1309
1348
  return problems;
1310
1349
  }
1350
+ /**
1351
+ * Resolve the effective `Secure` attribute for the session cookie: an explicit
1352
+ * `secureCookies` always wins; unset falls back to automatic — Secure when the
1353
+ * gateway terminates TLS itself or a trusted terminator is declared. The
1354
+ * override exists for a trusted proxy that authenticates users but speaks plain
1355
+ * HTTP to browsers: `encryptedIngress` is a fair proxy for "a proxy is in front"
1356
+ * but not for "the browser leg is encrypted", and a Secure cookie on a plain
1357
+ * HTTP origin is silently dropped, looping the login.
1358
+ *
1359
+ * Exported for tests.
1360
+ */
1361
+ function resolveSecureCookies(cfg) {
1362
+ if (typeof cfg.secureCookies === "boolean") return cfg.secureCookies;
1363
+ return cfg.tlsEnabled || cfg.trustedTerminator !== void 0;
1364
+ }
1311
1365
  /** Resolve the TLS material for a config, or undefined when TLS is off. */
1312
1366
  function resolveTls(cfg) {
1313
1367
  if (!cfg.tlsEnabled) return void 0;
@@ -1320,8 +1374,15 @@ function resolveTls(cfg) {
1320
1374
  });
1321
1375
  return material;
1322
1376
  }
1323
- /** Config fields that require a listener restart when they change. */
1324
- function listenerKey(cfg) {
1377
+ /**
1378
+ * Config fields that require a listener restart when they change, plus whether
1379
+ * a shared upstream session relay is available at all. The relay flag belongs
1380
+ * in the key: a listener that started before the `connection` service appeared
1381
+ * was built without a relay and must restart once the service attaches,
1382
+ * otherwise it silently forwards every request anonymously (the upstream 401s)
1383
+ * while the status line still claims the relay is active.
1384
+ */
1385
+ function listenerKey(cfg, relayAvailable) {
1325
1386
  return JSON.stringify([
1326
1387
  cfg.gatewayPort,
1327
1388
  cfg.dshTargetPort,
@@ -1336,7 +1397,9 @@ function listenerKey(cfg) {
1336
1397
  cfg.tlsSelfSignedHosts,
1337
1398
  cfg.tlsCertMaxAgeDays,
1338
1399
  cfg.allowInsecurePlaintext,
1339
- cfg.trustedTerminator
1400
+ cfg.trustedTerminator,
1401
+ cfg.secureCookies,
1402
+ relayAvailable
1340
1403
  ]);
1341
1404
  }
1342
1405
  /** One-line TLS description for status output. */
@@ -1413,6 +1476,7 @@ function apply(ctx, config) {
1413
1476
  const dshPort = cfg.dshTargetPort ?? ctx.webServer.port;
1414
1477
  const tls = resolveTls(cfg);
1415
1478
  const encryptedIngress = cfg.tlsEnabled || cfg.trustedTerminator !== void 0;
1479
+ const secureCookies = resolveSecureCookies(cfg);
1416
1480
  const next = new LanGateway({
1417
1481
  gatewayPort: cfg.gatewayPort,
1418
1482
  dshPort,
@@ -1420,13 +1484,13 @@ function apply(ctx, config) {
1420
1484
  lanPasswordless: cfg.lanPasswordless,
1421
1485
  cookieMaxAgeDays: cfg.cookieMaxAgeDays,
1422
1486
  cookieName: cfg.cookieName,
1423
- secureCookies: encryptedIngress,
1487
+ secureCookies,
1424
1488
  ...tls !== void 0 ? { tls } : {},
1425
1489
  ...makeRelay !== void 0 ? { upstreamSession: makeRelay(dshPort) } : {}
1426
1490
  }, state);
1427
1491
  await next.listen();
1428
1492
  gateway = next;
1429
- startedWith = listenerKey(cfg);
1493
+ startedWith = listenerKey(cfg, makeRelay !== void 0);
1430
1494
  ctx.logger.info(`dsh-lan-gateway: listening on 0.0.0.0:${cfg.gatewayPort}${tls !== void 0 ? " (TLS)" : ""} -> 127.0.0.1:${dshPort}${encryptedIngress ? "" : " (plaintext, explicit allowInsecurePlaintext)"}${makeRelay !== void 0 ? " [shared upstream session relay]" : " [no upstream session relay: base has no browser-session auth]"}`);
1431
1495
  };
1432
1496
  const stopGateway = async () => {
@@ -1448,7 +1512,7 @@ function apply(ctx, config) {
1448
1512
  if (gateway === void 0) {
1449
1513
  if (shouldRun) await startGateway(cfg);
1450
1514
  } else if (!shouldRun) await stopGateway();
1451
- else if (startedWith !== listenerKey(cfg)) {
1515
+ else if (startedWith !== listenerKey(cfg, makeRelay !== void 0)) {
1452
1516
  await stopGateway();
1453
1517
  await startGateway(cfg);
1454
1518
  }
@@ -1475,9 +1539,11 @@ function apply(ctx, config) {
1475
1539
  });
1476
1540
  ctx.inject(["connection"], (ccx) => {
1477
1541
  upstreamSessionAvailable = true;
1542
+ ctx.logger.info("dsh-lan-gateway: connection service attached; upstream session relay enabled");
1478
1543
  makeRelay = (dshPort) => new UpstreamSessionRelay({
1479
1544
  port: dshPort,
1480
- authenticatedUrl: () => ccx.connection.authenticatedUrl(`http://127.0.0.1:${dshPort}`)
1545
+ authenticatedUrl: () => ccx.connection.authenticatedUrl(`http://127.0.0.1:${dshPort}`),
1546
+ log: (message) => ctx.logger.info(`dsh-lan-gateway relay: ${message}`)
1481
1547
  });
1482
1548
  syncGateway("connection attach");
1483
1549
  });
@@ -1570,7 +1636,7 @@ function apply(ctx, config) {
1570
1636
  const encrypted = cfg.tlsEnabled || cfg.trustedTerminator !== void 0;
1571
1637
  return {
1572
1638
  ok: true,
1573
- message: `LAN gateway: ${gateway !== void 0 ? `LISTENING on 0.0.0.0:${cfg.gatewayPort}` : "stopped"}\n- dsh target: 127.0.0.1:${dshPort}\n- password: ${state.password !== void 0 ? "set" : "NOT SET"}\n- login required for all sources: true${cfg.lanPasswordless ? " (LAN/loopback exempt via lanPasswordless)" : ""}\n- session epoch: ${state.sessionEpoch}\n- upstream session relay: ${upstreamSessionAvailable ? "active (dsh browser-session auth present)" : "absent (older dsh base)"}\n- ingress: ${cfg.tlsEnabled ? `TLS (${tlsStatusLine(cfg)})` : cfg.trustedTerminator !== void 0 ? `TLS terminated by trusted proxy (${cfg.trustedTerminator})` : encrypted ? "encrypted" : cfg.allowInsecurePlaintext ? "PLAINTEXT (explicit allowInsecurePlaintext)" : "plaintext — will not start"}\n- session cookie: ${cfg.cookieName}, ${cfg.cookieMaxAgeDays}d` + (manualOverride !== void 0 ? `\n- manual override: ${manualOverride ? "enabled" : "disabled"}` : "") + (lastError !== void 0 ? `\n- last error: ${lastError}` : "")
1639
+ message: `LAN gateway: ${gateway !== void 0 ? `LISTENING on 0.0.0.0:${cfg.gatewayPort}` : "stopped"}\n- dsh target: 127.0.0.1:${dshPort}\n- password: ${state.password !== void 0 ? "set" : "NOT SET"}\n- login required for all sources: true${cfg.lanPasswordless ? " (LAN/loopback exempt via lanPasswordless)" : ""}\n- session epoch: ${state.sessionEpoch}\n- upstream session relay: ${upstreamSessionAvailable ? "active (dsh browser-session auth present)" : "absent (older dsh base)"}\n- ingress: ${cfg.tlsEnabled ? `TLS (${tlsStatusLine(cfg)})` : cfg.trustedTerminator !== void 0 ? `trusted proxy (${cfg.trustedTerminator}, ${resolveSecureCookies(cfg) ? "TLS" : "plaintext"} browser ingress)` : encrypted ? "encrypted" : cfg.allowInsecurePlaintext ? "PLAINTEXT (explicit allowInsecurePlaintext)" : "plaintext — will not start"}\n- session cookie: ${cfg.cookieName}, ${cfg.cookieMaxAgeDays}d, ${resolveSecureCookies(cfg) ? "Secure" : "no Secure attribute (plaintext browser ingress)"}` + (manualOverride !== void 0 ? `\n- manual override: ${manualOverride ? "enabled" : "disabled"}` : "") + (lastError !== void 0 ? `\n- last error: ${lastError}` : "")
1574
1640
  };
1575
1641
  },
1576
1642
  async enable() {
@@ -1673,4 +1739,4 @@ function apply(ctx, config) {
1673
1739
  }, "dsh-lan-gateway: listener lifecycle");
1674
1740
  }
1675
1741
  //#endregion
1676
- export { Config, apply, gatewayStartProblems, inject, isTrustedConfigRequest, name };
1742
+ export { Config, apply, gatewayStartProblems, inject, isTrustedConfigRequest, name, resolveSecureCookies };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@riceawa/dsh-lan-gateway",
3
3
  "description": "LAN/internet reverse-proxy gateway for the DeepSeek Harness web GUI: binds 0.0.0.0 and forwards to the loopback dsh web server. Default-deny: every source (loopback, LAN, internet) must sign in with an HMAC session cookie unless lanPasswordless is explicitly enabled; against dsh >= 0.1.2-rc.1 the gateway relays one shared upstream browser session, so the harness's own authorization still gates every request. Fail-closed start guard (password required, plaintext needs an explicit opt-in), session revocation by epoch (password changes and secret rotation kill cookies and live WebSockets), same-site/Origin fence on HTTP and WebSocket upgrades, optional TLS (auto self-signed or user-supplied certs), and a Settings → Plugins card for live adjustment of port, CIDRs, auth, and TLS. Includes an insecure-origin UUID shim client bundle: on gateway-served plain-HTTP origins browsers lack crypto.randomUUID, so the client half patches a getRandomValues-backed randomUUID onto the Crypto prototype, fixing workspace open over LAN without touching DSH source.",
4
- "version": "0.5.1",
4
+ "version": "0.5.3",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -22,6 +22,13 @@
22
22
  "skills",
23
23
  "cordis.patch.yml"
24
24
  ],
25
+ "scripts": {
26
+ "build": "tsdown",
27
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
28
+ "test": "vitest run",
29
+ "build:client": "tsdown",
30
+ "prepack": "tsdown"
31
+ },
25
32
  "dsh": {
26
33
  "bundle": {
27
34
  "patch": "./cordis.patch.yml"
@@ -80,10 +87,5 @@
80
87
  "url": "https://github.com/rice-awa/dsh-lan-gateway/issues"
81
88
  },
82
89
  "homepage": "https://github.com/rice-awa/dsh-lan-gateway#readme",
83
- "scripts": {
84
- "build": "tsdown",
85
- "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
86
- "test": "vitest run",
87
- "build:client": "tsdown"
88
- }
89
- }
90
+ "packageManager": "pnpm@11.20.0"
91
+ }